πŸ‘¨β€πŸ³Cookbook

ZK Void Confidential Transfer Integration Cookbook

This cookbook provides practical examples and patterns for integrating confidential transfers into your dApps using the ZK Void API.

Table of Contents

  • Quick Start

  • Common Patterns

  • Use Cases

  • Advanced Examples

  • Troubleshooting

  • Best Practices

Quick Start

1. Basic Setup

import { Connection, PublicKey, Transaction } from '@solana/web3.js';
import { useWallet } from '@solana/wallet-adapter-react';

const API_BASE_URL = 'https://api.zkvoid.com';
const RPC_URL = 'https://api.mainnet-beta.solana.com'; // Use your preferred RPC

class ConfidentialTransferClient {
  private connection: Connection;

  constructor(rpcUrl: string = RPC_URL) {
    this.connection = new Connection(rpcUrl, 'confirmed');
  }

  async callAPI<T>(endpoint: string, data: any): Promise<T> {
    const response = await fetch(`${API_BASE_URL}${endpoint}`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data),
    });

    if (!response.ok) {
      throw new Error(`API request failed: ${response.statusText}`);
    }

    const result = await response.json();
    if (!result.success) {
      throw new Error(result.error);
    }

    return result.data;
  }

  async getKeyDerivationSignature(wallet: any): Promise<string> {
    const message = `Confidential Transfer Key Derivation: ${wallet.publicKey.toString()}`;
    const messageBytes = new TextEncoder().encode(message);
    const signature = await wallet.signMessage(messageBytes);
    return Buffer.from(signature).toString('base64');
  }

  async executeTransaction(wallet: any, transactionBase64: string): Promise<string> {
    const transaction = Transaction.from(Buffer.from(transactionBase64, 'base64'));
    const { blockhash } = await this.connection.getLatestBlockhash();
    
    transaction.recentBlockhash = blockhash;
    transaction.feePayer = wallet.publicKey;
    
    const signedTx = await wallet.signTransaction(transaction);
    const signature = await this.connection.sendRawTransaction(signedTx.serialize());
    await this.connection.confirmTransaction(signature, 'confirmed');
    
    return signature;
  }
}

2. React Component Example

import React, { useState, useEffect } from 'react';
import { useWallet } from '@solana/wallet-adapter-react';

interface ConfidentialWalletProps {
  mintAddress: string;
}

export function ConfidentialWallet({ mintAddress }: ConfidentialWalletProps) {
  const wallet = useWallet();
  const [client] = useState(() => new ConfidentialTransferClient());
  const [balance, setBalance] = useState({ available: 0, pending: 0, public: 0 });
  const [loading, setLoading] = useState(false);

  const loadBalance = async () => {
    if (!wallet.publicKey || !wallet.signMessage) return;

    setLoading(true);
    try {
      const signature = await client.getKeyDerivationSignature(wallet);
      const balanceData = await client.callAPI('/balance', {
        owner_pubkey: wallet.publicKey.toString(),
        signature,
        mint_address: mintAddress,
      });

      setBalance({
        available: balanceData.available_balance,
        pending: balanceData.pending_balance,
        public: balanceData.public_balance,
      });
    } catch (error) {
      console.error('Failed to load balance:', error);
    } finally {
      setLoading(false);
    }
  };

  const setupAccount = async () => {
    if (!wallet.publicKey || !wallet.signMessage) return;

    setLoading(true);
    try {
      const signature = await client.getKeyDerivationSignature(wallet);
      const response = await client.callAPI('/setup-account', {
        owner_pubkey: wallet.publicKey.toString(),
        signature,
        mint_address: mintAddress,
      });

      if (response.transaction !== "Account already exists") {
        await client.executeTransaction(wallet, response.transaction);
      }
      
      await loadBalance();
    } catch (error) {
      console.error('Failed to setup account:', error);
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    if (wallet.connected) {
      loadBalance();
    }
  }, [wallet.connected]);

  return (
    <div className="p-6 bg-gray-800 rounded-lg">
      <h2 className="text-xl font-bold mb-4">Confidential Wallet</h2>
      
      {wallet.connected ? (
        <div>
          <div className="grid grid-cols-3 gap-4 mb-6">
            <div className="text-center">
              <p className="text-sm text-gray-400">Available</p>
              <p className="text-lg font-bold">{balance.available}</p>
            </div>
            <div className="text-center">
              <p className="text-sm text-gray-400">Pending</p>
              <p className="text-lg font-bold">{balance.pending}</p>
            </div>
            <div className="text-center">
              <p className="text-sm text-gray-400">Public</p>
              <p className="text-lg font-bold">{balance.public}</p>
            </div>
          </div>

          <div className="space-y-2">
            <button
              onClick={setupAccount}
              disabled={loading}
              className="w-full py-2 px-4 bg-blue-600 text-white rounded disabled:opacity-50"
            >
              {loading ? 'Processing...' : 'Setup Account'}
            </button>
            
            <button
              onClick={loadBalance}
              disabled={loading}
              className="w-full py-2 px-4 bg-gray-600 text-white rounded disabled:opacity-50"
            >
              Refresh Balance
            </button>
          </div>
        </div>
      ) : (
        <p>Please connect your wallet</p>
      )}
    </div>
  );
}

