Dodawanie Nowych Signerów

Niniejszy przewodnik jest przeznaczony dla dostawców usług portfeli i programistów, którzy chcą zintegrować nowe rozwiązania zarządzania kluczami z biblioteką solana-keychain. Dodając swoją implementację signera, umożliwisz programistom korzystanie z Twojej usługi do bezpiecznego podpisywania transakcji Solana poprzez ujednolicony interfejs.

Keychain utrzymuje swoje implementacje w Rust, TypeScript, Python i Go na tym samym poziomie funkcjonalności. Zdecydowanie preferowane są PR-y zawierające wszystkie cztery. Jeśli możesz wnieść tylko jedną, to również jest w porządku, ale spodziewaj się, że pozostałe będą wymagane, zanim signer trafi do wydania.

Używasz Claude Code? Skill add-signer-ci z repozytorium konfiguruje przepływy CI dla nowego signera dodanego przez PR z forka.

Przegląd Architektury

Biblioteka korzysta z architektury opartej na cechach (traits), zdefiniowanych w src/traits.rs. Każdy signer implementuje bazową cechę SolanaSigner (pubkey, sign_message, is_available) oraz dokładnie jedną cechę możliwości odpowiadającą charakterowi dostawcy:

  • TransactionSigner (sign_transaction): dostawca podpisuje bajty przekazane przez wywołującego, a wywołujący rozgłasza transakcję. Dotyczy większości backendów.
  • ModifyingSigner (modify_and_sign_transaction): dostawca przepisuje transakcję, a następnie podpisuje ją bez rozgłaszania.
  • SendingSigner (sign_and_send_transaction): dostawca podpisuje i rozgłasza transakcję po stronie serwera.

Biblioteka udostępnia również ujednolicone wyliczenie Signer, które opakowuje wszystkie implementacje i pozwala wybierać backendy podpisywania w czasie wykonania. Implementuje tylko cechę bazową; dostęp do możliwości uzyskuje się przez as_transaction_signer(), as_modifying_signer() i as_sending_signer().

Szybka Lista Kontrolna Integracji

  • Utwórz moduł swojego signera z implementacją
  • Zaimplementuj bazową cechę SolanaSigner (pubkey, sign_message, is_available) oraz jedną cechę możliwości odpowiadającą charakterowi Twojego dostawcy (zazwyczaj TransactionSigner)
  • Dodaj flagę funkcji w Cargo.toml, wraz z wewnętrznym znacznikiem _remote, jeśli Twój signer używa reqwest
  • Zaktualizuj wyliczenie Signer w src/lib.rs (wariant, gałąź dispatch_signer! oraz gałąź w akcesorze możliwości odpowiadającym Twojemu typowi)
  • Wymuś HTTPS i skonfiguruj limity czasu w klientach HTTP
  • Podpisz bajty wiadomości i zweryfikuj zwrócony podpis przed jego dołączeniem
  • Dodaj kompleksowe testy jednostkowe (oparte na wiremock, w Twoim module)
  • Dodaj plik testów integracyjnych i zaktualizuj .env.example
  • Dodaj implementacje w TypeScript, Python i Go
  • Zaktualizuj dokumentację
  • Aktualizacje przepływów CI (skoordynuj z opiekunami, zobacz Aktualizacje przepływów CI)
  • Wyślij PR

Krok 1: Utwórz Moduł Swojego Signera

Utwórz nowy katalog w src/ dla swojej implementacji:

src/
├── your_service/
│ ├── mod.rs # Main implementation with SolanaSigner trait
│ └── types.rs # API request/response types (if needed)

Krok 2: Zdefiniuj Strukturę Swojego Signera

W src/your_service/mod.rs zdefiniuj strukturę swojego signera:

//! YourService API signer integration
use crate::sdk_adapter::{Pubkey, Signature, VersionedTransaction};
use crate::{error::SignerError, traits::SolanaSigner};
use std::str::FromStr;
/// YourService-based signer using YourService's API
#[derive(Clone)]
pub struct YourServiceSigner {
api_key: String,
api_secret: String,
wallet_id: String,
api_base_url: String,
client: reqwest::Client,
public_key: Pubkey,
}
impl std::fmt::Debug for YourServiceSigner {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("YourServiceSigner")
.field("public_key", &self.public_key)
.finish_non_exhaustive()
}
}

