Withdraw
How to withdraw tokens privately using the relayer
Withdraw
Section titled “Withdraw”Withdrawing sends tokens from the privacy pool to any address without revealing which deposit is being spent. The relayer submits the transaction, preserving your privacy.
Overview
Section titled “Overview”Step-by-Step Guide
Section titled “Step-by-Step Guide”-
Load Your Deposit Secrets
Retrieve the secrets you stored during deposit:
interface DepositSecrets {nullifier: Uint8Array;secret: Uint8Array;leafIndex: number;mint: string;amount: number;}function loadDeposit(txSignature: string): DepositSecrets {const encrypted = localStorage.getItem(`deposit_${txSignature}`);const decrypted = decrypt(encrypted);const data = JSON.parse(decrypted);return {nullifier: Uint8Array.from(data.nullifier),secret: Uint8Array.from(data.secret),leafIndex: data.leafIndex,mint: data.mint,amount: data.amount,};} -
Get Merkle Proof from Indexer
Fetch the proof path for your deposit:
async function getMerkleProof(mint: string,leafIndex: number,network: string = 'mainnet'): Promise<MerkleProof> {const response = await fetch(`${API_BASE}/get_proof/${mint}/32/${leafIndex}/${network}`,{ method: 'POST' });if (!response.ok) {throw new Error(`Failed to get proof: ${response.status}`);}return response.json();} -
Get Current Merkle Root
async function getMerkleRoot(mint: string,network: string = 'mainnet'): Promise<Uint8Array> {const response = await fetch(`${API_BASE}/get_merkle/${mint}/32/${network}`,{ method: 'POST' });const data = await response.json();return Uint8Array.from(data.json.root);} -
Generate ZK Proof
Generate the withdrawal proof client-side:
import { poseidon } from '[PLACEHOLDER: POSEIDON_NPM_PACKAGE]';async function generateWithdrawProof(nullifier: Uint8Array,secret: Uint8Array,merkleProof: Uint8Array[],merkleRoot: Uint8Array,recipient: string,leafIndex: number): Promise<{ proof: Uint8Array; nullifierHash: Uint8Array }> {// Compute nullifier hashconst nullifierHash = poseidon([nullifier]);// Compute commitmentconst commitment = poseidon([nullifier, secret]);// Generate SNARK proof// This requires the circuit and proving keyconst proof = await generateProof({nullifier,secret,commitment,merkleProof,merkleRoot,nullifierHash,recipient,leafIndex,});return { proof, nullifierHash };} -
Check OFAC Compliance
Get an OFAC signature before withdrawing:
async function getOfacSignature(authToken: string,network: string = 'mainnet'): Promise<string> {const response = await fetch(`${API_BASE}/get_ofac_signature/${network}/${authToken}`,{ method: 'POST' });if (!response.ok) {throw new Error('OFAC check failed');}return response.json();} -
Call Relayer
Submit the withdrawal to the relayer:
interface RelayParams {nullifierHash: number[];proof: number[];root: number[];recipient: string;depth: number;depositSize: number;mint: string;network?: string;}interface RelayResponse {status: number;signature?: string;error?: string;}async function relay(params: RelayParams): Promise<RelayResponse> {const response = await fetch(`${API_BASE}/relay`, {method: 'POST',headers: { 'Content-Type': 'application/json' },body: JSON.stringify(params),});return response.json();}
Complete Example
Section titled “Complete Example”async function withdraw( deposit: DepositSecrets, recipient: string, authToken: string, network: string = 'mainnet'): Promise<string> { // 1. Get Merkle proof const proofPath = await getMerkleProof(deposit.mint, deposit.leafIndex, network); const merkleRoot = await getMerkleRoot(deposit.mint, network);
// 2. Generate ZK proof const { proof, nullifierHash } = await generateWithdrawProof( deposit.nullifier, deposit.secret, proofPath, merkleRoot, recipient, deposit.leafIndex );
// 3. Check OFAC compliance await getOfacSignature(authToken, network);
// 4. Call relayer const result = await relay({ nullifierHash: Array.from(nullifierHash), proof: Array.from(proof), root: Array.from(merkleRoot), recipient, depth: 32, depositSize: deposit.amount, mint: deposit.mint, network, });
if (result.status !== 200 || !result.signature) { throw new Error(result.error || 'Withdrawal failed'); }
// 5. Mark deposit as spent locally markDepositSpent(deposit);
return result.signature;}A withdrawal fee is deducted from the withdrawn amount:
- Relay Fee: [PLACEHOLDER: FEE_WITHDRAW_PCT]
The recipient receives: depositAmount - relayFee
Error Handling
Section titled “Error Handling”| Error | Cause | Solution |
|---|---|---|
NullifierAlreadySpent | Already withdrawn | Check local records |
InvalidProof | Proof verification failed | Regenerate proof |
InvalidRoot | Merkle root too old | Refetch current root |
OFACCheckFailed | Address is sanctioned | Cannot proceed |
Checking Nullifier Status
Section titled “Checking Nullifier Status”Before attempting withdrawal, check if the nullifier was already spent:
async function isNullifierSpent( mint: string, nullifierHash: Uint8Array, network: string = 'mainnet'): Promise<boolean> { const response = await fetch( `${API_BASE}/get_nullifier/${mint}/32/${network}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ nullifierHash: Array.from(nullifierHash), }), } );
const data = await response.json(); return data.json !== null; // If account exists, nullifier is spent}Privacy Best Practices
Section titled “Privacy Best Practices”| Practice | Reason |
|---|---|
| Wait before withdrawing | Larger anonymity set |
| Use a fresh recipient address | No link to your identity |
| Don’t withdraw immediately after deposit | Timing analysis protection |
| Withdraw full amount | Partial withdrawals may reduce privacy |
Next Steps
Section titled “Next Steps”- Withdraw with Swap - Swap tokens during withdrawal
- API Reference - Full relay API docs