Skip to main content

Deposit SOL (Shield)

Deposit SOL into the privacy pool by creating a commitment and inserting it into the Merkle tree.

Complete Deposit Example

Based on ~/workspace/source/anchor/tests/sol_tests.ts:677-916:
import * as anchor from '@coral-xyz/anchor';
import { Program } from '@coral-xyz/anchor';
import { Connection, PublicKey, LAMPORTS_PER_SOL } from '@solana/web3.js';
import { WasmFactory } from '@lightprotocol/hasher.rs';
import { Utxo, MerkleTree, getExtDataHash, prove } from '@privacy-cash/sdk';
import BN from 'bn.js';
import path from 'path';

const PROGRAM_ID = new PublicKey('9fhQBbumKEFuXtMBDw8AaQyAjCorLGJQiS3skWZdQyQD');
const FEE_RECIPIENT = new PublicKey('AWexibGxNFKTa1b5R5MN4PJr9HWnWRwf8EW9g8cLx3dM');
const DEPOSIT_FEE_RATE = 0; // 0% deposit fee

async function depositSOL() {
  // Initialize
  const connection = new Connection('https://api.mainnet-beta.solana.com');
  const lightWasm = await WasmFactory.getInstance();
  const merkleTree = new MerkleTree(26, lightWasm);
  
  const depositAmount = 20000; // 0.00002 SOL
  const calculatedDepositFee = Math.floor((depositAmount * DEPOSIT_FEE_RATE) / 10000);
  
  // Create external data
  const extData = {
    recipient: recipientPublicKey,
    extAmount: new BN(depositAmount),
    encryptedOutput1: Buffer.from('encryptedOutput1Data'),
    encryptedOutput2: Buffer.from('encryptedOutput2Data'),
    fee: new BN(calculatedDepositFee),
    feeRecipient: FEE_RECIPIENT,
    mintAddress: new PublicKey('11111111111111111111111111111112'), // SOL
  };
  
  // Create inputs (empty for deposit)
  const inputs = [
    new Utxo({ lightWasm }),
    new Utxo({ lightWasm })
  ];
  
  // Create outputs
  const outputAmount = (depositAmount - calculatedDepositFee).toString();
  const outputs = [
    new Utxo({ 
      lightWasm, 
      amount: outputAmount,
      index: merkleTree._layers[0].length 
    }),
    new Utxo({ lightWasm, amount: '0' })
  ];
  
  // Prepare Merkle paths (zero paths for empty inputs)
  const inputMerklePathIndices = inputs.map(() => 0);
  const inputMerklePathElements = inputs.map(() => {
    return [...new Array(merkleTree.levels).fill(0)];
  });
  
  // Generate commitments and nullifiers
  const inputNullifiers = await Promise.all(inputs.map(x => x.getNullifier()));
  const outputCommitments = await Promise.all(outputs.map(x => x.getCommitment()));
  
  const root = merkleTree.root();
  const calculatedExtDataHash = getExtDataHash(extData);
  const publicAmountNumber = new BN(depositAmount - calculatedDepositFee);
  
  // Prepare circuit inputs
  const circuitInput = {
    root: root,
    publicAmount: publicAmountNumber.toString(),
    extDataHash: calculatedExtDataHash,
    mintAddress: '11111111111111111111111111111112',
    
    inputNullifier: inputNullifiers,
    inAmount: inputs.map(x => x.amount.toString(10)),
    inPrivateKey: inputs.map(x => x.keypair.privkey),
    inBlinding: inputs.map(x => x.blinding.toString(10)),
    inPathIndices: inputMerklePathIndices,
    inPathElements: inputMerklePathElements,
    
    outputCommitment: outputCommitments,
    outAmount: outputs.map(x => x.amount.toString(10)),
    outBlinding: outputs.map(x => x.blinding.toString(10)),
    outPubkey: outputs.map(x => x.keypair.pubkey),
  };
  
  // Generate proof
  const keyBasePath = path.resolve('./circuits/transaction2');
  const { proof, publicSignals } = await prove(circuitInput, keyBasePath);
  
  // Parse proof for submission
  const proofInBytes = parseProofToBytesArray(proof);
  const inputsInBytes = parseToBytesArray(publicSignals);
  
  const proofToSubmit = {
    proofA: proofInBytes.proofA,
    proofB: proofInBytes.proofB.flat(),
    proofC: proofInBytes.proofC,
    root: inputsInBytes[0],
    publicAmount: inputsInBytes[1],
    extDataHash: inputsInBytes[2],
    inputNullifiers: [inputsInBytes[3], inputsInBytes[4]],
    outputCommitments: [inputsInBytes[5], inputsInBytes[6]],
  };
  
  // Find PDAs
  const [nullifier0PDA] = PublicKey.findProgramAddressSync(
    [Buffer.from('nullifier0'), Buffer.from(proofToSubmit.inputNullifiers[0])],
    PROGRAM_ID
  );
  const [nullifier1PDA] = PublicKey.findProgramAddressSync(
    [Buffer.from('nullifier1'), Buffer.from(proofToSubmit.inputNullifiers[1])],
    PROGRAM_ID
  );
  
  // Submit transaction
  const tx = await program.methods
    .transact(
      proofToSubmit,
      { extAmount: extData.extAmount, fee: extData.fee },
      extData.encryptedOutput1,
      extData.encryptedOutput2
    )
    .accounts({
      treeAccount: treeAccountPDA,
      nullifier0: nullifier0PDA,
      nullifier1: nullifier1PDA,
      nullifier2: crossCheckNullifier2PDA,
      nullifier3: crossCheckNullifier3PDA,
      recipient: extData.recipient,
      feeRecipientAccount: FEE_RECIPIENT,
      treeTokenAccount: treeTokenAccountPDA,
      globalConfig: globalConfigPDA,
      signer: signer.publicKey,
      systemProgram: anchor.web3.SystemProgram.programId
    })
    .signers([signer])
    .rpc();
  
  // Update local Merkle tree
  for (const commitment of outputCommitments) {
    merkleTree.insert(commitment);
  }
  
  console.log('Deposit successful! Transaction:', tx);
  return { outputs, outputCommitments };
}
Store the output UTXOs and their encrypted data securely. You’ll need them to withdraw funds later.

