Rust

Installation

Add solana-keychain to your Cargo.toml with only the backends you need:

Cargo.toml
[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 line
solana-keychain = { version = "2.0", default-features = false, features = ["all", "sdk-v4"] }

Upgrading from 1.x? See the Migrating to 2.0 guide.

Feature Flags

FeatureDescription
memoryLocal keypair signing (default)
vaultHashiCorp Vault Transit engine
aws_kmsAWS KMS with Ed25519
gcp_kmsGCP Cloud KMS with Ed25519
privyPrivy embedded wallets
turnkeyTurnkey API
fireblocksFireblocks MPC
cdpCoinbase Developer Platform
crossmintCrossmint managed wallets
dfnsDfns wallet infrastructure
openfortOpenfort embedded wallets
paraPara MPC wallets
utilaUtila MPC custody
fordefiFordefi MPC custody
ledgerLedger hardware wallet over USB-HID (Rust only, opt-in, not in all)
allEnable every backend except ledger
sdk-v2Solana SDK 2.x (default)
sdk-v3Solana SDK 3.x
sdk-v4Solana SDK 4.x (required for v1 transactions)
unsafe-debugLogs 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 key
let 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 Partial
let 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

BackendCapability traitsign_message
CrossmintSendingSignerNot supported
Fordefi native autoSendingSignerYes
Fordefi native manualModifyingSignerYes
UtilaTransactionSignerNot supported
CDPTransactionSignerUTF-8 payloads only
LedgerTransactionSignerYes, over an off-chain envelope
All others (incl. Fordefi black box)TransactionSignerYes

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 key
let signer = Signer::from_memory("base58_private_key")?;
// From JSON keypair file path
let signer = Signer::from_memory_file("/path/to/keypair.json")?;
// From byte array string
let 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 key
None, // 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 key
Some("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 ID
None, // 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 key
None, // 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 devnet
api_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 ID
api_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 address
network: Some(CDP_NETWORK_MAINNET.to_string()), // Required to sign transactions with address lookup tables
api_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 ID
None, // 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:

chainpush_modeTypeCapabilityBehavior
NoneanyFordefiBlackBoxSignerTransactionSignerSigns raw bytes; you broadcast
Some(..)None or AutoFordefiNativeAutoSignerSendingSignerFordefi may rewrite the blockhash and fees, signs and broadcasts
Some(..)ManualFordefiNativeManualSignerModifyingSignerFordefi 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 address
chain: Some(SolanaChainUniqueId::SolanaDevnet), // None for black box mode
fee: Some(FordefiSolanaFee::Priority {
priority_level: FordefiPriorityLevel::Medium,
}),
push_mode: Some(FordefiPushMode::Auto), // Manual: sign without broadcasting
api_base_url: None, // Default: https://api.fordefi.com
poll_interval_ms: None, // Default: 2000
max_poll_attempts: None, // Default: 50
http_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 generically
let 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?

Table of Contents

Edit Page
© 2026 Solana Foundation. All rights reserved.
Rust | Solana