Docs/Sdk/Examples

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.

typescript
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.

typescript
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.

typescript
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:

bash
# 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.

typescript
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:

typescript
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.

typescript
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.

typescript
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.

typescript
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);