Ajout de nouveaux signataires

Ce guide s'adresse aux fournisseurs de services de portefeuille et aux développeurs qui souhaitent intégrer de nouvelles solutions de gestion de clés dans la bibliothèque solana-keychain. En ajoutant votre implémentation de signataire, vous permettrez aux développeurs d'utiliser votre service pour la signature sécurisée de transactions Solana via une interface unifiée.

Vous utilisez un LLM ? Consultez la Compétence Ajout de Signataires.

Présentation de l'architecture

La bibliothèque utilise une architecture basée sur les traits où tous les signataires implémentent le trait SolanaSigner défini dans src/traits.rs. La bibliothèque fournit également une énumération unifiée Signer qui encapsule toutes les implémentations, permettant la sélection à l'exécution des backends de signature tout en maintenant une API cohérente.

Liste de contrôle pour l'intégration rapide

  • Créer votre module de signataire avec implémentation
  • Implémenter le trait SolanaSigner (3 méthodes async + pubkey())
  • Ajouter un indicateur de fonctionnalité dans Cargo.toml
  • Mettre à jour l'énumération Signer dans src/lib.rs (4 branches match)
  • Mettre à jour la porte cfg de l'impl From reqwest src/error.rs (si votre signataire utilise reqwest)
  • Imposer HTTPS et configurer les délais d'expiration sur les clients HTTP
  • Ajouter des tests complets
  • Mettre à jour la documentation
  • Soumettre une PR

Étape 1 : Créer votre module de signataire

Créez un nouveau répertoire sous src/ pour votre implémentation :

src/
├── your_service/
│ ├── mod.rs # Main implementation with SolanaSigner trait
│ └── types.rs # API request/response types (if needed)

Étape 2 : Définir votre structure de signataire

Dans src/your_service/mod.rs, définissez votre structure de signataire :

//! 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()
}
}

Étape 3 : Implémenter le constructeur et les méthodes auxiliaires

Les signataires distants doivent imposer HTTPS et configurer les délais d'expiration HTTP. Utilisez la structure partagée HttpClientConfig pour les paramètres de délai d'expiration.

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

Étape 4 : Implémenter le trait SolanaSigner

Le trait comporte 3 méthodes async (sign_transaction, sign_message, is_available) plus pubkey(). Notez que sign_transaction renvoie SignTransactionResult — une énumération étiquetée indiquant si la transaction est entièrement signée ou partiellement signée.

Utilisez les helpers partagés TransactionUtil pour la signature et la sérialisation.

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

Étape 5 : Ajouter des types d'API (facultatif)

Si votre API nécessite des types personnalisés, créez 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,
}

Étape 6 : Ajouter un indicateur de fonctionnalité

Mettez à jour Cargo.toml pour ajouter votre signataire en tant que fonctionnalité facultative :

[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

Étape 7 : Mettre à jour l'énumération Signer

Ajoutez votre signataire à src/lib.rs. Vous avez besoin de 4 branches de correspondance dans l'implémentation SolanaSigner : pubkey, sign_transaction, sign_message et 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,
}
}
}

Si votre signataire utilise reqwest, ajoutez votre fonctionnalité à la porte #[cfg(any(...))] sur l'implémentation From<reqwest::Error> dans src/error.rs.

Étape 8 : Ajouter des tests complets

Ajoutez des tests à votre module (en bas 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());
}
}

Étape 9 : Mettre à jour la documentation

Ajoutez votre signataire au tableau des backends pris en charge dans README.md :

BackendCas d'usageIndicateur de fonctionnalité
MemoryPaires de clés locales, développement, testsmemory
VaultGestion de clés d'entreprise avec HashiCorp Vaultvault
PrivyPortefeuilles intégrés avec l'infrastructure Privyprivy
TurnkeyGestion de clés non-custodiale via Turnkeyturnkey
YourServiceBrève description de votre serviceyour_service

Ajoutez un exemple d'utilisation :

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

Tester votre intégration

Exécutez les tests pour votre fonctionnalité :

