Skip to content

Withdrawing Tokens

This guide covers the complete withdrawal flow — from looking up a transaction to generating a zero-knowledge proof and claiming tokens.

Withdrawal proves you know a valid commitment in the Merkle tree without revealing which one. The proof is generated client-side using Groth16 (via WASM) and verified on-chain.

You need:

  • A claiming key (received from the depositor)
  • Your Starknet address (the recipient specified during deposit)
  • A Starknet wallet to sign the withdrawal transaction
import {
initCore,
txSecret,
txHash,
fetchTxAssets,
getTxIndexInTree,
calculateMerkleRootAndProof,
full_prove,
getChamber,
} from "@mistcash/sdk";
await initCore();
  1. Look up the transaction

    First, verify the deposit exists and get its asset details:

    const chamber = getChamber(provider);
    const asset = await fetchTxAssets(chamber, claimingKey, myAddress);
    console.log("Token:", asset.addr);
    console.log("Amount:", asset.amount.toString());
  2. Get the Merkle tree state

    Fetch all transaction leaves and find the index of your transaction:

    const leaves = await chamber.tx_array();
    const txIndex = await getTxIndexInTree(
    leaves,
    claimingKey,
    myAddress,
    asset.addr,
    asset.amount.toString()
    );
  3. Compute the Merkle proof

    const [root, ...proof] = calculateMerkleRootAndProof(leaves, txIndex);
  4. Build the witness

    The witness contains everything the ZK circuit needs to generate a proof:

    const witness = {
    ClaimingKey: claimingKey,
    Owner: myAddress,
    TxAsset: asset.addr,
    Withdraw: {
    Amount: asset.amount.toString(),
    },
    MerkleRoot: root.toString(),
    MerkleProof: proof.map(String),
    Tx1Secret: txSecret(claimingKey, myAddress),
    };
  5. Generate the proof and submit

    const calldata = await full_prove(witness);
    await chamber.connect(account).handle_zkp(calldata);

    This generates a Groth16 proof in WASM and formats it as Starknet calldata using Garaga.

async function withdraw(account, provider, claimingKey, myAddress) {
await initCore();
const chamber = getChamber(provider);
// 1. Look up transaction
const asset = await fetchTxAssets(chamber, claimingKey, myAddress);
// 2. Get tree state
const leaves = await chamber.tx_array();
const txIndex = await getTxIndexInTree(
leaves, claimingKey, myAddress, asset.addr, asset.amount.toString()
);
// 3. Compute Merkle proof
const [root, ...proof] = calculateMerkleRootAndProof(leaves, txIndex);
// 4. Build witness
const witness = {
ClaimingKey: claimingKey,
Owner: myAddress,
TxAsset: asset.addr,
Withdraw: { Amount: asset.amount.toString() },
MerkleRoot: root.toString(),
MerkleProof: proof.map(String),
Tx1Secret: txSecret(claimingKey, myAddress),
};
// 5. Generate proof and withdraw
const calldata = await full_prove(witness);
await chamber.connect(account).handle_zkp(calldata);
}

The useMist hook handles this entire flow automatically:

import { useMist } from "@mistcash/react";
function WithdrawUI({ provider, sendTx }) {
const mist = useMist(provider, sendTx);
const handleWithdraw = async () => {
const asset = await mist.fetchAsset();
await mist.handleWithdraw(asset);
};
return (
<div>
<input
placeholder="Claiming Key"
value={mist.valKey}
onChange={(e) => mist.setKey(e.target.value)}
/>
<input
placeholder="Your Address"
value={mist.valTo}
onChange={(e) => mist.setTo(e.target.value)}
/>
<button onClick={handleWithdraw} disabled={mist.isPending}>
{mist.isPending ? "Generating proof..." : "Withdraw"}
</button>
</div>
);
}