Withdraw SOL (Unshield)

Withdraw SOL from the privacy pool to any recipient address.

Complete Withdrawal Example

Based on ~/workspace/source/anchor/tests/sol_tests.ts:918-1103:
import { FIELD_SIZE } from '@privacy-cash/sdk';

const WITHDRAW_FEE_RATE = 35; // 0.35%

function calculateWithdrawalFee(amount: number): number {
  return Math.floor((amount * WITHDRAW_FEE_RATE) / 10000);
}

async function withdrawSOL(depositOutputs: Utxo[], merkleTree: MerkleTree) {
  const lightWasm = await WasmFactory.getInstance();
  
  // Use deposit outputs as withdrawal inputs
  const withdrawInputs = [
    depositOutputs[0], // UTXO from deposit
    new Utxo({ lightWasm }) // Empty UTXO
  ];
  
  const withdrawOutputs = [
    new Utxo({ 
      lightWasm, 
      amount: '3000',
      index: merkleTree._layers[0].length 
    }),
    new Utxo({ lightWasm, amount: '0' })
  ];
  
  // Calculate amounts
  const withdrawInputsSum = withdrawInputs.reduce((sum, x) => sum.add(x.amount), new BN(0));
  const withdrawOutputsSum = withdrawOutputs.reduce((sum, x) => sum.add(x.amount), new BN(0));
  const withdrawalAmount = withdrawInputsSum.sub(withdrawOutputsSum);
  const withdrawFee = new BN(calculateWithdrawalFee(withdrawalAmount.toNumber()));
  const extAmount = new BN(withdrawFee).add(withdrawOutputsSum).sub(withdrawInputsSum);
  
  // For circom, handle negative numbers with field modular arithmetic
  const withdrawPublicAmount = new BN(extAmount)
    .sub(new BN(withdrawFee))
    .add(FIELD_SIZE)
    .mod(FIELD_SIZE)
    .toString();
  
  const withdrawExtData = {
    recipient: recipientPublicKey,
    extAmount: extAmount,
    encryptedOutput1: Buffer.from('withdrawEncryptedOutput1'),
    encryptedOutput2: Buffer.from('withdrawEncryptedOutput2'),
    fee: withdrawFee,
    feeRecipient: FEE_RECIPIENT,
    mintAddress: new PublicKey('11111111111111111111111111111112'),
  };
  
  const withdrawExtDataHash = getExtDataHash(withdrawExtData);
  
  // Build Merkle paths
  const withdrawalInputMerklePathIndices = [];
  const withdrawalInputMerklePathElements = [];
  
  for (let i = 0; i < withdrawInputs.length; i++) {
    const withdrawInput = withdrawInputs[i];
    if (withdrawInput.amount.gt(new BN(0))) {
      // Get actual Merkle path for non-empty UTXO
      const commitment = await depositOutputs[i].getCommitment();
      withdrawInput.index = merkleTree.indexOf(commitment);
      
      if (withdrawInput.index < 0) {
        throw new Error(`Input commitment not found in tree`);
      }
      
      withdrawalInputMerklePathIndices.push(withdrawInput.index);
      withdrawalInputMerklePathElements.push(
        merkleTree.path(withdrawInput.index).pathElements
      );
    } else {
      // Zero path for empty UTXO
      withdrawalInputMerklePathIndices.push(0);
      withdrawalInputMerklePathElements.push(
        new Array(merkleTree.levels).fill(0)
      );
    }
  }
  
  const withdrawInputNullifiers = await Promise.all(
    withdrawInputs.map(x => x.getNullifier())
  );
  const withdrawOutputCommitments = await Promise.all(
    withdrawOutputs.map(x => x.getCommitment())
  );
  
  // Generate proof
  const withdrawInput = {
    root: merkleTree.root(),
    inputNullifier: withdrawInputNullifiers,
    outputCommitment: withdrawOutputCommitments,
    publicAmount: withdrawPublicAmount,
    extDataHash: withdrawExtDataHash,
    
    inAmount: withdrawInputs.map(x => x.amount.toString(10)),
    inPrivateKey: withdrawInputs.map(x => x.keypair.privkey),
    inBlinding: withdrawInputs.map(x => x.blinding.toString(10)),
    mintAddress: '11111111111111111111111111111112',
    inPathIndices: withdrawalInputMerklePathIndices,
    inPathElements: withdrawalInputMerklePathElements,
    
    outAmount: withdrawOutputs.map(x => x.amount.toString(10)),
    outBlinding: withdrawOutputs.map(x => x.blinding.toString(10)),
    outPubkey: withdrawOutputs.map(x => x.keypair.pubkey),
  };
  
  const keyBasePath = path.resolve('./circuits/transaction2');
  const withdrawProofResult = await prove(withdrawInput, keyBasePath);
  const withdrawProofInBytes = parseProofToBytesArray(withdrawProofResult.proof);
  const withdrawInputsInBytes = parseToBytesArray(withdrawProofResult.publicSignals);
  
  const withdrawProofToSubmit = {
    proofA: withdrawProofInBytes.proofA,
    proofB: withdrawProofInBytes.proofB.flat(),
    proofC: withdrawProofInBytes.proofC,
    root: withdrawInputsInBytes[0],
    publicAmount: withdrawInputsInBytes[1],
    extDataHash: withdrawInputsInBytes[2],
    inputNullifiers: [withdrawInputsInBytes[3], withdrawInputsInBytes[4]],
    outputCommitments: [withdrawInputsInBytes[5], withdrawInputsInBytes[6]],
  };
  
  // Find PDAs and submit transaction...
  // (Same pattern as deposit)
  
  console.log('Withdrawal successful!');
}

