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 (zazwyczajTransactionSigner) - Dodaj flagę funkcji w
Cargo.toml, wraz z wewnętrznym znacznikiem_remote, jeśli Twój signer używa reqwest - Zaktualizuj wyliczenie
Signerwsrc/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 integrationuse 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 APIasync 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 textif !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 featureall = ["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 methodimpl 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_availablemacro_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 shapeimpl 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 constdla nazw zmiennych środowiskowych - helpera
get_signer(), który odczytuje zmienne środowiskowe przezdotenvy - trzech funkcji testowych:
test_<name>_sign_message,test_<name>_sign_transactionitest_<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-keyYOUR_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:
| Backend | Przypadek użycia | Flaga funkcji |
|---|---|---|
| Memory | Lokalne pary kluczy, rozwój, testowanie | memory |
| Vault | Zarządzanie kluczami przedsiębiorstwa z HashiCorp Vault | vault |
| Privy | Portfele wbudowane z infrastrukturą Privy | privy |
| Turnkey | Zarządzanie kluczami non-custodial przez Turnkey | turnkey |
| TwojaUsługa | Krótki opis Twojej usługi | your_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 matricesjust 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 macierzybackend(przedall) oraz swoją funkcję testu integracyjnego do macierzytesttypescript-ci.yml: dodaj swój pakiet do macierzy testów jednostkowych i integracyjnych oraz dodaj zmienne środowiskowe do kroku testów integracyjnychtypescript-publish.yml: dodaj swój pakiet doPUBLISH_PACKAGES, do tablicypackagesw GitHub Release oraz do tabeli podsumowaniapython-ci.yml: dodaj backend do bramkowaniaCI_SIGNER_*wresolve-signersoraz do macierzy integracyjnejgo-ci.yml: dodaj backend do bramkowaniaCI_SIGNER_*oraz domoduleBySigner
Signer TypeScript
Jeśli dodajesz również pakiet signer TypeScript, utwórz go w
typescript/packages/your-signer/. Kluczowe wzorce:
- Funkcja fabryczna
createYourSigner()zwracaSolanaTransactionSigner<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ń
SolanaMessageSigneri w ogóle nie udostępniaj metodysignMessages: 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
apiBaseUrlza 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?.abortSignalw każdej metodzie podpisywania, przekazując go dofetchSignerJson()isignBatchStaggered() - Waliduj
requestDelayMsza pomocąvalidateRequestDelayMs()i grupuj żądania przezsignBatchStaggered() - Dodaj
@throwsJSDoc 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 zrunSignerIntegrationTestz@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ępniajsignTransactions(anisignMessages, chyba że backend faktycznie podpisuje wiadomości) - Respektuj
config?.abortSignal. Gdy dostawca zaakceptuje utworzenie transakcji, zgłaszaj każdą awarię (w tym przerwanie) jakoBROADCAST_UNCONFIRMEDwraz z identyfikatorem transakcji dostawcy, a przyczynę przerwania zachowaj jakocause - Dodaj typowane przeciążenie
createKeychainSignerzwracające typ Twojego signera wysyłającego - Wyklucz swój backend z
KeychainKitPluginConfigwtypescript/packages/kit-plugin/src/keychain-plugin.ts, ponieważ signer wysyłający nie może pełnić rolipayeraniidentityklienta Kit - Zweryfikuj w testach jednostkowych, że
isTransactionPartialSigner()zwraca false, aisTransactionSendingSigner()zwraca true dla Twoich instancji
Aktualizacja pakietu głównego
Zaktualizuj typescript/packages/keychain/ (7 plików do zmodyfikowania):
src/types.ts: DodajYourSignerConfigdo unii rozłącznejKeychainSignerConfigsrc/create-keychain-signer.ts: Dodaj przypadek w instrukcji switch (każdy pakiet backendu jest ładowany dynamicznie przezimport())src/resolve-address.ts: Dodaj do przypadku w switch dla ścieżki szybkiej lub ścieżki pobieraniasrc/index.ts: Dodaj eksporty typu konfiguracji, przestrzeni nazw i funkcji fabrycznejpackage.json: Dodaj zależność@solana/keychain-your-signer: "workspace:*"tsconfig.json: Dodaj odwołanie{ "path": "../your-signer" }typescript/scripts/test-treeshake-umbrella.mjs: Dodaj swój pakiet doSIGNER_MARKERSi swoją fabrykę doFACTORIES
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
SolanaSignerzsolana_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()) lubSendingSigner(async sign_and_send_transaction()) - Wyeksportuj fabrykę
async create_your_signer(), która oczekuje nainit(), gdy backend rozwiązuje swój adres zdalnie; przed inicjalizacją podpisywanie zgłaszaSignerErrorCode.NOT_INITIALIZED - Wyeksportuj dataclass konfiguracji (
YourSignerConfig), w której sekrety są oznaczonefield(repr=False) - Wymuszaj HTTPS w polach bazowego adresu URL za pomocą
assert_https_url()i kieruj każde żądanie przezfetch_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
_BACKENDSwpython/src/solana_keychain/keychain.py - Jeśli backend wymaga SDK dostawcy, zabezpiecz import, zgłoś
ImportErrorze wskazaniempip install 'solana-keychain[your-signer]', zadeklaruj dodatek (extra) wpython/pyproject.tomli nigdy nie importuj backendu zachłannie z katalogu głównego pakietu - Testy jednostkowe z pytest:
respxdla backendów HTTP, klienty zastępcze (stub) dla backendów SDK oraz sprawdzenie, że żaden sekret nie pojawia się wstr()anirepr()zgłaszanych błędów - Dodaj fabrykę
make_your_signer()dopython/tests/integration/test_live_signers.pyi zarejestruj ją w_MESSAGE_CAPABLE(lub w_TRANSACTION_ONLYdla backendów bezsign_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.ModifyingSignerlubcore.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 metodySignTransaction - Wyeksponuj strukturę
Configi konstruktorNew(New(ctx, cfg), gdy backend ustala swój adres zdalnie; do tego czasu zwracajcore.NewNotInitializedError) - Twórz klientów HTTP za pomocą
core.NewHTTPClientlubcore.ResolveHTTPClientoraz 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.WrapSignerErrori stabilnych kodów, takich jakcore.CodeSigningFailed - Testy jednostkowe z
httptest; testy integracyjne wintegration_test.gopod 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,
_BACKENDSw 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 messagereturn Err(SignerError::remote_api(format!("YourService API returned status {status}")));// Good: converts from standard errorslet 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_UNCONFIRMEDzamiast 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 niePubkey::default()) dla klucza publicznego przedinit()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 przezHttpClientConfig(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, synchronicznysrc/privy/mod.rs: wymaga inicjalizacjisrc/turnkey/mod.rs: złożona obsługa podpisówsrc/vault/mod.rs: zewnętrzna biblioteka klienckatypescript/packages/para/,python/src/solana_keychain/para/igo/signers/para/: kompletne wzorce w pozostałych językach
- Kluczowe pliki:
src/traits.rs(definicje traitów),src/transaction_util.rsisrc/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 integrationAdds 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 tableCloses #1337
Is this page helpful?