Nieuwe Ondertekenaars Toevoegen

Deze handleiding is bedoeld voor wallet service providers en ontwikkelaars die nieuwe key management-oplossingen willen integreren in de solana-keychain library. Door jouw signer-implementatie toe te voegen, stel je ontwikkelaars in staat om jouw service te gebruiken voor veilige Solana-transactieondertekening via een uniforme interface.

Keychain houdt zijn Rust-, TypeScript-, Python- en Go-implementaties gelijk aan elkaar. PR's die alle vier bevatten hebben sterk de voorkeur. Als je er maar één kunt bijdragen, is dat prima, maar verwacht dat de andere vereist zijn voordat de signer in een release wordt opgenomen.

Gebruik je Claude Code? De add-signer-ci-skill richtt van de repo de CI-workflows in voor een nieuwe signer die via een fork-PR wordt aangeleverd.

Architectuuroverzicht

De bibliotheek gebruikt een architectuur op basis van traits, gedefinieerd in src/traits.rs. Elke signer implementeert de basistrait SolanaSigner (pubkey, sign_message, is_available) plus precies één capability-trait die past bij de opzet van de provider:

  • TransactionSigner (sign_transaction): de provider ondertekent de bytes van de aanroeper en de aanroeper verzendt ze zelf. De meeste backends.
  • ModifyingSigner (modify_and_sign_transaction): de provider herschrijft de transactie en ondertekent die vervolgens zonder te verzenden.
  • SendingSigner (sign_and_send_transaction): de provider ondertekent en verzendt aan de serverzijde.

De bibliotheek biedt ook een uniforme Signer-enum die alle implementaties omvat, waardoor je tijdens runtime een signing-backend kunt kiezen. De enum implementeert alleen de basistrait; capabilities zijn bereikbaar via as_transaction_signer(), as_modifying_signer() en as_sending_signer().

Snelle Integratie Checklist

  • Creëer je signer-module met implementatie
  • Implementeer de basistrait SolanaSigner (pubkey, sign_message, is_available) plus de ene capability-trait die past bij de opzet van je provider (meestal TransactionSigner)
  • Voeg een feature flag toe in Cargo.toml, inclusief de interne _remote-marker als je signer reqwest gebruikt
  • Update de Signer-enum in src/lib.rs (variant, dispatch_signer!-arm en een arm in de capability-accessor die bij jouw opzet past)
  • Forceer HTTPS en configureer timeouts op HTTP-clients
  • Onderteken de bytes van het bericht en verifieer de geretourneerde handtekening voordat je die toevoegt
  • Voeg uitgebreide unittests toe (op basis van wiremock, in je module)
  • Voeg een integratietestbestand toe en update .env.example
  • Voeg de TypeScript-, Python- en Go-implementaties toe
  • Update documentatie
  • Updates van CI-workflows (stem af met de maintainers, zie Updates van CI-workflows)
  • Dien PR in

Stap 1: Creëer Je Signer-Module

Maak een nieuwe directory aan onder src/ voor jouw implementatie:

src/
├── your_service/
│ ├── mod.rs # Main implementation with SolanaSigner trait
│ └── types.rs # API request/response types (if needed)

Stap 2: Definieer Je Signer Struct

Definieer in src/your_service/mod.rs je signer struct:

//! 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()
}
}

Stap 3: Implementeer Constructor en Hulpmethoden

Remote signers moeten HTTPS afdwingen en HTTP-timeouts configureren. Gebruik de gedeelde HttpClientConfig struct voor timeout-instellingen.

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

Stap 4: De signer-traits implementeren

De basistrait SolanaSigner heeft 2 asynchrone methoden (sign_message, is_available) plus pubkey(). Het ondertekenen van transacties gebeurt in een capability-trait; de meeste providers implementeren TransactionSigner, waarvan sign_transaction SignTransactionResult retourneert (een getagde enum die aangeeft dat de transactie volledig of gedeeltelijk is ondertekend).

Onderteken de bytes van het bericht, nooit de volledige wire-transactie, en verifieer de handtekening van de provider daartegen voordat je die toevoegt. Gebruik de gedeelde TransactionUtil-helpers voor ondertekening en serialisatie.

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

