Neue Signer hinzufügen

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 Regel TransactionSigner)
  • Fügen Sie in Cargo.toml ein Feature-Flag hinzu, einschließlich des internen _remote-Markers, falls Ihr Signer reqwest verwendet
  • Aktualisieren Sie das Signer-Enum in src/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 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()
}
}

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

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

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 über dotenvy liest
  • Drei Testfunktionen: test_<name>_sign_message, test_<name>_sign_transaction und test_<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-key
YOUR_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:

BackendAnwendungsfallFeature-Flag
MemoryLokale Schlüsselpaare, Entwicklung, Testingmemory
VaultEnterprise-Schlüsselverwaltung mit HashiCorp Vaultvault
PrivyEingebettete Wallets mit Privy-Infrastrukturprivy
TurnkeyNicht-verwaltete Schlüsselverwaltung über Turnkeyturnkey
IhrServiceKurze Beschreibung Ihres Dienstesyour_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 matrices
just 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 zur backend-Matrix (vor all) und Ihre Integrationstestfunktion zur test-Matrix hinzu
  • typescript-ci.yml: Fügen Sie Ihr Paket zu den Unit- und Integrationstest-Matrizen hinzu und ergänzen Sie Umgebungsvariablen im Integrationstest-Schritt
  • typescript-publish.yml: Fügen Sie Ihr Paket zu PUBLISH_PACKAGES, dem packages-Array des GitHub Release und der Übersichtstabelle hinzu
  • python-ci.yml: Fügen Sie das Backend zum CI_SIGNER_*-Gating in resolve-signers und zur Integrationsmatrix hinzu
  • go-ci.yml: Fügen Sie das Backend zum CI_SIGNER_*-Gating und zu moduleBySigner hinzu

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() gibt SolanaTransactionSigner<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 SolanaMessageSigner weg und stellen Sie überhaupt keine signMessages-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 mit assertHttpsUrl() aus @solana/keychain-core
  • Führen Sie Remote-Aufrufe über fetchSignerJson() aus @solana/keychain-core aus, 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-core verwenden
  • Berücksichtigen Sie config?.abortSignal in jeder Signiermethode und reichen Sie es an fetchSignerJson() und signBatchStaggered() weiter
  • Validieren Sie requestDelayMs mit validateRequestDelayMs() und verarbeiten Sie Batches mit signBatchStaggered()
  • @throws JSDoc 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 mit runSignerIntegrationTest aus @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 kein signTransactions bereit (ebenso wenig signMessages, 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) als BROADCAST_UNCONFIRMED mit der Transaktions-ID des Anbieters, wobei der Abbruchgrund als cause erhalten bleibt
  • Fügen Sie eine typisierte createKeychainSigner-Überladung hinzu, die Ihren Sending-Signer-Typ zurückgibt
  • Schließen Sie Ihr Backend aus KeychainKitPluginConfig in typescript/packages/kit-plugin/src/keychain-plugin.ts aus, da ein Sending Signer nicht als payer oder identity eines Kit-Clients dienen kann
  • Prüfen Sie in Unit-Tests, dass isTransactionPartialSigner() für Ihre Instanzen false und isTransactionSendingSigner() true ist

Umbrella-Paket aktualisieren

Aktualisieren Sie typescript/packages/keychain/ (7 zu ändernde Dateien):

  1. src/types.ts: Fügen Sie YourSignerConfig zur diskriminierten Union KeychainSignerConfig hinzu
  2. src/create-keychain-signer.ts: Fügen Sie den Switch-Case hinzu (jedes Backend-Paket wird mit einem dynamischen import() geladen)
  3. src/resolve-address.ts: Fügen Sie ihn dem Switch-Case für den Fast-Path oder den Fetch-Path hinzu
  4. src/index.ts: Fügen Sie die Exporte des Konfigurationstyps, des Namespace und der Factory-Funktion hinzu
  5. package.json: Fügen Sie die Abhängigkeit @solana/keychain-your-signer: "workspace:*" hinzu
  6. tsconfig.json: Fügen Sie die Referenz { "path": "../your-signer" } hinzu
  7. typescript/scripts/test-treeshake-umbrella.mjs: Fügen Sie Ihr Paket zu SIGNER_MARKERS und Ihre Factory zu FACTORIES hinzu

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 SolanaSigner aus solana_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()) oder SendingSigner (async sign_and_send_transaction())
  • Exportieren Sie eine async create_your_signer()-Factory, die init() abwartet, wenn das Backend seine Adresse remote auflöst; vor dem Init löst das Signieren SignerErrorCode.NOT_INITIALIZED aus
  • Exportieren Sie eine Konfigurations-Dataclass (YourSignerConfig), bei der Secrets mit field(repr=False) markiert sind
  • Erzwingen Sie HTTPS bei Base-URL-Feldern mit assert_https_url() und leiten Sie jede Anfrage über fetch_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 _BACKENDS in python/src/solana_keychain/keychain.py
  • Wenn das Backend ein Provider-SDK benötigt, den Import absichern, einen ImportError auslösen, der pip install 'solana-keychain[your-signer]' nennt, das Extra in python/pyproject.toml deklarieren und das Backend niemals eager aus dem Paket-Root importieren
  • Unit-Tests mit pytest: respx für HTTP-Backends, Stub-Clients für SDK-Backends und eine Prüfung, dass in str() oder repr() ausgelöster Fehler kein Secret erscheint
  • Eine make_your_signer()-Factory zu python/tests/integration/test_live_signers.py hinzufügen und in _MESSAGE_CAPABLE registrieren (oder in _TRANSACTION_ONLY für Backends ohne sign_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.SolanaSigner implementieren (Pubkey(), SignMessage(ctx, message), IsAvailable(ctx)) sowie genau ein Capability-Interface: core.TransactionSigner, core.ModifyingSigner oder core.SendingSigner. Die Capability wird per Type Assertion erkannt. Ein Backend, das keine Transaktion signieren kann, darf daher überhaupt keine SignTransaction-Methode besitzen
  • Ein Config-Struct und einen New-Konstruktor exportieren (New(ctx, cfg), wenn das Backend seine Adresse remote auflöst; davor core.NewNotInitializedError zurückgeben)
  • HTTP-Clients mit core.NewHTTPClient oder core.ResolveHTTPClient erstellen und Basis-URLs mit core.NormalizeHTTPSBaseURL prüfen
  • Transaktionen mit core.SignTransactionWith signieren, das die Message-Bytes signiert und die Signatur anhängt; Signaturen des Providers mit core.VerifySignature verifizieren
  • Fehler mit core.NewSignerError / core.WrapSignerError und stabilen Codes wie core.CodeSigningFailed zurückgeben
  • Unit-Tests mit httptest; Integrationstests in integration_test.go hinter 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 HttpClientConfig konfiguriert
  • 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.example aktualisiert
  • 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 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}")))?;

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_UNCONFIRMED statt 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> (nicht Pubkey::default()) für den öffentlichen Schlüssel vor init() 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 über HttpClientConfig konfigurieren (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, synchron
    • src/privy/mod.rs: Erfordert Initialisierung
    • src/turnkey/mod.rs: Komplexe Signaturverarbeitung
    • src/vault/mod.rs: Externe Client-Bibliothek
    • typescript/packages/para/, python/src/solana_keychain/para/ und go/signers/para/: vollständige Referenzen in den anderen Sprachen
  • Wichtige Dateien: src/traits.rs (Trait-Definitionen), src/transaction_util.rs und src/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 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 Solana Foundation. Alle Rechte vorbehalten.