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.
Keychain maintient ses implémentations Rust, TypeScript, Python et Go à parité. Les PR qui incluent les quatre sont fortement privilégiées. Si vous ne pouvez en fournir qu'une, ce n'est pas un problème, mais attendez-vous à ce que les autres soient exigées avant que le signataire ne soit livré dans une version.
Vous utilisez Claude Code ? La
compétence add-signer-ci
du dépôt configure les workflows CI pour un nouveau signataire proposé via une PR depuis un fork.
Présentation de l'architecture
La bibliothèque utilise une architecture basée sur les traits, définie dans src/traits.rs. Chaque
signataire implémente le trait de base SolanaSigner (pubkey, sign_message,
is_available) ainsi qu'exactement un trait de capacité correspondant au fonctionnement du fournisseur :
TransactionSigner(sign_transaction) : le fournisseur signe les octets de l'appelant et l'appelant diffuse la transaction. La plupart des backends.ModifyingSigner(modify_and_sign_transaction) : le fournisseur réécrit la transaction, puis la signe sans la diffuser.SendingSigner(sign_and_send_transaction) : le fournisseur signe et diffuse côté serveur.
La bibliothèque fournit également une énumération Signer unifiée qui encapsule toutes les
implémentations, permettant de sélectionner le backend de signature à l'exécution. Elle n'implémente
que le trait de base ; les capacités sont accessibles via as_transaction_signer(),
as_modifying_signer() et as_sending_signer().
Liste de contrôle pour l'intégration rapide
- Créer votre module de signataire avec implémentation
- Implémenter le trait de base
SolanaSigner(pubkey,sign_message,is_available) ainsi que le trait de capacité correspondant au fonctionnement de votre fournisseur (généralementTransactionSigner) - Ajouter un indicateur de fonctionnalité dans
Cargo.toml, en incluant le marqueur interne_remotesi votre signataire utilise reqwest - Mettre à jour l'énumération
Signerdanssrc/lib.rs(variante, branchedispatch_signer!et une branche dans l'accesseur de capacité correspondant à votre cas) - Imposer HTTPS et configurer les délais d'expiration sur les clients HTTP
- Signer les octets du message et vérifier la signature retournée avant de l'attacher
- Ajouter des tests unitaires complets (basés sur wiremock, dans votre module)
- Ajouter un fichier de tests d'intégration et mettre à jour
.env.example - Ajouter les implémentations TypeScript, Python et Go
- Mettre à jour la documentation
- Mises à jour des workflows CI (à coordonner avec les mainteneurs, voir Mises à jour des workflows CI)
- 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::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()}}
É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::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))}}
Étape 4 : Implémenter les traits Signer
Le trait de base SolanaSigner comporte 2 méthodes asynchrones (sign_message,
is_available) ainsi que pubkey(). La signature des transactions se trouve dans un trait de capacité ;
la plupart des fournisseurs implémentent TransactionSigner, dont sign_transaction retourne
SignTransactionResult (une énumération étiquetée indiquant que la transaction est entièrement ou
partiellement signée).
Signez les octets du message, jamais la transaction complète au format wire, et vérifiez la
signature du fournisseur par rapport à ces octets avant de l'attacher. Utilisez les utilitaires partagés
TransactionUtil pour la signature et la sérialisation.
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 votre fournisseur réécrit la transaction avant de la signer, implémentez
ModifyingSigner à la place et remplacez intégralement la transaction de l'appelant par
les octets du fournisseur. S'il diffuse côté serveur, implémentez SendingSigner
et laissez la transaction de l'appelant intacte. N'implémentez jamais une capacité que votre
fournisseur ne peut pas assurer.
É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. Les backends distants
incluent le marqueur interne _remote, qui conditionne l'implémentation From<reqwest::Error>
dans src/error.rs ; sans lui, ? sur les appels reqwest ne compilera pas lorsque seule
votre fonctionnalité est activée.
[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
Étape 7 : Mettre à jour l'énumération Signer
Ajoutez votre signataire dans src/lib.rs. L'implémentation de base SolanaSigner délègue via
la macro dispatch_signer!, ajoutez donc une branche à cet endroit. L'accès aux capacités se trouve dans
les accesseurs as_transaction_signer(), as_modifying_signer() et as_sending_signer() :
ajoutez une branche Some(s) à celui qui correspond à votre trait de capacité (et,
pour un backend d'envoi ou de modification, une branche None dans 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),}}}
É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());}}
Couvrez également l'échec de vérification de la signature : une réponse simulée dont la signature n'est pas valide doit retourner une erreur.
Étape 9 : Ajouter des tests d'intégration
Créez rust/src/tests/test_<name>_integration.rs. Les tests d'intégration s'exécutent
contre l'API réelle du service (et non wiremock) et sont conditionnés par la
fonctionnalité integration-tests. Chaque fichier doit contenir :
- Des déclarations
pub constpour les noms des variables d'environnement - Une fonction utilitaire
get_signer()qui lit les variables d'environnement viadotenvy - Trois fonctions de test :
test_<name>_sign_message,test_<name>_sign_transactionettest_<name>_is_available - Conditions de fonctionnalité :
#[cfg(feature = "your_service")]sur le module et#[cfg(feature = "integration-tests")]sur chaque test
Consultez rust/src/tests/test_para_integration.rs pour une référence complète, et
déclarez votre module dans rust/src/tests/mod.rs :
pub mod test_your_service_integration;
Étape 10 : Mettre à jour les variables d'environnement
Mettez à jour le fichier .env.example à la racine avec les variables d'environnement de votre signataire, en suivant
le modèle existant :
# 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
Listez d'abord les variables obligatoires avec des valeurs d'exemple, puis les variables facultatives en commentaire
avec leurs valeurs par défaut. Chaque variable d'environnement utilisée dans les tests d'intégration doit figurer dans
.env.example, et les suites TypeScript, Python et Go utilisent les mêmes noms.
Étape 11 : 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 également votre backend au tableau des capacités des signataires dans le README de chaque langage. 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
Utilisez les recettes just plutôt que cargo directement. Les fonctionnalités du SDK (sdk-v2,
sdk-v3, sdk-v4) sont mutuellement exclusives, donc cargo test --all-features
échoue.
# 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
Mises à jour des workflows CI (PR depuis un fork)
La CI se déroule en deux phases. Coordonnez-vous avec les mainteneurs pour préparer main avant
que votre PR puisse être testée : ils mettent à jour fork-external-live-manual.yml, ajoutent les variables d'environnement
à ci.yml et configurent les secrets GitHub.
Votre PR doit inclure ces modifications de la phase 2 en plus du code de votre signataire :
ci.yml: ajoutez votre fonctionnalité Rust à la matricebackend(avantall) et votre fonction de test d'intégration à la matricetesttypescript-ci.yml: ajoutez votre package aux matrices de tests unitaires et d'intégration, et ajoutez les variables d'environnement à l'étape des tests d'intégrationtypescript-publish.yml: ajoutez votre package àPUBLISH_PACKAGES, au tableaupackagesde la GitHub Release et au tableau récapitulatifpython-ci.yml: ajoutez le backend à la conditionCI_SIGNER_*dansresolve-signerset à la matrice d'intégrationgo-ci.yml: ajoutez le backend à la conditionCI_SIGNER_*et àmoduleBySigner
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()retourneSolanaTransactionSigner<TAddress> & SolanaMessageSigner<TAddress>; gardez la classe du signataire interne et n'exportez que la factory et son type de configuration - Exporter l'interface de configuration (
YourSignerConfig) - Si votre backend ne peut pas signer de messages, omettez
SolanaMessageSigneret n'exposez aucune méthodesignMessages: Kit classe les signataires selon la présence des méthodes, donc un stub qui lève une erreur entraînerait une mauvaise classification du signataire - Imposez HTTPS sur les champs de configuration
apiBaseUrlavecassertHttpsUrl()de@solana/keychain-core - Effectuez les appels distants via
fetchSignerJson()de@solana/keychain-core, qui gère le mappage des erreurs, l'assainissement des réponses, le rejet des redirections et le délai d'expiration par défaut - Assainissez tout autre texte d'erreur d'API distante avec
sanitizeRemoteErrorResponse()de@solana/keychain-core - Utiliser
throwSignerError(SignerErrorCode.*, { cause, message })depuis@solana/keychain-core - Respectez
config?.abortSignaldans chaque méthode de signature, en le transmettant àfetchSignerJson()et àsignBatchStaggered() - Validez
requestDelayMsavecvalidateRequestDelayMs()et traitez par lots avecsignBatchStaggered() - Ajouter la documentation JSDoc
@throwsaux fonctions factory listant les codes d'erreur - Déclarez les packages
@solana/*comme dépendances peer en>=8.1.0 - Tests unitaires avec vitest et
fetchsimulé ; tests d'intégration avecrunSignerIntegrationTestde@solana/keychain-test-utils
Signataires d'envoi
Si votre fournisseur réécrit la transaction ou la diffuse côté serveur, sa
signature ne peut pas être appliquée à la transaction de l'appelant ; il doit donc être un
SolanaSendingSigner plutôt qu'un SolanaTransactionSigner :
- Implémentez
signAndSendTransactions()et n'exposez aucune méthodesignTransactions(nisignMessages, sauf si le backend signe réellement des messages) - Respectez
config?.abortSignal. Une fois que le fournisseur a accepté la création, signalez toute défaillance (y compris une annulation) parBROADCAST_UNCONFIRMEDen incluant l'identifiant de transaction du fournisseur, la raison de l'annulation étant conservée danscause - Ajoutez une surcharge typée de
createKeychainSignerretournant le type de votre signataire d'envoi - Excluez votre backend de
KeychainKitPluginConfigdanstypescript/packages/kit-plugin/src/keychain-plugin.ts, car un signataire d'envoi ne peut pas servir depayerou d'identitypour un client Kit - Vérifiez dans les tests unitaires que
isTransactionPartialSigner()retourne false et queisTransactionSendingSigner()retourne true pour vos instances
Mettre à jour le package umbrella
Mettez à jour typescript/packages/keychain/ (7 fichiers à modifier) :
src/types.ts: ajoutezYourSignerConfigà l'union discriminéeKeychainSignerConfigsrc/create-keychain-signer.ts: ajoutez le cas du switch (chaque package de backend est chargé via unimport()dynamique)src/resolve-address.ts: ajoutez-le au cas du switch du chemin rapide ou du chemin avec requêtesrc/index.ts: ajoutez les exports du type de configuration, du namespace et de la fonction factorypackage.json: ajoutez la dépendance@solana/keychain-your-signer: "workspace:*"tsconfig.json: ajoutez la référence{ "path": "../your-signer" }typescript/scripts/test-treeshake-umbrella.mjs: ajoutez votre package àSIGNER_MARKERSet votre factory àFACTORIES
Les instructions switch comportent des vérifications never exhaustives, donc TypeScript signalera une erreur
si vous ajoutez un élément à l'union sans traiter le cas correspondant. Exécutez ensuite just ts-treeshake :
chaque package et le package parapluie doivent rester compatibles avec le tree-shaking.
Signataire Python
Créez le module dans python/src/solana_keychain/your_signer/ (__init__.py
et signer.py). Principaux modèles :
- Héritez de
SolanaSignerdepuissolana_keychain.core(propriétépubkey,async sign_message(),async is_available()) ainsi que d'exactement une classe de capacité :TransactionSigner(async sign_transaction()),ModifyingSigner(async modify_and_sign_transaction()) ouSendingSigner(async sign_and_send_transaction()) - Exportez une factory
async create_your_signer()qui attendinit()lorsque le backend résout son adresse à distance ; avant l'initialisation, la signature lèveSignerErrorCode.NOT_INITIALIZED - Exportez une dataclass de configuration (
YourSignerConfig) avec les secrets marquésfield(repr=False) - Imposez HTTPS sur les champs d'URL de base avec
assert_https_url(), et faites passer chaque requête parfetch_signer_json() - Vérifiez la signature du fournisseur avant de la retourner ; utilisez
signed_message_bytes()pour obtenir les octets couverts par une signature, jamais un préfixe de version écrit à la main - Enregistrez le backend dans
_BACKENDSdanspython/src/solana_keychain/keychain.py - Si le backend nécessite un SDK de fournisseur, protégez l'import, levez une
ImportErrorindiquantpip install 'solana-keychain[your-signer]', déclarez l'extra danspython/pyproject.toml, et n'importez jamais le backend de manière anticipée depuis la racine du package - Tests unitaires avec pytest :
respxpour les backends HTTP, clients factices pour les backends SDK, et une vérification qu'aucun secret n'apparaît dansstr()ourepr()des erreurs levées - Ajoutez une fabrique
make_your_signer()àpython/tests/integration/test_live_signers.pyet enregistrez-la dans_MESSAGE_CAPABLE(ou_TRANSACTION_ONLYpour les backends sanssign_message)
Signer Go
Chaque backend Go est son propre module, dans go/signers/yoursigner/ avec le chemin de module
github.com/solana-foundation/solana-keychain/go/signers/yoursigner/v2. Consultez
go/signers/para/ comme référence. Principaux schémas :
- Implémentez
core.SolanaSigner(Pubkey(),SignMessage(ctx, message),IsAvailable(ctx)) ainsi qu'une seule interface de capacité :core.TransactionSigner,core.ModifyingSigneroucore.SendingSigner. La capacité est détectée par assertion de type : un backend qui ne peut pas signer une transaction ne doit donc comporter aucune méthodeSignTransaction - Exportez une structure
Configet un constructeurNew(New(ctx, cfg)lorsque le backend résout son adresse à distance ; avant cela, renvoyezcore.NewNotInitializedError) - Construisez les clients HTTP avec
core.NewHTTPClientoucore.ResolveHTTPClient, et validez les URL de base aveccore.NormalizeHTTPSBaseURL - Signez les transactions avec
core.SignTransactionWith, qui signe les octets du message et joint la signature ; vérifiez les signatures du fournisseur aveccore.VerifySignature - Renvoyez les erreurs avec
core.NewSignerError/core.WrapSignerErroret des codes stables tels quecore.CodeSigningFailed - Tests unitaires avec
httptest; tests d'intégration dansintegration_test.goderrière le tag//go:build integration
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)
- Pas de
.expect()ni de.unwrap()sur des réponses d'API non fiables - HTTPS imposé sur les clients HTTP distants
- Délais d'expiration HTTP configurés via
HttpClientConfig - Signatures renvoyées vérifiées par rapport aux octets du message calculés localement
- Respecte les conventions de nommage (snake_case pour Rust et Python, camelCase pour TypeScript)
- Tests d'intégration ajoutés et
.env.examplemis à jour - Implémentations TypeScript, Python et Go incluses (ou prévues avec les mainteneurs)
- Packages parapluie mis à jour (parapluie TypeScript,
_BACKENDSPython) - Modifications de la CI incluses
- Ajouté aux tableaux des backends pris en charge et des capacités des signers du README.md
Conseils d'implémentation
Gestion des erreurs
Utilisez toujours les variantes SignerError existantes. Display et Debug sur
SignerError sont tous deux expurgés ; gardez donc des messages génériques et n'incluez jamais de données brutes de
réponse d'API. N'utilisez jamais .expect() ni .unwrap() sur des réponses d'API non fiables :
// 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}")))?;
Exigences de sécurité
Ce ne sont pas de simples suggestions : les consommateurs choisissent un backend en se fondant sur elles. Si
votre fournisseur en rend une impossible, indiquez-le dans la PR et dans le fichier
docs/SECURITY_MODEL.md du dépôt.
- Vérifiez ce que le fournisseur a signé. Calculez localement les octets du message et vérifiez la signature renvoyée par rapport à ceux-ci, à la position requise pour votre signer, avant de la joindre. Une signature qui ne se vérifie pas doit provoquer un échec et ne jamais être jointe.
- Indiquez si le fournisseur diffuse la transaction. S'il l'exécute côté serveur, implémentez la forme « sending » et renvoyez la signature qui identifie la transaction confirmée. Si le fournisseur n'a pas de point de terminaison de signature seule, faites du backend un backend à envoi uniquement plutôt que de soumettre la transaction à l'insu de l'appelant.
- Ne laissez jamais l'appelant avec une transaction que la signature ne couvre pas. Un fournisseur qui réécrit la transaction signe ses propres octets : soit laissez la transaction de l'appelant intacte (cas de la diffusion gérée), soit adoptez la forme « modifying » et remplacez-la intégralement par les octets du fournisseur.
- Signalez l'incertitude de diffusion en tant que telle. Un échec après soumission a pu
quand même aboutir. Remontez
BROADCAST_UNCONFIRMEDplutôt qu'une erreur générique, joignez l'identifiant de transaction du fournisseur dès que vous en disposez, et envoyez une clé d'idempotence dérivée du message à chaque création afin qu'un renvoi identique octet pour octet soit sans risque. - Ne signez jamais avant l'initialisation. Utilisez
Option<Pubkey>(et nonPubkey::default()) pour la clé publique avantinit(), et échouez avec une erreur de configuration si une signature est tentée au préalable. - Ne journalisez jamais de données sensibles (clés privées, secrets d'API, réponses brutes d'API), et
utilisez une implémentation de
Debugqui masque les champs sensibles - Imposez HTTPS (
https_only(true), conditionné par#[cfg(not(test))]pour wiremock) et configurez les délais d'expiration de requête et de connexion viaHttpClientConfig(par défaut : 30 s/5 s) - Effacez (zeroize) le matériel de clé intermédiaire avec
zeroize::Zeroizing<Vec<u8>>
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/privy/mod.rs: nécessite une initialisationsrc/turnkey/mod.rs: gestion complexe des signaturessrc/vault/mod.rs: bibliothèque cliente externetypescript/packages/para/,python/src/solana_keychain/para/etgo/signers/para/: références complètes dans les autres langages
- Fichiers clés :
src/traits.rs(définitions des traits),src/transaction_util.rsetsrc/signature_util.rs(utilitaires partagés),src/http_client_config.rs(configuration des délais d'expiration) - Le guide complet du dépôt : docs/ADDING_SIGNERS.md
- 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 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?