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
Signerdanssrc/lib.rs(4 branches match) - Mettre à jour la porte cfg de l'impl
Fromreqwestsrc/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 integrationuse 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 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::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 positionTransactionUtil::add_signature_to_transaction(tx, &self.public_key, signature)?;// Serialize and classify as Complete or Partiallet 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 featureall = ["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 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 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 :
| Backend | Cas d'usage | Indicateur de fonctionnalité |
|---|---|---|
| Memory | Paires de clés locales, développement, tests | memory |
| Vault | Gestion de clés d'entreprise avec HashiCorp Vault | vault |
| Privy | Portefeuilles intégrés avec l'infrastructure Privy | privy |
| Turnkey | Gestion de clés non-custodiale via Turnkey | turnkey |
| YourService | Brève description de votre service | your_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 signercargo test --features your_service# Test with all featurescargo 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()retourneSolanaSigner<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
@throwsaux fonctions factory listant les codes d'erreur
Mettre à jour le package umbrella
Mettre à jour typescript/packages/keychain/ — 6 fichiers à modifier :
src/types.ts— AjouterYourSignerConfigà l'union discriminéeKeychainSignerConfigsrc/create-keychain-signer.ts— Importer la factory, ajouter le cas switchsrc/resolve-address.ts— Ajouter au cas switch fast-path ou fetch-pathsrc/index.ts— Ajouter les exports du type de configuration, du namespace, de la fonction factory et de la classepackage.json— Ajouter la dépendance@solana/keychain-your-signer: "workspace:*"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 messagesreturn Err(SignerError::RemoteApiError(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}")))?;
Bonnes pratiques de sécurité
- Ne jamais journaliser de données sensibles (clés privées, secrets API)
- Utiliser l'implémentation
Debugqui 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 nonPubkey::default()) pour le champ de clé publique avantinit()
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, synchronesrc/para/mod.rs— Nécessite une initialisation (à utiliser comme modèle pour les nouveaux signataires)src/turnkey/mod.rs— Gestion complexe des signaturessrc/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 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 trait for YourServiceSigner- [X] Added feature flag 'your_service'- [X] HTTPS enforced, HTTP timeouts configured- [X] Added to README.md supported backends tableCloses #1337
Is this page helpful?