Krok 3: Zaimplementuj Konstruktor i Metody Pomocnicze

Zdalne signery muszą wymuszać HTTPS i konfigurować limity czasu HTTP. Użyj wspólnej struktury HttpClientConfig do ustawień limitów czasu.

use crate::http_client_config::HttpClientConfig;
impl YourServiceSigner {
pub fn new(
api_key: String,
api_secret: String,
wallet_id: String,
public_key: String,
http_config: Option<HttpClientConfig>,
) -> Result<Self, SignerError> {
let pubkey = Pubkey::from_str(&public_key)
.map_err(|e| SignerError::InvalidPublicKey(format!("Invalid public key: {e}")))?;
let http = http_config.unwrap_or_default();
let builder = reqwest::Client::builder()
.timeout(http.resolved_request_timeout())
.connect_timeout(http.resolved_connect_timeout());
// Enforce HTTPS in production; wiremock uses HTTP in tests
#[cfg(not(test))]
let builder = builder.https_only(true);
let client = builder.build().map_err(|e| {
SignerError::ConfigError(format!("Failed to build HTTP client: {e}"))
})?;
Ok(Self {
api_key,
api_secret,
wallet_id,
api_base_url: "https://api.yourservice.com/v1".to_string(),
client,
public_key: pubkey,
})
}
/// Sign raw bytes using your service's API
async fn sign(&self, message: &[u8]) -> Result<Signature, SignerError> {
let encoded_message = base64::engine::general_purpose::STANDARD.encode(message);
let url = format!("{}/sign", self.api_base_url);
let response = self
.client
.post(&url)
.header("Authorization", format!("Bearer {}", self.api_key))
.json(&serde_json::json!({
"wallet_id": self.wallet_id,
"message": encoded_message,
}))
.send()
.await?;
// Use generic error messages: never expose raw API response text
if !response.status().is_success() {
let status = response.status().as_u16();
return Err(SignerError::remote_api(format!(
"YourService API returned status {status}"
)));
}
// Parse response: always use map_err, never .expect() or .unwrap()
let response_data: SignResponse = response
.json()
.await
.map_err(|e| SignerError::SerializationError(format!("Failed to parse response: {e}")))?;
let sig_bytes = base64::engine::general_purpose::STANDARD
.decode(&response_data.signature)
.map_err(|e| SignerError::SerializationError(format!("Failed to decode signature: {e}")))?;
let sig_array: [u8; 64] = sig_bytes
.try_into()
.map_err(|_| SignerError::SigningFailed("Invalid signature length".to_string()))?;
Ok(Signature::from(sig_array))
}
}

Krok 4: Zaimplementuj cechy Signer

Bazowa cecha SolanaSigner ma 2 metody asynchroniczne (sign_message, is_available) oraz pubkey(). Podpisywanie transakcji znajduje się w cesze możliwości; większość dostawców implementuje TransactionSigner, którego sign_transaction zwraca SignTransactionResult (oznaczone wyliczenie wskazujące, że transakcja jest w pełni lub częściowo podpisana).

Podpisuj bajty wiadomości, nigdy całej transakcji w formacie przesyłanym w sieci, i zweryfikuj podpis dostawcy względem nich przed dołączeniem. Do podpisywania i serializacji używaj współdzielonych helperów TransactionUtil.

use crate::signature_util::verify_or_reject;
use crate::traits::{SignTransactionResult, TransactionSigner};
use crate::transaction_util::TransactionUtil;
#[async_trait::async_trait]
impl SolanaSigner for YourServiceSigner {
fn pubkey(&self) -> Pubkey {
self.public_key
}
async fn sign_message(&self, message: &[u8]) -> Result<Signature, SignerError> {
let signature = self.sign(message).await?;
verify_or_reject(&signature, &self.public_key, message)?;
Ok(signature)
}
async fn is_available(&self) -> bool {
let url = format!("{}/health", self.api_base_url);
self.client
.get(&url)
.send()
.await
.map(|r| r.status().is_success())
.unwrap_or(false)
}
}
#[async_trait::async_trait]
impl TransactionSigner for YourServiceSigner {
async fn sign_transaction(
&self,
tx: &mut VersionedTransaction,
) -> Result<SignTransactionResult, SignerError> {
let message = tx.message.serialize();
let signature = self.sign(&message).await?;
verify_or_reject(&signature, &self.public_key, &message)?;
TransactionUtil::add_signature_to_transaction(tx, &self.public_key, signature)?;
let serialized = TransactionUtil::serialize_transaction(tx)?;
Ok(TransactionUtil::classify_signed_transaction(
tx,
(serialized, signature),
))
}
}