Deposit SPL Tokens

Deposit SPL tokens (like USDC) into the privacy pool.

SPL Token Deposit Example

Based on ~/workspace/source/anchor/tests/spl_tests.ts:896-1131:
import { getAssociatedTokenAddress, TOKEN_PROGRAM_ID } from '@solana/spl-token';
import { getMintAddressField } from '@privacy-cash/sdk';

const USDC_MINT = new PublicKey('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v');

async function depositSPLToken() {
  const lightWasm = await WasmFactory.getInstance();
  const splMerkleTree = new MerkleTree(26, lightWasm);
  
  const depositAmount = 20000; // 0.02 USDC (6 decimals)
  const calculatedDepositFee = 0; // Free deposits
  
  // Get token accounts
  const signerTokenAccount = await getAssociatedTokenAddress(
    USDC_MINT,
    signer.publicKey
  );
  const recipientTokenAccount = await getAssociatedTokenAddress(
    USDC_MINT,
    recipient.publicKey
  );
  const feeRecipientTokenAccount = await getAssociatedTokenAddress(
    USDC_MINT,
    FEE_RECIPIENT
  );
  
  const extData = {
    recipient: recipientTokenAccount,
    extAmount: new BN(depositAmount),
    encryptedOutput1: Buffer.from('encryptedOutput1Data'),
    encryptedOutput2: Buffer.from('encryptedOutput2Data'),
    fee: new BN(calculatedDepositFee),
    feeRecipient: feeRecipientTokenAccount,
    mintAddress: USDC_MINT,
  };
  
  // Convert mint to field representation
  const mintAddressBase58 = USDC_MINT.toBase58();
  const mintAddressField = getMintAddressField(USDC_MINT);
  
  const inputs = [
    new Utxo({ lightWasm, mintAddress: mintAddressBase58 }),
    new Utxo({ lightWasm, mintAddress: mintAddressBase58 })
  ];
  
  const outputAmount = (depositAmount - calculatedDepositFee).toString();
  const outputs = [
    new Utxo({ 
      lightWasm, 
      amount: outputAmount,
      index: splMerkleTree._layers[0].length,
      mintAddress: mintAddressBase58 
    }),
    new Utxo({ lightWasm, amount: '0', mintAddress: mintAddressBase58 })
  ];
  
  // Generate proof (same pattern as SOL deposit)
  const circuitInput = {
    root: splMerkleTree.root(),
    publicAmount: new BN(depositAmount - calculatedDepositFee).toString(),
    extDataHash: getExtDataHash(extData),
    mintAddress: mintAddressField, // Use field representation for SPL tokens
    // ... rest of inputs
  };
  
  // Submit using transactSpl instruction
  const [splTreeAccountPDA] = PublicKey.findProgramAddressSync(
    [Buffer.from('merkle_tree'), USDC_MINT.toBuffer()],
    PROGRAM_ID
  );
  
  const treeAta = await getAssociatedTokenAddress(
    USDC_MINT,
    globalConfigPDA,
    true // allowOwnerOffCurve
  );
  
  const depositTx = await program.methods
    .transactSpl(
      proofToSubmit,
      { extAmount: extData.extAmount, fee: extData.fee },
      extData.encryptedOutput1,
      extData.encryptedOutput2
    )
    .accounts({
      treeAccount: splTreeAccountPDA,
      nullifier0: nullifier0PDA,
      nullifier1: nullifier1PDA,
      nullifier2: nullifier2PDA,
      nullifier3: nullifier3PDA,
      globalConfig: globalConfigPDA,
      signer: signer.publicKey,
      recipient: recipient.publicKey,
      mint: USDC_MINT,
      signerTokenAccount: signerTokenAccount,
      recipientTokenAccount: recipientTokenAccount,
      treeAta: treeAta,
      feeRecipientAta: feeRecipientTokenAccount,
      tokenProgram: TOKEN_PROGRAM_ID,
      associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
      systemProgram: anchor.web3.SystemProgram.programId
    })
    .signers([signer])
    .rpc();
  
  console.log('SPL deposit successful!', depositTx);
}
SPL tokens require separate Merkle trees per mint. You cannot withdraw USDC using a SOL deposit UTXO.

