Complete Example: End-to-End Flow
Full working example showing login, OFAC check, deposit, and withdrawal
Complete Integration Example
Section titled “Complete Integration Example”This guide walks through a complete private transfer flow: authentication, OFAC compliance, depositing tokens, and withdrawing to a different address.
Flow Overview
Section titled “Flow Overview”Prerequisites
Section titled “Prerequisites”// Required packagesimport { Connection, PublicKey, Transaction } from '@solana/web3.js';import { useWallet } from '@solana/wallet-adapter-react';import bs58 from 'bs58';Configuration
Section titled “Configuration”const CONFIG = { // API endpoints API_URL: 'https://worker.turbine.cash',
// Program addresses PROGRAM_ID: 'FGKoWNsvTTDCGW9JyR2DJWNzSXpejWk7yXcKsFFj9GQp',
// Merkle tree depth (only 32 supported) DEPTH: 32,
// Network NETWORK: 'mainnet', // or 'devnet'
// Token mints USDC_MINT: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',};Step 1: Authentication
Section titled “Step 1: Authentication”Sign a timestamp with your wallet to authenticate with the relay API.
interface LoginRequest { wallet: string; signature: string; message: number;}
interface LoginResponse { id: string; // UUID auth token}
async function login(wallet: WalletContextState): Promise<string> { // Generate timestamp (Unix seconds) const message = Math.floor(Date.now() / 1000);
// Sign the timestamp const messageBytes = new TextEncoder().encode(String(message)); const signatureBytes = await wallet.signMessage!(messageBytes); const signature = bs58.encode(signatureBytes);
// Call login endpoint const response = await fetch(`${CONFIG.API_URL}/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ wallet: wallet.publicKey!.toBase58(), signature, message, } as LoginRequest), });
if (!response.ok) { throw new Error(`Login failed: ${await response.text()}`); }
const { id } = await response.json() as LoginResponse; return id; // This is your auth token}Step 2: OFAC Compliance Check
Section titled “Step 2: OFAC Compliance Check”Before depositing, obtain an OFAC compliance signature from the server.
interface OfacSignature { signature: number[]; // 64-byte Ed25519 signature message: number; // Block timestamp}
async function getOfacSignature(authToken: string): Promise<OfacSignature> { const response = await fetch( `${CONFIG.API_URL}/get_ofac_signature/${CONFIG.NETWORK}/${authToken}`, { method: 'POST' } );
if (!response.ok) { const error = await response.text(); if (error.includes('high risk')) { throw new Error('Wallet flagged by OFAC screening. Cannot proceed.'); } throw new Error(`OFAC check failed: ${error}`); }
return await response.json();}Step 3: Deposit
Section titled “Step 3: Deposit”3.1 Generate Cryptographic Commitment
Section titled “3.1 Generate Cryptographic Commitment”// Types for deposit datainterface DepositCredentials { id: string; secret: bigint; nullifier: bigint; index: number; mint: string; amount: number; depth: number; network: string; timestamp: number;}
// Generate random 256-bit valuesfunction generateRandomBigInt(): bigint { const bytes = new Uint8Array(32); crypto.getRandomValues(bytes); return BigInt('0x' + Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join(''));}
async function generateCommitment(): Promise<{ secret: bigint; nullifier: bigint; commitment: Uint8Array;}> { // Generate random secret and nullifier const secret = generateRandomBigInt(); const nullifier = generateRandomBigInt();
// Compute commitment = Poseidon(secret, nullifier) // Note: Use the Poseidon implementation that matches the circuit const commitment = await poseidonHash([secret, nullifier]);
return { secret, nullifier, commitment };}3.2 Build and Send Deposit Transaction
Section titled “3.2 Build and Send Deposit Transaction”async function deposit( wallet: WalletContextState, connection: Connection, mint: string, amount: number, ofacSignature: OfacSignature,): Promise<{ txSignature: string; credentials: DepositCredentials }> { // Generate commitment const { secret, nullifier, commitment } = await generateCommitment();
// Get current merkle tree state to find next leaf index const merkleState = await getMerkleState(mint, CONFIG.DEPTH); const index = merkleState.nextIndex;
// Generate deposit proof (Groth16) // This proves you know the preimage of the commitment const depositProof = await generateDepositProof({ secret, nullifier, commitment, // ... other circuit inputs });
// Build deposit instruction const depositInstruction = await buildDepositInstruction({ signer: wallet.publicKey!, mint: new PublicKey(mint), index, commitment: Array.from(commitment), depositSize: amount, signature: ofacSignature.signature, message: ofacSignature.message, proof: depositProof, depth: CONFIG.DEPTH, });
// Create and send transaction const transaction = new Transaction().add(depositInstruction); transaction.feePayer = wallet.publicKey!; transaction.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
const signedTx = await wallet.signTransaction!(transaction); const txSignature = await connection.sendRawTransaction(signedTx.serialize()); await connection.confirmTransaction(txSignature);
// Save credentials locally const credentials: DepositCredentials = { id: crypto.randomUUID(), secret, nullifier, index, mint, amount, depth: CONFIG.DEPTH, network: CONFIG.NETWORK, timestamp: Date.now(), };
saveDepositLocally(credentials);
return { txSignature, credentials };}
function saveDepositLocally(credentials: DepositCredentials): void { // Store in encrypted localStorage const existing = JSON.parse(localStorage.getItem('turbine_deposits') || '[]'); existing.push({ ...credentials, secret: credentials.secret.toString(), nullifier: credentials.nullifier.toString(), }); localStorage.setItem('turbine_deposits', JSON.stringify(existing));}Step 4: Withdraw
Section titled “Step 4: Withdraw”After waiting for your deposit to join the anonymity set (recommended: wait for several other deposits), you can withdraw.
4.1 Load Saved Credentials
Section titled “4.1 Load Saved Credentials”function loadDeposit(depositId: string): DepositCredentials | null { const deposits = JSON.parse(localStorage.getItem('turbine_deposits') || '[]'); const deposit = deposits.find((d: any) => d.id === depositId);
if (!deposit) return null;
return { ...deposit, secret: BigInt(deposit.secret), nullifier: BigInt(deposit.nullifier), };}4.2 Generate Withdrawal Proof
Section titled “4.2 Generate Withdrawal Proof”async function withdraw( depositId: string, recipientAddress: string,): Promise<string> { // Load deposit credentials const deposit = loadDeposit(depositId); if (!deposit) { throw new Error('Deposit not found'); }
// Compute nullifier hash const nullifierHash = await poseidonHash([deposit.nullifier]);
// Check if already spent const isSpent = await checkNullifierSpent( deposit.mint, deposit.depth, nullifierHash, ); if (isSpent) { throw new Error('Deposit already withdrawn'); }
// Fetch merkle proof from indexer const merkleProof = await getMerkleProof( deposit.mint, deposit.depth, deposit.index, );
// Generate withdrawal proof (Groth16) // Proves: "I know secret & nullifier for a commitment in this merkle tree" const withdrawProof = await generateWithdrawProof({ secret: deposit.secret, nullifier: deposit.nullifier, pathElements: merkleProof.pathElements, pathIndices: merkleProof.pathIndices, root: merkleProof.root, recipient: recipientAddress, nullifierHash, });
// Submit to relay const response = await fetch(`${CONFIG.API_URL}/relay`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ nullifierHash: Array.from(nullifierHash), proof: Array.from(withdrawProof), root: Array.from(merkleProof.root), recipient: recipientAddress, depth: deposit.depth, depositSize: deposit.amount, mint: deposit.mint, network: CONFIG.NETWORK, }), });
const result = await response.json();
if (result.status !== 200) { throw new Error(`Withdrawal failed: ${result.error}`); }
// Mark deposit as spent locally markDepositAsSpent(depositId);
return result.signature;}4.3 Helper Functions
Section titled “4.3 Helper Functions”async function getMerkleState(mint: string, depth: number) { const response = await fetch( `${CONFIG.API_URL}/get_merkle/${mint}/${depth}/${CONFIG.NETWORK}`, { method: 'POST' } ); const { json } = await response.json(); return json;}
async function getMerkleProof(mint: string, depth: number, index: number) { const response = await fetch( `${CONFIG.API_URL}/get_proof/${mint}/${depth}/${index}/${CONFIG.NETWORK}`, { method: 'POST' } ); const { json } = await response.json(); return { pathElements: json.path, pathIndices: json.indices, root: await getCurrentRoot(mint, depth), };}
async function checkNullifierSpent( mint: string, depth: number, nullifierHash: Uint8Array,): Promise<boolean> { const response = await fetch( `${CONFIG.API_URL}/get_nullifier/${mint}/${depth}/${CONFIG.NETWORK}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ nullifierHash: Array.from(nullifierHash) }), } ); const { json } = await response.json(); return json !== null;}Complete Usage Example
Section titled “Complete Usage Example”async function privateTransfer( wallet: WalletContextState, connection: Connection, amount: number, recipientAddress: string,) { console.log('1. Logging in...'); const authToken = await login(wallet);
console.log('2. Getting OFAC signature...'); const ofacSignature = await getOfacSignature(authToken);
console.log('3. Depositing...'); const { txSignature, credentials } = await deposit( wallet, connection, CONFIG.USDC_MINT, amount, ofacSignature, ); console.log(` Deposit tx: ${txSignature}`); console.log(` Deposit ID: ${credentials.id}`);
console.log('4. Waiting for anonymity set...'); // In production, wait for more deposits to join the pool // This is crucial for privacy await new Promise(resolve => setTimeout(resolve, 60000));
console.log('5. Withdrawing...'); const withdrawTx = await withdraw(credentials.id, recipientAddress); console.log(` Withdraw tx: ${withdrawTx}`);
console.log('Transfer complete!');}Error Handling
Section titled “Error Handling”| Error | Cause | Solution |
|---|---|---|
Login failed | Invalid signature or stale timestamp | Re-sign with fresh timestamp |
high risk | OFAC screening flagged wallet | Cannot use this wallet |
NullifierAlreadySpent | Already withdrawn | Check deposit status |
InvalidProof | Proof verification failed | Verify secret/nullifier match |
InvalidRoot | Merkle root not recognized | Wait for indexer sync, retry |
RelayFailed | Transaction submission error | Check Solana network status |
Security Checklist
Section titled “Security Checklist”- Secret and nullifier are generated client-side only
- Credentials are stored encrypted in browser storage
- Credentials are backed up (e.g., encrypted file download)
- Never transmit secret/nullifier to any server
- Wait for sufficient anonymity set before withdrawing
- Use a fresh wallet address for receiving withdrawals