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 Declared in the shared IPrecompile error set as InvalidNumberOfArgs(uint256 expected, uint256 got), but this function does not revert with it for a wrong argument count: calldata too short for the two arguments reverts with ABISetupFailed, and bytes after them are ignored.
ABISetupFailed ABISetupFailed Inherited from IPrecompile as ABISetupFailed(string reason). Reverts when the calldata cannot be decoded against the precompile's ABI: an unknown 4-byte selector gives the reason no method with id: 0x… (usually a stale ABI), and calldata too short for the arguments gives the decoder's message, for example abi: cannot marshal in to go type: length insufficient 32 require 64. An unknown selector does not revert with UnknownMethod, although the precompile declares it.

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 { iPrivacyAbi } from "@maroo-chain/contracts/abi/precompiles/privacy/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: iPrivacyAbi,
  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 { BaseError, ContractFunctionRevertedError, decodeErrorResult } from "viem";
import { iPrivacyAbi } from "@maroo-chain/contracts/abi/precompiles/privacy/IPrivacy";

try {
  await wallet.writeContract({
    address: PRIVACY,
    abi: iPrivacyAbi,
    functionName: "batchTransfer",
    args: [batchId, requests],
  });
} catch (err) {
  // viem keeps the revert data on the ContractFunctionRevertedError in the cause chain, not on err.
  const revert = err instanceof BaseError ? err.walk((e) => e instanceof ContractFunctionRevertedError) : null;
  if (!(revert instanceof ContractFunctionRevertedError) || !revert.raw) throw err;
  const decoded = decodeErrorResult({ abi: iPrivacyAbi, data: revert.raw });
  // 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