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.
A Keychain mantém suas implementações em Rust, TypeScript, Python e Go em paridade. PRs que incluem as quatro são fortemente preferidos. Se você só puder contribuir com uma, tudo bem, mas espere que as outras sejam exigidas antes de o assinante ser incluído em uma release.
Usando o Claude Code? A
skill add-signer-ci
do repositório configura os workflows de CI para um novo assinante contribuído por meio de um PR de fork.
Visão Geral da Arquitetura
A biblioteca usa uma arquitetura baseada em traits, definida em src/traits.rs. Cada
assinante implementa a trait base SolanaSigner (pubkey, sign_message,
is_available) mais exatamente uma trait de capacidade correspondente ao formato do provedor:
TransactionSigner(sign_transaction): o provedor assina os bytes do chamador e o chamador faz o broadcast. A maioria dos backends.ModifyingSigner(modify_and_sign_transaction): o provedor reescreve a transação e depois a assina, sem fazer o broadcast.SendingSigner(sign_and_send_transaction): o provedor assina e faz o broadcast no lado do servidor.
A biblioteca também fornece um enum Signer unificado que encapsula todas as
implementações, permitindo a seleção em tempo de execução dos backends de assinatura. Ele implementa
apenas a trait base; as capacidades são acessadas por meio de as_transaction_signer(),
as_modifying_signer() e as_sending_signer().
Lista de Verificação Rápida de Integração
- Crie seu módulo de signatário com implementação
- Implemente a trait base
SolanaSigner(pubkey,sign_message,is_available) mais a trait de capacidade correspondente ao formato do seu provedor (geralmenteTransactionSigner) - Adicione uma flag de recurso em
Cargo.toml, incluindo o marcador interno_remotese seu assinante usar reqwest - Atualize o enum
Signeremsrc/lib.rs(variante, braço dedispatch_signer!e um braço no acessor de capacidade correspondente ao seu formato) - Imponha HTTPS e configure timeouts em clientes HTTP
- Assine os bytes da mensagem e verifique a assinatura retornada antes de anexá-la
- Adicione testes unitários abrangentes (baseados em wiremock, no seu módulo)
- Adicione um arquivo de teste de integração e atualize
.env.example - Adicione as implementações em TypeScript, Python e Go
- Atualize a documentação
- Atualizações do workflow de CI (alinhe com os mantenedores, veja Atualizações do Workflow de CI)
- 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::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()}}
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::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))}}
Passo 4: Implementar as Traits do Assinante
A trait base SolanaSigner tem 2 métodos assíncronos (sign_message,
is_available) mais pubkey(). A assinatura de transações fica em uma trait de capacidade;
A maioria dos provedores implementa TransactionSigner, cujo sign_transaction retorna
SignTransactionResult (um enum etiquetado que marca a transação como totalmente ou
parcialmente assinada).
Assine os bytes da mensagem, nunca a transação inteira no formato wire, e verifique a
assinatura do provedor em relação a eles antes de anexá-la. Use os helpers compartilhados de
TransactionUtil para assinatura e serialização.
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 o seu provedor reescreve a transação antes de assinar, implemente
ModifyingSigner e substitua integralmente a transação do chamador pelos
bytes do provedor. Se ele faz o broadcast no lado do servidor, implemente SendingSigner
e deixe a transação do chamador intacta. Nunca implemente uma capacidade que seu
provedor não possa atender.
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. Backends remotos
incluem o marcador interno _remote, que controla a impl de From<reqwest::Error>
em src/error.rs; sem ele, ? em chamadas reqwest não compilará quando apenas
o seu recurso estiver habilitado.
[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
Passo 7: Atualizar o Enum do Assinante
Adicione seu assinante em src/lib.rs. A impl base de SolanaSigner faz o despacho por meio
do macro dispatch_signer!, então adicione um braço ali. O acesso às capacidades fica nos
acessores as_transaction_signer(), as_modifying_signer() e as_sending_signer():
adicione um braço Some(s) àquele que corresponde à sua trait de capacidade (e,
para um backend de envio ou modificação, um braço None em 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),}}}
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());}}
Cubra também a falha na verificação de assinatura: uma resposta simulada cuja assinatura não seja verificada deve retornar um erro.
Passo 9: Adicionar Testes de Integração
Crie rust/src/tests/test_<name>_integration.rs. Os testes de integração são executados
contra a API real do serviço (não wiremock) e ficam protegidos pelo
recurso integration-tests. Cada arquivo precisa de:
- Declarações
pub constpara os nomes das variáveis de ambiente - Um helper
get_signer()que lê as variáveis de ambiente viadotenvy - Três funções de teste:
test_<name>_sign_message,test_<name>_sign_transactionetest_<name>_is_available - Feature gates:
#[cfg(feature = "your_service")]no módulo e#[cfg(feature = "integration-tests")]em cada teste
Veja rust/src/tests/test_para_integration.rs para uma referência completa e
declare seu módulo em rust/src/tests/mod.rs:
pub mod test_your_service_integration;
Passo 10: Atualizar as Variáveis de Ambiente
Atualize o .env.example da raiz com as variáveis de ambiente do seu assinante, seguindo o
padrão existente:
# 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
Liste primeiro as variáveis obrigatórias com valores de exemplo e deixe as opcionais comentadas
com seus valores padrão. Toda variável de ambiente usada nos testes de integração deve aparecer em
.env.example, e as suítes de TypeScript, Python e Go usam os mesmos nomes.
Passo 11: 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 também seu backend à tabela de capacidades dos assinantes no README de cada linguagem. Adicione um 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
Use as receitas do just em vez do cargo puro. Os recursos do SDK (sdk-v2,
sdk-v3, sdk-v4) são mutuamente exclusivos, portanto cargo test --all-features
falha.
# 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
Atualizações do Workflow de CI (PRs de Fork)
O CI é um processo em duas fases. Alinhe com os mantenedores para preparar a main antes
que seu PR possa ser testado: eles atualizam fork-external-live-manual.yml, adicionam variáveis de ambiente
ao ci.yml e configuram os GitHub Secrets.
Seu PR deve incluir estas alterações da Fase 2 junto com o código do seu assinante:
ci.yml: adicione seu recurso Rust à matrizbackend(antes deall) e sua função de teste de integração à matriztesttypescript-ci.yml: adicione seu pacote às matrizes de testes unitários e de integração e adicione as variáveis de ambiente à etapa de testes de integraçãotypescript-publish.yml: adicione seu pacote aPUBLISH_PACKAGES, ao arraypackagesdo GitHub Release e à tabela de resumopython-ci.yml: adicione o backend ao controleCI_SIGNER_*emresolve-signerse à matriz de integraçãogo-ci.yml: adicione o backend ao controleCI_SIGNER_*e amoduleBySigner
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()retornaSolanaTransactionSigner<TAddress> & SolanaMessageSigner<TAddress>; mantenha a classe do assinante interna e exporte apenas a factory e o tipo de configuração dela - Exportar interface de configuração (
YourSignerConfig) - Se o seu backend não consegue assinar mensagens, omita
SolanaMessageSignere não exponha nenhum métodosignMessages: o Kit classifica os assinantes pela presença de métodos, então um stub que lança erro classificaria o assinante incorretamente - Imponha HTTPS nos campos de configuração
apiBaseUrlcomassertHttpsUrl()de@solana/keychain-core - Faça chamadas remotas por meio de
fetchSignerJson()de@solana/keychain-core, que cuida do mapeamento de erros, da sanitização de respostas, da rejeição de redirecionamentos e do timeout padrão - Sanitize qualquer outro texto de erro da API remota com
sanitizeRemoteErrorResponse()de@solana/keychain-core - Usar
throwSignerError(SignerErrorCode.*, { cause, message })de@solana/keychain-core - Respeite
config?.abortSignalem todos os métodos de assinatura, repassando-o parafetchSignerJson()esignBatchStaggered() - Valide
requestDelayMscomvalidateRequestDelayMs()e processe em lote comsignBatchStaggered() - Adicionar JSDoc
@throwsàs funções factory listando códigos de erro - Declare os pacotes
@solana/*como peer dependencies em>=8.1.0 - Testes unitários com vitest e
fetchsimulado; testes de integração comrunSignerIntegrationTestde@solana/keychain-test-utils
Assinantes de Envio
Se o seu provedor reescreve a transação ou faz o broadcast no lado do servidor, sua
assinatura não pode ser aplicada à transação do chamador, então ele deve ser um
SolanaSendingSigner em vez de um SolanaTransactionSigner:
- Implemente
signAndSendTransactions()e não exponhasignTransactions(nemsignMessages, a menos que o backend realmente assine mensagens) - Respeite
config?.abortSignal. Depois que o provedor aceitar a criação, informe qualquer falha (inclusive um aborto) comoBROADCAST_UNCONFIRMED, contendo o id da transação do provedor, com o motivo do aborto mantido comocause - Adicione uma sobrecarga tipada de
createKeychainSignerque retorne o tipo do seu assinante de envio - Exclua seu backend de
KeychainKitPluginConfigemtypescript/packages/kit-plugin/src/keychain-plugin.ts, já que um assinante de envio não pode servir comopayerouidentityde um cliente Kit - Verifique nos testes unitários que
isTransactionPartialSigner()é falso eisTransactionSendingSigner()é verdadeiro para suas instâncias
Atualizar Pacote Guarda-Chuva
Atualize typescript/packages/keychain/ (7 arquivos a modificar):
src/types.ts: adicioneYourSignerConfigà união discriminadaKeychainSignerConfigsrc/create-keychain-signer.ts: adicione o case do switch (cada pacote de backend é carregado com umimport()dinâmico)src/resolve-address.ts: adicione ao case do switch do caminho rápido ou do caminho de buscasrc/index.ts: adicione as exportações do tipo de configuração, do namespace e da função factorypackage.json: adicione a dependência@solana/keychain-your-signer: "workspace:*"tsconfig.json: adicione a referência{ "path": "../your-signer" }typescript/scripts/test-treeshake-umbrella.mjs: adicione seu pacote aSIGNER_MARKERSe sua factory aFACTORIES
As instruções switch têm verificações never exaustivas, então o TypeScript apontará erro
se você adicionar à união mas esquecer um case. Execute just ts-treeshake depois:
todos os pacotes e o umbrella devem continuar compatíveis com tree-shaking.
Assinante em Python
Crie o módulo em python/src/solana_keychain/your_signer/ (__init__.py
mais signer.py). Padrões principais:
- Crie uma subclasse de
SolanaSignerdesolana_keychain.core(propriedadepubkey,async sign_message(),async is_available()) mais exatamente uma classe de capacidade:TransactionSigner(async sign_transaction()),ModifyingSigner(async modify_and_sign_transaction()) ouSendingSigner(async sign_and_send_transaction()) - Exporte uma factory
async create_your_signer()que aguardainit()quando o backend resolve seu endereço remotamente; antes do init, a assinatura geraSignerErrorCode.NOT_INITIALIZED - Exporte uma dataclass de configuração (
YourSignerConfig) com os segredos marcados comfield(repr=False) - Imponha HTTPS nos campos de URL base com
assert_https_url()e direcione toda requisição por meio defetch_signer_json() - Verifique a assinatura do provedor antes de retorná-la; use
signed_message_bytes()para os bytes que uma assinatura cobre, nunca um prefixo de versão feito à mão - Registre o backend em
_BACKENDSempython/src/solana_keychain/keychain.py - Se o backend precisar de um SDK de provedor, proteja a importação, lance um
ImportErrorindicandopip install 'solana-keychain[your-signer]', declare o extra empython/pyproject.tomle nunca importe o backend de forma antecipada a partir da raiz do pacote - Testes unitários com pytest:
respxpara backends HTTP, clientes stub para backends de SDK e uma verificação de que nenhum segredo aparece emstr()ourepr()dos erros lançados - Adicione uma fábrica
make_your_signer()apython/tests/integration/test_live_signers.pye registe-a em_MESSAGE_CAPABLE(ou_TRANSACTION_ONLYpara backends semsign_message)
Signer em Go
Cada backend em Go é o seu próprio módulo, em go/signers/yoursigner/ com o caminho de módulo
github.com/solana-foundation/solana-keychain/go/signers/yoursigner/v2. Consulte
go/signers/para/ como referência. Principais padrões:
- Implemente
core.SolanaSigner(Pubkey(),SignMessage(ctx, message),IsAvailable(ctx)) e exatamente uma interface de capacidade:core.TransactionSigner,core.ModifyingSigneroucore.SendingSigner. A capacidade é detetada por type assertion, pelo que um backend que não consiga assinar uma transação não deve ter qualquer métodoSignTransaction - Exporte uma struct
Confige um construtorNew(New(ctx, cfg)quando o backend resolve o seu endereço remotamente; antes disso, devolvacore.NewNotInitializedError) - Crie clientes HTTP com
core.NewHTTPClientoucore.ResolveHTTPCliente valide os URLs base comcore.NormalizeHTTPSBaseURL - Assine transações com
core.SignTransactionWith, que assina os bytes da mensagem e anexa a assinatura; verifique as assinaturas do provedor comcore.VerifySignature - Devolva erros com
core.NewSignerError/core.WrapSignerErrore códigos estáveis comocore.CodeSigningFailed - Testes unitários com
httptest; testes de integração emintegration_test.goatrás da tag//go:build integration
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)
- Nenhum
.expect()ou.unwrap()em respostas de API não confiáveis - HTTPS forçado em clientes HTTP remotos
- Timeouts HTTP configurados via
HttpClientConfig - Assinaturas devolvidas verificadas com os bytes da mensagem calculados localmente
- Segue as convenções de nomenclatura (snake_case para Rust e Python, camelCase para TypeScript)
- Testes de integração adicionados e
.env.exampleatualizado - Implementações em TypeScript, Python e Go incluídas (ou planeadas com os mantenedores)
- Pacotes agregadores atualizados (agregador de TypeScript,
_BACKENDSde Python) - Alterações de CI incluídas
- Adicionado às tabelas de backends suportados e de capacidades dos signers no README.md
Dicas de Implementação
Tratamento de Erros
Utilize sempre as variantes SignerError existentes. Tanto Display como Debug em
SignerError são censurados, por isso mantenha as mensagens genéricas e nunca inclua dados
brutos de respostas da API. Nunca use .expect() ou .unwrap() em respostas de API não confiáveis:
// 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}")))?;
Requisitos de Segurança
Estes requisitos não são sugestões: os consumidores escolhem um backend com base neles. Se
o seu provedor tornar algum deles impossível, indique-o no PR e no
docs/SECURITY_MODEL.md do repositório.
- Verifique o que o provedor assinou. Calcule localmente os bytes da mensagem e verifique a assinatura devolvida com base neles, na posição exigida pelo seu signer, antes de a anexar. Uma assinatura que não seja válida deve falhar e nunca ser anexada.
- Declare se o provedor faz broadcast. Se executar do lado do servidor, implemente o formato de envio e devolva a assinatura que identifica a transação confirmada. Se o provedor não tiver um endpoint só de assinatura, torne o backend apenas de envio, em vez de submeter nas costas do chamador.
- Nunca deixe o chamador com uma transação que a assinatura não cobre. Um provedor que reescreve a transação assina os seus próprios bytes: ou deixe a transação do chamador intacta (o caso de broadcast gerido), ou adote o formato de modificação e substitua-a por inteiro pelos bytes do provedor.
- Comunique a incerteza do broadcast como tal. Uma falha após a submissão pode ter
sido, ainda assim, confirmada. Apresente
BROADCAST_UNCONFIRMEDem vez de um erro genérico, inclua o ID de transação do provedor sempre que o tiver e envie uma chave de idempotência derivada da mensagem em cada criação, para que um reenvio idêntico byte a byte seja seguro. - Nunca assine antes de inicializar. Use
Option<Pubkey>(e nãoPubkey::default()) para a chave pública antes deinit()e falhe com um erro de configuração se for tentada uma assinatura antes disso. - Nunca registe em log dados sensíveis (chaves privadas, segredos de API, respostas brutas da API) e
use uma implementação de
Debugque oculte os campos sensíveis - Imponha HTTPS (
https_only(true), condicionado com#[cfg(not(test))]para wiremock) e configure tempos limite de pedido e de ligação através deHttpClientConfig(predefinições: 30 s/5 s) - Aplique zeroize ao material de chave intermédio com
zeroize::Zeroizing<Vec<u8>>
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/privy/mod.rs: Requer inicializaçãosrc/turnkey/mod.rs: Tratamento de assinaturas complexosrc/vault/mod.rs: Biblioteca de cliente externatypescript/packages/para/,python/src/solana_keychain/para/ego/signers/para/: referências completas nas outras linguagens
- Ficheiros principais:
src/traits.rs(definições de traits),src/transaction_util.rsesrc/signature_util.rs(auxiliares partilhados),src/http_client_config.rs(configuração de tempos limite) - O guia completo do repositório: docs/ADDING_SIGNERS.md
- 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 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?