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:
Start Simple: Begin with basic setup and balance checking
Handle Errors Gracefully: Implement robust error handling and retry logic
Provide Great UX: Keep users informed with progress indicators and clear messaging
Security First: Always validate inputs and verify transaction contents
Test Thoroughly: Use mocks and integration tests to ensure reliability
Monitor Performance: Track usage and optimize for better user experience
For more detailed API reference, see the API Documentation.
Last updated