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.

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éralement TransactionSigner)
  • Ajouter un indicateur de fonctionnalité dans Cargo.toml, en incluant le marqueur interne _remote si votre signataire utilise reqwest
  • Mettre à jour l'énumération Signer dans src/lib.rs (variante, branche dispatch_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 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()
}
}

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

É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 const pour les noms des variables d'environnement
  • Une fonction utilitaire get_signer() qui lit les variables d'environnement via dotenvy
  • Trois fonctions de test : test_<name>_sign_message, test_<name>_sign_transaction et test_<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-key
YOUR_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 :

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 é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 matrices
just 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 matrice backend (avant all) et votre fonction de test d'intégration à la matrice test
  • typescript-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égration
  • typescript-publish.yml : ajoutez votre package à PUBLISH_PACKAGES, au tableau packages de la GitHub Release et au tableau récapitulatif
  • python-ci.yml : ajoutez le backend à la condition CI_SIGNER_* dans resolve-signers et à la matrice d'intégration
  • go-ci.yml : ajoutez le backend à la condition CI_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() retourne SolanaTransactionSigner<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 SolanaMessageSigner et n'exposez aucune méthode signMessages : 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 apiBaseUrl avec assertHttpsUrl() 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?.abortSignal dans chaque méthode de signature, en le transmettant à fetchSignerJson() et à signBatchStaggered()
  • Validez requestDelayMs avec validateRequestDelayMs() et traitez par lots avec signBatchStaggered()
  • Ajouter la documentation JSDoc @throws aux 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 fetch simulé ; tests d'intégration avec runSignerIntegrationTest de @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éthode signTransactions (ni signMessages, 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) par BROADCAST_UNCONFIRMED en incluant l'identifiant de transaction du fournisseur, la raison de l'annulation étant conservée dans cause
  • Ajoutez une surcharge typée de createKeychainSigner retournant le type de votre signataire d'envoi
  • Excluez votre backend de KeychainKitPluginConfig dans typescript/packages/kit-plugin/src/keychain-plugin.ts, car un signataire d'envoi ne peut pas servir de payer ou d'identity pour un client Kit
  • Vérifiez dans les tests unitaires que isTransactionPartialSigner() retourne false et que isTransactionSendingSigner() retourne true pour vos instances

Mettre à jour le package umbrella

Mettez à jour typescript/packages/keychain/ (7 fichiers à modifier) :

  1. src/types.ts : ajoutez YourSignerConfig à l'union discriminée KeychainSignerConfig
  2. src/create-keychain-signer.ts : ajoutez le cas du switch (chaque package de backend est chargé via un import() dynamique)
  3. src/resolve-address.ts : ajoutez-le au cas du switch du chemin rapide ou du chemin avec requête
  4. src/index.ts : ajoutez les exports du type de configuration, du namespace et de la fonction factory
  5. package.json : ajoutez la dépendance @solana/keychain-your-signer: "workspace:*"
  6. tsconfig.json : ajoutez la référence { "path": "../your-signer" }
  7. typescript/scripts/test-treeshake-umbrella.mjs : ajoutez votre package à SIGNER_MARKERS et 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 SolanaSigner depuis solana_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()) ou SendingSigner (async sign_and_send_transaction())
  • Exportez une factory async create_your_signer() qui attend init() lorsque le backend résout son adresse à distance ; avant l'initialisation, la signature lève SignerErrorCode.NOT_INITIALIZED
  • Exportez une dataclass de configuration (YourSignerConfig) avec les secrets marqués field(repr=False)
  • Imposez HTTPS sur les champs d'URL de base avec assert_https_url(), et faites passer chaque requête par fetch_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 _BACKENDS dans python/src/solana_keychain/keychain.py
  • Si le backend nécessite un SDK de fournisseur, protégez l'import, levez une ImportError indiquant pip install 'solana-keychain[your-signer]', déclarez l'extra dans python/pyproject.toml, et n'importez jamais le backend de manière anticipée depuis la racine du package
  • Tests unitaires avec pytest : respx pour les backends HTTP, clients factices pour les backends SDK, et une vérification qu'aucun secret n'apparaît dans str() ou repr() des erreurs levées
  • Ajoutez une fabrique make_your_signer() à python/tests/integration/test_live_signers.py et enregistrez-la dans _MESSAGE_CAPABLE (ou _TRANSACTION_ONLY pour les backends sans sign_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.ModifyingSigner ou core.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éthode SignTransaction
  • Exportez une structure Config et un constructeur New (New(ctx, cfg) lorsque le backend résout son adresse à distance ; avant cela, renvoyez core.NewNotInitializedError)
  • Construisez les clients HTTP avec core.NewHTTPClient ou core.ResolveHTTPClient, et validez les URL de base avec core.NormalizeHTTPSBaseURL
  • Signez les transactions avec core.SignTransactionWith, qui signe les octets du message et joint la signature ; vérifiez les signatures du fournisseur avec core.VerifySignature
  • Renvoyez les erreurs avec core.NewSignerError / core.WrapSignerError et des codes stables tels que core.CodeSigningFailed
  • Tests unitaires avec httptest ; tests d'intégration dans integration_test.go derriè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.example mis à jour
  • Implémentations TypeScript, Python et Go incluses (ou prévues avec les mainteneurs)
  • Packages parapluie mis à jour (parapluie TypeScript, _BACKENDS Python)
  • 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 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}")))?;

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_UNCONFIRMED plutô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 non Pubkey::default()) pour la clé publique avant init(), 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 Debug qui 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 via HttpClientConfig (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, synchrone
    • src/privy/mod.rs : nécessite une initialisation
    • src/turnkey/mod.rs : gestion complexe des signatures
    • src/vault/mod.rs : bibliothèque cliente externe
    • typescript/packages/para/, python/src/solana_keychain/para/ et go/signers/para/ : références complètes dans les autres langages
  • Fichiers clés : src/traits.rs (définitions des traits), src/transaction_util.rs et src/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 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?

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