Jeśli Twój dostawca przepisuje transakcję przed podpisaniem, zaimplementuj zamiast tego ModifyingSigner i zastąp transakcję wywołującego w całości bajtami dostawcy. Jeśli rozgłasza ją po stronie serwera, zaimplementuj SendingSigner i pozostaw transakcję wywołującego nietkniętą. Nigdy nie implementuj możliwości, której Twój dostawca nie może obsłużyć.

Krok 5: Dodaj typy API (opcjonalnie)

Jeśli Twoje API wymaga niestandardowych typów, utwórz src/your_service/types.rs:

use serde::{Deserialize, Serialize};
#[derive(Serialize)]
pub struct SignRequest {
pub wallet_id: String,
pub message: String,
}
#[derive(Deserialize)]
pub struct SignResponse {
pub signature: String,
}

Krok 6: Dodaj flagę funkcji

Zaktualizuj Cargo.toml, aby dodać swój signer jako opcjonalną funkcję. Backendy zdalne zawierają wewnętrzny znacznik _remote, który włącza implementację From<reqwest::Error> w src/error.rs; bez niego ? na wywołaniach reqwest nie skompiluje się, gdy włączona jest tylko Twoja funkcja.

[features]
default = ["memory", "sdk-v2"]
memory = []
vault = ["dep:reqwest", "_remote"]
privy = ["dep:reqwest", "dep:p256", "_remote"]
your_service = ["dep:reqwest", "_remote"] # Add your feature
all = ["memory", "vault", "privy", "...", "your_service"] # Update all

Krok 7: Zaktualizuj wyliczenie Signer

Dodaj swój signer do src/lib.rs. Bazowa implementacja SolanaSigner deleguje wywołania przez makro dispatch_signer!, więc dodaj tam jedną gałąź. Dostęp do możliwości realizują akcesory as_transaction_signer(), as_modifying_signer() i as_sending_signer(): dodaj gałąź Some(s) do tego, który odpowiada Twojej cesze możliwości (a w przypadku backendu wysyłającego lub modyfikującego także gałąź None w as_transaction_signer()).

// Add feature-gated module
#[cfg(feature = "your_service")]
pub mod your_service;
// Re-export your signer type
#[cfg(feature = "your_service")]
pub use your_service::YourServiceSigner;
// Add to Signer enum
#[derive(Debug)]
pub enum Signer {
#[cfg(feature = "memory")]
Memory(MemorySigner),
// ... existing variants
#[cfg(feature = "your_service")]
YourService(YourServiceSigner), // Add your variant
}
// Add constructor method
impl Signer {
#[cfg(feature = "your_service")]
pub fn from_your_service(
api_key: String,
api_secret: String,
wallet_id: String,
public_key: String,
) -> Result<Self, SignerError> {
Ok(Self::YourService(YourServiceSigner::new(
api_key,
api_secret,
wallet_id,
public_key,
None, // uses default HttpClientConfig
)?))
}
}
// Update the dispatch macro: the base SolanaSigner impl uses it for
// pubkey, sign_message and is_available
macro_rules! dispatch_signer {
($self:ident, $signer:pat => $body:expr) => {
match $self {
// ... existing variants
#[cfg(feature = "your_service")]
Signer::YourService($signer) => $body,
}
};
}
// Add your arm to the capability accessor matching your shape
impl Signer {
pub fn as_transaction_signer(&self) -> Option<&dyn TransactionSigner> {
match self {
// ... existing variants
#[cfg(feature = "your_service")]
Signer::YourService(s) => Some(s),
}
}
}

Krok 8: Dodaj kompleksowe testy

Dodaj testy do swojego modułu (na dole src/your_service/mod.rs):

