This guide covers how wallet developers can integrate Solana's Confidential Balances feature using Rust.
Wallets integrating confidential transfers need to handle:
- Encryption Key Management - Deriving and storing ElGamal and AES keys
- Balance Display - Decrypting confidential balances for users
- Transaction Building - Creating confidential transfer transactions with proofs
- Pending Balance Handling - Managing the two-stage balance model
Each confidential token account requires two encryption keys derived from wallet signatures:
┌───────────────────────────────────────────────────────────┐
│ KEY DERIVATION FLOW │
├───────────────────────────────────────────────────────────┤
│ │
│ 1. ElGamal Key Derivation │
│ ├─ Seed Message: "ElGamalSecretKey" │
│ ├─ Sign: wallet.signMessage(seed + publicSeed) │
│ └─ Derive: ElGamalKeypair.fromSignature(signature) │
│ │
│ 2. AES Key Derivation │
│ ├─ Seed Message: "AeKey" │
│ ├─ Sign: wallet.signMessage(seed + publicSeed) │
│ └─ Derive: AeKey.fromSignature(signature) │
│ │
└───────────────────────────────────────────────────────────┘
use solana_sdk::signer::Signer;
use spl_token_2022::solana_zk_sdk::encryption::{
auth_encryption::AeKey,
elgamal::ElGamalKeypair,
};
/// Derive encryption keys for a token account
fn derive_encryption_keys(
signer: &dyn Signer,
token_account: &solana_sdk::pubkey::Pubkey,
) -> Result<(ElGamalKeypair, AeKey), Box<dyn std::error::Error>> {
// Derive ElGamal keypair deterministically from signer
let elgamal_keypair = ElGamalKeypair::new_from_signer(
signer,
&token_account.to_bytes(),
)?;
// Derive AES key for efficient balance viewing
let aes_key = AeKey::new_from_signer(
signer,
&token_account.to_bytes(),
)?;
Ok((elgamal_keypair, aes_key))
}- Deterministic Derivation - Same signature always produces same keys
- Key Storage - Either derive on-the-fly or store encrypted locally
- Never Transmit - Keys must never be shared with unauthorized parties
- Backup Critical - Loss of keys = permanent loss of confidential balance
| Balance Type | Encryption | Who Can View |
|---|---|---|
| Public | None | Anyone |
| Pending | ElGamal | Owner (decrypt), Auditor |
| Available | ElGamal | Owner (decrypt), Auditor |
| Decryptable Available | AES | Owner only (efficient) |
use solana_client::rpc_client::RpcClient;
use spl_token_2022::extension::{
confidential_transfer::ConfidentialTransferAccount,
BaseStateWithExtensions, StateWithExtensions,
};
use spl_token_2022::state::Account as TokenAccount;
use spl_associated_token_account::get_associated_token_address_with_program_id;
/// Get confidential balance for display
fn get_confidential_balance(
client: &RpcClient,
owner: &dyn Signer,
mint: &solana_sdk::pubkey::Pubkey,
) -> Result<u64, Box<dyn std::error::Error>> {
let token_account = get_associated_token_address_with_program_id(
&owner.pubkey(),
mint,
&spl_token_2022::id(),
);
// Derive encryption keys
let (elgamal_keypair, aes_key) = derive_encryption_keys(owner, &token_account)?;
// Fetch account data
let account_data = client.get_account(&token_account)?;
let account = StateWithExtensions::<TokenAccount>::unpack(&account_data.data)?;
// Get confidential transfer extension
let ct_extension = account.get_extension::<ConfidentialTransferAccount>()?;
// Decrypt available balance using AES (most efficient)
let decryptable_balance: spl_token_2022::solana_zk_sdk::encryption::auth_encryption::AeCiphertext =
ct_extension.decryptable_available_balance.try_into()?;
let available_balance = aes_key.decrypt(&decryptable_balance)
.ok_or("Failed to decrypt balance")?;
Ok(available_balance)
}
/// Get all balance types for comprehensive display
fn get_all_balances(
client: &RpcClient,
owner: &dyn Signer,
mint: &solana_sdk::pubkey::Pubkey,
) -> Result<BalanceBreakdown, Box<dyn std::error::Error>> {
let token_account = get_associated_token_address_with_program_id(
&owner.pubkey(),
mint,
&spl_token_2022::id(),
);
let (elgamal_keypair, aes_key) = derive_encryption_keys(owner, &token_account)?;
let account_data = client.get_account(&token_account)?;
let account = StateWithExtensions::<TokenAccount>::unpack(&account_data.data)?;
let ct_extension = account.get_extension::<ConfidentialTransferAccount>()?;
// Decrypt pending balance
let pending_lo: spl_token_2022::solana_zk_sdk::encryption::elgamal::ElGamalCiphertext =
ct_extension.pending_balance_lo.try_into()?;
let pending_hi: spl_token_2022::solana_zk_sdk::encryption::elgamal::ElGamalCiphertext =
ct_extension.pending_balance_hi.try_into()?;
let pending_lo_amount = pending_lo.decrypt_u32(elgamal_keypair.secret())
.ok_or("Failed to decrypt pending_lo")?;
let pending_hi_amount = pending_hi.decrypt_u32(elgamal_keypair.secret())
.ok_or("Failed to decrypt pending_hi")?;
let pending_total = pending_lo_amount + (pending_hi_amount << 16);
// Decrypt available balance
let decryptable_balance: spl_token_2022::solana_zk_sdk::encryption::auth_encryption::AeCiphertext =
ct_extension.decryptable_available_balance.try_into()?;
let available = aes_key.decrypt(&decryptable_balance)
.ok_or("Failed to decrypt available balance")?;
Ok(BalanceBreakdown {
public: account.base.amount,
pending: pending_total,
available,
total: account.base.amount + pending_total + available,
})
}
#[derive(Debug)]
struct BalanceBreakdown {
pub public: u64,
pub pending: u64,
pub available: u64,
pub total: u64,
}┌──────────────────────────────────────────────────────────────┐
│ TRANSFER FLOW │
├──────────────────────────────────────────────────────────────┤
│ │
│ Sender Receiver │
│ │ │ │
│ │ ───► DEPOSIT (public → pending) │ │
│ │ ───► APPLY (pending → available) │ │
│ │ │ │
│ │ ─────────── TRANSFER ───────────────────►│ │
│ │ (7 transactions) │ │
│ │ │ │
│ │ APPLY ◄─────────│ │
│ │ (pending → │ │
│ │ available) │ │
│ │
└──────────────────────────────────────────────────────────────┘
See src/transfer.rs for the full implementation. Key steps:
- Fetch recipient's ElGamal public key from their account
- Generate ZK proofs for the transfer
- Create proof context state accounts (3 transactions)
- Execute transfer (1 transaction)
- Close proof accounts (3 instructions)
use crate::transfer::transfer_confidential;
/// Example: Send confidential transfer
async fn send_confidential_transfer(
client: &RpcClient,
payer: &dyn Signer,
sender: &Keypair,
mint: &solana_sdk::pubkey::Pubkey,
recipient: &solana_sdk::pubkey::Pubkey,
amount: u64,
) -> Result<Vec<solana_sdk::signature::Signature>, Box<dyn std::error::Error>> {
// The transfer function handles all complexity:
// - Fetches recipient's ElGamal pubkey from their account
// - Fetches auditor's ElGamal pubkey from mint
// - Generates proofs
// - Creates proof context accounts
// - Executes transfer
// - Closes proof accounts
let signatures = transfer_confidential(
client,
payer,
sender,
mint,
recipient,
amount,
).await?;
println!("Transfer complete with {} transactions", signatures.len());
Ok(signatures)
}use spl_token_2022::extension::confidential_transfer::ConfidentialTransferAccount;
/// Check if account has pending balance to apply
fn has_pending_balance(
client: &RpcClient,
token_account: &solana_sdk::pubkey::Pubkey,
) -> Result<bool, Box<dyn std::error::Error>> {
let account_data = client.get_account(token_account)?;
let account = StateWithExtensions::<TokenAccount>::unpack(&account_data.data)?;
let ct_extension = account.get_extension::<ConfidentialTransferAccount>()?;
let credit_count: u64 = ct_extension.pending_balance_credit_counter.into();
Ok(credit_count > 0)
}use crate::apply_pending::apply_pending_balance;
/// Automatically apply pending balance if present
async fn auto_apply_pending(
client: &RpcClient,
owner: &dyn Signer,
mint: &solana_sdk::pubkey::Pubkey,
) -> Result<Option<solana_sdk::signature::Signature>, Box<dyn std::error::Error>> {
let token_account = get_associated_token_address_with_program_id(
&owner.pubkey(),
mint,
&spl_token_2022::id(),
);
if !has_pending_balance(client, &token_account)? {
return Ok(None);
}
let signature = apply_pending_balance(client, owner, mint).await?;
Ok(Some(signature))
}┌────────────────────────────────────────────────────────────┐
│ BALANCE DISPLAY │
├────────────────────────────────────────────────────────────┤
│ │
│ Token: USDC (Confidential) │
│ │
│ Available Balance: 1,234.56 USDC │
│ Pending Balance: 100.00 USDC [Apply] │
│ Public Balance: 50.00 USDC │
│ ───────────────────────────────────── │
│ Total: 1,384.56 USDC │
│ │
│ [Deposit] [Withdraw] [Transfer] │
│ │
└────────────────────────────────────────────────────────────┘
Present multi-tx operations as single action:
┌────────────────────────────────────────────────────────────┐
│ CONFIRM TRANSFER │
├────────────────────────────────────────────────────────────┤
│ │
│ Sending: 100.00 USDC │
│ To: Bob.sol │
│ Type: Confidential Transfer │
│ │
│ ⚠️ This will require 7 transaction signatures │
│ │
│ Estimated fees: ~0.01 SOL │
│ │
│ [Cancel] [Confirm & Sign All] │
│ │
└────────────────────────────────────────────────────────────┘
| Error | User Message | Action |
|---|---|---|
| Insufficient balance | "Insufficient confidential balance" | Show available vs requested |
| Pending not applied | "Please apply pending balance first" | Auto-apply or prompt |
| Proof generation failed | "Failed to generate transfer proof" | Retry or contact support |
| Transaction timeout | "Transfer incomplete - checking status" | Check and resume |
# Start local validator with token-2022
solana-test-validator --quiet --reset &
# Run integration tests
cargo test --test integration_test
# Run end-to-end transfer example
cargo run --example run_transfer- Key derivation produces consistent results
- Balance decryption shows correct values
- Deposit → Apply → Transfer → Apply flow works
- Withdrawal with proof generation succeeds
- Error states are handled gracefully
- Recovery from partial failures works
- Confidential Balances Sample (Rust)
- Token-2022 CLI Examples
- QuickNode Integration Guide
- This Repository's Implementation - Production-ready Rust code