IPrivacy.singleProofBatchTransfer
singleProofBatchTransfer(
PrivacySingleProofBatchTransferRequest request
) external returns (bool success) Applies a batch join-split (multiple inputs, multiple outputs) validated by a single Groth16 proof. Input count must be in 1..BatchJoinSplitV1MaxInputs, output count must be in 1..BatchJoinSplitV1MaxOutputs; out-of-range counts revert with PrivacyBatchSizeOutOfRange(count, max). Failures surface as typed custom errors on IPrivacy or inherited on IPrecompile.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
request | PrivacySingleProofBatchTransferRequest | ✓ | The single-proof batch payload — one proof, one root, an array of input nullifiers, an array of output entries (each with commitment + ciphertext + disclosure fields), audit-key metadata, audit disclosure target pubkey, and expiresAtUnix. |
Returns
bool Returns true on success. Failures revert with a typed custom error rather than returning false.
Errors
| Code | Name | Description |
|---|---|---|
PrivacyBatchSizeOutOfRange | PrivacyBatchSizeOutOfRange | Reverts when input count is outside 1..BatchJoinSplitV1MaxInputs or output count is outside 1..BatchJoinSplitV1MaxOutputs. Payload is (count, max). |
PrivacyRequestExpired | PrivacyRequestExpired | Reverts when request.expiresAtUnix is zero or has already passed. |
PrivacyDuplicateNullifier | PrivacyDuplicateNullifier | Reverts when the same nullifier appears more than once in request.nullifiers. |
PrivacyNullifierAlreadySpent | PrivacyNullifierAlreadySpent | Reverts when any nullifier is already recorded on-chain. |
PrivacyDuplicateCommitment | PrivacyDuplicateCommitment | Reverts when the same output commitment appears more than once in request.outputs. |
PrivacyCommitmentAlreadyExists | PrivacyCommitmentAlreadyExists | Reverts when any output commitment already exists in the Merkle tree. |
PrivacyMerkleCapacityExceeded | PrivacyMerkleCapacityExceeded | Reverts when appending the outputs would exceed the shielded-pool Merkle tree capacity. |
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 one argument. |
Examples
Merge two notes into one
A single proof authorises the whole join-split — cheaper than a multi-proof batchTransfer when all inputs belong to the same sender.
import { createWalletClient, http } 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"),
});
// request.nullifiers.length in 1..BatchJoinSplitV1MaxInputs;
// request.outputs.length in 1..BatchJoinSplitV1MaxOutputs.
await wallet.writeContract({
address: PRIVACY,
abi: privacyAbi,
functionName: "singleProofBatchTransfer",
args: [request],
}); Reacting to a size violation
PrivacyBatchSizeOutOfRange fires for both inputs and outputs; log the offending value against the interface's BatchJoinSplitV1MaxInputs / BatchJoinSplitV1MaxOutputs limits.
import { decodeErrorResult } from "viem";
import { privacyAbi } from "@maroo-chain/contracts/abi/IPrivacy";
try {
await wallet.writeContract({
address: PRIVACY,
abi: privacyAbi,
functionName: "singleProofBatchTransfer",
args: [request],
});
} catch (err: any) {
if (!err?.data) throw err;
const decoded = decodeErrorResult({ abi: privacyAbi, data: err.data });
if (decoded.errorName === "PrivacyBatchSizeOutOfRange") {
const [count, max] = decoded.args as [bigint, bigint];
console.warn(`batch input or output count ${count} exceeds max ${max}`);
} else {
throw err;
}
}