Aggiunta di Nuovi Firmatari

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 solito TransactionSigner)
  • Aggiungi un flag di funzionalità in Cargo.toml, includendo il marker interno _remote se il tuo signer usa reqwest
  • Aggiorna l'enum Signer in src/lib.rs (variante, ramo di dispatch_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 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()
}
}

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 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))
}
}

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 feature
all = ["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 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),
}
}
}

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 const per i nomi delle variabili d'ambiente
  • Un helper get_signer() che legge le variabili d'ambiente tramite dotenvy
  • Tre funzioni di test: test_<name>_sign_message, test_<name>_sign_transaction e test_<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-key
YOUR_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:

BackendCaso d'UsoFlag di Funzionalità
MemoryKeypair locali, sviluppo, testingmemory
VaultGestione chiavi enterprise con HashiCorp Vaultvault
PrivyWallet integrati con infrastruttura Privyprivy
TurnkeyGestione chiavi non custodial tramite Turnkeyturnkey
TuoServizioBreve descrizione del tuo servizioyour_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 matrices
just 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 matrice backend (prima di all) e la tua funzione di test di integrazione alla matrice test
  • typescript-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 integrazione
  • typescript-publish.yml: aggiungi il tuo pacchetto a PUBLISH_PACKAGES, all'array packages della GitHub Release e alla tabella di riepilogo
  • python-ci.yml: aggiungi il backend al gating CI_SIGNER_* in resolve-signers e alla matrice di integrazione
  • go-ci.yml: aggiungi il backend al gating CI_SIGNER_* e a moduleBySigner

Signer TypeScript

Se stai anche aggiungendo un package signer TypeScript, crealo in typescript/packages/your-signer/. Pattern chiave:

  • La funzione factory createYourSigner() restituisce SolanaTransactionSigner<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 SolanaMessageSigner e non esporre alcun metodo signMessages: 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 apiBaseUrl con assertHttpsUrl() 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?.abortSignal in ogni metodo di firma, passandolo a fetchSignerJson() e signBatchStaggered()
  • Convalida requestDelayMs con validateRequestDelayMs() ed esegui i batch con signBatchStaggered()
  • Aggiungere JSDoc @throws alle funzioni factory elencando i codici di errore
  • Dichiara i pacchetti @solana/* come peer dependency a >=8.1.0
  • Unit test con vitest e fetch simulato; test di integrazione con runSignerIntegrationTest di @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 nessun signTransactions (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) come BROADCAST_UNCONFIRMED con l'id della transazione del provider, mantenendo il motivo dell'abort come cause
  • Aggiungi un overload tipizzato di createKeychainSigner che restituisca il tipo del tuo signer di invio
  • Escludi il tuo backend da KeychainKitPluginConfig in typescript/packages/kit-plugin/src/keychain-plugin.ts, poiché un signer di invio non può fungere da payer o identity del client Kit
  • Verifica nei unit test che isTransactionPartialSigner() sia false e isTransactionSendingSigner() sia true per le tue istanze

Aggiornare il Pacchetto Ombrello

Aggiorna typescript/packages/keychain/ (7 file da modificare):

  1. src/types.ts: aggiungi YourSignerConfig all'unione discriminata KeychainSignerConfig
  2. src/create-keychain-signer.ts: aggiungi il case dello switch (ogni pacchetto backend viene caricato con un import() dinamico)
  3. src/resolve-address.ts: aggiungi al case dello switch del percorso rapido o del percorso di fetch
  4. src/index.ts: aggiungi le esportazioni del tipo di configurazione, del namespace e della funzione factory
  5. package.json: aggiungi la dipendenza @solana/keychain-your-signer: "workspace:*"
  6. tsconfig.json: aggiungi il riferimento { "path": "../your-signer" }
  7. typescript/scripts/test-treeshake-umbrella.mjs: aggiungi il tuo pacchetto a SIGNER_MARKERS e la tua factory a FACTORIES

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 SolanaSigner da solana_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()) o SendingSigner (async sign_and_send_transaction())
  • Esporta una factory async create_your_signer() che attende init() quando il backend risolve il proprio indirizzo da remoto; prima dell'inizializzazione, la firma genera SignerErrorCode.NOT_INITIALIZED
  • Esporta una dataclass di configurazione (YourSignerConfig) con i segreti contrassegnati field(repr=False)
  • Imponi HTTPS sui campi dell'URL di base con assert_https_url() e instrada ogni richiesta tramite fetch_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 _BACKENDS in python/src/solana_keychain/keychain.py
  • Se il backend richiede un SDK del provider, proteggi l'import, solleva un ImportError che indichi pip install 'solana-keychain[your-signer]', dichiara l'extra in python/pyproject.toml e non importare mai il backend in modo eager dalla root del pacchetto
  • Unit test con pytest: respx per i backend HTTP, client stub per i backend SDK e un controllo che verifichi che nessun segreto compaia in str() o repr() degli errori sollevati
  • Aggiungi una factory make_your_signer() a python/tests/integration/test_live_signers.py e registrala in _MESSAGE_CAPABLE (oppure in _TRANSACTION_ONLY per i backend privi di sign_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.ModifyingSigner oppure core.SendingSigner. La capacità viene rilevata tramite type assertion, quindi un backend che non può firmare una transazione non deve avere alcun metodo SignTransaction
  • Esporta una struct Config e un costruttore New (New(ctx, cfg) quando il backend risolve il proprio indirizzo da remoto; prima di allora, restituisci core.NewNotInitializedError)
  • Crea i client HTTP con core.NewHTTPClient o core.ResolveHTTPClient e convalida gli URL di base con core.NormalizeHTTPSBaseURL
  • Firma le transazioni con core.SignTransactionWith, che firma i byte del messaggio e allega la firma; verifica le firme del provider con core.VerifySignature
  • Restituisci gli errori con core.NewSignerError / core.WrapSignerError e codici stabili come core.CodeSigningFailed
  • Unit test con httptest; test di integrazione in integration_test.go dietro 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.example aggiornato
  • Implementazioni TypeScript, Python e Go incluse (o pianificate con i maintainer)
  • Pacchetti umbrella aggiornati (umbrella TypeScript, _BACKENDS di 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 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}")))?;

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_UNCONFIRMED anziché 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> (non Pubkey::default()) per la chiave pubblica prima di init() 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 Debug che 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 tramite HttpClientConfig (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, sincrono
    • src/privy/mod.rs: richiede inizializzazione
    • src/turnkey/mod.rs: gestione complessa delle firme
    • src/vault/mod.rs: libreria client esterna
    • typescript/packages/para/, python/src/solana_keychain/para/ e go/signers/para/: riferimenti completi negli altri linguaggi
  • File principali: src/traits.rs (definizioni dei trait), src/transaction_util.rs e src/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 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?

© 2026 Solana Foundation. Tutti i diritti riservati.