Dieser Leitfaden richtet sich an Wallet-Dienstanbieter und Entwickler, die neue
Schlüsselverwaltungslösungen in die solana-keychain-Bibliothek integrieren
möchten. Durch das Hinzufügen Ihrer Signer-Implementierung ermöglichen Sie
Entwicklern, Ihren Dienst für die sichere Signierung von Solana-Transaktionen
über eine einheitliche Schnittstelle zu nutzen.
Keychain hält seine Implementierungen in Rust, TypeScript, Python und Go auf demselben Stand. PRs, die alle vier enthalten, werden ausdrücklich bevorzugt. Wenn Sie nur eine beisteuern können, ist das in Ordnung, aber rechnen Sie damit, dass die anderen erforderlich sind, bevor der Signer in einem Release ausgeliefert wird.
Nutzen Sie Claude Code? Der
add-signer-ci-Skill
des Repos richtet die CI-Workflows für einen neuen Signer ein, der über einen Fork-PR beigesteuert wird.
Architekturübersicht
Die Bibliothek verwendet eine Trait-basierte Architektur, die in src/traits.rs definiert ist. Jeder
Signer implementiert den Basis-Trait SolanaSigner (pubkey, sign_message,
is_available) sowie genau einen Capability-Trait, der zur Struktur des Anbieters passt:
TransactionSigner(sign_transaction): Der Anbieter signiert die Bytes des Aufrufers und der Aufrufer sendet sie per Broadcast. Die meisten Backends.ModifyingSigner(modify_and_sign_transaction): Der Anbieter schreibt die Transaktion um und signiert sie anschließend, ohne sie per Broadcast zu senden.SendingSigner(sign_and_send_transaction): Der Anbieter signiert und sendet serverseitig per Broadcast.
Die Bibliothek stellt außerdem ein einheitliches Signer-Enum bereit, das alle
Implementierungen umschließt und die Auswahl der Signing-Backends zur Laufzeit ermöglicht. Es implementiert
nur den Basis-Trait; auf die Capabilities greifen Sie über as_transaction_signer(),
as_modifying_signer() und as_sending_signer() zu.
Schnelle Integrationscheckliste
- Erstellen Sie Ihr Signer-Modul mit Implementierung
- Implementieren Sie den Basis-Trait
SolanaSigner(pubkey,sign_message,is_available) sowie den einen Capability-Trait, der zur Struktur Ihres Anbieters passt (in der RegelTransactionSigner) - Fügen Sie in
Cargo.tomlein Feature-Flag hinzu, einschließlich des internen_remote-Markers, falls Ihr Signer reqwest verwendet - Aktualisieren Sie das
Signer-Enum insrc/lib.rs(Variante,dispatch_signer!-Arm und ein Arm im Capability-Accessor, der zu Ihrer Struktur passt) - Erzwingen Sie HTTPS und konfigurieren Sie Timeouts für HTTP-Clients
- Signieren Sie die Message-Bytes und verifizieren Sie die zurückgegebene Signatur, bevor Sie sie anhängen
- Fügen Sie umfassende Unit-Tests hinzu (auf Basis von wiremock, in Ihrem Modul)
- Fügen Sie eine Integrationstestdatei hinzu und aktualisieren Sie
.env.example - Fügen Sie die Implementierungen in TypeScript, Python und Go hinzu
- Aktualisieren Sie die Dokumentation
- Aktualisierungen der CI-Workflows (stimmen Sie sich mit den Maintainern ab, siehe Aktualisierungen der CI-Workflows)
- Reichen Sie einen PR ein
Schritt 1: Erstellen Sie Ihr Signer-Modul
Erstellen Sie ein neues Verzeichnis unter src/ für Ihre Implementierung:
src/├── your_service/│ ├── mod.rs # Main implementation with SolanaSigner trait│ └── types.rs # API request/response types (if needed)
Schritt 2: Definieren Sie Ihre Signer-Struktur
Definieren Sie in src/your_service/mod.rs Ihre Signer-Struktur:
//! 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()}}
Schritt 3: Implementieren Sie Konstruktor- und Hilfsmethoden
Remote-Signer müssen HTTPS erzwingen und HTTP-Timeouts konfigurieren.
Verwenden Sie die gemeinsame HttpClientConfig-Struktur für
Timeout-Einstellungen.
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))}}
Schritt 4: Signer-Traits implementieren
Der Basis-Trait SolanaSigner hat 2 asynchrone Methoden (sign_message,
is_available) sowie pubkey(). Das Signieren von Transaktionen gehört in einen Capability-Trait;
die meisten Anbieter implementieren TransactionSigner, dessen sign_transaction
SignTransactionResult zurückgibt (ein getaggtes Enum, das die Transaktion als vollständig oder
teilweise signiert kennzeichnet).
Signieren Sie die Message-Bytes, niemals die gesamte Wire-Transaktion, und verifizieren Sie die
Signatur des Anbieters anhand dieser Bytes, bevor Sie sie anhängen. Verwenden Sie die gemeinsamen
TransactionUtil-Helfer zum Signieren und Serialisieren.
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),))}}
Wenn Ihr Anbieter die Transaktion vor dem Signieren umschreibt, implementieren Sie stattdessen
ModifyingSigner und ersetzen Sie die Transaktion des Aufrufers vollständig durch
die Bytes des Anbieters. Wenn er serverseitig per Broadcast sendet, implementieren Sie SendingSigner
und lassen Sie die Transaktion des Aufrufers unverändert. Implementieren Sie niemals eine Capability, die Ihr
Anbieter nicht bereitstellen kann.
Schritt 5: API-Typen hinzufügen (Optional)
Wenn Ihre API benutzerdefinierte Typen benötigt, erstellen Sie
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,}
Schritt 6: Feature-Flag hinzufügen
Aktualisieren Sie Cargo.toml, um Ihren Signer als optionales Feature hinzuzufügen. Remote-Backends
enthalten den internen _remote-Marker, der die From<reqwest::Error>-Implementierung
in src/error.rs steuert; ohne ihn lässt sich ? bei reqwest-Aufrufen nicht kompilieren, wenn nur
Ihr Feature aktiviert ist.
[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
Schritt 7: Signer-Enum aktualisieren
Fügen Sie Ihren Signer zu src/lib.rs hinzu. Die Basis-Implementierung von SolanaSigner leitet über
das Makro dispatch_signer! weiter, fügen Sie dort also einen Arm hinzu. Der Capability-Zugriff liegt in
den Accessoren as_transaction_signer(), as_modifying_signer() und as_sending_signer():
Fügen Sie dem Accessor, der zu Ihrem Capability-Trait passt, einen Some(s)-Arm hinzu (und,
bei einem sendenden oder modifizierenden Backend, einen None-Arm in 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),}}}
Schritt 8: Umfassende Tests hinzufügen
Fügen Sie Tests zu Ihrem Modul hinzu (am Ende von 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());}}
Decken Sie auch den Fehlschlag der Signaturverifizierung ab: Eine gemockte Antwort, deren Signatur nicht verifiziert werden kann, muss einen Fehler zurückgeben.
Schritt 9: Integrationstests hinzufügen
Erstellen Sie rust/src/tests/test_<name>_integration.rs. Integrationstests laufen
gegen die echte Service-API (nicht wiremock) und sind hinter dem Feature
integration-tests geschützt. Jede Datei benötigt:
pub const-Deklarationen für die Namen der Umgebungsvariablen- Einen
get_signer()-Helfer, der Umgebungsvariablen überdotenvyliest - Drei Testfunktionen:
test_<name>_sign_message,test_<name>_sign_transactionundtest_<name>_is_available - Feature-Gates:
#[cfg(feature = "your_service")]für das Modul und#[cfg(feature = "integration-tests")]für jeden Test
Eine vollständige Referenz finden Sie in rust/src/tests/test_para_integration.rs. Deklarieren Sie
Ihr Modul in rust/src/tests/mod.rs:
pub mod test_your_service_integration;
Schritt 10: Umgebungsvariablen aktualisieren
Aktualisieren Sie die .env.example im Stammverzeichnis mit den Umgebungsvariablen Ihres Signers und folgen Sie dabei dem
bestehenden Muster:
# 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
Führen Sie erforderliche Variablen zuerst mit Platzhalterwerten auf und optionale Variablen auskommentiert
mit ihren Standardwerten. Jede in Integrationstests verwendete Umgebungsvariable muss in
.env.example vorkommen, und die Test-Suites für TypeScript, Python und Go verwenden dieselben Namen.
Schritt 11: Dokumentation aktualisieren
Fügen Sie Ihren Signer zur Tabelle der unterstützten Backends in der README.md hinzu:
| Backend | Anwendungsfall | Feature-Flag |
|---|---|---|
| Memory | Lokale Schlüsselpaare, Entwicklung, Testing | memory |
| Vault | Enterprise-Schlüsselverwaltung mit HashiCorp Vault | vault |
| Privy | Eingebettete Wallets mit Privy-Infrastruktur | privy |
| Turnkey | Nicht-verwaltete Schlüsselverwaltung über Turnkey | turnkey |
| IhrService | Kurze Beschreibung Ihres Dienstes | your_service |
Fügen Sie Ihr Backend außerdem der Tabelle der Signer-Capabilities in der README jeder Sprache hinzu. Fügen Sie ein Anwendungsbeispiel hinzu:
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(())}
Testen Ihrer Integration
Verwenden Sie die just-Recipes statt des reinen cargo. Die SDK-Features (sdk-v2,
sdk-v3, sdk-v4) schließen sich gegenseitig aus, daher schlägt cargo test --all-features
fehl.
# 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
Aktualisierungen der CI-Workflows (Fork-PRs)
CI ist ein zweiphasiger Prozess. Stimmen Sie sich mit den Maintainern ab, um main vorzubereiten, bevor
Ihr PR getestet werden kann: Sie aktualisieren fork-external-live-manual.yml, fügen Umgebungsvariablen
zu ci.yml hinzu und konfigurieren GitHub Secrets.
Ihr PR muss diese Änderungen der Phase 2 zusammen mit Ihrem Signer-Code enthalten:
ci.yml: Fügen Sie Ihr Rust-Feature zurbackend-Matrix (vorall) und Ihre Integrationstestfunktion zurtest-Matrix hinzutypescript-ci.yml: Fügen Sie Ihr Paket zu den Unit- und Integrationstest-Matrizen hinzu und ergänzen Sie Umgebungsvariablen im Integrationstest-Schritttypescript-publish.yml: Fügen Sie Ihr Paket zuPUBLISH_PACKAGES, dempackages-Array des GitHub Release und der Übersichtstabelle hinzupython-ci.yml: Fügen Sie das Backend zumCI_SIGNER_*-Gating inresolve-signersund zur Integrationsmatrix hinzugo-ci.yml: Fügen Sie das Backend zumCI_SIGNER_*-Gating und zumoduleBySignerhinzu
TypeScript-Signer
Wenn Sie auch ein TypeScript-Signer-Paket hinzufügen, erstellen Sie es unter
typescript/packages/your-signer/. Wichtige Muster:
- Die Factory-Funktion
createYourSigner()gibtSolanaTransactionSigner<TAddress> & SolanaMessageSigner<TAddress>zurück; halten Sie die Signer-Klasse intern und exportieren Sie nur die Factory und ihren Konfigurationstyp - Config-Interface exportieren (
YourSignerConfig) - Wenn Ihr Backend keine Messages signieren kann, lassen Sie
SolanaMessageSignerweg und stellen Sie überhaupt keinesignMessages-Methode bereit: Kit klassifiziert Signer anhand des Vorhandenseins von Methoden, daher würde ein Stub, der einen Fehler auslöst, den Signer falsch klassifizieren - Erzwingen Sie HTTPS bei
apiBaseUrl-Konfigurationsfeldern mitassertHttpsUrl()aus@solana/keychain-core - Führen Sie Remote-Aufrufe über
fetchSignerJson()aus@solana/keychain-coreaus, das Fehlerzuordnung, Bereinigung von Antworten, Ablehnung von Weiterleitungen und das Standard-Timeout übernimmt - Bereinigen Sie jeden anderen Fehlertext der Remote-API mit
sanitizeRemoteErrorResponse()aus@solana/keychain-core throwSignerError(SignerErrorCode.*, { cause, message })aus@solana/keychain-coreverwenden- Berücksichtigen Sie
config?.abortSignalin jeder Signiermethode und reichen Sie es anfetchSignerJson()undsignBatchStaggered()weiter - Validieren Sie
requestDelayMsmitvalidateRequestDelayMs()und verarbeiten Sie Batches mitsignBatchStaggered() @throwsJSDoc zu Factory-Funktionen hinzufügen, die Fehlercodes auflisten- Deklarieren Sie
@solana/*-Pakete als Peer-Dependencies mit>=8.1.0 - Unit-Tests mit vitest und gemocktem
fetch; Integrationstests mitrunSignerIntegrationTestaus@solana/keychain-test-utils
Sending Signer
Wenn Ihr Anbieter die Transaktion umschreibt oder serverseitig per Broadcast sendet, kann seine
Signatur nicht auf die Transaktion des Aufrufers angewendet werden. Daher muss es ein
SolanaSendingSigner statt eines SolanaTransactionSigner sein:
- Implementieren Sie
signAndSendTransactions()und stellen Sie keinsignTransactionsbereit (ebenso wenigsignMessages, es sei denn, das Backend signiert tatsächlich Messages) - Berücksichtigen Sie
config?.abortSignal. Sobald der Anbieter das Erstellen akzeptiert hat, melden Sie jeden Fehler (einschließlich eines Abbruchs) alsBROADCAST_UNCONFIRMEDmit der Transaktions-ID des Anbieters, wobei der Abbruchgrund alscauseerhalten bleibt - Fügen Sie eine typisierte
createKeychainSigner-Überladung hinzu, die Ihren Sending-Signer-Typ zurückgibt - Schließen Sie Ihr Backend aus
KeychainKitPluginConfigintypescript/packages/kit-plugin/src/keychain-plugin.tsaus, da ein Sending Signer nicht alspayeroderidentityeines Kit-Clients dienen kann - Prüfen Sie in Unit-Tests, dass
isTransactionPartialSigner()für Ihre Instanzen false undisTransactionSendingSigner()true ist
Umbrella-Paket aktualisieren
Aktualisieren Sie typescript/packages/keychain/ (7 zu ändernde Dateien):
src/types.ts: Fügen SieYourSignerConfigzur diskriminierten UnionKeychainSignerConfighinzusrc/create-keychain-signer.ts: Fügen Sie den Switch-Case hinzu (jedes Backend-Paket wird mit einem dynamischenimport()geladen)src/resolve-address.ts: Fügen Sie ihn dem Switch-Case für den Fast-Path oder den Fetch-Path hinzusrc/index.ts: Fügen Sie die Exporte des Konfigurationstyps, des Namespace und der Factory-Funktion hinzupackage.json: Fügen Sie die Abhängigkeit@solana/keychain-your-signer: "workspace:*"hinzutsconfig.json: Fügen Sie die Referenz{ "path": "../your-signer" }hinzutypescript/scripts/test-treeshake-umbrella.mjs: Fügen Sie Ihr Paket zuSIGNER_MARKERSund Ihre Factory zuFACTORIEShinzu
Die Switch-Anweisungen enthalten erschöpfende never-Prüfungen, sodass TypeScript einen Fehler meldet,
wenn Sie die Union erweitern, aber einen Case vergessen. Führen Sie anschließend just ts-treeshake aus:
Jedes Paket und das Umbrella-Paket müssen Tree-Shaking-fähig bleiben.
Python-Signer
Erstellen Sie das Modul unter python/src/solana_keychain/your_signer/ (__init__.py
plus signer.py). Zentrale Muster:
- Erben Sie von
SolanaSigneraussolana_keychain.core(pubkey-Property,async sign_message(),async is_available()) sowie von genau einer Capability- Klasse:TransactionSigner(async sign_transaction()),ModifyingSigner(async modify_and_sign_transaction()) oderSendingSigner(async sign_and_send_transaction()) - Exportieren Sie eine
async create_your_signer()-Factory, dieinit()abwartet, wenn das Backend seine Adresse remote auflöst; vor dem Init löst das SignierenSignerErrorCode.NOT_INITIALIZEDaus - Exportieren Sie eine Konfigurations-Dataclass (
YourSignerConfig), bei der Secrets mitfield(repr=False)markiert sind - Erzwingen Sie HTTPS bei Base-URL-Feldern mit
assert_https_url()und leiten Sie jede Anfrage überfetch_signer_json() - Verifizieren Sie die Signatur des Anbieters, bevor Sie sie zurückgeben; verwenden Sie
signed_message_bytes()für die Bytes, die eine Signatur abdeckt, niemals ein selbstgebautes Versionspräfix - Registrieren Sie das Backend in
_BACKENDSinpython/src/solana_keychain/keychain.py - Wenn das Backend ein Provider-SDK benötigt, den Import absichern, einen
ImportErrorauslösen, derpip install 'solana-keychain[your-signer]'nennt, das Extra inpython/pyproject.tomldeklarieren und das Backend niemals eager aus dem Paket-Root importieren - Unit-Tests mit pytest:
respxfür HTTP-Backends, Stub-Clients für SDK-Backends und eine Prüfung, dass instr()oderrepr()ausgelöster Fehler kein Secret erscheint - Eine
make_your_signer()-Factory zupython/tests/integration/test_live_signers.pyhinzufügen und in_MESSAGE_CAPABLEregistrieren (oder in_TRANSACTION_ONLYfür Backends ohnesign_message)
Go-Signer
Jedes Go-Backend ist ein eigenes Modul unter go/signers/yoursigner/ mit dem Modulpfad
github.com/solana-foundation/solana-keychain/go/signers/yoursigner/v2. Als Referenz siehe
go/signers/para/. Wichtige Muster:
core.SolanaSignerimplementieren (Pubkey(),SignMessage(ctx, message),IsAvailable(ctx)) sowie genau ein Capability-Interface:core.TransactionSigner,core.ModifyingSignerodercore.SendingSigner. Die Capability wird per Type Assertion erkannt. Ein Backend, das keine Transaktion signieren kann, darf daher überhaupt keineSignTransaction-Methode besitzen- Ein
Config-Struct und einenNew-Konstruktor exportieren (New(ctx, cfg), wenn das Backend seine Adresse remote auflöst; davorcore.NewNotInitializedErrorzurückgeben) - HTTP-Clients mit
core.NewHTTPClientodercore.ResolveHTTPClienterstellen und Basis-URLs mitcore.NormalizeHTTPSBaseURLprüfen - Transaktionen mit
core.SignTransactionWithsignieren, das die Message-Bytes signiert und die Signatur anhängt; Signaturen des Providers mitcore.VerifySignatureverifizieren - Fehler mit
core.NewSignerError/core.WrapSignerErrorund stabilen Codes wiecore.CodeSigningFailedzurückgeben - Unit-Tests mit
httptest; Integrationstests inintegration_test.gohinter dem Tag//go:build integration
Checkliste für die Einreichung
Vor dem Einreichen Ihres PRs:
- Code kompiliert ohne Warnungen (
just build) - Alle Tests bestehen (
just test) - Code ist formatiert/Linting besteht (
just fmt) - Keine hartcodierten Werte oder Secrets im Code
- Fehlermeldungen sind generisch (kein roher API-Antworttext)
- Kein
.expect()oder.unwrap()bei nicht vertrauenswürdigen API-Antworten - HTTPS auf Remote-HTTP-Clients erzwungen
- HTTP-Timeouts über
HttpClientConfigkonfiguriert - Zurückgegebene Signaturen werden gegen lokal berechnete Message-Bytes verifiziert
- Befolgt die Namenskonventionen (snake_case für Rust und Python, camelCase für TypeScript)
- Integrationstests hinzugefügt und
.env.exampleaktualisiert - TypeScript-, Python- und Go-Implementierungen enthalten (oder mit den Maintainern geplant)
- Umbrella-Pakete aktualisiert (TypeScript-Umbrella, Python
_BACKENDS) - CI-Änderungen enthalten
- Zu den Tabellen der unterstützten Backends und der Signer-Capabilities in der README.md hinzugefügt
Implementierungstipps
Fehlerbehandlung
Immer die vorhandenen SignerError-Varianten verwenden. Sowohl Display als auch Debug von
SignerError sind geschwärzt. Halte Meldungen daher generisch und nimm niemals rohe API-
Antwortdaten auf. Niemals .expect() oder .unwrap() bei nicht vertrauenswürdigen API-Antworten verwenden:
// 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}")))?;
Sicherheitsanforderungen
Das sind keine Empfehlungen: Nutzer wählen ein Backend auf Grundlage dieser Anforderungen. Wenn
dein Provider eine davon unmöglich macht, weise im PR und in der
docs/SECURITY_MODEL.md des Repos darauf hin.
- Verifiziere, was der Provider signiert hat. Berechne die Message-Bytes lokal und verifiziere die zurückgegebene Signatur an der von deinem Signer geforderten Position dagegen, bevor du sie anhängst. Eine Signatur, die sich nicht verifizieren lässt, muss fehlschlagen und darf nie angehängt werden.
- Gib an, ob der Provider broadcastet. Wenn er serverseitig ausführt, implementiere die Sending-Variante und gib die Signatur zurück, die die gelandete Transaktion identifiziert. Wenn der Provider keinen Endpunkt nur zum Signieren hat, mache das Backend sending-only, statt hinter dem Rücken des Aufrufers zu senden.
- Lass den Aufrufer niemals mit einer Transaktion zurück, die die Signatur nicht abdeckt. Ein Provider, der die Transaktion umschreibt, signiert seine eigenen Bytes: Lass entweder die Transaktion des Aufrufers unverändert (der Fall mit verwaltetem Broadcast) oder wähle die Modifying-Variante und ersetze sie vollständig durch die Bytes des Providers.
- Melde Broadcast-Unsicherheit als solche. Ein Fehler nach dem Absenden kann dennoch
gelandet sein. Gib
BROADCAST_UNCONFIRMEDstatt eines generischen Fehlers aus, füge die Transaktions-ID des Providers bei, sobald du eine hast, und sende bei jedem Create einen aus der Message abgeleiteten Idempotency-Key, damit ein byte-identisches erneutes Senden sicher ist. - Signiere niemals vor init. Verwende
Option<Pubkey>(nichtPubkey::default()) für den öffentlichen Schlüssel vorinit()und brich mit einem Konfigurationsfehler ab, wenn zuvor signiert werden soll. - Protokolliere niemals sensible Daten (private Schlüssel, API-Secrets, rohe API-Antworten) und
verwende eine
Debug-Implementierung, die sensible Felder verbirgt - HTTPS erzwingen (
https_only(true), mit#[cfg(not(test))]für wiremock abgesichert) und Request- sowie Connect-Timeouts überHttpClientConfigkonfigurieren (Standardwerte: 30 s/5 s) - Zwischenwerte von Schlüsselmaterial mit
zeroize::Zeroizing<Vec<u8>>nullen
Testen mit Mocks
Verwenden Sie wiremock zum Mocken von HTTP-APIs. Prüfen Sie nur den Fehlertyp,
nicht den Fehlermeldungstext:
#[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}}
Hilfe erhalten
- Überprüfen Sie vorhandene Signer-Implementierungen auf Muster:
src/memory/mod.rs: Einfach, synchronsrc/privy/mod.rs: Erfordert Initialisierungsrc/turnkey/mod.rs: Komplexe Signaturverarbeitungsrc/vault/mod.rs: Externe Client-Bibliothektypescript/packages/para/,python/src/solana_keychain/para/undgo/signers/para/: vollständige Referenzen in den anderen Sprachen
- Wichtige Dateien:
src/traits.rs(Trait-Definitionen),src/transaction_util.rsundsrc/signature_util.rs(gemeinsame Helfer),src/http_client_config.rs(Timeout- Konfiguration) - Der vollständige Repository-Leitfaden: docs/ADDING_SIGNERS.md
- Eröffnen Sie ein Issue für Design-Diskussionen, bevor Sie mit der Arbeit beginnen
Beispiel für eine PR-Struktur
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?