Common Patterns

1. Token Discovery and Selection

interface TokenInfo {
  mint: string;
  name: string;
  symbol: string;
  decimals: number;
  balance: number;
  hasConfidentialTransfer: boolean;
}

class TokenDiscovery {
  private connection: Connection;

  constructor(rpcUrl: string) {
    this.connection = new Connection(rpcUrl, 'confirmed');
  }

  async getConfidentialTokens(walletPublicKey: PublicKey): Promise<TokenInfo[]> {
    // Get all Token 2022 accounts
    const tokenAccounts = await this.connection.getParsedTokenAccountsByOwner(
      walletPublicKey,
      { programId: new PublicKey('TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb') }
    );

    const confidentialTokens: TokenInfo[] = [];

    for (const account of tokenAccounts.value) {
      const accountInfo = account.account.data.parsed.info;
      const mint = accountInfo.mint;
      const balance = parseFloat(accountInfo.tokenAmount.uiAmountString || '0');

      if (balance === 0) continue;

      // Check for confidential transfer extension
      const hasConfidentialTransfer = await this.checkConfidentialExtension(mint);
      
      if (hasConfidentialTransfer) {
        const metadata = await this.getTokenMetadata(mint);
        
        confidentialTokens.push({
          mint,
          name: metadata?.name || `Token ${mint.slice(0, 8)}...`,
          symbol: metadata?.symbol || 'UNKNOWN',
          decimals: accountInfo.tokenAmount.decimals,
          balance,
          hasConfidentialTransfer: true,
        });
      }
    }

    return confidentialTokens;
  }

  private async checkConfidentialExtension(mint: string): Promise<boolean> {
    try {
      const mintAccount = await this.connection.getAccountInfo(new PublicKey(mint));
      if (!mintAccount) return false;

      // Simple heuristic: Token 2022 mints with extensions are larger than 82 bytes
      return mintAccount.data.length > 82;
    } catch {
      return false;
    }
  }

  private async getTokenMetadata(mint: string) {
    try {
      // Try Jupiter token list
      const response = await fetch('https://token.jup.ag/strict');
      const tokens = await response.json();
      return tokens.find((t: any) => t.address === mint);
    } catch {
      return null;
    }
  }
}

2. Multi-Step Transaction Handler

class MultiTransactionHandler {
  private client: ConfidentialTransferClient;

  constructor(client: ConfidentialTransferClient) {
    this.client = client;
  }

  async executeMultiTransaction(
    wallet: any,
    response: { transactions: string[]; descriptions: string[]; additional_signers?: string[] },
    onProgress?: (step: number, total: number, description: string) => void
  ): Promise<string[]> {
    const signatures: string[] = [];
    const total = response.transactions.length;

    for (let i = 0; i < total; i++) {
      const description = response.descriptions[i] || `Transaction ${i + 1}`;
      onProgress?.(i + 1, total, description);

      try {
        let signature: string;

        if (response.additional_signers?.[i]) {
          signature = await this.executeWithAdditionalSigners(
            wallet,
            response.transactions[i],
            response.additional_signers[i]
          );
        } else {
          signature = await this.client.executeTransaction(wallet, response.transactions[i]);
        }

        signatures.push(signature);
      } catch (error) {
        console.error(`Failed to execute transaction ${i + 1}:`, error);
        throw new Error(`Transaction ${i + 1} failed: ${error.message}`);
      }
    }

    return signatures;
  }