Als je provider de transactie herschrijft voordat die wordt ondertekend, implementeer dan ModifyingSigner en vervang de transactie van de aanroeper volledig door de bytes van de provider. Als de provider aan de serverzijde verzendt, implementeer dan SendingSigner en laat de transactie van de aanroeper ongemoeid. Implementeer nooit een capability die je provider niet kan bieden.

Stap 5: API-types toevoegen (optioneel)

Als je API aangepaste types nodig heeft, maak dan src/your_service/types.rs aan:

use serde::{Deserialize, Serialize};
#[derive(Serialize)]
pub struct SignRequest {
pub wallet_id: String,
pub message: String,
}
#[derive(Deserialize)]
pub struct SignResponse {
pub signature: String,
}

Stap 6: Feature flag toevoegen

Update Cargo.toml om je signer als optionele feature toe te voegen. Remote backends nemen de interne _remote-marker op, die de From<reqwest::Error>- impl in src/error.rs activeert; zonder deze marker compileert ? op reqwest-aanroepen niet als alleen jouw feature is ingeschakeld.

[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

Stap 7: De signer-enum updaten

Voeg je signer toe aan src/lib.rs. De basis-impl van SolanaSigner dispatcht via de macro dispatch_signer!, dus voeg daar één arm toe. Capability-toegang zit in de accessors as_transaction_signer(), as_modifying_signer() en as_sending_signer(): voeg een Some(s)-arm toe aan de accessor die bij jouw capability-trait past (en, voor een verzendende of wijzigende backend, een 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),
}
}
}

Stap 8: Uitgebreide tests toevoegen

Voeg tests toe aan je module (onderaan 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());
}
}

Dek ook het falen van de handtekeningverificatie af: een gemockte respons waarvan de handtekening niet verifieert, moet een fout retourneren.

Stap 9: Integratietests toevoegen

Maak rust/src/tests/test_<name>_integration.rs aan. Integratietests draaien tegen de echte service-API (niet wiremock) en zijn afgeschermd achter de feature integration-tests. Elk bestand heeft nodig:

  • pub const-declaraties voor namen van omgevingsvariabelen
  • Een get_signer()-helper die omgevingsvariabelen leest via dotenvy
  • Drie testfuncties: test_<name>_sign_message, test_<name>_sign_transaction en test_<name>_is_available
  • Feature gates: #[cfg(feature = "your_service")] op de module en #[cfg(feature = "integration-tests")] op elke test

Zie rust/src/tests/test_para_integration.rs voor een volledige referentie en declareer je module in rust/src/tests/mod.rs:

pub mod test_your_service_integration;

Stap 10: Omgevingsvariabelen updaten

Update de .env.example in de root met de omgevingsvariabelen van je signer, volgens het bestaande patroon:

# 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

Zet vereiste variabelen eerst, met placeholderwaarden, en zet optionele variabelen uitgecommentarieerd met hun standaardwaarden. Elke omgevingsvariabele die in integratietests wordt gebruikt, moet in .env.example staan, en de TypeScript-, Python- en Go-suites gebruiken dezelfde namen.

Stap 11: Documentatie updaten

Voeg je signer toe aan de tabel met ondersteunde backends in README.md:

BackendGebruikssituatieFeature flag
MemoryLokale keypairs, ontwikkeling, testenmemory
VaultEnterprise key management met HashiCorp Vaultvault
PrivyEmbedded wallets met Privy-infrastructuurprivy
TurnkeyNon-custodial key management via Turnkeyturnkey
YourServiceKorte beschrijving van je serviceyour_service

Voeg je backend ook toe aan de tabel met signer-capabilities in de README van elke taal. Voeg een gebruiksvoorbeeld toe:

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

Je integratie testen

Gebruik de just-recipes in plaats van kale cargo. De SDK-features (sdk-v2, sdk-v3, sdk-v4) sluiten elkaar uit, dus cargo test --all-features faalt.

# 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

