Este guia é destinado a provedores de serviços de carteira e desenvolvedores que
desejam integrar novas soluções de gerenciamento de chaves na biblioteca
solana-keychain. Ao adicionar sua implementação de signatário, você permitirá
que os desenvolvedores usem seu serviço para assinatura segura de transações
Solana através de uma interface unificada.
Usando um LLM? Confira a Habilidade de Adicionar Signatários.
Visão Geral da Arquitetura
A biblioteca usa uma arquitetura baseada em traits onde todos os signatários
implementam a trait SolanaSigner definida em src/traits.rs. A biblioteca
também fornece um enum Signer unificado que envolve todas as implementações,
permitindo seleção em tempo de execução de backends de assinatura mantendo uma
API consistente.
Lista de Verificação Rápida de Integração
- Crie seu módulo de signatário com implementação
- Implemente a trait
SolanaSigner(3 métodos assíncronos +pubkey()) - Adicione uma flag de feature em
Cargo.toml - Atualize o enum
Signeremsrc/lib.rs(4 braços de match) - Atualize o gate cfg da impl
Fromde reqwest emsrc/error.rs(se seu signatário usar reqwest) - Imponha HTTPS e configure timeouts em clientes HTTP
- Adicione testes abrangentes
- Atualize a documentação
- Envie PR
Passo 1: Crie Seu Módulo de Signatário
Crie um novo diretório em src/ para sua implementação:
src/├── your_service/│ ├── mod.rs # Main implementation with SolanaSigner trait│ └── types.rs # API request/response types (if needed)
Passo 2: Defina Sua Struct de Signatário
Em src/your_service/mod.rs, defina sua struct de signatário:
//! YourService API signer integrationuse crate::{error::SignerError, traits::SolanaSigner};use solana_sdk::{pubkey::Pubkey, signature::Signature, transaction::Transaction};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()}}
Passo 3: Implemente Construtor e Métodos Auxiliares
Signatários remotos devem impor HTTPS e configurar timeouts HTTP. Use a
struct compartilhada HttpClientConfig para configurações de 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::RemoteApiError(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))}}
Passo 4: Implemente a Trait SolanaSigner
A trait possui 3 métodos assíncronos (sign_transaction, sign_message,
is_available) além de pubkey(). Observe que sign_transaction retorna
SignTransactionResult — um enum marcado indicando se a transação está
totalmente assinada ou parcialmente assinada.
Use os auxiliares compartilhados TransactionUtil para assinatura e
serialização.
use crate::transaction_util::TransactionUtil;use crate::traits::SignTransactionResult;#[async_trait::async_trait]impl SolanaSigner for YourServiceSigner {fn pubkey(&self) -> Pubkey {self.public_key}async fn sign_transaction(&self,tx: &mut Transaction,) -> Result<SignTransactionResult, SignerError> {let tx_bytes = bincode::serialize(tx).map_err(|e| SignerError::SerializationError(format!("Failed to serialize: {e}")))?;let signature = self.sign(&tx_bytes).await?;// Add the signature at the correct positionTransactionUtil::add_signature_to_transaction(tx, &self.public_key, signature)?;// Serialize and classify as Complete or Partiallet serialized = TransactionUtil::serialize_transaction(tx)?;Ok(TransactionUtil::classify_signed_transaction(tx,(serialized, signature),))}async fn sign_message(&self, message: &[u8]) -> Result<Signature, SignerError> {self.sign(message).await}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)}}
Passo 5: Adicionar Tipos de API (Opcional)
Se sua API precisar de tipos personalizados, crie 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,}
Passo 6: Adicionar Flag de Recurso
Atualize Cargo.toml para adicionar seu assinante como um recurso opcional:
[features]default = ["memory"]memory = []vault = ["dep:reqwest", "dep:vaultrs", "dep:base64"]privy = ["dep:reqwest", "dep:base64"]turnkey = ["dep:reqwest", "dep:base64", "dep:p256", "dep:hex", "dep:chrono"]your_service = ["dep:reqwest", "dep:base64"] # Add your featureall = ["memory", "vault", "privy", "turnkey", "your_service"] # Update all
Passo 7: Atualizar o Enum do Assinante
Adicione seu assinante a src/lib.rs. Você precisa de 4 braços de
correspondência no SolanaSigner impl: pubkey, sign_transaction,
sign_message e is_available.
// 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 trait implementation — 4 match arms#[async_trait::async_trait]impl SolanaSigner for Signer {fn pubkey(&self) -> sdk_adapter::Pubkey {match self {// ... existing variants#[cfg(feature = "your_service")]Signer::YourService(s) => s.pubkey(),}}async fn sign_transaction(&self,tx: &mut sdk_adapter::Transaction,) -> Result<SignTransactionResult, SignerError> {match self {// ... existing variants#[cfg(feature = "your_service")]Signer::YourService(s) => s.sign_transaction(tx).await,}}async fn sign_message(&self,message: &[u8],) -> Result<sdk_adapter::Signature, SignerError> {match self {// ... existing variants#[cfg(feature = "your_service")]Signer::YourService(s) => s.sign_message(message).await,}}async fn is_available(&self) -> bool {match self {// ... existing variants#[cfg(feature = "your_service")]Signer::YourService(s) => s.is_available().await,}}}
Se seu assinante usar reqwest, adicione seu recurso ao gate #[cfg(any(...))]
no impl From<reqwest::Error> em src/error.rs.
Passo 8: Adicionar Testes Abrangentes
Adicione testes ao seu módulo (na parte inferior de 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());}}
Passo 9: Atualizar a Documentação
Adicione seu assinante à tabela de backends suportados no README.md:
| Backend | Caso de Uso | Flag de Recurso |
|---|---|---|
| Memory | Pares de chaves locais, desenvolvimento, testes | memory |
| Vault | Gerenciamento de chaves empresariais com HashiCorp Vault | vault |
| Privy | Carteiras incorporadas com infraestrutura Privy | privy |
| Turnkey | Gerenciamento de chaves não-custodial via Turnkey | turnkey |
| SeuServiço | Breve descrição do seu serviço | your_service |
Adicione exemplo de uso:
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(())}
Testando Sua Integração
Execute testes para seu recurso:
# Test only your signercargo test --features your_service# Test with all featurescargo test --all-features
Assinante TypeScript
Se você também estiver adicionando um pacote de assinante TypeScript, crie-o em
typescript/packages/your-signer/. Padrões principais:
- A função factory
createYourSigner()retornaSolanaSigner<TAddress> - Exportar interface de configuração (
YourSignerConfig) - Forçar HTTPS nos campos de configuração
apiBaseUrl - Sanitizar texto de erro de API remota com
sanitizeRemoteErrorResponse()de@solana/keychain-core - Proteger contra JSON malformado com encadeamento opcional e try/catch
- Usar
throwSignerError(SignerErrorCode.*, { cause, message })de@solana/keychain-core - Adicionar JSDoc
@throwsàs funções factory listando códigos de erro
Atualizar Pacote Guarda-Chuva
Atualizar typescript/packages/keychain/ — 6 arquivos a modificar:
src/types.ts— AdicionarYourSignerConfigà união discriminadaKeychainSignerConfigsrc/create-keychain-signer.ts— Importar factory, adicionar caso switchsrc/resolve-address.ts— Adicionar ao caso switch de caminho rápido ou caminho de buscasrc/index.ts— Adicionar tipo de configuração, namespace, função factory e exportações de classepackage.json— Adicionar dependência@solana/keychain-your-signer: "workspace:*"tsconfig.json— Adicionar referência{ "path": "../your-signer" }
As instruções switch possuem verificações exaustivas de never — TypeScript
retornará erro se você adicionar à união mas omitir um caso.
Lista de Verificação para Submissão
Antes de submeter seu PR:
- O código compila sem avisos (
just build) - Todos os testes passam (
just test) - O código está formatado/linting passa (
just fmt) - Sem valores fixos no código ou segredos no código
- Mensagens de erro são genéricas (sem texto bruto de resposta de API)
- HTTPS forçado em clientes HTTP remotos
- Timeouts HTTP configurados via
HttpClientConfig - Segue convenções de nomenclatura Rust (snake_case)
- Adicionado à tabela de backends suportados no README.md
Dicas de Implementação
Tratamento de Erros
Sempre use as variantes SignerError existentes. Nunca use .expect() ou
.unwrap() em respostas de API não confiáveis:
// Good — uses existing error types with generic messagesreturn Err(SignerError::RemoteApiError(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}")))?;
Melhores Práticas de Segurança
- Nunca registre dados sensíveis (chaves privadas, segredos de API)
- Use impl
Debugque oculta campos sensíveis - Valide todas as entradas (chaves públicas, assinaturas)
- Use HTTPS para todas as chamadas de API remotas (forçado via
https_only(true)) - Configure timeouts de requisição e conexão via
HttpClientConfig - Nunca exponha texto bruto de erro de API remota em mensagens de erro
- Use
Option<Pubkey>(nãoPubkey::default()) para o campo de chave pública antes deinit()
Testando com Mocks
Use wiremock para simular APIs HTTP. Afirme apenas o tipo de erro, não o texto
da mensagem de erro:
#[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}}
Obtendo Ajuda
- Revise as implementações de signatários existentes para identificar padrões:
src/memory/mod.rs— Simples, síncronosrc/para/mod.rs— Requer inicialização (use como padrão para novos signatários)src/turnkey/mod.rs— Manipulação complexa de assinaturassrc/vault/mod.rs— Biblioteca cliente externa
- Arquivos principais:
src/traits.rs(definição de trait),src/transaction_util.rs(auxiliares compartilhados),src/http_client_config.rs(configuração de timeout) - Abra uma issue para discussões de design antes de começar o trabalho
Estrutura de Exemplo de 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 trait for YourServiceSigner- [X] Added feature flag 'your_service'- [X] HTTPS enforced, HTTP timeouts configured- [X] Added to README.md supported backends tableCloses #1337
Is this page helpful?