Complete Transaction Cycle

Full cycle: deposit, wait, then withdraw to break the link.
import { setTimeout } from 'timers/promises';

async function completePrivacyTransaction() {
  // Step 1: Deposit
  console.log('Step 1: Depositing SOL...');
  const { outputs, outputCommitments } = await depositSOL();
  
  // Step 2: Wait for anonymity set to grow
  console.log('Step 2: Waiting for more deposits (anonymity set)...');
  await setTimeout(60000); // Wait 1 minute
  
  // Step 3: Fetch updated Merkle tree from chain
  console.log('Step 3: Syncing Merkle tree...');
  const merkleTree = await fetchMerkleTreeFromChain();
  
  // Step 4: Withdraw to different address
  console.log('Step 4: Withdrawing to recipient...');
  await withdrawSOL(outputs, merkleTree);
  
  console.log('Privacy transaction complete!');
  console.log('The link between deposit and withdrawal is now broken.');
}

Error Handling

async function safeWithdraw(outputs: Utxo[], merkleTree: MerkleTree) {
  try {
    await withdrawSOL(outputs, merkleTree);
  } catch (error) {
    if (error.toString().includes('UnknownRoot')) {
      console.error('Merkle root not found. Tree may be out of sync.');
      // Refresh Merkle tree and retry
    } else if (error.toString().includes('NullifierExists')) {
      console.error('UTXO already spent (double-spend detected).');
    } else if (error.toString().includes('InvalidProof')) {
      console.error('ZK proof verification failed.');
    } else {
      console.error('Transaction failed:', error);
    }
  }
}

Next Steps

API Reference

Explore detailed API documentation

Advanced Usage

Learn advanced integration patterns

Build docs developers (and LLMs) love