Updates van CI-workflows (fork-PR's)

CI is een proces in twee fasen. Stem af met de maintainers om main voor te bereiden voordat jouw PR kan worden getest: zij updaten fork-external-live-manual.yml, voegen omgevingsvariabelen toe aan ci.yml en configureren de GitHub Secrets.

Je PR moet deze wijzigingen uit fase 2 bevatten naast je signer-code:

  • ci.yml: voeg je Rust-feature toe aan de backend-matrix (vóór all) en je integratietestfunctie aan de test-matrix
  • typescript-ci.yml: voeg je package toe aan de unit- en integratietestmatrices en voeg omgevingsvariabelen toe aan de integratieteststap
  • typescript-publish.yml: voeg je package toe aan PUBLISH_PACKAGES, de packages-array van de GitHub Release en de overzichtstabel
  • python-ci.yml: voeg de backend toe aan de CI_SIGNER_*-gating in resolve-signers en aan de integratiematrix
  • go-ci.yml: voeg de backend toe aan de CI_SIGNER_*-gating en aan moduleBySigner

TypeScript Signer

Als je ook een TypeScript signer-package toevoegt, maak deze dan aan op typescript/packages/your-signer/. Belangrijkste patronen:

  • De factoryfunctie createYourSigner() retourneert SolanaTransactionSigner<TAddress> & SolanaMessageSigner<TAddress>; houd de signer-klasse intern en exporteer alleen de factory en het bijbehorende configuratietype
  • Exporteer config-interface (YourSignerConfig)
  • Als je backend geen berichten kan ondertekenen, laat dan SolanaMessageSigner weg en stel helemaal geen signMessages-methode beschikbaar: Kit classificeert signers op basis van de aanwezigheid van methoden, dus een stub die een fout gooit zou de signer verkeerd classificeren
  • Dwing HTTPS af op apiBaseUrl-configuratievelden met assertHttpsUrl() uit @solana/keychain-core
  • Doe remote aanroepen via fetchSignerJson() uit @solana/keychain-core, dat foutmapping, responssanering, het weigeren van redirects en de standaardtimeout afhandelt
  • Saneer alle andere foutteksten van remote API's met sanitizeRemoteErrorResponse() uit @solana/keychain-core
  • Gebruik throwSignerError(SignerErrorCode.*, { cause, message }) van @solana/keychain-core
  • Respecteer config?.abortSignal in elke ondertekeningsmethode en geef het door aan fetchSignerJson() en signBatchStaggered()
  • Valideer requestDelayMs met validateRequestDelayMs() en verwerk batches met signBatchStaggered()
  • Voeg @throws JSDoc toe aan factory-functies met een lijst van foutcodes
  • Declareer @solana/*-packages als peer dependencies op >=8.1.0
  • Unittests met vitest en gemockte fetch; integratietests met runSignerIntegrationTest uit @solana/keychain-test-utils

Sending signers

Als je provider de transactie herschrijft of aan de serverzijde verzendt, kan de handtekening niet op de transactie van de aanroeper worden toegepast, dus moet het een SolanaSendingSigner zijn in plaats van een SolanaTransactionSigner:

  • Implementeer signAndSendTransactions() en stel geen signTransactions beschikbaar (en ook geen signMessages, tenzij de backend echt berichten ondertekent)
  • Respecteer config?.abortSignal. Zodra de provider de create heeft geaccepteerd, meld dan elke fout (inclusief een abort) als BROADCAST_UNCONFIRMED met het transactie-id van de provider, waarbij de abort-reden als cause behouden blijft
  • Voeg een getypeerde createKeychainSigner-overload toe die het type van je sending signer retourneert
  • Sluit je backend uit van KeychainKitPluginConfig in typescript/packages/kit-plugin/src/keychain-plugin.ts, aangezien een sending signer niet kan dienen als payer of identity van een Kit-client
  • Controleer in unittests dat isTransactionPartialSigner() false en isTransactionSendingSigner() true is voor je instanties

Update Overkoepelend Pakket

Update typescript/packages/keychain/ (7 bestanden om te wijzigen):

  1. src/types.ts: voeg YourSignerConfig toe aan de discriminated union KeychainSignerConfig
  2. src/create-keychain-signer.ts: voeg de switch case toe (elk backend-package wordt geladen met een dynamische import())
  3. src/resolve-address.ts: voeg toe aan de switch case van het fast-path of fetch-path
  4. src/index.ts: voeg de exports van het configuratietype, de namespace en de factoryfunctie toe
  5. package.json: voeg de dependency @solana/keychain-your-signer: "workspace:*" toe
  6. tsconfig.json: voeg de verwijzing { "path": "../your-signer" } toe
  7. typescript/scripts/test-treeshake-umbrella.mjs: voeg je package toe aan SIGNER_MARKERS en je factory aan FACTORIES

De switch-statements hebben exhaustive never-checks, dus TypeScript geeft een foutmelding als je iets aan de union toevoegt maar een case mist. Draai daarna just ts-treeshake: elk package en de umbrella moeten tree-shakable blijven.

Python-signer

Maak de module aan op python/src/solana_keychain/your_signer/ (__init__.py plus signer.py). Belangrijke patronen:

  • Maak een subklasse van SolanaSigner uit solana_keychain.core (pubkey-property, async sign_message(), async is_available()) plus precies één capability- klasse: TransactionSigner (async sign_transaction()), ModifyingSigner (async modify_and_sign_transaction()) of SendingSigner (async sign_and_send_transaction())
  • Exporteer een async create_your_signer()-factory die init() afwacht als de backend zijn adres remote opvraagt; vóór init geeft ondertekenen de fout SignerErrorCode.NOT_INITIALIZED
  • Exporteer een configuratie-dataclass (YourSignerConfig) waarbij geheimen zijn gemarkeerd met field(repr=False)
  • Dwing HTTPS af op base-URL-velden met assert_https_url() en leid elk verzoek via fetch_signer_json()
  • Verifieer de handtekening van de provider voordat je die retourneert; gebruik signed_message_bytes() voor de bytes die een handtekening dekt, nooit een zelfgemaakt versieprefix
  • Registreer de backend in _BACKENDS in python/src/solana_keychain/keychain.py
  • Als de backend een provider-SDK nodig heeft, bescherm dan de import, geef een ImportError weer met pip install 'solana-keychain[your-signer]', declareer de extra in python/pyproject.toml en importeer de backend nooit direct vanuit de root van het pakket
  • Unittests met pytest: respx voor HTTP-backends, stub-clients voor SDK-backends en een controle dat er geen geheim voorkomt in str() of repr() van opgeworpen fouten
  • Voeg een make_your_signer()-factory toe aan python/tests/integration/test_live_signers.py en registreer deze in _MESSAGE_CAPABLE (of _TRANSACTION_ONLY voor backends zonder sign_message)

Go-signer

Elke Go-backend is een eigen module, in go/signers/yoursigner/ met modulepad github.com/solana-foundation/solana-keychain/go/signers/yoursigner/v2. Zie go/signers/para/ voor een referentie. Belangrijkste patronen:

  • Implementeer core.SolanaSigner (Pubkey(), SignMessage(ctx, message), IsAvailable(ctx)) plus precies één capability-interface: core.TransactionSigner, core.ModifyingSigner of core.SendingSigner. De capability wordt gedetecteerd via type assertion, dus een backend die geen transactie kan ondertekenen mag helemaal geen SignTransaction-methode hebben
  • Exporteer een Config-struct en een New-constructor (New(ctx, cfg) wanneer de backend zijn adres extern ophaalt; geef daarvoor core.NewNotInitializedError terug)
  • Bouw HTTP-clients met core.NewHTTPClient of core.ResolveHTTPClient en valideer basis-URL's met core.NormalizeHTTPSBaseURL
  • Onderteken transacties met core.SignTransactionWith, dat de bytes van het bericht ondertekent en de handtekening toevoegt; verifieer handtekeningen van de provider met core.VerifySignature
  • Geef fouten terug met core.NewSignerError / core.WrapSignerError en stabiele codes zoals core.CodeSigningFailed
  • Unittests met httptest; integratietests in integration_test.go achter de tag //go:build integration

Indiening Checklist

Voordat je je PR indient:

  • Code compileert zonder waarschuwingen (just build)
  • Alle tests slagen (just test)
  • Code is geformatteerd/linting slaagt (just fmt)
  • Geen hardcoded waarden of secrets in code
  • Foutmeldingen zijn generiek (geen ruwe API-response tekst)
  • Geen .expect() of .unwrap() op onbetrouwbare API-responses
  • HTTPS afgedwongen op externe HTTP-clients
  • HTTP-timeouts geconfigureerd via HttpClientConfig
  • Geretourneerde handtekeningen geverifieerd tegen lokaal berekende berichtbytes
  • Volgt de naamconventies (snake_case voor Rust en Python, camelCase voor TypeScript)
  • Integratietests toegevoegd en .env.example bijgewerkt
  • TypeScript-, Python- en Go-implementaties inbegrepen (of gepland met maintainers)
  • Overkoepelende pakketten bijgewerkt (TypeScript-umbrella, Python _BACKENDS)
  • CI-wijzigingen inbegrepen
  • Toegevoegd aan de tabellen met ondersteunde backends en signer-capabilities in README.md

Implementatietips

Foutafhandeling

Gebruik altijd de bestaande SignerError-varianten. Zowel Display als Debug op SignerError zijn geredigeerd, houd berichten dus algemeen en neem nooit ruwe API- responsdata op. Gebruik nooit .expect() of .unwrap() op onbetrouwbare API-responses:

// 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}")))?;

Beveiligingsvereisten

Dit zijn geen suggesties: gebruikers kiezen een backend op basis hiervan. Als uw provider er een onmogelijk maakt, vermeld dit dan in de PR en in de docs/SECURITY_MODEL.md van de repo.

  • Verifieer wat de provider heeft ondertekend. Bereken de berichtbytes lokaal en verifieer de geretourneerde handtekening ertegen op de vereiste positie van uw signer voordat u deze toevoegt. Een handtekening die niet verifieert moet mislukken en mag nooit worden toegevoegd.
  • Geef aan of de provider broadcast. Als de provider server-side uitvoert, implementeer dan de sending-variant en geef de handtekening terug die de verwerkte transactie identificeert. Als de provider geen sign-only-endpoint heeft, maak de backend dan sending-only in plaats van achter de rug van de aanroeper om te versturen.
  • Laat de aanroeper nooit achter met een transactie die de handtekening niet dekt. Een provider die de transactie herschrijft, ondertekent zijn eigen bytes: laat ofwel de transactie van de aanroeper ongemoeid (het geval met beheerde broadcast), of hanteer de modifying-variant en vervang de transactie volledig door de bytes van de provider.
  • Meld onzekerheid over de broadcast als zodanig. Een fout na het versturen kan nog steeds zijn verwerkt. Geef BROADCAST_UNCONFIRMED terug in plaats van een algemene fout, neem de transactie-id van de provider mee wanneer u die hebt, en stuur bij elke create een van het bericht afgeleide idempotency-sleutel mee, zodat een byte-identieke herhaling veilig is.
  • Onderteken nooit vóór init. Gebruik Option<Pubkey> (niet Pubkey::default()) voor de publieke sleutel vóór init() en laat het mislukken met een configuratiefout als er eerder wordt geprobeerd te ondertekenen.
  • Log nooit gevoelige gegevens (privésleutels, API-geheimen, ruwe API-responses) en gebruik een Debug-implementatie die gevoelige velden verbergt
  • Dwing HTTPS af (https_only(true), afgeschermd met #[cfg(not(test))] voor wiremock) en configureer request- en connect-timeouts via HttpClientConfig (standaard: 30s/5s)
  • Zeroize tussenliggend sleutelmateriaal met zeroize::Zeroizing<Vec<u8>>

Testen met Mocks

Gebruik wiremock voor het mocken van HTTP-API's. Assert alleen op het fouttype, niet op de fouttekst:

#[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
}
}

Hulp krijgen

  • Bekijk bestaande signer-implementaties voor patronen:
    • src/memory/mod.rs: Eenvoudig, synchroon
    • src/privy/mod.rs: Vereist initialisatie
    • src/turnkey/mod.rs: Complexe afhandeling van handtekeningen
    • src/vault/mod.rs: Externe clientbibliotheek
    • typescript/packages/para/, python/src/solana_keychain/para/ en go/signers/para/: volledige referenties in de andere talen
  • Belangrijke bestanden: src/traits.rs (trait-definities), src/transaction_util.rs en src/signature_util.rs (gedeelde hulpfuncties), src/http_client_config.rs (timeout- configuratie)
  • De volledige repositorygids: docs/ADDING_SIGNERS.md
  • Open een issue voor ontwerpbesprekingen voordat je aan het werk gaat

Voorbeeld PR-structuur

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 rechten voorbehouden.