> For the complete documentation index, see [llms.txt](https://zerc20.gitbook.io/zerc20/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://zerc20.gitbook.io/zerc20/for-developers/quickstart/proof-generation.md).

# Proof Generation

After scanning for incoming transfers (see [Receiving](/zerc20/for-developers/quickstart/receiving.md)), you generate a zero-knowledge proof to claim your tokens on-chain. The SDK provides a high-level `prepareRedeemTransactionForRecipient()` function that verifies the recipient address, handles proof mode selection, generates the proof, and assembles the transaction in a single call.

## Two Proof Modes

| Mode                       | Use case                            | Gas cost            |
| -------------------------- | ----------------------------------- | ------------------- |
| **Single** (Groth16)       | Claiming one transfer               | Higher per-transfer |
| **Batch** (Nova + Decider) | Claiming multiple transfers at once | Lower per-transfer  |

The SDK automatically selects the proof mode based on the number of eligible events in the `RedeemContext`.

## Root Modes

Each proof can be generated against one of two root sources:

| `teleportMode` | Root source                     | Decider circuit   | Verifier calldata                        |
| -------------- | ------------------------------- | ----------------- | ---------------------------------------- |
| `"global"`     | Hub aggregation root            | `withdraw_global` | `isGlobal=true`, `rootHint=latestAggSeq` |
| `"local"`      | Destination Verifier local root | `withdraw_local`  | `isGlobal=false`, `rootHint=treeIndex`   |

Use `"global"` for normal cross-chain redeem. Use `"local"` for local-only single-chain tokens that should redeem against their own Verifier without waiting for the Hub aggregation root. Local transaction preparation currently supports only eligible events from a single primary chain.

## Recommended: prepareRedeemTransactionForRecipient

The simplest way to generate proofs and build the on-chain transaction:

```typescript
import {
  prepareRedeemTransactionForRecipient,
  submitRedeemTransaction,
} from "@zerc20/client-sdk";

// redeemContext comes from collectRedeemContext() -- see Receiving page
const redeemTx = await prepareRedeemTransactionForRecipient({
  redeemContext,
  burn,                         // BurnArtifacts
  expectedRecipientAddress: address,
  teleportProofClient: sdk.teleportProofs,
  decider: sdk.decider,         // required for batch proofs
  teleportMode: "global",       // or "local" with a local-mode redeemContext
});

// Submit the prepared transaction
const { transactionHash } = await submitRedeemTransaction({
  writeProvider,    // EvmWriteProvider (e.g., viem WalletClient)
  tx: redeemTx,
  readProvider,     // optional: for receipt polling
});
```

`prepareRedeemTransactionForRecipient` first checks that `burn.generalRecipient.address` matches `expectedRecipientAddress`, then inspects the eligible events and chooses:

* **1 event** -- Groth16 single proof via `Verifier.singleTeleport()`
* **2+ events** -- Nova batch proof finalized through the Decider, submitted via `Verifier.teleport()`

The legacy `prepareRedeemTransaction()` helper is still exported for compatibility, but it is deprecated because it does not validate the burn recipient before proving.

## Single Proof (Advanced)

Use the low-level API when you need fine-grained control over the single proof workflow:

```typescript
const sdk = createSdk();

// `redeemContext` comes from collectRedeemContext() -- see Receiving page
const artifacts = await sdk.teleportProofs.createSingleTeleportProof({
  aggregationState: redeemContext.aggregationState,
  recipientFr,
  secretHex,
  event: redeemContext.events.eligible[0],
  proof: redeemContext.globalProofs[0],
});
```

For a local-mode single proof, use the local counterpart and pass the local proof from `redeemContext.eligibleProofs`:

```typescript
const artifacts = await sdk.teleportProofs.createSingleLocalTeleportProof({
  aggregationState: redeemContext.aggregationState,
  chainId: redeemContext.token.chainId,
  recipientFr,
  secretHex,
  proof: redeemContext.eligibleProofs[0],
});
```

## Batch Proof (Advanced)

Use the low-level API for batch proofs when you need to manage the Decider interaction yourself. Batch proofs are finalized through a **Decider** service that converts the Nova IVC proof into a Groth16 proof for on-chain verification.

```typescript
import {
  buildBatchRedeemTransactionForRecipient,
  createSdk,
  HttpDeciderClient,
} from "@zerc20/client-sdk";

const sdk = createSdk();
const decider = new HttpDeciderClient("https://decider.intmax.io");

const nova = await sdk.teleportProofs.createNovaProof({
  aggregationState: redeemContext.aggregationState,
  recipientFr,
  secretHex,
  events: redeemContext.events.eligible,
  proofs: redeemContext.globalProofs,
});

const deciderProof = await sdk.teleportProofs.requestDeciderProof(
  decider,
  nova.ivcProof,
  "global",
);

const redeemTx = buildBatchRedeemTransactionForRecipient({
  redeemContext,
  burn,
  expectedRecipientAddress: address,
  deciderProof,
  teleportMode: "global",
});
```

For local-mode batch proving, run local Nova and request a Decider proof with `mode: "local"` so the Decider uses `withdraw_local`:

```typescript
const nova = await sdk.teleportProofs.createNovaLocalProof({
  aggregationState: redeemContext.aggregationState,
  chainId: redeemContext.token.chainId,
  recipientFr,
  secretHex,
  proofs: redeemContext.eligibleProofs,
});

const deciderProof = await sdk.teleportProofs.requestDeciderProof(
  decider,
  nova.ivcProof,
  "local",
);
```

If you already have a Decider proof from an external source, use `buildBatchRedeemTransactionForRecipient()` to assemble the transaction without re-proving:

```typescript
import { buildBatchRedeemTransactionForRecipient } from "@zerc20/client-sdk";

const redeemTx = buildBatchRedeemTransactionForRecipient({
  redeemContext,
  burn,
  expectedRecipientAddress: address,
  deciderProof: proofBytes,  // Uint8Array from Decider
  teleportMode: "global",    // or "local" if proofBytes came from withdraw_local
});
```

`buildBatchRedeemTransaction()` remains available for older callers but is deprecated because it does not validate the burn recipient before calldata assembly.

## Next Steps

* [Receiving](/zerc20/for-developers/quickstart/receiving.md) -- scanning for incoming transfers and building proof inputs
* [SDK Quick Start](/zerc20/for-developers/quickstart.md) -- installation and first private send
* [Wrap and Unwrap](/zerc20/for-developers/quickstart/wrap-unwrap.md) -- converting between underlying tokens and zERC20