#[cfg(test)]
mod tests {
use super::*;
use solana_sdk::{signature::Keypair, signer::Signer};
use wiremock::{
matchers::{header, method, path},
Mock, MockServer, ResponseTemplate,
};
#[tokio::test]
async fn test_new() {
let keypair = Keypair::new();
let signer = YourServiceSigner::new(
"test-key".to_string(),
"test-secret".to_string(),
"test-wallet".to_string(),
keypair.pubkey().to_string(),
None,
);
assert!(signer.is_ok());
}
#[tokio::test]
async fn test_sign_message() {
let mock_server = MockServer::start().await;
let keypair = Keypair::new();
let message = b"test message";
let signature = keypair.sign_message(message);
Mock::given(method("POST"))
.and(path("/sign"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"signature": base64::engine::general_purpose::STANDARD.encode(signature.as_ref())
})))
.expect(1)
.mount(&mock_server)
.await;
let mut signer = YourServiceSigner::new(
"test-key".to_string(),
"test-secret".to_string(),
"test-wallet".to_string(),
keypair.pubkey().to_string(),
None,
).unwrap();
signer.api_base_url = mock_server.uri();
let result = signer.sign_message(message).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_sign_unauthorized() {
let mock_server = MockServer::start().await;
let keypair = Keypair::new();
Mock::given(method("POST"))
.and(path("/sign"))
.respond_with(ResponseTemplate::new(401))
.expect(1)
.mount(&mock_server)
.await;
let mut signer = YourServiceSigner::new(
"bad-key".to_string(),
"bad-secret".to_string(),
"test-wallet".to_string(),
keypair.pubkey().to_string(),
None,
).unwrap();
signer.api_base_url = mock_server.uri();
let result = signer.sign_message(b"test").await;
assert!(result.is_err());
}
}

Uwzględnij też przypadek niepowodzenia weryfikacji podpisu: zamockowana odpowiedź, której podpis się nie weryfikuje, musi zwracać błąd.

Krok 9: Dodaj testy integracyjne

Utwórz rust/src/tests/test_<name>_integration.rs. Testy integracyjne działają na prawdziwym API usługi (nie wiremock) i są uruchamiane tylko z włączoną funkcją integration-tests. Każdy plik wymaga:

  • deklaracji pub const dla nazw zmiennych środowiskowych
  • helpera get_signer(), który odczytuje zmienne środowiskowe przez dotenvy
  • trzech funkcji testowych: test_<name>_sign_message, test_<name>_sign_transaction i test_<name>_is_available
  • bramek funkcji: #[cfg(feature = "your_service")] na module i #[cfg(feature = "integration-tests")] na każdym teście

Kompletny wzorzec znajdziesz w rust/src/tests/test_para_integration.rs, a swój moduł zadeklaruj w rust/src/tests/mod.rs:

pub mod test_your_service_integration;

Krok 10: Zaktualizuj zmienne środowiskowe

Zaktualizuj główny plik .env.example, dodając zmienne środowiskowe swojego signera zgodnie z istniejącym wzorcem:

# YourService Configuration (for SIGNER_TYPE=your_service)
YOUR_SERVICE_API_KEY=your-api-key
YOUR_SERVICE_WALLET_ID=your-wallet-id
# YOUR_SERVICE_API_BASE_URL=https://api.yourservice.com/v1 # Optional, defaults to this

Najpierw wypisz wymagane zmienne z wartościami zastępczymi, a opcjonalne zmienne zakomentuj wraz z ich wartościami domyślnymi. Każda zmienna środowiskowa używana w testach integracyjnych musi znaleźć się w .env.example, a zestawy testów TypeScript, Python i Go używają tych samych nazw.

Krok 11: Zaktualizuj dokumentację

Dodaj swój signer do tabeli obsługiwanych backendów w README.md:

BackendPrzypadek użyciaFlaga funkcji
MemoryLokalne pary kluczy, rozwój, testowaniememory
VaultZarządzanie kluczami przedsiębiorstwa z HashiCorp Vaultvault
PrivyPortfele wbudowane z infrastrukturą Privyprivy
TurnkeyZarządzanie kluczami non-custodial przez Turnkeyturnkey
TwojaUsługaKrótki opis Twojej usługiyour_service

Dodaj też swój backend do tabeli możliwości signerów w pliku README każdego języka. Dodaj przykład użycia:

