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.

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 Signer em src/lib.rs (4 braços de match)
  • Atualize o gate cfg da impl From de reqwest em src/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 integration
use 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 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::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 position
TransactionUtil::add_signature_to_transaction(tx, &self.public_key, signature)?;
// Serialize and classify as Complete or Partial
let 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 feature
all = ["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 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 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:

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 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 signer
cargo test --features your_service
# Test with all features
cargo 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() retorna SolanaSigner<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:

  1. src/types.ts — Adicionar YourSignerConfig à união discriminada KeychainSignerConfig
  2. src/create-keychain-signer.ts — Importar factory, adicionar caso switch
  3. src/resolve-address.ts — Adicionar ao caso switch de caminho rápido ou caminho de busca
  4. src/index.ts — Adicionar tipo de configuração, namespace, função factory e exportações de classe
  5. package.json — Adicionar dependência @solana/keychain-your-signer: "workspace:*"
  6. 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 messages
return Err(SignerError::RemoteApiError(
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}")))?;

Melhores Práticas de Segurança

  • Nunca registre dados sensíveis (chaves privadas, segredos de API)
  • Use impl Debug que 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ão Pubkey::default()) para o campo de chave pública antes de init()

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/para/mod.rs — Requer inicialização (use como padrão para novos signatários)
    • src/turnkey/mod.rs — Manipulação complexa de assinaturas
    • src/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 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 trait for YourServiceSigner
- [X] Added feature flag 'your_service'
- [X] HTTPS enforced, HTTP timeouts configured
- [X] Added to README.md supported backends table
Closes #1337

Is this page helpful?

Gerenciado por

© 2026 Fundação Solana.
Todos os direitos reservados.
Conecte-se
  • Blog