  private async executeWithAdditionalSigners(
    wallet: any,
    transactionBase64: string,
    additionalSignerBase64: string
  ): Promise<string> {
    const transaction = Transaction.from(Buffer.from(transactionBase64, 'base64'));
    const { blockhash } = await this.client.connection.getLatestBlockhash();
    
    transaction.recentBlockhash = blockhash;
    transaction.feePayer = wallet.publicKey;

    // Add additional signers
    if (additionalSignerBase64) {
      const { Keypair } = await import('@solana/web3.js');
      const signerBytes = Buffer.from(additionalSignerBase64, 'base64');
      
      // Handle multiple keypairs (64 bytes each)
      const keypairSize = 64;
      const numKeypairs = signerBytes.length / keypairSize;
      
      for (let i = 0; i < numKeypairs; i++) {
        const start = i * keypairSize;
        const end = start + keypairSize;
        const keypairBytes = signerBytes.slice(start, end);
        
        if (keypairBytes.length === keypairSize) {
          const signer = Keypair.fromSecretKey(keypairBytes);
          transaction.partialSign(signer);
        }
      }
    }

    const signedTx = await wallet.signTransaction(transaction);
    const signature = await this.client.connection.sendRawTransaction(signedTx.serialize());
    await this.client.connection.confirmTransaction(signature, 'confirmed');
    
    return signature;
  }
}

3. Error Handling and Retry Logic

class RobustConfidentialClient extends ConfidentialTransferClient {
  async callAPIWithRetry<T>(
    endpoint: string,
    data: any,
    maxRetries: number = 3,
    delay: number = 1000
  ): Promise<T> {
    let lastError: Error;

    for (let attempt = 1; attempt <= maxRetries; attempt++) {
      try {
        return await this.callAPI<T>(endpoint, data);
      } catch (error) {
        lastError = error as Error;
        console.warn(`Attempt ${attempt} failed:`, error.message);

        if (attempt < maxRetries) {
          await new Promise(resolve => setTimeout(resolve, delay * attempt));
        }
      }
    }

    throw new Error(`Failed after ${maxRetries} attempts: ${lastError.message}`);
  }

  async executeTransactionWithRetry(
    wallet: any,
    transactionBase64: string,
    maxRetries: number = 3
  ): Promise<string> {
    let lastError: Error;

    for (let attempt = 1; attempt <= maxRetries; attempt++) {
      try {
        return await this.executeTransaction(wallet, transactionBase64);
      } catch (error) {
        lastError = error as Error;
        console.warn(`Transaction attempt ${attempt} failed:`, error.message);

        // Don't retry certain errors
        if (error.message.includes('insufficient funds') || 
            error.message.includes('invalid signature')) {
          throw error;
        }

        if (attempt < maxRetries) {
          await new Promise(resolve => setTimeout(resolve, 2000 * attempt));
        }
      }
    }

    throw new Error(`Transaction failed after ${maxRetries} attempts: ${lastError.message}`);
  }
}

Use Cases

1. Private Payment System

class PrivatePaymentSystem {
  private client: ConfidentialTransferClient;
  private mintAddress: string;

  constructor(mintAddress: string) {
    this.client = new ConfidentialTransferClient();
    this.mintAddress = mintAddress;
  }

  async sendPrivatePayment(
    senderWallet: any,
    recipientAddress: string,
    amount: number,
    onProgress?: (step: string) => void
  ): Promise<{ signatures: string[]; totalFee: number }> {
    onProgress?.('Preparing transfer...');

    const signature = await this.client.getKeyDerivationSignature(senderWallet);
    
    // Check sender balance
    const balance = await this.client.callAPI('/balance', {
      owner_pubkey: senderWallet.publicKey.toString(),
      signature,
      mint_address: this.mintAddress,
    });

    if (balance.available_balance < amount) {
      throw new Error('Insufficient confidential balance');
    }

    onProgress?.('Generating zero-knowledge proofs...');

    // Execute transfer
    const response = await this.client.callAPI('/transfer', {
      sender_pubkey: senderWallet.publicKey.toString(),
      sender_signature: signature,
      recipient_pubkey: recipientAddress,
      amount,
      mint_address: this.mintAddress,
    });

    onProgress?.('Executing transactions...');

    const handler = new MultiTransactionHandler(this.client);
    const signatures = await handler.executeMultiTransaction(
      senderWallet,
      response,
      (step, total, desc) => onProgress?.(`${desc} (${step}/${total})`)
    );

    // Calculate total fees (estimate)
    const totalFee = signatures.length * 5000; // ~0.000005 SOL per transaction

    return { signatures, totalFee };
  }

