Adicionando Novos Signatários

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 (geralmente TransactionSigner)
  • Adicione uma flag de recurso em Cargo.toml, incluindo o marcador interno _remote se seu assinante usar reqwest
  • Atualize o enum Signer em src/lib.rs (variante, braço de dispatch_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 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()
}
}

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

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

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 const para os nomes das variáveis de ambiente
  • Um helper get_signer() que lê as variáveis de ambiente via dotenvy
  • Três funções de teste: test_<name>_sign_message, test_<name>_sign_transaction e test_<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-key
YOUR_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:

BackendCaso de UsoFlag de Recurso
MemoryPares de chaves locais, desenvolvimento, testesmemory
VaultGerenciamento de chaves empresariais com HashiCorp Vaultvault
PrivyCarteiras incorporadas com infraestrutura Privyprivy
TurnkeyGerenciamento de chaves não-custodial via Turnkeyturnkey
SeuServiçoBreve descrição do seu serviçoyour_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 matrices
just 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 à matriz backend (antes de all) e sua função de teste de integração à matriz test
  • typescript-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ção
  • typescript-publish.yml: adicione seu pacote a PUBLISH_PACKAGES, ao array packages do GitHub Release e à tabela de resumo
  • python-ci.yml: adicione o backend ao controle CI_SIGNER_* em resolve-signers e à matriz de integração
  • go-ci.yml: adicione o backend ao controle CI_SIGNER_* e a moduleBySigner

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() retorna SolanaTransactionSigner<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 SolanaMessageSigner e não exponha nenhum método signMessages: 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 apiBaseUrl com assertHttpsUrl() 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?.abortSignal em todos os métodos de assinatura, repassando-o para fetchSignerJson() e signBatchStaggered()
  • Valide requestDelayMs com validateRequestDelayMs() e processe em lote com signBatchStaggered()
  • 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 fetch simulado; testes de integração com runSignerIntegrationTest de @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 exponha signTransactions (nem signMessages, 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) como BROADCAST_UNCONFIRMED, contendo o id da transação do provedor, com o motivo do aborto mantido como cause
  • Adicione uma sobrecarga tipada de createKeychainSigner que retorne o tipo do seu assinante de envio
  • Exclua seu backend de KeychainKitPluginConfig em typescript/packages/kit-plugin/src/keychain-plugin.ts, já que um assinante de envio não pode servir como payer ou identity de um cliente Kit
  • Verifique nos testes unitários que isTransactionPartialSigner() é falso e isTransactionSendingSigner() é verdadeiro para suas instâncias

Atualizar Pacote Guarda-Chuva

Atualize typescript/packages/keychain/ (7 arquivos a modificar):

  1. src/types.ts: adicione YourSignerConfig à união discriminada KeychainSignerConfig
  2. src/create-keychain-signer.ts: adicione o case do switch (cada pacote de backend é carregado com um import() dinâmico)
  3. src/resolve-address.ts: adicione ao case do switch do caminho rápido ou do caminho de busca
  4. src/index.ts: adicione as exportações do tipo de configuração, do namespace e da função factory
  5. package.json: adicione a dependência @solana/keychain-your-signer: "workspace:*"
  6. tsconfig.json: adicione a referência { "path": "../your-signer" }
  7. typescript/scripts/test-treeshake-umbrella.mjs: adicione seu pacote a SIGNER_MARKERS e sua factory a FACTORIES

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 SolanaSigner de solana_keychain.core (propriedade pubkey, async sign_message(), async is_available()) mais exatamente uma classe de capacidade: TransactionSigner (async sign_transaction()), ModifyingSigner (async modify_and_sign_transaction()) ou SendingSigner (async sign_and_send_transaction())
  • Exporte uma factory async create_your_signer() que aguarda init() quando o backend resolve seu endereço remotamente; antes do init, a assinatura gera SignerErrorCode.NOT_INITIALIZED
  • Exporte uma dataclass de configuração (YourSignerConfig) com os segredos marcados com field(repr=False)
  • Imponha HTTPS nos campos de URL base com assert_https_url() e direcione toda requisição por meio de fetch_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 _BACKENDS em python/src/solana_keychain/keychain.py
  • Se o backend precisar de um SDK de provedor, proteja a importação, lance um ImportError indicando pip install 'solana-keychain[your-signer]', declare o extra em python/pyproject.toml e nunca importe o backend de forma antecipada a partir da raiz do pacote
  • Testes unitários com pytest: respx para backends HTTP, clientes stub para backends de SDK e uma verificação de que nenhum segredo aparece em str() ou repr() dos erros lançados
  • Adicione uma fábrica make_your_signer() a python/tests/integration/test_live_signers.py e registe-a em _MESSAGE_CAPABLE (ou _TRANSACTION_ONLY para backends sem sign_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.ModifyingSigner ou core.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étodo SignTransaction
  • Exporte uma struct Config e um construtor New (New(ctx, cfg) quando o backend resolve o seu endereço remotamente; antes disso, devolva core.NewNotInitializedError)
  • Crie clientes HTTP com core.NewHTTPClient ou core.ResolveHTTPClient e valide os URLs base com core.NormalizeHTTPSBaseURL
  • Assine transações com core.SignTransactionWith, que assina os bytes da mensagem e anexa a assinatura; verifique as assinaturas do provedor com core.VerifySignature
  • Devolva erros com core.NewSignerError / core.WrapSignerError e códigos estáveis como core.CodeSigningFailed
  • Testes unitários com httptest; testes de integração em integration_test.go atrá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.example atualizado
  • Implementações em TypeScript, Python e Go incluídas (ou planeadas com os mantenedores)
  • Pacotes agregadores atualizados (agregador de TypeScript, _BACKENDS de 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 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}")))?;

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_UNCONFIRMED em 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ão Pubkey::default()) para a chave pública antes de init() 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 Debug que 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 de HttpClientConfig (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íncrono
    • src/privy/mod.rs: Requer inicialização
    • src/turnkey/mod.rs: Tratamento de assinaturas complexo
    • src/vault/mod.rs: Biblioteca de cliente externa
    • typescript/packages/para/, python/src/solana_keychain/para/ e go/signers/para/: referências completas nas outras linguagens
  • Ficheiros principais: src/traits.rs (definições de traits), src/transaction_util.rs e src/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 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 Fundação Solana. Todos os direitos reservados.