use solana_keychain::{Signer, SolanaSigner};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let signer = Signer::from_your_service(
"your-api-key".to_string(),
"your-api-secret".to_string(),
"your-wallet-id".to_string(),
"your-public-key".to_string(),
)?;
let pubkey = signer.pubkey();
println!("Public key: {}", pubkey);
Ok(())
}

Testowanie integracji

Używaj przepisów just zamiast surowego cargo. Funkcje SDK (sdk-v2, sdk-v3, sdk-v4) wzajemnie się wykluczają, więc cargo test --all-features zakończy się niepowodzeniem.

# Unit tests across the sdk-v2, sdk-v3 and sdk-v4 matrices
just rust-test
# Live integration tests (require credentials in .env)
just rust-test-integration

Aktualizacje przepływów CI (PR-y z forków)

CI to proces dwufazowy. Skoordynuj z opiekunami przygotowanie gałęzi main, zanim Twój PR będzie mógł zostać przetestowany: aktualizują oni fork-external-live-manual.yml, dodają zmienne środowiskowe do ci.yml i konfigurują GitHub Secrets.

Twój PR musi zawierać te zmiany fazy 2 razem z kodem signera:

  • ci.yml: dodaj swoją funkcję Rust do macierzy backend (przed all) oraz swoją funkcję testu integracyjnego do macierzy test
  • typescript-ci.yml: dodaj swój pakiet do macierzy testów jednostkowych i integracyjnych oraz dodaj zmienne środowiskowe do kroku testów integracyjnych
  • typescript-publish.yml: dodaj swój pakiet do PUBLISH_PACKAGES, do tablicy packages w GitHub Release oraz do tabeli podsumowania
  • python-ci.yml: dodaj backend do bramkowania CI_SIGNER_* w resolve-signers oraz do macierzy integracyjnej
  • go-ci.yml: dodaj backend do bramkowania CI_SIGNER_* oraz do moduleBySigner

Signer TypeScript

