IPrivacy.batchTransfer
batchTransfer(
bytes32 batchId,
PrivacyTransferRequest[] requests
) external returns (bool success) Applies multiple independently-proved shielded transfers atomically inside a single transaction, all keyed by batchId. Each element is preflighted for duplicate nullifiers, duplicate commitments, and Merkle capacity against the whole batch — a single failure reverts the entire call. Failures surface as typed custom errors on IPrivacy or inherited on IPrecompile.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
batchId | bytes32 | ✓ | A caller-chosen batch identifier that must be non-zero and unique within the transaction. Each batch item event and log carries it so off-chain indexers can group them. |
requests | PrivacyTransferRequest[] | ✓ | The batch items. Length must be in 1..MaxPrivacyMultiProofBatchItems — empty or oversize batches revert with PrivacyBatchSizeOutOfRange(count, max). Every nullifier and every new commitment must be unique across the whole batch and not already recorded on-chain. |
Returns
bool Returns true when all items commit. Any failure reverts the whole batch.
Errors
| Code | Name | Description |
|---|---|---|
PrivacyBatchSizeOutOfRange | PrivacyBatchSizeOutOfRange | Reverts when requests is empty or larger than the protocol maximum. Payload is (count, max). |
PrivacyDuplicateNullifier | PrivacyDuplicateNullifier | Reverts when the same nullifier appears in more than one item of the batch. |
PrivacyNullifierAlreadySpent | PrivacyNullifierAlreadySpent | Reverts when any nullifier in the batch is already recorded on-chain. |
PrivacyDuplicateCommitment | PrivacyDuplicateCommitment | Reverts when the same new commitment appears in more than one item of the batch. |
PrivacyCommitmentAlreadyExists | PrivacyCommitmentAlreadyExists | Reverts when any new commitment in the batch already exists in the Merkle tree. |
PrivacyMerkleCapacityExceeded | PrivacyMerkleCapacityExceeded | Reverts when appending the batch's outputs would exceed the shielded-pool Merkle tree capacity. |
PrivacyRequestExpired | PrivacyRequestExpired | Reverts when any item's expiresAtUnix is zero or has already passed. |
PrivacyTxLimitExceeded | PrivacyTxLimitExceeded | Reverts when the transaction has already exceeded the global stateful-privacy call limit. |
InvalidNumberOfArgs | InvalidNumberOfArgs | Inherited from IPrecompile. Reverts when the call does not carry exactly two arguments. |
Examples
Bundle five shielded transfers
Each of the 5 items proves independently, but they commit or revert together. Use a fresh batchId per transaction so PrivacyBatchTransferItem events cluster cleanly.
import { createWalletClient, http, keccak256, toHex } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { privacyAbi } from "@maroo-chain/contracts/abi/IPrivacy";
const PRIVACY = "0x100000000000000000000000000000000000000b" as const;
const wallet = createWalletClient({
account: privateKeyToAccount(process.env.SENDER_KEY as `0x${string}`),
transport: http("https://rpc-testnet.maroo.io"),
});
const batchId = keccak256(toHex(`payroll-${Date.now()}`));
await wallet.writeContract({
address: PRIVACY,
abi: privacyAbi,
functionName: "batchTransfer",
args: [batchId, requests], // requests.length in 1..MaxPrivacyMultiProofBatchItems
}); Diagnosing which item made the batch fail
Batch preflight errors are shape-level (dedupe, capacity) — a single failing item taints the whole batch. Split the batch and retry to isolate the item if needed.
import { decodeErrorResult } from "viem";
import { privacyAbi } from "@maroo-chain/contracts/abi/IPrivacy";
try {
await wallet.writeContract({
address: PRIVACY,
abi: privacyAbi,
functionName: "batchTransfer",
args: [batchId, requests],
});
} catch (err: any) {
if (!err?.data) throw err;
const decoded = decodeErrorResult({ abi: privacyAbi, data: err.data });
// The typed error identifies WHICH invariant failed. Recompute preflight
// client-side (dedupe nullifiers/commitments, refresh note set) and retry.
console.warn("batch preflight failed:", decoded.errorName, decoded.args);
}