Esta guía está dirigida a proveedores de servicios de billetera y
desarrolladores que desean integrar nuevas soluciones de gestión de claves en la
biblioteca solana-keychain. Al añadir tu implementación de firmante,
permitirás a los desarrolladores usar tu servicio para la firma segura de
transacciones de Solana a través de una interfaz unificada.
Keychain mantiene sus implementaciones en Rust, TypeScript, Python y Go en paridad. Se prefieren encarecidamente los PR que incluyan las cuatro. Si solo puedes contribuir con una, no hay problema, pero ten en cuenta que se exigirán las demás antes de que el firmante se publique en una versión.
¿Usas Claude Code? La
skill add-signer-ci del repositorio
configura los flujos de trabajo de CI para un nuevo firmante aportado mediante un PR desde un fork.
Descripción General de la Arquitectura
La biblioteca utiliza una arquitectura basada en traits definida en src/traits.rs. Cada
firmante implementa el trait base SolanaSigner (pubkey, sign_message,
is_available) más exactamente un trait de capacidad que coincida con el formato del proveedor:
TransactionSigner(sign_transaction): el proveedor firma los bytes del llamador y este los transmite. La mayoría de los backends.ModifyingSigner(modify_and_sign_transaction): el proveedor reescribe la transacción y luego la firma sin transmitirla.SendingSigner(sign_and_send_transaction): el proveedor firma y transmite desde el servidor.
La biblioteca también ofrece un enum Signer unificado que envuelve todas las
implementaciones y permite seleccionar en tiempo de ejecución el backend de firma. Implementa
solo el trait base; se accede a las capacidades mediante as_transaction_signer(),
as_modifying_signer() y as_sending_signer().
Lista de Verificación Rápida para la Integración
- Crear tu módulo de firmante con la implementación
- Implementa el trait base
SolanaSigner(pubkey,sign_message,is_available) más el trait de capacidad que coincida con el formato de tu proveedor (normalmenteTransactionSigner) - Agrega un feature flag en
Cargo.toml, incluido el marcador interno_remotesi tu firmante usa reqwest - Actualiza el enum
Signerensrc/lib.rs(variante, rama dedispatch_signer!y una rama en el accesor de capacidad que coincida con tu formato) - Aplicar HTTPS y configurar tiempos de espera en clientes HTTP
- Firma los bytes del mensaje y verifica la firma devuelta antes de adjuntarla
- Agrega pruebas unitarias completas (basadas en wiremock, en tu módulo)
- Agrega un archivo de pruebas de integración y actualiza
.env.example - Agrega las implementaciones en TypeScript, Python y Go
- Actualizar documentación
- Actualizaciones del flujo de trabajo de CI (coordínalas con los mantenedores, consulta Actualizaciones del Flujo de Trabajo de CI)
- Enviar PR
Paso 1: Crear tu Módulo de Firmante
Crea un nuevo directorio bajo src/ para tu implementación:
src/├── your_service/│ ├── mod.rs # Main implementation with SolanaSigner trait│ └── types.rs # API request/response types (if needed)
Paso 2: Definir tu Struct de Firmante
En src/your_service/mod.rs, define tu struct de firmante:
//! 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()}}
Paso 3: Implementar el Constructor y Métodos Auxiliares
Los firmantes remotos deben aplicar HTTPS y configurar tiempos de espera
HTTP. Usa el struct compartido HttpClientConfig para la configuración de
tiempos de espera.
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))}}
Paso 4: Implementar los Traits del Firmante
El trait base SolanaSigner tiene 2 métodos asíncronos (sign_message,
is_available) más pubkey(). La firma de transacciones va en un trait de capacidad;
la mayoría de los proveedores implementan TransactionSigner, cuyo método sign_transaction devuelve
SignTransactionResult (un enum etiquetado que indica si la transacción está firmada por completo o
de forma parcial).
Firma los bytes del mensaje, nunca la transacción completa en formato wire, y verifica la
firma del proveedor contra ellos antes de adjuntarla. Usa los helpers compartidos de
TransactionUtil para la firma y la serialización.
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),))}}
Si tu proveedor reescribe la transacción antes de firmarla, implementa
ModifyingSigner en su lugar y reemplaza por completo la transacción del llamador con
los bytes del proveedor. Si transmite desde el servidor, implementa SendingSigner
y deja intacta la transacción del llamador. Nunca implementes una capacidad que tu
proveedor no pueda ofrecer.
Paso 5: Agregar Tipos de API (Opcional)
Si tu API necesita tipos personalizados, 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,}
Paso 6: Agregar Feature Flag
Actualiza Cargo.toml para agregar tu firmante como feature opcional. Los backends remotos
incluyen el marcador interno _remote, que controla la implementación de From<reqwest::Error>
en src/error.rs; sin él, ? en las llamadas a reqwest no compilará cuando solo
esté habilitada tu feature.
[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
Paso 7: Actualizar el Enum de Firmantes
Agrega tu firmante a src/lib.rs. La implementación base de SolanaSigner despacha mediante
la macro dispatch_signer!, así que agrega una rama allí. El acceso a las capacidades reside en
los accesores as_transaction_signer(), as_modifying_signer() y as_sending_signer():
agrega una rama Some(s) al que corresponda a tu trait de capacidad (y,
para un backend de envío o de modificación, una rama None en 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),}}}
Paso 8: Agregar Pruebas Exhaustivas
Agrega pruebas a tu módulo (al final 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());}}
Cubre también el fallo de verificación de firma: una respuesta simulada cuya firma no se verifique debe devolver un error.
Paso 9: Agregar Pruebas de Integración
Crea rust/src/tests/test_<name>_integration.rs. Las pruebas de integración se ejecutan
contra la API real del servicio (no wiremock) y están protegidas por la
feature integration-tests. Cada archivo necesita:
- Declaraciones
pub constpara los nombres de las variables de entorno - Un helper
get_signer()que lee las variables de entorno mediantedotenvy - Tres funciones de prueba:
test_<name>_sign_message,test_<name>_sign_transactionytest_<name>_is_available - Feature gates:
#[cfg(feature = "your_service")]en el módulo y#[cfg(feature = "integration-tests")]en cada prueba
Consulta rust/src/tests/test_para_integration.rs para ver una referencia completa y
declara tu módulo en rust/src/tests/mod.rs:
pub mod test_your_service_integration;
Paso 10: Actualizar las Variables de Entorno
Actualiza el archivo .env.example de la raíz con las variables de entorno de tu firmante, siguiendo el
patrón 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
Enumera primero las variables obligatorias con valores de ejemplo y deja comentadas las opcionales
con sus valores predeterminados. Toda variable de entorno usada en las pruebas de integración debe aparecer en
.env.example, y las suites de TypeScript, Python y Go usan los mismos nombres.
Paso 11: Actualizar la Documentación
Agrega tu firmante a la tabla de backends compatibles en README.md:
| Backend | Caso de Uso | Feature Flag |
|---|---|---|
| Memory | Pares de claves locales, desarrollo, pruebas | memory |
| Vault | Gestión empresarial de claves con HashiCorp Vault | vault |
| Privy | Billeteras embebidas con infraestructura Privy | privy |
| Turnkey | Gestión de claves sin custodia mediante Turnkey | turnkey |
| TuServicio | Breve descripción de tu servicio | your_service |
Agrega también tu backend a la tabla de capacidades de los firmantes en el README de cada lenguaje. Agrega un ejemplo 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(())}
Probar Tu Integración
Usa las recetas de just en lugar de cargo directamente. Las features del SDK (sdk-v2,
sdk-v3, sdk-v4) son mutuamente excluyentes, por lo que cargo test --all-features
falla.
# 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
Actualizaciones del Flujo de Trabajo de CI (PRs desde Forks)
La CI es un proceso de dos fases. Coordina con los mantenedores para preparar main antes de que
tu PR pueda probarse: ellos actualizan fork-external-live-manual.yml, agregan las variables de entorno
a ci.yml y configuran los GitHub Secrets.
Tu PR debe incluir estos cambios de la Fase 2 junto con el código de tu firmante:
ci.yml: agrega tu feature de Rust a la matrizbackend(antes deall) y tu función de prueba de integración a la matriztesttypescript-ci.yml: agrega tu paquete a las matrices de pruebas unitarias y de integración, y agrega las variables de entorno al paso de pruebas de integracióntypescript-publish.yml: agrega tu paquete aPUBLISH_PACKAGES, al arraypackagesdel GitHub Release y a la tabla de resumenpython-ci.yml: agrega el backend al controlCI_SIGNER_*enresolve-signersy a la matriz de integracióngo-ci.yml: agrega el backend al controlCI_SIGNER_*y amoduleBySigner
Firmante TypeScript
Si también estás agregando un paquete de firmante TypeScript, créalo en
typescript/packages/your-signer/. Patrones clave:
- La función factory
createYourSigner()devuelveSolanaTransactionSigner<TAddress> & SolanaMessageSigner<TAddress>; mantén la clase del firmante como interna y exporta solo la factory y su tipo de configuración - Exportar interfaz de configuración (
YourSignerConfig) - Si tu backend no puede firmar mensajes, omite
SolanaMessageSignery no expongas ningún métodosignMessages: Kit clasifica los firmantes según la presencia de métodos, por lo que un stub que lance una excepción clasificaría mal al firmante - Exige HTTPS en los campos de configuración
apiBaseUrlconassertHttpsUrl()de@solana/keychain-core - Realiza las llamadas remotas mediante
fetchSignerJson()de@solana/keychain-core, que se encarga del mapeo de errores, la sanitización de respuestas, el rechazo de redirecciones y el tiempo de espera predeterminado - Sanitiza cualquier otro texto de error de la API remota con
sanitizeRemoteErrorResponse()de@solana/keychain-core - Usar
throwSignerError(SignerErrorCode.*, { cause, message })desde@solana/keychain-core - Respeta
config?.abortSignalen cada método de firma, pasándolo afetchSignerJson()ysignBatchStaggered() - Valida
requestDelayMsconvalidateRequestDelayMs()y procesa por lotes consignBatchStaggered() - Agregar
@throwsJSDoc a las funciones factory listando códigos de error - Declara los paquetes
@solana/*como peer dependencies en>=8.1.0 - Pruebas unitarias con vitest y
fetchsimulado; pruebas de integración conrunSignerIntegrationTestde@solana/keychain-test-utils
Firmantes de Envío
Si tu proveedor reescribe la transacción o la transmite desde el servidor, su
firma no puede aplicarse a la transacción del llamador, por lo que debe ser un
SolanaSendingSigner en lugar de un SolanaTransactionSigner:
- Implementa
signAndSendTransactions()y no expongassignTransactions(nisignMessages, a menos que el backend realmente firme mensajes) - Respeta
config?.abortSignal. Una vez que el proveedor haya aceptado la creación, informa cualquier fallo (incluida una cancelación) comoBROADCAST_UNCONFIRMEDcon el id de transacción del proveedor, conservando el motivo de la cancelación comocause - Agrega una sobrecarga tipada de
createKeychainSignerque devuelva el tipo de tu firmante de envío - Excluye tu backend de
KeychainKitPluginConfigentypescript/packages/kit-plugin/src/keychain-plugin.ts, ya que un firmante de envío no puede servir comopayeroidentityde un cliente de Kit - Verifica en las pruebas unitarias que
isTransactionPartialSigner()sea false y queisTransactionSendingSigner()sea true para tus instancias
Actualizar Paquete Umbrella
Actualiza typescript/packages/keychain/ (7 archivos por modificar):
src/types.ts: agregaYourSignerConfiga la unión discriminadaKeychainSignerConfigsrc/create-keychain-signer.ts: agrega el caso del switch (cada paquete de backend se carga con unimport()dinámico)src/resolve-address.ts: agrégalo al caso del switch de ruta rápida o de ruta de obtenciónsrc/index.ts: agrega las exportaciones del tipo de configuración, el namespace y la función factorypackage.json: agrega la dependencia@solana/keychain-your-signer: "workspace:*"tsconfig.json: agrega la referencia{ "path": "../your-signer" }typescript/scripts/test-treeshake-umbrella.mjs: agrega tu paquete aSIGNER_MARKERSy tu factory aFACTORIES
Los switch tienen comprobaciones exhaustivas con never, por lo que TypeScript mostrará un error
si agregas a la unión pero omites un caso. Después, ejecuta just ts-treeshake:
cada paquete y el paquete umbrella deben seguir siendo compatibles con tree-shaking.
Firmante de Python
Crea el módulo en python/src/solana_keychain/your_signer/ (__init__.py
más signer.py). Patrones clave:
- Crea una subclase de
SolanaSignerdesolana_keychain.core(propiedadpubkey,async sign_message(),async is_available()) más exactamente una clase de capacidad:TransactionSigner(async sign_transaction()),ModifyingSigner(async modify_and_sign_transaction()) oSendingSigner(async sign_and_send_transaction()) - Exporta una factory
async create_your_signer()que haga await deinit()cuando el backend resuelva su dirección de forma remota; antes de init, la firma lanzaSignerErrorCode.NOT_INITIALIZED - Exporta una dataclass de configuración (
YourSignerConfig) con los secretos marcados confield(repr=False) - Exige HTTPS en los campos de URL base con
assert_https_url()y enruta cada solicitud mediantefetch_signer_json() - Verifica la firma del proveedor antes de devolverla; usa
signed_message_bytes()para los bytes que cubre una firma, nunca un prefijo de versión improvisado - Registra el backend en
_BACKENDSenpython/src/solana_keychain/keychain.py - Si el backend necesita un SDK del proveedor, protege la importación, lanza un
ImportErrorque indiquepip install 'solana-keychain[your-signer]', declara el extra enpython/pyproject.tomly no importes nunca el backend de forma anticipada desde la raíz del paquete - Pruebas unitarias con pytest:
respxpara backends HTTP, clientes simulados para backends de SDK y una comprobación de que ningún secreto aparece enstr()ni enrepr()de los errores lanzados - Añade una factoría
make_your_signer()apython/tests/integration/test_live_signers.pyy regístrala en_MESSAGE_CAPABLE(o en_TRANSACTION_ONLYpara backends sinsign_message)
Firmante en Go
Cada backend de Go es su propio módulo, en go/signers/yoursigner/ con la ruta de módulo
github.com/solana-foundation/solana-keychain/go/signers/yoursigner/v2. Consulta
go/signers/para/ como referencia. Patrones clave:
- Implementa
core.SolanaSigner(Pubkey(),SignMessage(ctx, message),IsAvailable(ctx)) más exactamente una interfaz de capacidad:core.TransactionSigner,core.ModifyingSignerocore.SendingSigner. La capacidad se detecta mediante aserción de tipo, así que un backend que no puede firmar una transacción no debe tener ningún métodoSignTransaction - Exporta una estructura
Configy un constructorNew(New(ctx, cfg)cuando el backend resuelve su dirección de forma remota; antes de eso, devuelvecore.NewNotInitializedError) - Crea los clientes HTTP con
core.NewHTTPClientocore.ResolveHTTPClienty valida las URL base concore.NormalizeHTTPSBaseURL - Firma las transacciones con
core.SignTransactionWith, que firma los bytes del mensaje y adjunta la firma; verifica las firmas del proveedor concore.VerifySignature - Devuelve los errores con
core.NewSignerError/core.WrapSignerErrory códigos estables comocore.CodeSigningFailed - Pruebas unitarias con
httptest; pruebas de integración enintegration_test.gotras la etiqueta//go:build integration
Lista de Verificación para Envío
Antes de enviar tu PR:
- El código compila sin advertencias (
just build) - Todas las pruebas pasan (
just test) - El código está formateado/el linting pasa (
just fmt) - No hay valores hardcodeados ni secretos en el código
- Los mensajes de error son genéricos (sin texto de respuesta de API en bruto)
- Sin
.expect()ni.unwrap()en respuestas de API no confiables - HTTPS forzado en clientes HTTP remotos
- Timeouts HTTP configurados mediante
HttpClientConfig - Las firmas devueltas se verifican con los bytes del mensaje calculados localmente
- Sigue las convenciones de nomenclatura (snake_case para Rust y Python, camelCase para TypeScript)
- Pruebas de integración añadidas y
.env.exampleactualizado - Implementaciones de TypeScript, Python y Go incluidas (o planificadas con los mantenedores)
- Paquetes paraguas actualizados (paquete paraguas de TypeScript,
_BACKENDSde Python) - Cambios de CI incluidos
- Añadido a las tablas de backends compatibles y de capacidades de firmantes del README.md
Consejos de Implementación
Manejo de Errores
Usa siempre las variantes existentes de SignerError. Tanto Display como Debug en
SignerError están redactados, así que mantén los mensajes genéricos y no incluyas nunca datos
sin procesar de las respuestas de la API. No uses nunca .expect() ni .unwrap() en respuestas de API no confiables:
// 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 seguridad
No son sugerencias: los consumidores eligen un backend basándose en ellos. Si
tu proveedor hace imposible alguno, indícalo en el PR y en el archivo
docs/SECURITY_MODEL.md del repositorio.
- Verifica lo que firmó el proveedor. Calcula los bytes del mensaje localmente y verifica con ellos la firma devuelta, en la posición que requiere tu firmante, antes de adjuntarla. Una firma que no se verifique debe provocar un error y nunca adjuntarse.
- Declara si el proveedor difunde la transacción. Si la ejecuta en el servidor, implementa la variante de envío y devuelve la firma que identifica la transacción confirmada. Si el proveedor no tiene un endpoint de solo firma, haz que el backend sea solo de envío en lugar de enviar a espaldas de quien llama.
- No dejes nunca a quien llama con una transacción que la firma no cubre. Un proveedor que reescribe la transacción firma sus propios bytes: o bien deja intacta la transacción de quien llama (el caso en que la difusión la gestiona el proveedor), o bien adopta la variante de modificación y sustitúyela por completo con los bytes del proveedor.
- Informa de la incertidumbre de la difusión como tal. Un fallo posterior al envío puede
haberse confirmado igualmente. Devuelve
BROADCAST_UNCONFIRMEDen lugar de un error genérico, incluye el ID de transacción del proveedor siempre que lo tengas y envía una clave de idempotencia derivada del mensaje en cada creación, de modo que reenviar los mismos bytes sea seguro. - No firmes nunca antes de inicializar. Usa
Option<Pubkey>(noPubkey::default()) para la clave pública antes deinit()y falla con un error de configuración si se intenta firmar antes. - No registres nunca datos sensibles (claves privadas, secretos de API, respuestas de API sin procesar) y
usa una implementación de
Debugque oculte los campos sensibles - Exige HTTPS (
https_only(true), condicionado con#[cfg(not(test))]para wiremock) y configura los tiempos de espera de solicitud y de conexión medianteHttpClientConfig(valores predeterminados: 30 s/5 s) - Borra de la memoria el material de claves intermedio con
zeroize::Zeroizing<Vec<u8>>
Pruebas con Mocks
Utiliza wiremock para simular APIs HTTP. Verifica solo el tipo de error, no el
texto del mensaje de error:
#[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}}
Obtener Ayuda
- Revisa las implementaciones de firmantes existentes para encontrar patrones:
src/memory/mod.rs: simple y síncronosrc/privy/mod.rs: requiere inicializaciónsrc/turnkey/mod.rs: gestión compleja de firmassrc/vault/mod.rs: biblioteca cliente externatypescript/packages/para/,python/src/solana_keychain/para/ygo/signers/para/: referencias completas en los otros lenguajes
- Archivos clave:
src/traits.rs(definiciones de traits),src/transaction_util.rsysrc/signature_util.rs(utilidades compartidas),src/http_client_config.rs(configuración de tiempos de espera) - La guía completa del repositorio: docs/ADDING_SIGNERS.md
- Abre un issue para discusiones de diseño antes de comenzar a trabajar
Estructura de PR de Ejemplo
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?