Jeśli dodajesz również pakiet signer TypeScript, utwórz go w typescript/packages/your-signer/. Kluczowe wzorce:

  • Funkcja fabryczna createYourSigner() zwraca SolanaTransactionSigner<TAddress> & SolanaMessageSigner<TAddress>; klasę signera zachowaj jako wewnętrzną i eksportuj tylko fabrykę oraz jej typ konfiguracji
  • Wyeksportuj interfejs konfiguracji (YourSignerConfig)
  • Jeśli Twój backend nie potrafi podpisywać wiadomości, pomiń SolanaMessageSigner i w ogóle nie udostępniaj metody signMessages: Kit klasyfikuje signery na podstawie obecności metod, więc stub rzucający wyjątek spowodowałby błędną klasyfikację signera
  • Wymuszaj HTTPS w polach konfiguracji apiBaseUrl za pomocą assertHttpsUrl() z @solana/keychain-core
  • Wykonuj zdalne wywołania przez fetchSignerJson() z @solana/keychain-core, który odpowiada za mapowanie błędów, sanityzację odpowiedzi, odrzucanie przekierowań i domyślny limit czasu
  • Sanityzuj każdy inny tekst błędu zdalnego API za pomocą sanitizeRemoteErrorResponse() z @solana/keychain-core
  • Użyj throwSignerError(SignerErrorCode.*, { cause, message }) z @solana/keychain-core
  • Respektuj config?.abortSignal w każdej metodzie podpisywania, przekazując go do fetchSignerJson() i signBatchStaggered()
  • Waliduj requestDelayMs za pomocą validateRequestDelayMs() i grupuj żądania przez signBatchStaggered()
  • Dodaj @throws JSDoc do funkcji fabrykujących z wykazem kodów błędów
  • Zadeklaruj pakiety @solana/* jako zależności peer w wersji >=8.1.0
  • Testy jednostkowe z vitest i zamockowanym fetch; testy integracyjne z runSignerIntegrationTest z @solana/keychain-test-utils

Signery wysyłające

Jeśli Twój dostawca przepisuje transakcję lub rozgłasza ją po stronie serwera, jego podpisu nie można zastosować do transakcji wywołującego, więc musi to być SolanaSendingSigner, a nie SolanaTransactionSigner:

  • Zaimplementuj signAndSendTransactions() i nie udostępniaj signTransactions (ani signMessages, chyba że backend faktycznie podpisuje wiadomości)
  • Respektuj config?.abortSignal. Gdy dostawca zaakceptuje utworzenie transakcji, zgłaszaj każdą awarię (w tym przerwanie) jako BROADCAST_UNCONFIRMED wraz z identyfikatorem transakcji dostawcy, a przyczynę przerwania zachowaj jako cause
  • Dodaj typowane przeciążenie createKeychainSigner zwracające typ Twojego signera wysyłającego
  • Wyklucz swój backend z KeychainKitPluginConfig w typescript/packages/kit-plugin/src/keychain-plugin.ts, ponieważ signer wysyłający nie może pełnić roli payer ani identity klienta Kit
  • Zweryfikuj w testach jednostkowych, że isTransactionPartialSigner() zwraca false, a isTransactionSendingSigner() zwraca true dla Twoich instancji

Aktualizacja pakietu głównego

Zaktualizuj typescript/packages/keychain/ (7 plików do zmodyfikowania):

  1. src/types.ts: Dodaj YourSignerConfig do unii rozłącznej KeychainSignerConfig
  2. src/create-keychain-signer.ts: Dodaj przypadek w instrukcji switch (każdy pakiet backendu jest ładowany dynamicznie przez import())
  3. src/resolve-address.ts: Dodaj do przypadku w switch dla ścieżki szybkiej lub ścieżki pobierania
  4. src/index.ts: Dodaj eksporty typu konfiguracji, przestrzeni nazw i funkcji fabrycznej
  5. package.json: Dodaj zależność @solana/keychain-your-signer: "workspace:*"
  6. tsconfig.json: Dodaj odwołanie { "path": "../your-signer" }
  7. typescript/scripts/test-treeshake-umbrella.mjs: Dodaj swój pakiet do SIGNER_MARKERS i swoją fabrykę do FACTORIES

Instrukcje switch zawierają wyczerpujące kontrole never, więc TypeScript zgłosi błąd, jeśli dodasz element do unii, ale pominiesz przypadek. Następnie uruchom just ts-treeshake: każdy pakiet i pakiet zbiorczy muszą pozostać przyjazne dla tree-shakingu.

Signer w Pythonie

Utwórz moduł w python/src/solana_keychain/your_signer/ (__init__.py oraz signer.py). Kluczowe wzorce:

  • Dziedzicz po SolanaSigner z solana_keychain.core (właściwość pubkey, async sign_message(), async is_available()) oraz po dokładnie jednej klasie możliwości: TransactionSigner (async sign_transaction()), ModifyingSigner (async modify_and_sign_transaction()) lub SendingSigner (async sign_and_send_transaction())
  • Wyeksportuj fabrykę async create_your_signer(), która oczekuje na init(), gdy backend rozwiązuje swój adres zdalnie; przed inicjalizacją podpisywanie zgłasza SignerErrorCode.NOT_INITIALIZED
  • Wyeksportuj dataclass konfiguracji (YourSignerConfig), w której sekrety są oznaczone field(repr=False)
  • Wymuszaj HTTPS w polach bazowego adresu URL za pomocą assert_https_url() i kieruj każde żądanie przez fetch_signer_json()
  • Zweryfikuj podpis dostawcy przed jego zwróceniem; używaj signed_message_bytes() dla bajtów objętych podpisem, nigdy własnoręcznie napisanego prefiksu wersji
  • Zarejestruj backend w _BACKENDS w python/src/solana_keychain/keychain.py
  • Jeśli backend wymaga SDK dostawcy, zabezpiecz import, zgłoś ImportError ze wskazaniem pip install 'solana-keychain[your-signer]', zadeklaruj dodatek (extra) w python/pyproject.toml i nigdy nie importuj backendu zachłannie z katalogu głównego pakietu
  • Testy jednostkowe z pytest: respx dla backendów HTTP, klienty zastępcze (stub) dla backendów SDK oraz sprawdzenie, że żaden sekret nie pojawia się w str() ani repr() zgłaszanych błędów
  • Dodaj fabrykę make_your_signer() do python/tests/integration/test_live_signers.py i zarejestruj ją w _MESSAGE_CAPABLE (lub w _TRANSACTION_ONLY dla backendów bez sign_message)

Signer w Go

Każdy backend w Go jest osobnym modułem, w go/signers/yoursigner/ ze ścieżką modułu github.com/solana-foundation/solana-keychain/go/signers/yoursigner/v2. Wzorcowy przykład znajdziesz w go/signers/para/. Kluczowe wzorce:

  • Zaimplementuj core.SolanaSigner (Pubkey(), SignMessage(ctx, message), IsAvailable(ctx)) oraz dokładnie jeden interfejs możliwości: core.TransactionSigner, core.ModifyingSigner lub core.SendingSigner. Możliwości są wykrywane przez asercję typu, więc backend, który nie potrafi podpisać transakcji, nie może mieć w ogóle metody SignTransaction
  • Wyeksponuj strukturę Config i konstruktor New (New(ctx, cfg), gdy backend ustala swój adres zdalnie; do tego czasu zwracaj core.NewNotInitializedError)
  • Twórz klientów HTTP za pomocą core.NewHTTPClient lub core.ResolveHTTPClient oraz waliduj bazowe adresy URL za pomocą core.NormalizeHTTPSBaseURL
  • Podpisuj transakcje za pomocą core.SignTransactionWith, która podpisuje bajty wiadomości i dołącza podpis; weryfikuj podpisy dostawcy za pomocą core.VerifySignature
  • Zwracaj błędy za pomocą core.NewSignerError / core.WrapSignerError i stabilnych kodów, takich jak core.CodeSigningFailed
  • Testy jednostkowe z httptest; testy integracyjne w integration_test.go pod tagiem //go:build integration

Lista kontrolna przed wysłaniem

Przed wysłaniem PR:

  • Kod kompiluje się bez ostrzeżeń (just build)
  • Wszystkie testy przechodzą (just test)
  • Kod jest sformatowany/linting przechodzi (just fmt)
  • Brak zakodowanych na stałe wartości lub sekretów w kodzie
  • Komunikaty błędów są ogólne (bez surowego tekstu odpowiedzi API)
  • Brak .expect() i .unwrap() na niezaufanych odpowiedziach API
  • HTTPS wymuszony na zdalnych klientach HTTP
  • Limity czasu HTTP skonfigurowane przez HttpClientConfig
  • Zwrócone podpisy zweryfikowane względem lokalnie obliczonych bajtów wiadomości
  • Przestrzega konwencji nazewnictwa (snake_case dla Rusta i Pythona, camelCase dla TypeScriptu)
  • Dodano testy integracyjne i zaktualizowano .env.example
  • Dołączono implementacje w TypeScripcie, Pythonie i Go (lub zaplanowano je razem z opiekunami)
  • Zaktualizowano pakiety zbiorcze (pakiet zbiorczy TypeScript, _BACKENDS w Pythonie)
  • Dołączono zmiany w CI
  • Dodano do tabel obsługiwanych backendów i możliwości signerów w README.md

Wskazówki implementacyjne

Obsługa błędów

Zawsze używaj istniejących wariantów SignerError. Zarówno Display, jak i Debug w SignerError są zredagowane, więc komunikaty powinny być ogólne i nigdy nie zawierać surowych danych odpowiedzi API. Nigdy nie używaj .expect() ani .unwrap() na niezaufanych odpowiedziach API:

// Good: uses the constructor with a generic message
return Err(SignerError::remote_api(format!(
"YourService API returned status {status}"
)));
// Good: converts from standard errors
let bytes = base64::decode(data)
.map_err(|e| SignerError::SerializationError(format!("Failed to decode: {e}")))?;

Wymagania bezpieczeństwa

To nie są sugestie: odbiorcy wybierają backend, opierając się na nich. Jeśli twój dostawca uniemożliwia spełnienie któregoś z nich, napisz o tym w PR oraz w docs/SECURITY_MODEL.md w repozytorium.

  • Weryfikuj to, co podpisał dostawca. Oblicz bajty wiadomości lokalnie i zweryfikuj zwrócony podpis względem nich na pozycji wymaganej przez twój signer, zanim go dołączysz. Podpis, który się nie weryfikuje, musi zakończyć się błędem i nigdy nie może zostać dołączony.
  • Zadeklaruj, czy dostawca rozgłasza transakcje. Jeśli wykonuje je po stronie serwera, zaimplementuj wariant wysyłający i zwróć podpis identyfikujący zatwierdzoną transakcję. Jeśli dostawca nie ma endpointu tylko do podpisywania, ustaw backend jako wyłącznie wysyłający, zamiast przesyłać transakcje za plecami wywołującego.
  • Nigdy nie zostawiaj wywołującego z transakcją, której nie obejmuje podpis. Dostawca, który przepisuje transakcję, podpisuje własne bajty: albo pozostaw transakcję wywołującego nietkniętą (przypadek rozgłaszania zarządzanego przez dostawcę), albo przyjmij wariant modyfikujący i w całości zastąp ją bajtami dostawcy.
  • Zgłaszaj niepewność co do rozgłoszenia jako taką. Błąd po przesłaniu transakcji mógł nie przeszkodzić jej w zatwierdzeniu. Zwróć BROADCAST_UNCONFIRMED zamiast ogólnego błędu, przekaż identyfikator transakcji dostawcy, gdy tylko go masz, i wysyłaj klucz idempotentności wyprowadzony z wiadomości przy każdym tworzeniu, aby ponowne wysłanie identycznych bajtów było bezpieczne.
  • Nigdy nie podpisuj przed inicjalizacją. Używaj Option<Pubkey> (a nie Pubkey::default()) dla klucza publicznego przed init() i zwracaj błąd konfiguracji, jeśli podpisywanie zostanie podjęte wcześniej.
  • Nigdy nie loguj danych wrażliwych (kluczy prywatnych, sekretów API, surowych odpowiedzi API) i używaj implementacji Debug, która ukrywa pola wrażliwe
  • Wymuszaj HTTPS (https_only(true), z bramką #[cfg(not(test))] dla wiremock) i skonfiguruj limity czasu żądania i połączenia przez HttpClientConfig (domyślnie: 30 s/5 s)
  • Zeruj pośredni materiał kluczowy za pomocą zeroize::Zeroizing<Vec<u8>>

Testowanie z mockami

Użyj wiremock do mockowania API HTTP. Sprawdzaj tylko typ błędu, a nie tekst komunikatu błędu:

#[cfg(test)]
mod tests {
use wiremock::{MockServer, Mock, ResponseTemplate};
#[tokio::test]
async fn test_api_call() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.respond_with(ResponseTemplate::new(200))
.mount(&mock_server)
.await;
// Use mock_server.uri() as your api_base_url
}
}

Uzyskiwanie pomocy

  • Przejrzyj istniejące implementacje signerów w poszukiwaniu wzorców:
    • src/memory/mod.rs: prosty, synchroniczny
    • src/privy/mod.rs: wymaga inicjalizacji
    • src/turnkey/mod.rs: złożona obsługa podpisów
    • src/vault/mod.rs: zewnętrzna biblioteka kliencka
    • typescript/packages/para/, python/src/solana_keychain/para/ i go/signers/para/: kompletne wzorce w pozostałych językach
  • Kluczowe pliki: src/traits.rs (definicje traitów), src/transaction_util.rs i src/signature_util.rs (współdzielone funkcje pomocnicze), src/http_client_config.rs (konfiguracja limitów czasu)
  • Pełny przewodnik po repozytorium: docs/ADDING_SIGNERS.md
  • Otwórz issue do dyskusji nad projektem przed rozpoczęciem pracy

Przykładowa struktura PR

feat(signer): add YourService signer integration
Adds support for YourService as a signing backend.
- [X] Code compiles without warnings (`just build`)
- [X] Code is formatted/linting passes (`just fmt`)
- [X] Add comprehensive tests with wiremock - All tests pass (`just test`)
- [X] Implemented SolanaSigner and TransactionSigner for YourServiceSigner
- [X] Added feature flag 'your_service'
- [X] HTTPS enforced, HTTP timeouts configured
- [X] Returned signatures verified before attaching
- [X] Added integration tests and updated .env.example
- [X] TypeScript, Python and Go implementations included
- [X] CI Phase 2 changes included
- [X] Added to README.md supported backends table
Closes #1337

Is this page helpful?