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

Type: 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);
}
ESC
Type to search