# Test only your signer
cargo test --features your_service
# Test with all features
cargo test --all-features

Signataire TypeScript

Si vous ajoutez également un package de signataire TypeScript, créez-le à typescript/packages/your-signer/. Modèles clés :

  • La fonction factory createYourSigner() retourne SolanaSigner<TAddress>
  • Exporter l'interface de configuration (YourSignerConfig)
  • Imposer HTTPS sur les champs de configuration apiBaseUrl
  • Assainir le texte d'erreur de l'API distante avec sanitizeRemoteErrorResponse() depuis @solana/keychain-core
  • Se protéger contre le JSON mal formé avec le chaînage optionnel et try/catch
  • Utiliser throwSignerError(SignerErrorCode.*, { cause, message }) depuis @solana/keychain-core
  • Ajouter la documentation JSDoc @throws aux fonctions factory listant les codes d'erreur

Mettre à jour le package umbrella

Mettre à jour typescript/packages/keychain/ — 6 fichiers à modifier :

  1. src/types.ts — Ajouter YourSignerConfig à l'union discriminée KeychainSignerConfig
  2. src/create-keychain-signer.ts — Importer la factory, ajouter le cas switch
  3. src/resolve-address.ts — Ajouter au cas switch fast-path ou fetch-path
  4. src/index.ts — Ajouter les exports du type de configuration, du namespace, de la fonction factory et de la classe
  5. package.json — Ajouter la dépendance @solana/keychain-your-signer: "workspace:*"
  6. tsconfig.json — Ajouter la référence { "path": "../your-signer" }

Les instructions switch ont des vérifications exhaustives never — TypeScript générera une erreur si vous ajoutez à l'union mais oubliez un cas.

Liste de vérification pour la soumission

Avant de soumettre votre PR :

  • Le code compile sans avertissements (just build)
  • Tous les tests passent (just test)
  • Le code est formaté/le linting passe (just fmt)
  • Aucune valeur codée en dur ni secret dans le code
  • Les messages d'erreur sont génériques (pas de texte brut de réponse API)
  • HTTPS imposé sur les clients HTTP distants
  • Délais d'expiration HTTP configurés via HttpClientConfig
  • Respecte les conventions de nommage Rust (snake_case)
  • Ajouté au tableau des backends pris en charge dans README.md

Conseils d'implémentation

Gestion des erreurs

Utilisez toujours les variantes SignerError existantes. N'utilisez jamais .expect() ou .unwrap() sur des réponses API non fiables :

// 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}")))?;

Bonnes pratiques de sécurité

  • Ne jamais journaliser de données sensibles (clés privées, secrets API)
  • Utiliser l'implémentation Debug qui masque les champs sensibles
  • Valider toutes les entrées (clés publiques, signatures)
  • Utiliser HTTPS pour tous les appels API distants (imposé via https_only(true))
  • Configurer les délais d'expiration de requête et de connexion via HttpClientConfig
  • Ne jamais exposer le texte d'erreur brut de l'API distante dans les messages d'erreur
  • Utiliser Option<Pubkey> (et non Pubkey::default()) pour le champ de clé publique avant init()

Tests avec des simulacres (mocks)

Utilisez wiremock pour simuler les API HTTP. Vérifiez uniquement le type d'erreur, pas le texte du message d'erreur :

#[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
}
}

Obtenir de l'aide

  • Consultez les implémentations de signataires existantes pour identifier les modèles :
    • src/memory/mod.rs — Simple, synchrone
    • src/para/mod.rs — Nécessite une initialisation (à utiliser comme modèle pour les nouveaux signataires)
    • src/turnkey/mod.rs — Gestion complexe des signatures
    • src/vault/mod.rs — Bibliothèque cliente externe
  • Fichiers clés : src/traits.rs (définition du trait), src/transaction_util.rs (fonctions d'aide partagées), src/http_client_config.rs (configuration du délai d'attente)
  • Ouvrez une issue pour discuter de la conception avant de commencer le travail

Exemple de structure 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?

Géré par

© 2026 Fondation Solana.
Tous droits réservés.
Restez connecté