Añadir Nuevos Firmantes

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 (normalmente TransactionSigner)
  • Agrega un feature flag en Cargo.toml, incluido el marcador interno _remote si tu firmante usa reqwest
  • Actualiza el enum Signer en src/lib.rs (variante, rama de dispatch_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 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()
}
}

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

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

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 const para los nombres de las variables de entorno
  • Un helper get_signer() que lee las variables de entorno mediante dotenvy
  • Tres funciones de prueba: test_<name>_sign_message, test_<name>_sign_transaction y test_<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-key
YOUR_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:

BackendCaso de UsoFeature Flag
MemoryPares de claves locales, desarrollo, pruebasmemory
VaultGestión empresarial de claves con HashiCorp Vaultvault
PrivyBilleteras embebidas con infraestructura Privyprivy
TurnkeyGestión de claves sin custodia mediante Turnkeyturnkey
TuServicioBreve descripción de tu servicioyour_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 matrices
just 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 matriz backend (antes de all) y tu función de prueba de integración a la matriz test
  • typescript-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ón
  • typescript-publish.yml: agrega tu paquete a PUBLISH_PACKAGES, al array packages del GitHub Release y a la tabla de resumen
  • python-ci.yml: agrega el backend al control CI_SIGNER_* en resolve-signers y a la matriz de integración
  • go-ci.yml: agrega el backend al control CI_SIGNER_* y a moduleBySigner

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() devuelve SolanaTransactionSigner<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 SolanaMessageSigner y no expongas ningún método signMessages: 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 apiBaseUrl con assertHttpsUrl() 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?.abortSignal en cada método de firma, pasándolo a fetchSignerJson() y signBatchStaggered()
  • Valida requestDelayMs con validateRequestDelayMs() y procesa por lotes con signBatchStaggered()
  • Agregar @throws JSDoc 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 fetch simulado; pruebas de integración con runSignerIntegrationTest de @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 expongas signTransactions (ni signMessages, 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) como BROADCAST_UNCONFIRMED con el id de transacción del proveedor, conservando el motivo de la cancelación como cause
  • Agrega una sobrecarga tipada de createKeychainSigner que devuelva el tipo de tu firmante de envío
  • Excluye tu backend de KeychainKitPluginConfig en typescript/packages/kit-plugin/src/keychain-plugin.ts, ya que un firmante de envío no puede servir como payer o identity de un cliente de Kit
  • Verifica en las pruebas unitarias que isTransactionPartialSigner() sea false y que isTransactionSendingSigner() sea true para tus instancias

Actualizar Paquete Umbrella

Actualiza typescript/packages/keychain/ (7 archivos por modificar):

  1. src/types.ts: agrega YourSignerConfig a la unión discriminada KeychainSignerConfig
  2. src/create-keychain-signer.ts: agrega el caso del switch (cada paquete de backend se carga con un import() dinámico)
  3. src/resolve-address.ts: agrégalo al caso del switch de ruta rápida o de ruta de obtención
  4. src/index.ts: agrega las exportaciones del tipo de configuración, el namespace y la función factory
  5. package.json: agrega la dependencia @solana/keychain-your-signer: "workspace:*"
  6. tsconfig.json: agrega la referencia { "path": "../your-signer" }
  7. typescript/scripts/test-treeshake-umbrella.mjs: agrega tu paquete a SIGNER_MARKERS y tu factory a FACTORIES

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 SolanaSigner de solana_keychain.core (propiedad pubkey, async sign_message(), async is_available()) más exactamente una clase de capacidad: TransactionSigner (async sign_transaction()), ModifyingSigner (async modify_and_sign_transaction()) o SendingSigner (async sign_and_send_transaction())
  • Exporta una factory async create_your_signer() que haga await de init() cuando el backend resuelva su dirección de forma remota; antes de init, la firma lanza SignerErrorCode.NOT_INITIALIZED
  • Exporta una dataclass de configuración (YourSignerConfig) con los secretos marcados con field(repr=False)
  • Exige HTTPS en los campos de URL base con assert_https_url() y enruta cada solicitud mediante fetch_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 _BACKENDS en python/src/solana_keychain/keychain.py
  • Si el backend necesita un SDK del proveedor, protege la importación, lanza un ImportError que indique pip install 'solana-keychain[your-signer]', declara el extra en python/pyproject.toml y no importes nunca el backend de forma anticipada desde la raíz del paquete
  • Pruebas unitarias con pytest: respx para backends HTTP, clientes simulados para backends de SDK y una comprobación de que ningún secreto aparece en str() ni en repr() de los errores lanzados
  • Añade una factoría make_your_signer() a python/tests/integration/test_live_signers.py y regístrala en _MESSAGE_CAPABLE (o en _TRANSACTION_ONLY para backends sin sign_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.ModifyingSigner o core.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étodo SignTransaction
  • Exporta una estructura Config y un constructor New (New(ctx, cfg) cuando el backend resuelve su dirección de forma remota; antes de eso, devuelve core.NewNotInitializedError)
  • Crea los clientes HTTP con core.NewHTTPClient o core.ResolveHTTPClient y valida las URL base con core.NormalizeHTTPSBaseURL
  • Firma las transacciones con core.SignTransactionWith, que firma los bytes del mensaje y adjunta la firma; verifica las firmas del proveedor con core.VerifySignature
  • Devuelve los errores con core.NewSignerError / core.WrapSignerError y códigos estables como core.CodeSigningFailed
  • Pruebas unitarias con httptest; pruebas de integración en integration_test.go tras 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.example actualizado
  • Implementaciones de TypeScript, Python y Go incluidas (o planificadas con los mantenedores)
  • Paquetes paraguas actualizados (paquete paraguas de TypeScript, _BACKENDS de 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 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 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_UNCONFIRMED en 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> (no Pubkey::default()) para la clave pública antes de init() 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 Debug que 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 mediante HttpClientConfig (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íncrono
    • src/privy/mod.rs: requiere inicialización
    • src/turnkey/mod.rs: gestión compleja de firmas
    • src/vault/mod.rs: biblioteca cliente externa
    • typescript/packages/para/, python/src/solana_keychain/para/ y go/signers/para/: referencias completas en los otros lenguajes
  • Archivos clave: src/traits.rs (definiciones de traits), src/transaction_util.rs y src/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 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?