Public. The source wallet address is visible in the public deposit.
Recipient
Public. The destination wallet address is visible in the public deposit.
Memo (optional)
Public. Emitted with the public deposit.
Resulting private balance
Private. Encrypted onchain.
The permissionless Ring is confidential with encrypted amount and asset.
A custom Ring can be configured as confidential or anonymous (encrypted sender, recipient, asset, and amount).
The examples use the Solana CLI wallet as the payer by default. The payer must hold devnet SOL. See How to Get Devnet SOL.
On localnet, the SDK starts the local test validator (:8899), Photon indexer (:8784), and prover (:3001), and the
client connects to them automatically without needing endpoint configuration.
cargo install --git https://github.com/helius-labs/zolana --tag v0.5.0-alpha zolana-clizolana dev start
import { createZolanaClient } from "@heliuslabs/zolana";const client = await createZolanaClient({});
The SDK returns instructions. The app signs and sends them.
sendAndConfirmFactory builds a Kit transaction, submits it, and returns the signature plus the landed slot.
Each SPL mint needs a token interface to hold deposited tokens. If the interface does not exist, create it in the same transaction as the deposit. The creator pays the account rent.
senderAddress is the recipient’s . This example deposits to the sender. The recipient can be the sender or a third-party wallet.
senderViewTag is senderAddress.confidentialViewTag(), the sender’s Solana public key in confidential rings. The indexer uses it to return matching encrypted outputs.
client.tree is the state Merkle tree that receives the new output commitment.
depositor / senderSigner funds and signs the public-to-private deposit.
DepositAsset.sol() selects SOL. DepositAsset.spl takes the mint, source token account, and token program for SPL and Token 2022 assets.
recipientOwnerHash is senderAddress.ownerHash(). That hash of the recipient’s signing and nullifier public keys is the owner field committed in the new private token account.
DEPOSIT_AMOUNT uses lamports for SOL, or the token’s base units for SPL and Token 2022 assets.
sendAndConfirm signs and submits depositIx as a Solana transaction.
Confirmation yields the landed slot used to gate the indexer fetch.
import { getSplAssetVaultAddress } from "@heliuslabs/zolana/addresses";import { getCreateSplInterfaceInstructionAsync } from "@heliuslabs/zolana/instructions";import { depositInstruction, DepositAsset } from "@heliuslabs/zolana/interface";import { TOKEN_PROGRAM_ADDRESS } from "@solana-program/token";import type { Instruction } from "@solana/kit";// 1. Check whether the mint already has an interface PDA, the escrow that holds deposited tokens.// A second create fails the transaction, so skip step 2 when the account exists.const interfaceAddress = await getSplAssetVaultAddress(mint);const interfaceAccount = await client.getAccount(interfaceAddress);const instructions: Instruction[] = [];// 2. Create the mint registry PDA and interface token account.// The authority signer pays their rent.if (!interfaceAccount) { const createInterfaceIx = await getCreateSplInterfaceInstructionAsync( { authority: senderSigner, mint, tokenProgram: TOKEN_PROGRAM_ADDRESS, }, ); instructions.push(createInterfaceIx);}// 3. Move public tokens into the sender's private balance.// A deposit from a public balance reveals sender, recipient, asset and amount.const depositIx = await depositInstruction({ tree: client.tree, depositor: senderSigner, deposits: [ { asset: DepositAsset.spl({ mint, sourceTokenAccount: sourceToken, tokenProgram: TOKEN_PROGRAM_ADDRESS, }), viewTag: senderAddress.confidentialViewTag(), recipientOwnerHash: senderAddress.ownerHash(), amount: DEPOSIT_AMOUNT, }, ],});// 4. Send the instructions in one transaction.instructions.push(depositIx);const depositTx = await sendAndConfirm(instructions);
git clone https://github.com/helius-labs/zolana-examples.gitcd zolana-examplescd typescript-clientpnpm installpnpm example examples/deposit_transfer_withdraw.ts
The examples use a confidential Ring on local/devnet here.
deposit_transfer_withdraw.ts
import { SOL_MINT, ShieldedKeypair, createZolanaClient,} from "@heliuslabs/zolana";import { LocalKeys, atSlot,} from "@heliuslabs/zolana/client";import { depositInstruction, transactInstruction, DepositAsset, TransactWithdrawal,} from "@heliuslabs/zolana/interface";import { AssetRegistry, ConfidentialTransfer, ProofInputUtxo, decryptToBalances, WithdrawalTarget,} from "@heliuslabs/zolana/transaction";import { cliKeypair, sendAndConfirmFactory, setup,} from "../src/lib.js";const DEPOSIT_AMOUNT = 10_000_000n;const TRANSFER_AMOUNT = 3_000_000n;const WITHDRAW_AMOUNT = 3_000_000n;async function main(): Promise<void> { const { clientConfig } = await setup(); const client = await createZolanaClient(clientConfig); // localnet: const client = await createZolanaClient({}); // Initialize the sender's private wallet and local authority // to decrypt transactions and sync balances. // The Solana signer and private wallet are derived from the same Ed25519 seed. const sender = ShieldedKeypair.fromKeypair( await cliKeypair(), ); const recipient = ShieldedKeypair.generate(); const senderSigner = sender.toSolanaSigner(); const senderAddress = sender.shieldedAddress(); const senderKeys = LocalKeys.fromKeypair( sender, client.proofService, ); // The SDK hands back instructions; the app owns signing and sending. const sendAndConfirm = sendAndConfirmFactory( client, senderSigner, ); // Mints that are registered with Solana Privacy Rings for privacy. const assets = new AssetRegistry(); // Deposit SOL into the sender's private balance. // A deposit from a public balance reveals // sender, recipient, asset and amount. // Alternatively, you can onramp fiat directly to a private balance. // 1. Move public SOL into the sender's private balance. // The view tag is the sender's Solana public key in confidential rings. // Used by the indexer to fetch the sender's outputs. const senderViewTag = senderAddress.confidentialViewTag(); const depositIx = await depositInstruction({ tree: client.tree, depositor: senderSigner, deposits: [ { asset: DepositAsset.sol(), viewTag: senderViewTag, recipientOwnerHash: senderAddress.ownerHash(), amount: DEPOSIT_AMOUNT, }, ], }); // 2. Send and confirm like any Solana transaction; confirmation yields the landed slot. const depositTx = await sendAndConfirm([ depositIx, ]); // 3. Fetch this transaction's outputs, gated on its confirmed slot. const depositResponse = await client.getShieldedTransactionsBySignature( depositTx.signature, atSlot(depositTx.slot), ); // 4. The sender decrypts the transaction outputs locally to read the funds deposited in this run. const balancesAfterDeposit = await decryptToBalances({ keypair: sender, registry: assets, transactions: depositResponse.transactions.map( ({ transaction }) => transaction, ), }); const depositBalance = balancesAfterDeposit.balance(SOL_MINT); if (depositBalance.amount !== DEPOSIT_AMOUNT) { throw new Error( `expected deposit amount ${DEPOSIT_AMOUNT}, got ${depositBalance.amount}`, ); } if (depositBalance.utxos.length !== 1) { throw new Error( `expected 1 deposit utxo, got ${depositBalance.utxos.length}`, ); } // Confidential SOL transfer to the recipient's private balance. // A confidential transfer reveals only sender and recipient, // not the asset or amount. // 1. Select private token accounts (UTXOs) that make up the private balance for the transfer. const transferUtxo = depositBalance.utxos[0]!; // 2. Prepare the selected UTXOs as inputs for the zero-knowledge proof. const transferInput = ProofInputUtxo.fromKeypair( transferUtxo, sender, ); // 3. Build and sign the confidential transfer. // Signing encrypts the asset and amount and produces the proof inputs for the ZK prover. const transfer = new ConfidentialTransfer( senderAddress, [transferInput], senderSigner.address, ); transfer.send( recipient.shieldedAddress(), SOL_MINT, TRANSFER_AMOUNT, ); const transferProofInputs = transfer.sign( sender, assets, ); // 4. Fetch the ZK proof to prove the sender can spend the balance without revealing asset and amount. const transferData = await client.proveTransact( transferProofInputs, senderKeys, ); // 5. Build the instruction with the state Merkle tree and Solana accounts required for the transfer. // Private transfers move balances only between private token accounts, not public token accounts. const transferInstruction = await transactInstruction({ payer: senderSigner, ownerSigners: [senderSigner], inputTree: client.tree, outputTree: client.tree, data: transferData, }); // 6. Send and confirm like any Solana transaction; confirmation yields the landed slot. const transferTx = await sendAndConfirm([ transferInstruction, ]); // 7. Fetch this transaction's outputs, gated on its confirmed slot. const transferResponse = await client.getShieldedTransactionsBySignature( transferTx.signature, atSlot(transferTx.slot), ); const balancesAfterTransfer = await decryptToBalances({ keypair: sender, registry: assets, transactions: transferResponse.transactions.map( ({ transaction }) => transaction, ), }); const transferBalance = balancesAfterTransfer.balance(SOL_MINT); if ( transferBalance.amount !== DEPOSIT_AMOUNT - TRANSFER_AMOUNT ) { throw new Error( `expected remaining amount from this run ${DEPOSIT_AMOUNT - TRANSFER_AMOUNT}, got ${transferBalance.amount}`, ); } if (transferBalance.utxos.length !== 1) { throw new Error( `expected 1 transfer utxo, got ${transferBalance.utxos.length}`, ); } // Withdraw SOL from the sender's private balance to their public balance. // A withdrawal reveals the sender, recipient, asset, and amount. // 1. Select private token accounts (UTXOs) that make up the private balance for the withdrawal. const withdrawalUtxo = transferBalance.utxos[0]!; // 2. Prepare the selected UTXOs as inputs for the zero-knowledge proof. const withdrawalInput = ProofInputUtxo.fromKeypair( withdrawalUtxo, sender, ); // 3. Build and sign the private-to-public withdrawal. // Signing encrypts the asset and amount of the remaining private balance // and produces the proof inputs for the ZK prover. const withdrawal = new ConfidentialTransfer( senderAddress, [withdrawalInput], senderSigner.address, ); withdrawal.withdraw( SOL_MINT, WITHDRAW_AMOUNT, WithdrawalTarget.sol({ recipient: senderSigner.address, }), ); const withdrawalProofInputs = withdrawal.sign( sender, assets, ); // 4. Fetch the ZK proof to prove the sender can spend the balance. const withdrawalData = await client.proveTransact( withdrawalProofInputs, senderKeys, ); // 5. Build the instruction with the state Merkle tree and Solana accounts required for the withdrawal. const withdrawalInstruction = await transactInstruction({ payer: senderSigner, ownerSigners: [senderSigner], inputTree: client.tree, outputTree: client.tree, withdrawal: TransactWithdrawal.sol({ recipient: senderSigner.address, }), data: withdrawalData, }); // 6. Send and confirm like any Solana transaction; confirmation yields the landed slot. const withdrawalTx = await sendAndConfirm([ withdrawalInstruction, ]); // 7. Fetch this transaction's outputs, gated on its confirmed slot. const withdrawalResponse = await client.getShieldedTransactionsBySignature( withdrawalTx.signature, atSlot(withdrawalTx.slot), ); const balancesAfterWithdrawal = await decryptToBalances({ keypair: sender, registry: assets, transactions: withdrawalResponse.transactions.map( ({ transaction }) => transaction, ), }); const withdrawalBalance = balancesAfterWithdrawal.balance(SOL_MINT); if ( withdrawalBalance.amount !== DEPOSIT_AMOUNT - TRANSFER_AMOUNT - WITHDRAW_AMOUNT ) { throw new Error( `expected remaining amount from this run ${DEPOSIT_AMOUNT - TRANSFER_AMOUNT - WITHDRAW_AMOUNT}, got ${withdrawalBalance.amount}`, ); } if (withdrawalBalance.utxos.length !== 1) { throw new Error( `expected 1 withdrawal utxo, got ${withdrawalBalance.utxos.length}`, ); } // 8. Read remaining private balance and the public balance. const solanaBalance = await client.getBalance( senderSigner.address, ); console.log( `withdraw private_balance=${withdrawalBalance.amount} ` + `solana_balance=${solanaBalance} tx=${withdrawalTx.signature}`, );}await main();
Each SPL mint needs a token interface to hold deposited tokens. If the interface does not exist, create it in the same transaction as the deposit. The creator pays the account rent.
1. Move public SOL into the sender's private balance
use zolana_program::instruction::{AssetDeposit, Deposit, DepositAsset}; // 1. Move public SOL into the sender's private balance. let deposit_ix = Deposit { tree, depositor: sender.pubkey(), deposits: vec![AssetDeposit { asset: DepositAsset::Sol, // SPL: asset: DepositAsset::Spl(zolana_program::instruction::DepositSplAccounts { // SPL: mint: spl.mint, // SPL: user_token: spl.user_token_account, // SPL: token_program: spl.token_program, // SPL: }), view_tag: sender_shielded_address.confidential_view_tag()?, owner: sender_shielded_address.owner_hash()?, amount: DEPOSIT_AMOUNT, memo: None, }], } .instruction()?;
sender_shielded_address is the recipient’s . This example deposits to the sender. The recipient can be the sender or a third-party wallet.
view_tag is sender_shielded_address.confidential_view_tag(), the sender’s Solana public key in confidential rings. The indexer uses it to return matching encrypted outputs.
tree is the state Merkle tree that receives the new output commitment.
depositor / sender funds and signs the public-to-private deposit.
DepositAsset::Sol selects SOL. The // SPL: comments show DepositAsset::Spl with mint, source token account, and token program for SPL and Token 2022 assets.
owner is sender_shielded_address.owner_hash(). That hash of the recipient’s signing and nullifier public keys is the owner field committed in the new private token account.
DEPOSIT_AMOUNT uses lamports for SOL, or the token’s base units for SPL and Token 2022 assets. memo is None.
create_and_send_transaction signs and submits deposit_ix as a Solana transaction.
landed_slot reads the confirmation slot used to gate the indexer fetch.
sender pays the fee and authorizes the public-to-private deposit.
use zolana_interface::pda;use zolana_program::instruction::{ AssetDeposit, CreateSplInterface, Deposit, DepositAsset, DepositSplAccounts,};// 1. Check whether the mint already has an interface PDA, the escrow that holds deposited tokens.// A second create fails the transaction, so skip step 2 when the account exists.let interface_address = pda::spl_interface(&mint);let interface_account = client.get_account(interface_address)?;let mut instructions = Vec::new();// 2. Create the mint registry PDA and interface token account.// The authority signer pays their rent.if interface_account.is_none() { let create_interface_ix = CreateSplInterface { authority: sender_pubkey, mint, token_program, } .instruction(); instructions.push(create_interface_ix);}// 3. Move public tokens into the sender's private balance.// A deposit from a public balance reveals sender, recipient, asset and amount.let deposit_ix = Deposit { tree, depositor: sender_pubkey, deposits: vec![AssetDeposit { asset: DepositAsset::Spl(DepositSplAccounts { mint, user_token: source_token, token_program, }), view_tag: sender_shielded_address.confidential_view_tag()?, owner: sender_shielded_address.owner_hash()?, amount: DEPOSIT_AMOUNT, memo: None, }],}.instruction()?;// 4. Send the instructions in one transaction.instructions.push(deposit_ix);let signature = client.create_and_send_transaction( &instructions, sender_pubkey, &[&sender_solana_keypair], client.compute_budget(),)?;let slot = landed_slot(&client, signature)?;
git clone https://github.com/helius-labs/zolana-examples.gitcd zolana-examplescd rust-clientcargo run -p rust-client-example --example deposit_transfer_withdraw
The examples use a confidential Ring on local/devnet here.
deposit_transfer_withdraw.rs
use anyhow::{anyhow, Result};use rust_client_example::{cli_keypair, landed_slot, setup};use solana_address::Address;use solana_keypair::Keypair;use solana_signer::Signer;use zolana_client::{IndexerRpcConfig, Rpc, SolanaRpc, ZolanaClient};use zolana_keypair::ShieldedKeypair;use zolana_program::instruction::{ AssetDeposit, Deposit, DepositAsset, Transact, TransactInterfaceTransferAccounts, TransactSolTransferAccounts,};use zolana_transaction::{ decrypt_spendable, instructions::transact::ConfidentialTransaction, AssetRegistry, SOL_MINT,};const DEPOSIT_AMOUNT: u64 = 10_000_000;const TRANSFER_AMOUNT: u64 = 3_000_000;const WITHDRAW_AMOUNT: u64 = 3_000_000;fn main() -> Result<()> { let context = setup()?; let client = context.client()?; run(&client, context.tree, &cli_keypair()?)}pub fn run(client: &ZolanaClient<SolanaRpc>, tree: Address, payer: &Keypair) -> Result<()> { // Mints that are registered with Solana Privacy Rings for privacy. let assets = AssetRegistry::default(); // SPL: assets.insert(spl.asset_id, spl.mint)?; // Initialize the sender's private wallet and local authority // to decrypt transactions and sync balances. // The Solana signer and private wallet are derived from the same Ed25519 seed. let sender = ShieldedKeypair::from_keypair(payer)?; let recipient = ShieldedKeypair::from_keypair(&Keypair::new())?; let sender_shielded_address = sender.shielded_address()?; // Deposit SOL into the sender's private balance. // A deposit from a public balance reveals // sender, recipient, asset and amount. // Alternatively, you can onramp fiat directly to a private balance. // 1. Move public SOL into the sender's private balance. let sender_balances_after_deposit = { let deposit_ix = Deposit { tree, depositor: sender.pubkey(), deposits: vec![AssetDeposit { asset: DepositAsset::Sol, // SPL: asset: DepositAsset::Spl(zolana_program::instruction::DepositSplAccounts { // SPL: mint: spl.mint, // SPL: user_token: spl.user_token_account, // SPL: token_program: spl.token_program, // SPL: }), view_tag: sender_shielded_address.confidential_view_tag()?, owner: sender_shielded_address.owner_hash()?, amount: DEPOSIT_AMOUNT, memo: None, }], } .instruction()?; // 2. Send and confirm like any Solana transaction; the landed slot gates // the indexer fetch below. let signature = client.create_and_send_transaction( &[deposit_ix], sender.pubkey(), &[&sender], client.compute_budget(), )?; let slot = landed_slot(client, signature)?; // 3. Fetch this transaction's outputs, gated on its confirmed slot. let response = client.get_shielded_transactions_by_signature( signature, Some(IndexerRpcConfig::at_slot(slot)), )?; let transactions = response .transactions .into_iter() .map(|indexed| indexed.transaction) .collect::<Vec<_>>(); // 4. The sender decrypts the transaction outputs locally to read the funds deposited in this run. let balances = decrypt_spendable(&sender, &transactions, &assets) .map_err(|e| anyhow!("decrypt sender transactions: {e:?}"))? .balances; let sender_balance = balances .get_balance(SOL_MINT) // SPL: .get_balance(spl.mint) .expect("failed to fetch sender's utxo"); assert_eq!(sender_balance.amount, DEPOSIT_AMOUNT); assert_eq!(sender_balance.utxos.len(), 1); balances }; // Confidential SOL transfer to the recipient's private balance. // A confidential transfer reveals only sender and recipient, // not the asset or amount. let sender_balances_after_transfer = { // 1. Select UTXOs that make up the private balance for the transfer. let transfer_utxo = sender_balances_after_deposit .get_balance(SOL_MINT) // SPL: .get_balance(spl.mint) .and_then(|balance| balance.utxos.first()) .expect("failed to fetch deposited utxo") .clone(); // 2. Prepare the selected UTXOs as inputs for the zero-knowledge proof. let mut transfer = ConfidentialTransaction::new(vec![transfer_utxo], sender.pubkey())?; // 3. Build and encrypt the confidential transfer. // Encryption hides the asset and amount and produces the proof inputs for the ZK prover. transfer.transfer_sol(&recipient.shielded_address()?, TRANSFER_AMOUNT)?; // SPL: transfer.transfer(&recipient.shielded_address()?, spl.mint, TRANSFER_AMOUNT)?; let proof_inputs = transfer.encrypt(&sender)?; // 4. Fetch the zk proof to prove the sender can spend the balance without revealing asset and amount. let transfer_data = client.prove_transact(proof_inputs, None, &sender)?; // 5. Construct the instruction. let transfer_ix = Transact { payer: sender.pubkey(), input_trees: vec![tree], output_tree: tree, owner_signers: vec![sender.pubkey()], interface_transfer_accounts: Vec::new(), data: transfer_data, } .instruction(); // 6. Send and confirm like any Solana transaction; confirmation yields the landed slot. let signature = client.create_and_send_transaction( &[transfer_ix], sender.pubkey(), &[&sender], client.compute_budget(), )?; let slot = landed_slot(client, signature)?; // 7. Fetch this transaction's outputs, gated on its confirmed slot. let response = client.get_shielded_transactions_by_signature( signature, Some(IndexerRpcConfig::at_slot(slot)), )?; let transactions = response .transactions .into_iter() .map(|indexed| indexed.transaction) .collect::<Vec<_>>(); let sender_balances = decrypt_spendable(&sender, &transactions, &assets) .map_err(|e| anyhow!("decrypt sender transactions: {e:?}"))? .balances; let sender_balance = sender_balances .get_balance(SOL_MINT) // SPL: .get_balance(spl.mint) .expect("failed to fetch sender's utxo"); assert_eq!(sender_balance.amount, DEPOSIT_AMOUNT - TRANSFER_AMOUNT); assert_eq!(sender_balance.utxos.len(), 1); sender_balances }; // Withdraw SOL back to the sender's public balance. // A withdrawal from a confidential balance reveals // sender, recipient, asset and amount. { // 1. Select UTXOs that make up the private balance for the withdrawal. let withdrawal_utxo = sender_balances_after_transfer .get_balance(SOL_MINT) // SPL: .get_balance(spl.mint) .and_then(|balance| balance.utxos.first()) .expect("failed to fetch sender's utxo") .clone(); // 2. Prepare the selected UTXOs as inputs for the zero-knowledge proof. let mut withdrawal = ConfidentialTransaction::new(vec![withdrawal_utxo], sender.pubkey())?; // 3. Build and encrypt the confidential withdrawal. // Encryption hides the private change and produces the ZK prover inputs. withdrawal.withdraw_sol(WITHDRAW_AMOUNT, sender.pubkey())?; // SPL: withdrawal.withdraw(spl.mint, WITHDRAW_AMOUNT, spl.user_token_account)?; let proof_inputs = withdrawal.encrypt(&sender)?; // 4. Fetch the ZK proof to prove the sender can spend the balance. let withdrawal_data = client.prove_transact(proof_inputs, None, &sender)?; // 5. Combine the proof and withdrawal accounts in a single instruction. let withdraw_ix = Transact { payer: sender.pubkey(), input_trees: vec![tree], output_tree: tree, owner_signers: vec![sender.pubkey()], interface_transfer_accounts: vec![TransactInterfaceTransferAccounts::Sol( TransactSolTransferAccounts { recipient: sender.pubkey(), }, )], // SPL: interface_transfer_accounts: vec![ // SPL: TransactInterfaceTransferAccounts::SplWithdrawal( // SPL: zolana_program::instruction::TransactSplWithdrawalAccounts { // SPL: mint: spl.mint, // SPL: spl_interface: spl.vault, // SPL: user_token_account: spl.user_token_account, // SPL: token_program: spl.token_program, // SPL: }, // SPL: ), // SPL: ], data: withdrawal_data, } .instruction(); // 6. Send and confirm like any Solana transaction. let signature = client.create_and_send_transaction( &[withdraw_ix], sender.pubkey(), &[&sender], client.compute_budget(), )?; let slot = landed_slot(client, signature)?; // 7. Fetch this transaction's outputs, gated on its confirmed slot. let response = client.get_shielded_transactions_by_signature( signature, Some(IndexerRpcConfig::at_slot(slot)), )?; let transactions = response .transactions .into_iter() .map(|indexed| indexed.transaction) .collect::<Vec<_>>(); let sender_balances = decrypt_spendable(&sender, &transactions, &assets) .map_err(|e| anyhow!("decrypt sender transactions: {e:?}"))? .balances; let sender_balance = sender_balances .get_balance(SOL_MINT) // SPL: .get_balance(spl.mint) .expect("failed to fetch sender's utxo"); assert_eq!( sender_balance.amount, DEPOSIT_AMOUNT - TRANSFER_AMOUNT - WITHDRAW_AMOUNT ); assert_eq!(sender_balance.utxos.len(), 1); // 8. Read the funds remaining from this run and the public SOL balance. let solana_balance = client.get_balance(sender.pubkey())?; println!("withdraw solana_balance={solana_balance} tx={signature}"); // SPL: println!( // SPL: "withdraw user_token={} tx={signature}", // SPL: spl.user_token_account, // SPL: ); } Ok(())}