  async setupRecipientAccount(recipientWallet: any): Promise<string> {
    const signature = await this.client.getKeyDerivationSignature(recipientWallet);
    
    const response = await this.client.callAPI('/setup-account', {
      owner_pubkey: recipientWallet.publicKey.toString(),
      signature,
      mint_address: this.mintAddress,
    });

    if (response.transaction === "Account already exists") {
      return "Account already configured";
    }

    return await this.client.executeTransaction(recipientWallet, response.transaction);
  }
}

2. Confidential Escrow Service

interface EscrowConfig {
  mintAddress: string;
  escrowDuration: number; // in seconds
  arbitratorAddress?: string;
}

class ConfidentialEscrow {
  private client: ConfidentialTransferClient;
  private config: EscrowConfig;

  constructor(config: EscrowConfig) {
    this.client = new ConfidentialTransferClient();
    this.config = config;
  }

  async createEscrow(
    buyerWallet: any,
    sellerAddress: string,
    amount: number
  ): Promise<{ escrowId: string; signatures: string[] }> {
    // 1. Setup buyer's confidential account if needed
    await this.ensureAccountSetup(buyerWallet);

    // 2. Check buyer has sufficient balance
    const balance = await this.getBalance(buyerWallet);
    if (balance.available < amount) {
      throw new Error('Insufficient balance for escrow');
    }

    // 3. Create escrow by transferring to escrow service
    // (In a real implementation, you'd transfer to an escrow program)
    const escrowId = this.generateEscrowId();
    
    // For demo: transfer to a temporary holding account
    const signatures = await this.transferToEscrow(buyerWallet, amount);

    return { escrowId, signatures };
  }

  async releaseEscrow(
    escrowId: string,
    buyerWallet: any,
    sellerAddress: string,
    amount: number
  ): Promise<string[]> {
    // Verify escrow exists and is valid
    // In production, check escrow program state
    
    // Transfer from escrow to seller
    const signature = await this.client.getKeyDerivationSignature(buyerWallet);
    
    const response = await this.client.callAPI('/transfer', {
      sender_pubkey: buyerWallet.publicKey.toString(),
      sender_signature: signature,
      recipient_pubkey: sellerAddress,
      amount,
      mint_address: this.config.mintAddress,
    });

    const handler = new MultiTransactionHandler(this.client);
    return await handler.executeMultiTransaction(buyerWallet, response);
  }

  private async ensureAccountSetup(wallet: any): Promise<void> {
    try {
      await this.getBalance(wallet);
    } catch (error) {
      if (error.message.includes('Account not found')) {
        const signature = await this.client.getKeyDerivationSignature(wallet);
        const response = await this.client.callAPI('/setup-account', {
          owner_pubkey: wallet.publicKey.toString(),
          signature,
          mint_address: this.config.mintAddress,
        });

        if (response.transaction !== "Account already exists") {
          await this.client.executeTransaction(wallet, response.transaction);
        }
      } else {
        throw error;
      }
    }
  }

  private async getBalance(wallet: any) {
    const signature = await this.client.getKeyDerivationSignature(wallet);
    return await this.client.callAPI('/balance', {
      owner_pubkey: wallet.publicKey.toString(),
      signature,
      mint_address: this.config.mintAddress,
    });
  }

