SDK Examples
Code examples for common ZKMix operations
SDK Examples
This page provides complete, working code examples for common ZKMix operations. Each example is self-contained and can be adapted to your application.
React Integration
Deposit Component
A React component that allows users to deposit SOL into a ZKMix pool using the Solana Wallet Adapter.
import { useState } from "react";
import { useConnection, useWallet } from "@solana/wallet-adapter-react";
import { LAMPORTS_PER_SOL } from "@solana/web3.js";
import { ZKMix } from "@zkmix/sdk";
type PoolDenomination = 0.1 | 1 | 10 | 100;
export function DepositForm() {
const { connection } = useConnection();
const wallet = useWallet();
const [denomination, setDenomination] = useState<PoolDenomination>(1);
const [loading, setLoading] = useState(false);
const [note, setNote] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const handleDeposit = async () => {
if (!wallet.publicKey || !wallet.signTransaction) {
setError("Please connect your wallet first.");
return;
}
setLoading(true);
setError(null);
setNote(null);
try {
const zkmix = new ZKMix({
connection,
cluster: "mainnet-beta",
provingKeyUrl: "/assets/zkmix_proving_key.zkey",
wasmUrl: "/assets/zkmix_circuit.wasm",
});
const result = await zkmix.deposit({
wallet: {
publicKey: wallet.publicKey,
signTransaction: wallet.signTransaction,
},
token: "SOL",
denomination: denomination * LAMPORTS_PER_SOL,
});
const noteString = result.note.serialize();
setNote(noteString);
// Prompt user to save the note
downloadNote(noteString, `zkmix-deposit-${result.leafIndex}.txt`);
} catch (err: any) {
setError(err.message || "Deposit failed");
} finally {
setLoading(false);
}
};
return (
<div>
<h2>Deposit SOL</h2>
<label htmlFor="denomination">Select amount:</label>
<select
id="denomination"
value={denomination}
onChange={(e) => setDenomination(Number(e.target.value) as PoolDenomination)}
disabled={loading}
>
<option value={0.1}>0.1 SOL</option>
<option value={1}>1 SOL</option>
<option value={10}>10 SOL</option>
<option value={100}>100 SOL</option>
</select>
<button onClick={handleDeposit} disabled={loading || !wallet.connected}>
{loading ? "Depositing..." : `Deposit ${denomination} SOL`}
</button>
{error && <p style={{ color: "red" }}>{error}</p>}
{note && (
<div>
<p style={{ color: "green" }}>Deposit successful!</p>
<p>
<strong>Save this note securely.</strong> You need it to withdraw your funds.
If you lose this note, your funds cannot be recovered.
</p>
<textarea readOnly value={note} rows={3} style={{ width: "100%" }} />
</div>
)}
</div>
);
}
function downloadNote(content: string, filename: string) {
const blob = new Blob([content], { type: "text/plain" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}Withdrawal Component
A React component for withdrawing funds using a deposit note, with relayer support.
import { useState } from "react";
import { useConnection } from "@solana/wallet-adapter-react";
import { PublicKey } from "@solana/web3.js";
import { ZKMix } from "@zkmix/sdk";
export function WithdrawForm() {
const { connection } = useConnection();
const [noteInput, setNoteInput] = useState("");
const [recipientInput, setRecipientInput] = useState("");
const [loading, setLoading] = useState(false);
const [status, setStatus] = useState("");
const [txSignature, setTxSignature] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const handleWithdraw = async () => {
setLoading(true);
setError(null);
setTxSignature(null);
try {
const zkmix = new ZKMix({
connection,
cluster: "mainnet-beta",
provingKeyUrl: "/assets/zkmix_proving_key.zkey",
wasmUrl: "/assets/zkmix_circuit.wasm",
});
const depositNote = zkmix.parseNote(noteInput.trim());
const recipient = new PublicKey(recipientInput.trim());
// Check if already spent
setStatus("Checking deposit status...");
const isSpent = await zkmix.isNoteSpent(depositNote);
if (isSpent) {
throw new Error("This deposit has already been withdrawn.");
}
// Generate proof (this is the slow step)
setStatus("Generating zero-knowledge proof (this may take 15-30 seconds)...");
const proofData = await zkmix.generateProof({
deposit: depositNote,
recipient,
relayerFee: 0, // Will be set by the relayer selection
});
setStatus("Submitting withdrawal through relayer...");
const result = await zkmix.withdraw({
deposit: depositNote,
recipient,
useRelayer: true,
});
setTxSignature(result.txSignature);
setStatus("Withdrawal complete!");
} catch (err: any) {
setError(err.message || "Withdrawal failed");
setStatus("");
} finally {
setLoading(false);
}
};
return (
<div>
<h2>Withdraw</h2>
<label htmlFor="note">Deposit Note:</label>
<textarea
id="note"
value={noteInput}
onChange={(e) => setNoteInput(e.target.value)}
placeholder="Paste your deposit note here..."
rows={3}
style={{ width: "100%" }}
disabled={loading}
/>
<label htmlFor="recipient">Recipient Address:</label>
<input
id="recipient"
type="text"
value={recipientInput}
onChange={(e) => setRecipientInput(e.target.value)}
placeholder="Solana address to receive funds"
style={{ width: "100%" }}
disabled={loading}
/>
<button onClick={handleWithdraw} disabled={loading || !noteInput || !recipientInput}>
{loading ? status : "Withdraw via Relayer"}
</button>
{error && <p style={{ color: "red" }}>{error}</p>}
{txSignature && (
<div>
<p style={{ color: "green" }}>Withdrawal successful!</p>
<p>
Transaction:{" "}
<a
href={`https://explorer.solana.com/tx/${txSignature}`}
target="_blank"
rel="noopener noreferrer"
>
{txSignature.slice(0, 20)}...
</a>
</p>
</div>
)}
</div>
);
}Node.js Script
A complete Node.js script for automating deposits and withdrawals, suitable for scripting and backend integrations.
import { Connection, Keypair, LAMPORTS_PER_SOL, PublicKey } from "@solana/web3.js";
import { ZKMix } from "@zkmix/sdk";
import fs from "fs";
import path from "path";
const RPC_URL = process.env.SOLANA_RPC_URL || "https://api.devnet.solana.com";
const CLUSTER = (process.env.CLUSTER || "devnet") as "devnet" | "mainnet-beta";
const KEYPAIR_PATH = process.env.KEYPAIR_PATH || path.join(
process.env.HOME || "",
".config",
"solana",
"id.json"
);
async function loadWallet(): Promise<Keypair> {
const keypairData = JSON.parse(fs.readFileSync(KEYPAIR_PATH, "utf-8"));
return Keypair.fromSecretKey(Uint8Array.from(keypairData));
}
async function deposit(denominationSol: number): Promise<string> {
const connection = new Connection(RPC_URL, "confirmed");
const wallet = await loadWallet();
const zkmix = new ZKMix({ connection, cluster: CLUSTER });
console.log(`Depositing ${denominationSol} SOL from ${wallet.publicKey.toBase58()}...`);
const result = await zkmix.deposit({
wallet,
token: "SOL",
denomination: denominationSol * LAMPORTS_PER_SOL,
});
const noteString = result.note.serialize();
// Save note to file
const noteFile = `deposit-note-${Date.now()}.txt`;
fs.writeFileSync(noteFile, noteString);
console.log(`Deposit successful! Leaf index: ${result.leafIndex}`);
console.log(`Note saved to: ${noteFile}`);
console.log(`Transaction: ${result.txSignature}`);
return noteString;
}
async function withdraw(noteString: string, recipientAddress: string): Promise<void> {
const connection = new Connection(RPC_URL, "confirmed");
const zkmix = new ZKMix({ connection, cluster: CLUSTER });
const depositNote = zkmix.parseNote(noteString);
const recipient = new PublicKey(recipientAddress);
// Check if already spent
const isSpent = await zkmix.isNoteSpent(depositNote);
if (isSpent) {
console.error("Error: This deposit has already been withdrawn.");
process.exit(1);
}
console.log(`Withdrawing to ${recipientAddress}...`);
console.log("Generating proof (this may take 10-30 seconds)...");
const result = await zkmix.withdraw({
deposit: depositNote,
recipient,
useRelayer: true,
});
console.log(`Withdrawal successful!`);
console.log(`Transaction: ${result.txSignature}`);
console.log(`Amount received: ${result.amountReceived / LAMPORTS_PER_SOL} SOL`);
console.log(`Relayer fee: ${result.relayerFee / LAMPORTS_PER_SOL} SOL`);
}
// CLI interface
const command = process.argv[2];
if (command === "deposit") {
const amount = parseFloat(process.argv[3] || "1");
deposit(amount).catch(console.error);
} else if (command === "withdraw") {
const noteFile = process.argv[3];
const recipient = process.argv[4];
if (!noteFile || !recipient) {
console.error("Usage: node script.js withdraw <note-file> <recipient-address>");
process.exit(1);
}
const noteString = fs.readFileSync(noteFile, "utf-8").trim();
withdraw(noteString, recipient).catch(console.error);
} else {
console.log("Usage:");
console.log(" node script.js deposit <amount-in-sol>");
console.log(" node script.js withdraw <note-file> <recipient-address>");
}Run it:
# Deposit 1 SOL
npx ts-node script.ts deposit 1
# Withdraw to a fresh address
npx ts-node script.ts withdraw deposit-note-1710512400.txt FreshAddress111...Monitoring Deposits
Monitor a pool for new deposits in real-time using WebSocket subscriptions. This is useful for analytics dashboards, anonymity set tracking, or building notification systems.
import { Connection, PublicKey } from "@solana/web3.js";
import { ZKMix } from "@zkmix/sdk";
async function monitorDeposits() {
const connection = new Connection("https://api.mainnet-beta.solana.com", {
commitment: "confirmed",
wsEndpoint: "wss://api.mainnet-beta.solana.com",
});
const zkmix = new ZKMix({ connection, cluster: "mainnet-beta" });
// Get the SOL-1 pool info
const pool = await zkmix.getPoolInfo("SOL", 1_000_000_000);
console.log(`Monitoring SOL-1 pool: ${pool.address.toBase58()}`);
console.log(`Current deposits: ${pool.depositCount}`);
console.log(`Anonymity set size: ${pool.anonymitySetSize}`);
console.log("---");
// Subscribe to deposit events via account change notifications
const treeAddress = pool.address; // The Merkle tree account
let lastKnownIndex = pool.depositCount;
connection.onAccountChange(
treeAddress,
async (accountInfo) => {
// Re-fetch pool info to get updated deposit count
const updatedPool = await zkmix.getPoolInfo("SOL", 1_000_000_000);
if (updatedPool.depositCount > lastKnownIndex) {
const newDeposits = updatedPool.depositCount - lastKnownIndex;
console.log(
`[${new Date().toISOString()}] ${newDeposits} new deposit(s) detected! ` +
`Total: ${updatedPool.depositCount}, ` +
`Anonymity set: ${updatedPool.anonymitySetSize}`
);
lastKnownIndex = updatedPool.depositCount;
}
},
"confirmed"
);
console.log("Listening for new deposits... (press Ctrl+C to stop)");
}
monitorDeposits().catch(console.error);Polling-Based Monitoring
If WebSocket connections are unreliable, you can use a polling approach instead:
import { Connection } from "@solana/web3.js";
import { ZKMix } from "@zkmix/sdk";
async function pollDeposits() {
const connection = new Connection("https://api.mainnet-beta.solana.com", "confirmed");
const zkmix = new ZKMix({ connection, cluster: "mainnet-beta" });
let lastKnownCount = 0;
// Initial fetch
const pool = await zkmix.getPoolInfo("SOL", 1_000_000_000);
lastKnownCount = pool.depositCount;
console.log(`Starting monitor. Current deposits: ${lastKnownCount}`);
// Poll every 10 seconds
setInterval(async () => {
try {
const updated = await zkmix.getPoolInfo("SOL", 1_000_000_000);
if (updated.depositCount > lastKnownCount) {
// Fetch the new deposit events
const newDeposits = await zkmix.getDeposits({
token: "SOL",
denomination: 1_000_000_000,
fromIndex: lastKnownCount,
toIndex: updated.depositCount,
});
for (const deposit of newDeposits) {
console.log(
`New deposit: leaf=${deposit.leafIndex}, ` +
`commitment=${deposit.commitment.slice(0, 16)}..., ` +
`tx=${deposit.txSignature.slice(0, 16)}...`
);
}
lastKnownCount = updated.depositCount;
}
} catch (err) {
console.error("Poll error:", err);
}
}, 10_000);
}
pollDeposits().catch(console.error);Batch Operations
Multiple Deposits
Deposit into multiple pools in sequence to distribute a larger amount across several denominations.
import { Connection, Keypair, LAMPORTS_PER_SOL } from "@solana/web3.js";
import { ZKMix, DepositNote } from "@zkmix/sdk";
import fs from "fs";
async function batchDeposit(totalSol: number) {
const connection = new Connection("https://api.devnet.solana.com", "confirmed");
const wallet = Keypair.fromSecretKey(/* ... */);
const zkmix = new ZKMix({ connection, cluster: "devnet" });
// Strategy: break down the total into available denominations
const denominations = [10, 1, 0.1]; // SOL, largest first
const deposits: { denomination: number; note: string }[] = [];
let remaining = totalSol;
for (const denom of denominations) {
while (remaining >= denom) {
console.log(`Depositing ${denom} SOL (${remaining - denom} SOL remaining after this)...`);
const result = await zkmix.deposit({
wallet,
token: "SOL",
denomination: denom * LAMPORTS_PER_SOL,
});
deposits.push({
denomination: denom,
note: result.note.serialize(),
});
remaining -= denom;
remaining = Math.round(remaining * 10) / 10; // Avoid floating point issues
// Small delay between deposits to avoid rate limiting
await new Promise((r) => setTimeout(r, 2000));
}
}
// Save all notes
const notesFile = `batch-deposit-${Date.now()}.json`;
fs.writeFileSync(notesFile, JSON.stringify(deposits, null, 2));
console.log(`\nBatch deposit complete! ${deposits.length} deposits made.`);
console.log(`Notes saved to: ${notesFile}`);
return deposits;
}
// Deposit 12.3 SOL across multiple pools:
// 1x 10 SOL, 2x 1 SOL, 3x 0.1 SOL
batchDeposit(12.3).catch(console.error);Multiple Withdrawals
Withdraw multiple deposits in sequence to a single address or to different addresses.
import { Connection, Keypair, PublicKey, LAMPORTS_PER_SOL } from "@solana/web3.js";
import { ZKMix } from "@zkmix/sdk";
import fs from "fs";
interface SavedDeposit {
denomination: number;
note: string;
}
async function batchWithdraw(notesFile: string, recipientAddress: string) {
const connection = new Connection("https://api.devnet.solana.com", "confirmed");
const zkmix = new ZKMix({ connection, cluster: "devnet" });
const recipient = new PublicKey(recipientAddress);
const deposits: SavedDeposit[] = JSON.parse(fs.readFileSync(notesFile, "utf-8"));
console.log(`Found ${deposits.length} deposits to withdraw.`);
let totalWithdrawn = 0;
let totalFees = 0;
for (let i = 0; i < deposits.length; i++) {
const { denomination, note: noteString } = deposits[i];
const depositNote = zkmix.parseNote(noteString);
console.log(`\n[${i + 1}/${deposits.length}] Withdrawing ${denomination} SOL...`);
// Check if already spent
const isSpent = await zkmix.isNoteSpent(depositNote);
if (isSpent) {
console.log(" Skipping: already withdrawn.");
continue;
}
try {
const result = await zkmix.withdraw({
deposit: depositNote,
recipient,
useRelayer: true,
});
totalWithdrawn += result.amountReceived;
totalFees += result.relayerFee;
console.log(` Success! Tx: ${result.txSignature.slice(0, 20)}...`);
console.log(` Received: ${result.amountReceived / LAMPORTS_PER_SOL} SOL`);
// Wait between withdrawals to reduce timing correlation
const delay = 30_000 + Math.random() * 60_000; // 30-90 seconds
console.log(` Waiting ${Math.round(delay / 1000)}s before next withdrawal...`);
await new Promise((r) => setTimeout(r, delay));
} catch (err: any) {
console.error(` Failed: ${err.message}`);
}
}
console.log(`\nBatch withdrawal complete.`);
console.log(`Total withdrawn: ${totalWithdrawn / LAMPORTS_PER_SOL} SOL`);
console.log(`Total relayer fees: ${totalFees / LAMPORTS_PER_SOL} SOL`);
}
batchWithdraw("batch-deposit-1710512400.json", "FreshAddress111...").catch(console.error);Note the random delay between withdrawals. This is a privacy best practice. Withdrawing multiple deposits in rapid succession to the same address can make it easier for an observer to correlate the deposits, especially if the deposits were also made in rapid succession. Adding random delays significantly increases the difficulty of timing-based analysis.
Checking Pool Anonymity Sets
Query all pools and display their anonymity set sizes to help users choose the most private pool.
import { Connection } from "@solana/web3.js";
import { ZKMix } from "@zkmix/sdk";
async function displayPoolStats() {
const connection = new Connection("https://api.mainnet-beta.solana.com", "confirmed");
const zkmix = new ZKMix({ connection, cluster: "mainnet-beta" });
const pools = await zkmix.getPools();
console.log("ZKMix Pool Statistics");
console.log("=====================\n");
const sortedPools = pools.sort(
(a, b) => a.token.localeCompare(b.token) || Number(a.denomination - b.denomination)
);
for (const pool of sortedPools) {
const utilization = (pool.treeUtilization * 100).toFixed(2);
const privacyRating =
pool.anonymitySetSize > 10000 ? "Excellent" :
pool.anonymitySetSize > 1000 ? "Good" :
pool.anonymitySetSize > 100 ? "Fair" :
"Low";
console.log(`${pool.denominationFormatted}`);
console.log(` Anonymity set: ${pool.anonymitySetSize.toLocaleString()} (${privacyRating})`);
console.log(` Total deposits: ${pool.depositCount.toLocaleString()}`);
console.log(` Tree utilization: ${utilization}%`);
console.log(` Active: ${pool.isActive ? "Yes" : "No"}`);
console.log("");
}
}
displayPoolStats().catch(console.error);