Questa guida è destinata ai fornitori di servizi wallet e agli sviluppatori che
desiderano integrare nuove soluzioni di gestione delle chiavi nella libreria
solana-keychain. Aggiungendo la tua implementazione del firmatario,
consentirai agli sviluppatori di utilizzare il tuo servizio per la firma sicura
delle transazioni Solana attraverso un'interfaccia unificata.
Keychain mantiene allineate le proprie implementazioni in Rust, TypeScript, Python e Go. Le PR che le includono tutte e quattro sono fortemente preferite. Se puoi contribuire con una sola, va bene, ma aspettati che le altre siano richieste prima che il signer venga distribuito in una release.
Usi Claude Code? La
skill add-signer-ci
del repository configura i workflow CI per un nuovo signer contribuito tramite una PR da fork.
Panoramica dell'Architettura
La libreria utilizza un'architettura basata su trait definita in src/traits.rs. Ogni
signer implementa il trait di base SolanaSigner (pubkey, sign_message,
is_available) più esattamente un trait di capacità corrispondente alla forma del provider:
TransactionSigner(sign_transaction): il provider firma i byte forniti dal chiamante e il chiamante effettua il broadcast. La maggior parte dei backend.ModifyingSigner(modify_and_sign_transaction): il provider riscrive la transazione, poi la firma senza effettuarne il broadcast.SendingSigner(sign_and_send_transaction): il provider firma ed effettua il broadcast lato server.
La libreria fornisce anche un enum Signer unificato che racchiude tutte le
implementazioni, consentendo la selezione a runtime dei backend di firma. Implementa
solo il trait di base; le capacità sono raggiungibili tramite as_transaction_signer(),
as_modifying_signer() e as_sending_signer().
Lista di Controllo per l'Integrazione Rapida
- Crea il tuo modulo firmatario con l'implementazione
- Implementa il trait di base
SolanaSigner(pubkey,sign_message,is_available) più l'unico trait di capacità corrispondente alla forma del tuo provider (di solitoTransactionSigner) - Aggiungi un flag di funzionalità in
Cargo.toml, includendo il marker interno_remotese il tuo signer usa reqwest - Aggiorna l'enum
Signerinsrc/lib.rs(variante, ramo didispatch_signer!e un ramo nell'accessor di capacità corrispondente alla tua forma) - Applica HTTPS e configura i timeout sui client HTTP
- Firma i byte del messaggio e verifica la firma restituita prima di allegarla
- Aggiungi unit test completi (basati su wiremock, nel tuo modulo)
- Aggiungi un file di test di integrazione e aggiorna
.env.example - Aggiungi le implementazioni in TypeScript, Python e Go
- Aggiorna la documentazione
- Aggiornamenti dei workflow CI (coordinati con i maintainer, vedi Aggiornamenti dei Workflow CI)
- Invia la PR
Passaggio 1: Crea il Tuo Modulo Firmatario
Crea una nuova directory sotto src/ per la tua implementazione:
src/├── your_service/│ ├── mod.rs # Main implementation with SolanaSigner trait│ └── types.rs # API request/response types (if needed)
Passaggio 2: Definisci la Tua Struct Firmatario
In src/your_service/mod.rs, definisci la tua struct firmatario:
//! 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()}}
Passaggio 3: Implementa il Costruttore e i Metodi Helper
I firmatari remoti devono applicare HTTPS e configurare i timeout HTTP. Usa
la struct condivisa HttpClientConfig per le impostazioni dei timeout.
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))}}
Passaggio 4: Implementare i Trait del Signer
Il trait di base SolanaSigner ha 2 metodi async (sign_message,
is_available) più pubkey(). La firma delle transazioni va in un trait di capacità;
la maggior parte dei provider implementa TransactionSigner, il cui sign_transaction restituisce
SignTransactionResult (un enum con tag che indica se la transazione è firmata completamente o
parzialmente).
Firma i byte del messaggio, mai l'intera transazione serializzata, e verifica la
firma del provider rispetto a essi prima di allegarla. Usa gli helper condivisi
TransactionUtil per la firma e la serializzazione.
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),))}}
Se il tuo provider riscrive la transazione prima di firmarla, implementa
ModifyingSigner e sostituisci integralmente la transazione del chiamante con
i byte del provider. Se effettua il broadcast lato server, implementa SendingSigner
e lascia intatta la transazione del chiamante. Non implementare mai una capacità che il tuo
provider non può offrire.
Passaggio 5: Aggiungere Tipi API (Facoltativo)
Se la tua API richiede tipi personalizzati, crea 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,}
Passaggio 6: Aggiungere Flag di Funzionalità
Aggiorna Cargo.toml per aggiungere il tuo signer come funzionalità opzionale. I backend remoti
includono il marker interno _remote, che controlla l'impl From<reqwest::Error>
in src/error.rs; senza di esso, ? sulle chiamate reqwest non compilerà quando è abilitata solo
la tua funzionalità.
[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
Passaggio 7: Aggiornare l'Enum del Signer
Aggiungi il tuo signer a src/lib.rs. L'impl base di SolanaSigner effettua il dispatch tramite
la macro dispatch_signer!, quindi aggiungi un ramo lì. L'accesso alle capacità risiede negli
accessor as_transaction_signer(), as_modifying_signer() e as_sending_signer():
aggiungi un ramo Some(s) a quello corrispondente al tuo trait di capacità (e,
per un backend di invio o di modifica, un ramo None in 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),}}}
Passaggio 8: Aggiungere Test Completi
Aggiungi test al tuo modulo (in fondo a 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());}}
Copri anche il caso di errore nella verifica della firma: una risposta simulata la cui firma non risulta valida deve restituire un errore.
Passaggio 9: Aggiungere i Test di Integrazione
Crea rust/src/tests/test_<name>_integration.rs. I test di integrazione vengono eseguiti
con l'API reale del servizio (non wiremock) e sono protetti dalla
funzionalità integration-tests. Ogni file richiede:
- Dichiarazioni
pub constper i nomi delle variabili d'ambiente - Un helper
get_signer()che legge le variabili d'ambiente tramitedotenvy - Tre funzioni di test:
test_<name>_sign_message,test_<name>_sign_transactionetest_<name>_is_available - Feature gate:
#[cfg(feature = "your_service")]sul modulo e#[cfg(feature = "integration-tests")]su ogni test
Consulta rust/src/tests/test_para_integration.rs per un riferimento completo e
dichiara il tuo modulo in rust/src/tests/mod.rs:
pub mod test_your_service_integration;
Passaggio 10: Aggiornare le Variabili d'Ambiente
Aggiorna il file .env.example nella root con le variabili d'ambiente del tuo signer, seguendo il
pattern esistente:
# 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
Elenca prima le variabili obbligatorie con valori segnaposto e le variabili opzionali commentate
con i rispettivi valori predefiniti. Ogni variabile d'ambiente usata nei test di integrazione deve comparire in
.env.example, e le suite TypeScript, Python e Go usano gli stessi nomi.
Passaggio 11: Aggiornare la Documentazione
Aggiungi il tuo signer alla tabella dei backend supportati in README.md:
| Backend | Caso d'Uso | Flag di Funzionalità |
|---|---|---|
| Memory | Keypair locali, sviluppo, testing | memory |
| Vault | Gestione chiavi enterprise con HashiCorp Vault | vault |
| Privy | Wallet integrati con infrastruttura Privy | privy |
| Turnkey | Gestione chiavi non custodial tramite Turnkey | turnkey |
| TuoServizio | Breve descrizione del tuo servizio | your_service |
Aggiungi inoltre il tuo backend alla tabella delle capacità dei signer nel README di ciascun linguaggio. Aggiungi un esempio di utilizzo:
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(())}
Testare la Tua Integrazione
Usa le ricette just anziché cargo diretto. Le funzionalità SDK (sdk-v2,
sdk-v3, sdk-v4) sono mutuamente esclusive, quindi cargo test --all-features
fallisce.
# 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
Aggiornamenti dei Workflow CI (PR da Fork)
La CI è un processo in due fasi. Coordinati con i maintainer per preparare main prima
che la tua PR possa essere testata: aggiornano fork-external-live-manual.yml, aggiungono le variabili d'ambiente
a ci.yml e configurano i GitHub Secrets.
La tua PR deve includere queste modifiche della Fase 2 insieme al codice del tuo signer:
ci.yml: aggiungi la tua funzionalità Rust alla matricebackend(prima diall) e la tua funzione di test di integrazione alla matricetesttypescript-ci.yml: aggiungi il tuo pacchetto alle matrici dei test unitari e di integrazione, e aggiungi le variabili d'ambiente al passaggio dei test di integrazionetypescript-publish.yml: aggiungi il tuo pacchetto aPUBLISH_PACKAGES, all'arraypackagesdella GitHub Release e alla tabella di riepilogopython-ci.yml: aggiungi il backend al gatingCI_SIGNER_*inresolve-signerse alla matrice di integrazionego-ci.yml: aggiungi il backend al gatingCI_SIGNER_*e amoduleBySigner
Signer TypeScript
Se stai anche aggiungendo un package signer TypeScript, crealo in
typescript/packages/your-signer/. Pattern chiave:
- La funzione factory
createYourSigner()restituisceSolanaTransactionSigner<TAddress> & SolanaMessageSigner<TAddress>; mantieni la classe del signer interna ed esporta solo la factory e il relativo tipo di configurazione - Esportare l'interfaccia di configurazione (
YourSignerConfig) - Se il tuo backend non può firmare messaggi, ometti
SolanaMessageSignere non esporre alcun metodosignMessages: Kit classifica i signer in base alla presenza dei metodi, quindi uno stub che genera un errore classificherebbe erroneamente il signer - Imponi HTTPS sui campi di configurazione
apiBaseUrlconassertHttpsUrl()di@solana/keychain-core - Effettua le chiamate remote tramite
fetchSignerJson()di@solana/keychain-core, che gestisce la mappatura degli errori, la sanitizzazione delle risposte, il rifiuto dei redirect e il timeout predefinito - Sanitizza qualsiasi altro testo di errore dell'API remota con
sanitizeRemoteErrorResponse()di@solana/keychain-core - Usare
throwSignerError(SignerErrorCode.*, { cause, message })da@solana/keychain-core - Rispetta
config?.abortSignalin ogni metodo di firma, passandolo afetchSignerJson()esignBatchStaggered() - Convalida
requestDelayMsconvalidateRequestDelayMs()ed esegui i batch consignBatchStaggered() - Aggiungere JSDoc
@throwsalle funzioni factory elencando i codici di errore - Dichiara i pacchetti
@solana/*come peer dependency a>=8.1.0 - Unit test con vitest e
fetchsimulato; test di integrazione conrunSignerIntegrationTestdi@solana/keychain-test-utils
Signer di Invio
Se il tuo provider riscrive la transazione o ne effettua il broadcast lato server, la sua
firma non può essere applicata alla transazione del chiamante, quindi deve essere un
SolanaSendingSigner anziché un SolanaTransactionSigner:
- Implementa
signAndSendTransactions()ed esponi nessunsignTransactions(nésignMessages, a meno che il backend non firmi davvero messaggi) - Rispetta
config?.abortSignal. Una volta che il provider ha accettato la creazione, segnala qualsiasi errore (incluso un abort) comeBROADCAST_UNCONFIRMEDcon l'id della transazione del provider, mantenendo il motivo dell'abort comecause - Aggiungi un overload tipizzato di
createKeychainSignerche restituisca il tipo del tuo signer di invio - Escludi il tuo backend da
KeychainKitPluginConfigintypescript/packages/kit-plugin/src/keychain-plugin.ts, poiché un signer di invio non può fungere dapayeroidentitydel client Kit - Verifica nei unit test che
isTransactionPartialSigner()sia false eisTransactionSendingSigner()sia true per le tue istanze
Aggiornare il Pacchetto Ombrello
Aggiorna typescript/packages/keychain/ (7 file da modificare):
src/types.ts: aggiungiYourSignerConfigall'unione discriminataKeychainSignerConfigsrc/create-keychain-signer.ts: aggiungi il case dello switch (ogni pacchetto backend viene caricato con unimport()dinamico)src/resolve-address.ts: aggiungi al case dello switch del percorso rapido o del percorso di fetchsrc/index.ts: aggiungi le esportazioni del tipo di configurazione, del namespace e della funzione factorypackage.json: aggiungi la dipendenza@solana/keychain-your-signer: "workspace:*"tsconfig.json: aggiungi il riferimento{ "path": "../your-signer" }typescript/scripts/test-treeshake-umbrella.mjs: aggiungi il tuo pacchetto aSIGNER_MARKERSe la tua factory aFACTORIES
Le istruzioni switch hanno controlli never esaustivi, quindi TypeScript segnalerà un errore
se aggiungi un elemento all'unione ma dimentichi un case. Esegui poi just ts-treeshake:
ogni pacchetto e l'umbrella devono rimanere compatibili con il tree-shaking.
Signer Python
Crea il modulo in python/src/solana_keychain/your_signer/ (__init__.py
più signer.py). Pattern chiave:
- Estendi
SolanaSignerdasolana_keychain.core(proprietàpubkey,async sign_message(),async is_available()) più esattamente una classe di capacità:TransactionSigner(async sign_transaction()),ModifyingSigner(async modify_and_sign_transaction()) oSendingSigner(async sign_and_send_transaction()) - Esporta una factory
async create_your_signer()che attendeinit()quando il backend risolve il proprio indirizzo da remoto; prima dell'inizializzazione, la firma generaSignerErrorCode.NOT_INITIALIZED - Esporta una dataclass di configurazione (
YourSignerConfig) con i segreti contrassegnatifield(repr=False) - Imponi HTTPS sui campi dell'URL di base con
assert_https_url()e instrada ogni richiesta tramitefetch_signer_json() - Verifica la firma del provider prima di restituirla; usa
signed_message_bytes()per i byte coperti da una firma, mai un prefisso di versione scritto a mano - Registra il backend in
_BACKENDSinpython/src/solana_keychain/keychain.py - Se il backend richiede un SDK del provider, proteggi l'import, solleva un
ImportErrorche indichipip install 'solana-keychain[your-signer]', dichiara l'extra inpython/pyproject.tomle non importare mai il backend in modo eager dalla root del pacchetto - Unit test con pytest:
respxper i backend HTTP, client stub per i backend SDK e un controllo che verifichi che nessun segreto compaia instr()orepr()degli errori sollevati - Aggiungi una factory
make_your_signer()apython/tests/integration/test_live_signers.pye registrala in_MESSAGE_CAPABLE(oppure in_TRANSACTION_ONLYper i backend privi disign_message)
Signer Go
Ogni backend Go è un modulo a sé stante, in go/signers/yoursigner/ con percorso del modulo
github.com/solana-foundation/solana-keychain/go/signers/yoursigner/v2. Consulta
go/signers/para/ come riferimento. Schemi principali:
- Implementa
core.SolanaSigner(Pubkey(),SignMessage(ctx, message),IsAvailable(ctx)) più una sola interfaccia di capacità:core.TransactionSigner,core.ModifyingSigneroppurecore.SendingSigner. La capacità viene rilevata tramite type assertion, quindi un backend che non può firmare una transazione non deve avere alcun metodoSignTransaction - Esporta una struct
Confige un costruttoreNew(New(ctx, cfg)quando il backend risolve il proprio indirizzo da remoto; prima di allora, restituiscicore.NewNotInitializedError) - Crea i client HTTP con
core.NewHTTPClientocore.ResolveHTTPCliente convalida gli URL di base concore.NormalizeHTTPSBaseURL - Firma le transazioni con
core.SignTransactionWith, che firma i byte del messaggio e allega la firma; verifica le firme del provider concore.VerifySignature - Restituisci gli errori con
core.NewSignerError/core.WrapSignerErrore codici stabili comecore.CodeSigningFailed - Unit test con
httptest; test di integrazione inintegration_test.godietro il tag//go:build integration
Checklist di Invio
Prima di inviare la vostra PR:
- Il codice compila senza avvisi (
just build) - Tutti i test passano (
just test) - Il codice è formattato/il linting passa (
just fmt) - Nessun valore hardcoded o segreto nel codice
- I messaggi di errore sono generici (nessun testo di risposta API grezzo)
- Nessun
.expect()o.unwrap()sulle risposte API non attendibili - HTTPS applicato sui client HTTP remoti
- Timeout HTTP configurati tramite
HttpClientConfig - Le firme restituite sono verificate rispetto ai byte del messaggio calcolati localmente
- Rispetta le convenzioni di denominazione (snake_case per Rust e Python, camelCase per TypeScript)
- Test di integrazione aggiunti e
.env.exampleaggiornato - Implementazioni TypeScript, Python e Go incluse (o pianificate con i maintainer)
- Pacchetti umbrella aggiornati (umbrella TypeScript,
_BACKENDSdi Python) - Modifiche alla CI incluse
- Aggiunto a README.md nelle tabelle dei backend supportati e delle capacità dei signer
Suggerimenti per l'Implementazione
Gestione degli Errori
Usa sempre le varianti SignerError esistenti. Sia Display che Debug su
SignerError sono oscurati (redacted), quindi mantieni i messaggi generici e non includere mai dati grezzi delle
risposte API. Non usare mai .expect() o .unwrap() sulle risposte API non attendibili:
// 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}")))?;
Requisiti di sicurezza
Non sono semplici suggerimenti: gli utenti scelgono un backend in base a questi requisiti. Se
il tuo provider ne rende impossibile uno, indicalo nella PR e nel file
docs/SECURITY_MODEL.md del repository.
- Verifica ciò che il provider ha firmato. Calcola localmente i byte del messaggio e verifica la firma restituita rispetto ad essi nella posizione richiesta dal tuo signer prima di allegarla. Una firma che non supera la verifica deve generare un errore e non essere mai allegata.
- Dichiara se il provider trasmette la transazione. Se la esegue lato server, implementa la forma sending e restituisci la firma che identifica la transazione confermata. Se il provider non offre un endpoint di sola firma, rendi il backend esclusivamente sending anziché inviare la transazione all'insaputa di chi effettua la chiamata.
- Non lasciare mai al chiamante una transazione non coperta dalla firma. Un provider che riscrive la transazione firma i propri byte: lascia intatta la transazione del chiamante (nel caso di broadcast gestito dal provider), oppure adotta la forma modifying e sostituiscila integralmente con i byte del provider.
- Segnala l'incertezza sul broadcast come tale. Un errore successivo all'invio potrebbe
comunque essere stato recepito dalla rete. Restituisci
BROADCAST_UNCONFIRMEDanziché un errore generico, includi l'ID della transazione del provider ogni volta che ne disponi e invia una chiave di idempotenza derivata dal messaggio a ogni creazione, in modo che un nuovo invio identico byte per byte sia sicuro. - Non firmare mai prima dell'inizializzazione. Usa
Option<Pubkey>(nonPubkey::default()) per la chiave pubblica prima diinit()e restituisci un errore di configurazione se si tenta di firmare prima. - Non registrare mai nei log dati sensibili (chiavi private, segreti API, risposte API grezze) e
usa un'implementazione di
Debugche nasconda i campi sensibili - Imponi HTTPS (
https_only(true), condizionato con#[cfg(not(test))]per wiremock) e configura i timeout di richiesta e di connessione tramiteHttpClientConfig(valori predefiniti: 30s/5s) - Azzera (zeroize) il materiale di chiave intermedio con
zeroize::Zeroizing<Vec<u8>>
Test con Mock
Usa wiremock per simulare le API HTTP. Verifica solo il tipo di errore, non il
testo del messaggio di errore:
#[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}}
Ottenere Aiuto
- Esamina le implementazioni esistenti dei signer per individuare pattern:
src/memory/mod.rs: semplice, sincronosrc/privy/mod.rs: richiede inizializzazionesrc/turnkey/mod.rs: gestione complessa delle firmesrc/vault/mod.rs: libreria client esternatypescript/packages/para/,python/src/solana_keychain/para/ego/signers/para/: riferimenti completi negli altri linguaggi
- File principali:
src/traits.rs(definizioni dei trait),src/transaction_util.rsesrc/signature_util.rs(helper condivisi),src/http_client_config.rs(configurazione dei timeout) - La guida completa del repository: docs/ADDING_SIGNERS.md
- Apri una issue per discussioni di progettazione prima di iniziare il lavoro
Struttura di Esempio di una 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?