  private generateEscrowId(): string {
    return `escrow_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
  }

  private async transferToEscrow(wallet: any, amount: number): Promise<string[]> {
    // Implementation would transfer to escrow program
    // For demo purposes, this is simplified
    return ['demo_escrow_signature'];
  }
}

3. Anonymous Voting System

interface VoteOption {
  id: string;
  title: string;
  description: string;
}

interface Poll {
  id: string;
  title: string;
  description: string;
  options: VoteOption[];
  endTime: Date;
  tokenWeight: number; // tokens required per vote
}

class AnonymousVoting {
  private client: ConfidentialTransferClient;
  private votingMint: string;
  private votingAuthority: string;

  constructor(votingMint: string, votingAuthority: string) {
    this.client = new ConfidentialTransferClient();
    this.votingMint = votingMint;
    this.votingAuthority = votingAuthority;
  }

  async castVote(
    voterWallet: any,
    pollId: string,
    optionId: string,
    voteWeight: number
  ): Promise<{ voteId: string; signature: string }> {
    // 1. Verify voter has sufficient tokens
    const balance = await this.getVoterBalance(voterWallet);
    if (balance.available < voteWeight) {
      throw new Error('Insufficient voting tokens');
    }

    // 2. Transfer tokens to voting authority (burns vote tokens)
    const signature = await this.client.getKeyDerivationSignature(voterWallet);
    
    const response = await this.client.callAPI('/transfer', {
      sender_pubkey: voterWallet.publicKey.toString(),
      sender_signature: signature,
      recipient_pubkey: this.votingAuthority,
      amount: voteWeight,
      mint_address: this.votingMint,
    });

    // 3. Execute the vote transfer
    const handler = new MultiTransactionHandler(this.client);
    const signatures = await handler.executeMultiTransaction(voterWallet, response);

    // 4. Record vote (in production, this would be on-chain)
    const voteId = this.generateVoteId(pollId, optionId);
    
    return { voteId, signature: signatures[0] };
  }

  async getVotingPower(voterWallet: any): Promise<number> {
    const balance = await this.getVoterBalance(voterWallet);
    return balance.available;
  }

  private async getVoterBalance(wallet: any) {
    const signature = await this.client.getKeyDerivationSignature(wallet);
    return await this.client.callAPI('/balance', {
      owner_pubkey: wallet.publicKey.toString(),
      signature,
      mint_address: this.votingMint,
    });
  }

  private generateVoteId(pollId: string, optionId: string): string {
    return `vote_${pollId}_${optionId}_${Date.now()}`;
  }
}

Advanced Examples

1. Batch Operations

class BatchOperations {
  private client: ConfidentialTransferClient;

  constructor() {
    this.client = new ConfidentialTransferClient();
  }

  async batchTransfer(
    senderWallet: any,
    transfers: Array<{ recipient: string; amount: number }>,
    mintAddress: string,
    onProgress?: (completed: number, total: number) => void
  ): Promise<string[]> {
    const allSignatures: string[] = [];
    const total = transfers.length;

    for (let i = 0; i < transfers.length; i++) {
      const { recipient, amount } = transfers[i];
      
      try {
        const signature = await this.client.getKeyDerivationSignature(senderWallet);
        
        const response = await this.client.callAPI('/transfer', {
          sender_pubkey: senderWallet.publicKey.toString(),
          sender_signature: signature,
          recipient_pubkey: recipient,
          amount,
          mint_address: mintAddress,
        });

        const handler = new MultiTransactionHandler(this.client);
        const signatures = await handler.executeMultiTransaction(senderWallet, response);
        
        allSignatures.push(...signatures);
        onProgress?.(i + 1, total);
        
        // Small delay to avoid rate limiting
        await new Promise(resolve => setTimeout(resolve, 1000));
        
      } catch (error) {
        console.error(`Failed to transfer to ${recipient}:`, error);
        throw new Error(`Batch transfer failed at recipient ${i + 1}: ${error.message}`);
      }
    }

    return allSignatures;
  }

  async batchSetup(
    wallets: any[],
    mintAddress: string,
    onProgress?: (completed: number, total: number) => void
  ): Promise<string[]> {
    const signatures: string[] = [];
    const total = wallets.length;

    for (let i = 0; i < wallets.length; i++) {
      const wallet = wallets[i];
      
      try {
        const signature = await this.client.getKeyDerivationSignature(wallet);
        
        const response = await this.client.callAPI('/setup-account', {
          owner_pubkey: wallet.publicKey.toString(),
          signature,
          mint_address: mintAddress,
        });

        if (response.transaction !== "Account already exists") {
          const txSignature = await this.client.executeTransaction(wallet, response.transaction);
          signatures.push(txSignature);
        }
        
        onProgress?.(i + 1, total);
        
      } catch (error) {
        console.error(`Failed to setup wallet ${i + 1}:`, error);
        // Continue with other wallets
      }
    }

    return signatures;
  }
}

2. Real-time Balance Monitoring

class BalanceMonitor {
  private client: ConfidentialTransferClient;
  private intervals: Map<string, NodeJS.Timeout> = new Map();

  constructor() {
    this.client = new ConfidentialTransferClient();
  }

  startMonitoring(
    wallet: any,
    mintAddress: string,
    callback: (balance: { available: number; pending: number; public: number }) => void,
    intervalMs: number = 10000
  ): string {
    const monitorId = `${wallet.publicKey.toString()}_${mintAddress}`;
    
    // Clear existing monitor if any
    this.stopMonitoring(monitorId);

    const interval = setInterval(async () => {
      try {
        const signature = await this.client.getKeyDerivationSignature(wallet);
        const balance = await this.client.callAPI('/balance', {
          owner_pubkey: wallet.publicKey.toString(),
          signature,
          mint_address: mintAddress,
        });

        callback({
          available: balance.available_balance,
          pending: balance.pending_balance,
          public: balance.public_balance,
        });
      } catch (error) {
        console.error('Balance monitoring error:', error);
      }
    }, intervalMs);

    this.intervals.set(monitorId, interval);
    return monitorId;
  }

  stopMonitoring(monitorId: string): void {
    const interval = this.intervals.get(monitorId);
    if (interval) {
      clearInterval(interval);
      this.intervals.delete(monitorId);
    }
  }

  stopAllMonitoring(): void {
    for (const [id, interval] of this.intervals) {
      clearInterval(interval);
    }
    this.intervals.clear();
  }
}

Troubleshooting

Common Issues and Solutions

1. "Account not found" Error

async function handleAccountNotFound(wallet: any, mintAddress: string) {
  try {
    // Try to get balance first
    const balance = await client.callAPI('/balance', {
      owner_pubkey: wallet.publicKey.toString(),
      signature: await client.getKeyDerivationSignature(wallet),
      mint_address: mintAddress,
    });
    return balance;
  } catch (error) {
    if (error.message.includes('Account not found')) {
      console.log('Setting up account...');
      
      // Setup the account
      const signature = await client.getKeyDerivationSignature(wallet);
      const response = await client.callAPI('/setup-account', {
        owner_pubkey: wallet.publicKey.toString(),
        signature,
        mint_address: mintAddress,
      });

      if (response.transaction !== "Account already exists") {
        await client.executeTransaction(wallet, response.transaction);
      }
      
      // Retry balance check
      return await client.callAPI('/balance', {
        owner_pubkey: wallet.publicKey.toString(),
        signature,
        mint_address: mintAddress,
      });
    }
    throw error;
  }
}

2. Transaction Failures

async function robustTransactionExecution(
  wallet: any,
  transactionBase64: string,
  maxRetries: number = 3
): Promise<string> {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await client.executeTransaction(wallet, transactionBase64);
    } catch (error) {
      console.warn(`Transaction attempt ${attempt} failed:`, error.message);
      
      // Check if it's a retryable error
      if (error.message.includes('blockhash not found') || 
          error.message.includes('network error')) {
        if (attempt < maxRetries) {
          await new Promise(resolve => setTimeout(resolve, 2000 * attempt));
          continue;
        }
      }
      
      // Non-retryable error or max retries reached
      throw error;
    }
  }
}

3. Rate Limiting

class RateLimitedClient extends ConfidentialTransferClient {
  private requestQueue: Array<() => Promise<any>> = [];
  private processing = false;
  private requestDelay = 1000; // 1 second between requests

  async callAPI<T>(endpoint: string, data: any): Promise<T> {
    return new Promise((resolve, reject) => {
      this.requestQueue.push(async () => {
        try {
          const result = await super.callAPI<T>(endpoint, data);
          resolve(result);
        } catch (error) {
          reject(error);
        }
      });

      this.processQueue();
    });
  }

  private async processQueue(): Promise<void> {
    if (this.processing || this.requestQueue.length === 0) {
      return;
    }

    this.processing = true;

    while (this.requestQueue.length > 0) {
      const request = this.requestQueue.shift()!;
      await request();
      
      if (this.requestQueue.length > 0) {
        await new Promise(resolve => setTimeout(resolve, this.requestDelay));
      }
    }

    this.processing = false;
  }
}

Best Practices

1. Error Handling

// Always wrap API calls in try-catch blocks
try {
  const result = await client.callAPI('/balance', requestData);
  // Handle success
} catch (error) {
  if (error.message.includes('Account not found')) {
    // Handle account setup
  } else if (error.message.includes('Insufficient balance')) {
    // Handle insufficient funds
  } else {
    // Handle other errors
    console.error('Unexpected error:', error);
  }
}

2. User Experience

// Provide clear feedback to users
const [status, setStatus] = useState('');
const [progress, setProgress] = useState(0);

const handleTransfer = async () => {
  setStatus('Preparing transfer...');
  setProgress(25);
  
  try {
    const signature = await client.getKeyDerivationSignature(wallet);
    setStatus('Generating proofs...');
    setProgress(50);
    
    const response = await client.callAPI('/transfer', requestData);
    setStatus('Executing transactions...');
    setProgress(75);
    
    const signatures = await handler.executeMultiTransaction(
      wallet,
      response,
      (step, total, desc) => {
        setStatus(`${desc} (${step}/${total})`);
        setProgress(75 + (step / total) * 25);
      }
    );
    
    setStatus('Transfer completed!');
    setProgress(100);
    return signatures;
  } catch (error) {
    setStatus(`Error: ${error.message}`);
    setProgress(0);
    throw error;
  }
};

3. Security

// Always validate inputs
function validateTransferInputs(amount: number, recipient: string) {
  if (!amount || amount <= 0) {
    throw new Error('Invalid amount');
  }
  
  if (!recipient || recipient.length !== 44) {
    throw new Error('Invalid recipient address');
  }
  
  try {
    new PublicKey(recipient);
  } catch {
    throw new Error('Invalid recipient public key format');
  }
}

// Verify transaction contents before signing
function verifyTransaction(transaction: Transaction, expectedAmount: number) {
  // Parse transaction instructions to verify amounts and recipients
  // This is a simplified example - implement based on your needs
  console.log('Verifying transaction:', transaction);
  
  // Check for suspicious instructions
  const suspiciousPrograms = [
    // Add known malicious program IDs
  ];
  
  for (const instruction of transaction.instructions) {
    if (suspiciousPrograms.includes(instruction.programId.toString())) {
      throw new Error('Suspicious transaction detected');
    }
  }
}

4. Performance Optimization

// Cache frequently used data
class CachedConfidentialClient extends ConfidentialTransferClient {
  private balanceCache = new Map<string, { balance: any; timestamp: number }>();
  private cacheTimeout = 30000; // 30 seconds

  async getCachedBalance(wallet: any, mintAddress: string) {
    const cacheKey = `${wallet.publicKey.toString()}_${mintAddress}`;
    const cached = this.balanceCache.get(cacheKey);
    
    if (cached && Date.now() - cached.timestamp < this.cacheTimeout) {
      return cached.balance;
    }

    const signature = await this.getKeyDerivationSignature(wallet);
    const balance = await this.callAPI('/balance', {
      owner_pubkey: wallet.publicKey.toString(),
      signature,
      mint_address: mintAddress,
    });

    this.balanceCache.set(cacheKey, {
      balance,
      timestamp: Date.now(),
    });

    return balance;
  }

  clearCache() {
    this.balanceCache.clear();
  }
}

// Batch operations when possible
class OptimizedOperations {
  private pendingOperations: Array<() => Promise<any>> = [];
  private batchTimeout: NodeJS.Timeout | null = null;

  queueOperation<T>(operation: () => Promise<T>): Promise<T> {
    return new Promise((resolve, reject) => {
      this.pendingOperations.push(async () => {
        try {
          const result = await operation();
          resolve(result);
        } catch (error) {
          reject(error);
        }
      });

      this.scheduleBatch();
    });
  }

  private scheduleBatch() {
    if (this.batchTimeout) {
      clearTimeout(this.batchTimeout);
    }

    this.batchTimeout = setTimeout(() => {
      this.executeBatch();
    }, 100); // 100ms batch window
  }

  private async executeBatch() {
    const operations = [...this.pendingOperations];
    this.pendingOperations = [];
    this.batchTimeout = null;

    // Execute operations with small delays
    for (let i = 0; i < operations.length; i++) {
      await operations[i]();
      if (i < operations.length - 1) {
        await new Promise(resolve => setTimeout(resolve, 200));
      }
    }
  }
}

5. Testing

// Mock client for testing
class MockConfidentialClient extends ConfidentialTransferClient {
  private mockResponses = new Map<string, any>();

  setMockResponse(endpoint: string, response: any) {
    this.mockResponses.set(endpoint, response);
  }

  async callAPI<T>(endpoint: string, data: any): Promise<T> {
    const mockResponse = this.mockResponses.get(endpoint);
    if (mockResponse) {
      return mockResponse;
    }
    
    // Fallback to real API in test environment
    return super.callAPI<T>(endpoint, data);
  }

  async executeTransaction(wallet: any, transactionBase64: string): Promise<string> {
    // Return mock signature in test environment
    return 'mock_signature_' + Date.now();
  }
}

// Test utilities
function createMockWallet(publicKey: string) {
  return {
    publicKey: new PublicKey(publicKey),
    signMessage: async (message: Uint8Array) => {
      // Return deterministic mock signature
      return new Uint8Array(64).fill(1);
    },
    signTransaction: async (transaction: Transaction) => {
      // Return mock signed transaction
      return transaction;
    },
  };
}

// Integration tests
describe('Confidential Transfer Integration', () => {
  let client: MockConfidentialClient;
  let mockWallet: any;

  beforeEach(() => {
    client = new MockConfidentialClient();
    mockWallet = createMockWallet('11111111111111111111111111111112');
  });

  test('should setup account successfully', async () => {
    client.setMockResponse('/setup-account', {
      transaction: 'mock_transaction_base64',
      description: 'Setup account',
    });

    const signature = await client.getKeyDerivationSignature(mockWallet);
    const response = await client.callAPI('/setup-account', {
      owner_pubkey: mockWallet.publicKey.toString(),
      signature,
      mint_address: 'mock_mint_address',
    });

    expect(response.transaction).toBe('mock_transaction_base64');
  });

  test('should handle balance retrieval', async () => {
    client.setMockResponse('/balance', {
      available_balance: 1000,
      pending_balance: 500,
      public_balance: 2000,
    });

    const signature = await client.getKeyDerivationSignature(mockWallet);
    const balance = await client.callAPI('/balance', {
      owner_pubkey: mockWallet.publicKey.toString(),
      signature,
      mint_address: 'mock_mint_address',
    });

    expect(balance.available_balance).toBe(1000);
    expect(balance.pending_balance).toBe(500);
    expect(balance.public_balance).toBe(2000);
  });
});

6. Monitoring and Analytics

class AnalyticsClient {
  private events: Array<{ event: string; data: any; timestamp: number }> = [];

  track(event: string, data: any = {}) {
    this.events.push({
      event,
      data,
      timestamp: Date.now(),
    });

    // Send to analytics service
    this.sendToAnalytics(event, data);
  }

  private async sendToAnalytics(event: string, data: any) {
    try {
      // Send to your analytics service
      await fetch('/api/analytics', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ event, data, timestamp: Date.now() }),
      });
    } catch (error) {
      console.warn('Analytics tracking failed:', error);
    }
  }

  getEvents() {
    return [...this.events];
  }
}

// Usage in your application
const analytics = new AnalyticsClient();

// Track user actions
analytics.track('confidential_transfer_initiated', {
  amount,
  mintAddress,
  recipientAddress: recipientAddress.slice(0, 8) + '...', // Privacy-safe
});

analytics.track('confidential_transfer_completed', {
  transactionCount: signatures.length,
  totalFee,
  duration: Date.now() - startTime,
});

Summary

This cookbook provides comprehensive examples for integrating the ZK Void Confidential Transfer API into your dApps. Key takeaways:

  1. Start Simple: Begin with basic setup and balance checking

  2. Handle Errors Gracefully: Implement robust error handling and retry logic

  3. Provide Great UX: Keep users informed with progress indicators and clear messaging

  4. Security First: Always validate inputs and verify transaction contents

  5. Test Thoroughly: Use mocks and integration tests to ensure reliability

  6. Monitor Performance: Track usage and optimize for better user experience

For more detailed API reference, see the API Documentation.

Last updated