Installation
Add solana-keychain to your Cargo.toml with only the backends you need:
[dependencies]# Memory signer only (default, Solana SDK 2.x)solana-keychain = "2.0"# Or specify backends explicitly (pick exactly one SDK feature)solana-keychain = { version = "2.0", default-features = false, features = ["vault", "aws_kms", "sdk-v2"] }# All remote backends on the Solana 4.x SDK linesolana-keychain = { version = "2.0", default-features = false, features = ["all", "sdk-v4"] }
Upgrading from 1.x? See the Migrating to 2.0 guide.
Feature Flags
| Feature | Description |
|---|---|
memory | Local keypair signing (default) |
vault | HashiCorp Vault Transit engine |
aws_kms | AWS KMS with Ed25519 |
gcp_kms | GCP Cloud KMS with Ed25519 |
privy | Privy embedded wallets |
turnkey | Turnkey API |
fireblocks | Fireblocks MPC |
cdp | Coinbase Developer Platform |
crossmint | Crossmint managed wallets |
dfns | Dfns wallet infrastructure |
openfort | Openfort embedded wallets |
para | Para MPC wallets |
utila | Utila MPC custody |
fordefi | Fordefi MPC custody |
ledger | Ledger hardware wallet over USB-HID (Rust only, opt-in, not in all) |
all | Enable every backend except ledger |
sdk-v2 | Solana SDK 2.x (default) |
sdk-v3 | Solana SDK 3.x |
sdk-v4 | Solana SDK 4.x (required for v1 transactions) |
unsafe-debug | Logs full provider error responses; never enable in production |
Exactly one of sdk-v2, sdk-v3 and sdk-v4 must be enabled.
Basic Usage
All signers implement the base
SolanaSigner trait
(pubkey, sign_message, is_available). Each backend also implements exactly
one capability trait:
TransactionSigner: signs your transaction as given.ModifyingSigner: the provider rewrites the transaction, then signs it.SendingSigner: the provider signs and broadcasts it.
use solana_keychain::{SignerError, SolanaSigner, TransactionSigner};use solana_sdk::transaction::VersionedTransaction;async fn sign_transaction(signer: &impl TransactionSigner,tx: &mut VersionedTransaction,) -> Result<(), SignerError> {// Get the signer's public keylet pubkey = signer.pubkey();println!("Signing with: {}", pubkey);// Check availability (useful for remote signers)if !signer.is_available().await {return Err(SignerError::NotAvailable("Signer offline".into()));}// Sign the transaction: returns Complete or Partiallet result = signer.sign_transaction(tx).await?;let (base64_tx, signature) = result.into_signed_transaction();println!("Signature: {}", signature);Ok(())}
Convert a legacy Transaction with .into(). Legacy and v0 transactions work
on every SDK feature; v1 transactions require sdk-v4.
Signer Capabilities
| Backend | Capability trait | sign_message |
|---|---|---|
| Crossmint | SendingSigner | Not supported |
| Fordefi native auto | SendingSigner | Yes |
| Fordefi native manual | ModifyingSigner | Yes |
| Utila | TransactionSigner | Not supported |
| CDP | TransactionSigner | UTF-8 payloads only |
| Ledger | TransactionSigner | Yes, over an off-chain envelope |
| All others (incl. Fordefi black box) | TransactionSigner | Yes |
The Signer::from_* constructors run init() for you. If you construct the
Privy, Fireblocks, Dfns, Crossmint, Para, Openfort or Utila signer types
directly, call init().await before use.
Backend Configuration
Memory Signer
For development and testing with local keypairs:
use solana_keychain::Signer;// From base58 private keylet signer = Signer::from_memory("base58_private_key")?;// From JSON keypair file pathlet signer = Signer::from_memory_file("/path/to/keypair.json")?;// From byte array stringlet signer = Signer::from_memory("[1,2,3,...]")?;
HashiCorp Vault
For self-hosted HSM with Transit engine:
use solana_keychain::Signer;let signer = Signer::from_vault("https://vault.example.com:8200".to_string(), // Vault address"hvs.xxxxx".to_string(), // Vault token"my-solana-key".to_string(), // Key name in Transit"base58_public_key".to_string(), // Expected public keyNone, // Default HTTP timeouts)?;
AWS KMS
For cloud-native signing with AWS KMS Ed25519 keys:
use solana_keychain::Signer;let signer = Signer::from_aws_kms("alias/my-solana-key".to_string(), // KMS key ID or alias"base58_public_key".to_string(), // Expected public keySome("us-east-1".to_string()), // Region (optional)).await?;
Privy
For embedded wallet signing:
use solana_keychain::Signer;let signer = Signer::from_privy("app_id".to_string(), // Privy app ID"app_secret".to_string(), // Privy app secret"wallet_id".to_string(), // Wallet IDNone, // Default HTTP timeouts).await?;
Turnkey
For non-custodial signing:
use solana_keychain::Signer;let signer = Signer::from_turnkey("api_public_key".to_string(), // Turnkey API public key"api_private_key".to_string(), // Turnkey API private key"org_id".to_string(), // Organization ID"private_key_id".to_string(), // Private key ID in Turnkey"base58_public_key".to_string(),// Expected public keyNone, // Default HTTP timeouts)?;
Fireblocks
For institutional MPC signing:
use solana_keychain::{Signer, FireblocksSignerConfig};let config = FireblocksSignerConfig {api_key: "api_key".to_string(),private_key_pem: "-----BEGIN RSA PRIVATE KEY-----\n...".to_string(),vault_account_id: "0".to_string(),asset_id: Some("SOL".to_string()), // None defaults to "SOL"; "SOL_TEST" for devnetapi_base_url: None,poll_interval_ms: None,max_poll_attempts: None,use_program_call: None, // Some(true) signs via PROGRAM_CALL (sign-only, legacy/v0, hot wallet)http_client_config: None,};let signer = Signer::from_fireblocks(config).await?;
GCP KMS
For cloud-native signing with GCP Cloud KMS Ed25519 keys:
use solana_keychain::Signer;let signer = Signer::from_gcp_kms("projects/my-project/locations/us-east1/keyRings/my-ring/cryptoKeys/my-key/cryptoKeyVersions/1".to_string(),"base58_public_key".to_string(),).await?;
CDP (Coinbase Developer Platform)
For Coinbase managed wallet signing:
use solana_keychain::{Signer, CdpSignerConfig, CDP_NETWORK_MAINNET};let signer = Signer::from_cdp(CdpSignerConfig {api_key_id: "api_key_id".to_string(), // CDP API key IDapi_key_secret: "api_key_secret".to_string(), // CDP API private key (base64 Ed25519)wallet_secret: "wallet_secret".to_string(), // CDP wallet secret (base64 PKCS#8 DER)address: "base58_address".to_string(), // Solana account addressnetwork: Some(CDP_NETWORK_MAINNET.to_string()), // Required to sign transactions with address lookup tablesapi_base_url: None,http_client_config: None, // Default HTTP timeouts})?;
CDP's sign_message accepts UTF-8 payloads only.
Crossmint
Crossmint is sending-only: its API executes every approved transaction
server-side, so there is no sign-only path, and sign_message fails. Call
sign_and_send_transaction, which leaves your transaction untouched and returns
the signature of the transaction that landed. That transaction may differ from
yours if Crossmint sponsors gas.
use solana_keychain::{Signer, CrossmintSignerConfig};let config = CrossmintSignerConfig {api_key: "api_key".to_string(),wallet_locator: "wallet_locator".to_string(),signer_secret: None,signer: None,api_base_url: None,poll_interval_ms: None,max_poll_attempts: None,};let signer = Signer::from_crossmint(config).await?;let signature = signer.as_sending_signer().expect("Crossmint is a sending signer").sign_and_send_transaction(&tx).await?;
Dfns
For Dfns wallet infrastructure signing:
use solana_keychain::{Signer, DfnsSignerConfig};let config = DfnsSignerConfig {auth_token: "auth_token".to_string(),cred_id: "cred_id".to_string(),private_key_pem: "-----BEGIN EC PRIVATE KEY-----\n...".to_string(),wallet_id: "wallet_id".to_string(),api_base_url: None,http_client_config: None,};let signer = Signer::from_dfns(config).await?;
Para
For Para MPC wallet signing:
use solana_keychain::Signer;let signer = Signer::from_para("api_key".to_string(), // Para API key"wallet_id".to_string(), // Para wallet IDNone, // Default API base URL).await?;
Openfort
For Openfort backend wallet signing:
use solana_keychain::Signer;let signer = Signer::from_openfort("sk_live_...".to_string(), // Openfort secret key"acc_...".to_string(), // Account ID"wallet_secret".to_string(), // Wallet secret (base64 PKCS#8 DER or PEM)None, // Default HTTP timeouts).await?;
Utila
For Utila MPC custody signing. sign_message is not supported, and you
broadcast the signed transaction yourself.
use solana_keychain::{Signer, UtilaSignerConfig};let signer = Signer::from_utila(UtilaSignerConfig {service_account_email: "sa@example.com".to_string(),service_account_private_key_pem: "-----BEGIN PRIVATE KEY-----\n...".to_string(),vault_id: "vault_id".to_string(),wallet_id: "wallet_id".to_string(),network: "networks/solana-devnet".to_string(),api_base_url: None,poll_interval_ms: None,max_poll_attempts: None,designated_signers: None,http_client_config: None,}).await?;
Fordefi
For Fordefi MPC custody signing. API requests are authenticated with an ECDSA
P-256 request signature. Provide exactly one of private_key_pem, or
request_signer (an Arc<dyn FordefiRequestSigner>, used to keep that key in a
KMS/HSM).
Fordefi comes as three signer types, fixed at construction by chain and
push_mode:
chain | push_mode | Type | Capability | Behavior |
|---|---|---|---|---|
None | any | FordefiBlackBoxSigner | TransactionSigner | Signs raw bytes; you broadcast |
Some(..) | None or Auto | FordefiNativeAutoSigner | SendingSigner | Fordefi may rewrite the blockhash and fees, signs and broadcasts |
Some(..) | Manual | FordefiNativeManualSigner | ModifyingSigner | Fordefi rewrites and signs; you broadcast the returned transaction |
Native manual mode replaces your transaction with the one Fordefi signed. Continue from it, never from the one you built. It must run before any other required signer.
use solana_keychain::{Signer, FordefiSignerConfig, FordefiPushMode, SolanaChainUniqueId, FordefiSolanaFee,FordefiPriorityLevel,};let config = FordefiSignerConfig {access_token: "api_token".to_string(),vault_id: "vault_uuid".to_string(),private_key_pem: Some(std::fs::read_to_string("./secret/private.pem")?),request_signer: None, // Or Some(Arc::new(your_kms_signer))public_key: "base58_public_key".to_string(), // Solana vault addresschain: Some(SolanaChainUniqueId::SolanaDevnet), // None for black box modefee: Some(FordefiSolanaFee::Priority {priority_level: FordefiPriorityLevel::Medium,}),push_mode: Some(FordefiPushMode::Auto), // Manual: sign without broadcastingapi_base_url: None, // Default: https://api.fordefi.compoll_interval_ms: None, // Default: 2000max_poll_attempts: None, // Default: 50http_client_config: None, // Default HTTP timeouts};let signer = Signer::from_fordefi(config).await?;
Ledger
For hardware wallet signing over USB-HID (Rust only). Requires the ledger
feature, an unlocked device running the Solana app and, on Linux, Ledger's udev
rules. Ledger is not covered by the current audit; see
audit status.
use solana_keychain::{LedgerConfig, Signer};let signer = Signer::from_ledger(LedgerConfig {confirm_pubkey_on_device: true,..LedgerConfig::default()}).await?;
sign_message signs an off-chain envelope wrapping your payload, not the raw
bytes; rebuild it with solana_keychain::ledger::ledger_offchain_envelope to
verify. Set auto_open_app: false for unattended use. A prompt declined on the
device returns SignerError::UserRejected.
Unified Signer Enum
Use the Signer enum to select backends at runtime:
use solana_keychain::{Signer, SolanaSigner, SignerError};async fn get_signer(backend: &str) -> Result<Signer, SignerError> {match backend {"memory" => Signer::from_memory("base58_key"),"vault" => Signer::from_vault(/* ... */),"kms" => Signer::from_aws_kms(/* ... */).await,_ => Err(SignerError::ConfigError("Unknown backend".into())),}}// Use it genericallylet signer = get_signer("kms").await?;let pubkey = signer.pubkey();
Signer implements only SolanaSigner. Reach a capability through
as_transaction_signer(), as_modifying_signer() or as_sending_signer(),
which return None when the backend lacks that shape:
if let Some(transaction_signer) = signer.as_transaction_signer() {let result = transaction_signer.sign_transaction(&mut tx).await?;}
Or let sign_and_send route by backend. The crate has no RPC client, so you
supply the broadcast; for a sending backend the closure is never called:
let signature = signer.sign_and_send(&mut tx, |encoded| async move { broadcast(encoded).await }).await?;
Error Handling
SignerError::BroadcastUnconfirmed { provider_tx_id, idempotency_key, transaction_signature, .. }
(from Crossmint, Fordefi native auto, or a failing sign_and_send closure)
means the transaction may have landed. Do not retry blindly: reconcile
provider_tx_id or transaction_signature first, or resend only the
byte-identical transaction. Exhaustive matches on SignerError need this arm.
See the
security model
for what each signing shape guarantees.
Adding Custom Signers
See the Adding Signers guide to integrate additional key management services.
Is this page helpful?