IPrivacy.singleProofBatchTransfer
singleProofBatchTransfer(
PrivacySingleProofBatchTransferRequest request
) external returns (bool success) Executes a single-proof batch transfer against the privacy precompile at 0x100000000000000000000000000000000000000b. One zero-knowledge proof authorizes spending 1..N input notes (identified by nullifiers) and creating 1..N output notes (outputs), so the amortized proving cost per transfer is far lower than calling transfer in a loop. Input and output counts are each bounded by the chain's BatchJoinSplitV1MaxInputs / BatchJoinSplitV1MaxOutputs limits, and the effective sender is the direct caller (msg.sender). Failures surface as plain string reverts — this precompile declares no typed custom errors.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
request | PrivacySingleProofBatchTransferRequest | ✓ | The batch payload. Fields: proof (single ZK proof covering all inputs and outputs), root (Merkle root snapshot the proof was built against), nullifiers (bytes[] of note nullifiers — must be unique and unspent), outputs (array of PrivacySingleProofBatchTransferOutput carrying commitment, ciphertext, viewTag, per-note disclosure policy fields, and audit disclosure payload), auditKeyId / auditKeyEpoch / auditDisclosureTargetPubkey (audit-key binding), and expiresAtUnix (must be non-zero and strictly greater than the block time — otherwise the call reverts). |
Returns
bool Returns true on success. Failures revert with a plain string reason (no typed custom error).
Errors
| Code | Name | Description |
|---|---|---|
expiresAtUnix is required | expiresAtUnix is required | String revert when request.expiresAtUnix is zero. expiresAtUnix must be a future unix timestamp; a zero value is treated as unset and rejected before the proof is evaluated. |
single-proof batch transfer payload has expired | single-proof batch transfer payload has expired | String revert when the block time has already reached expiresAtUnix. The payload's TTL has passed and it cannot be resubmitted without a fresh proof and expiry. |
privacy single-proof batch input count must be in 1..%d | privacy single-proof batch input count must be in 1..%d | String revert when nullifiers is empty or exceeds BatchJoinSplitV1MaxInputs. %d in the raw error is filled with the actual chain-configured maximum. |
privacy single-proof batch output count must be in 1..%d | privacy single-proof batch output count must be in 1..%d | String revert when outputs is empty or exceeds BatchJoinSplitV1MaxOutputs. |
duplicate privacy single-proof batch nullifier | duplicate privacy single-proof batch nullifier | String revert when the same nullifier appears twice inside request.nullifiers. |
privacy single-proof batch nullifier already spent | privacy single-proof batch nullifier already spent | String revert when one of the supplied nullifiers has already been consumed by a prior privacy call. |
duplicate privacy single-proof batch output commitment | duplicate privacy single-proof batch output commitment | String revert when two outputs in request.outputs share the same commitment. |
privacy single-proof batch output commitment already exists | privacy single-proof batch output commitment already exists | String revert when an output's commitment is already present in the on-chain commitment index. |
not enough merkle tree capacity for privacy single-proof batch outputs: %w | not enough merkle tree capacity for privacy single-proof batch outputs | String revert when appending len(outputs) new commitments would overflow the Merkle tree capacity. |
stateful privacy call global tx limit exceeded | stateful privacy call global tx limit exceeded | String revert when the per-block global cap on stateful privacy calls has already been consumed. |
stateful privacy call per-effective-sender tx limit exceeded | stateful privacy call per-effective-sender tx limit exceeded | String revert when the per-block per-effective-sender cap on stateful privacy calls has been consumed by prior calls in this block. |
Examples
Submit a single-proof batch transfer with viem
The single-proof form amortizes verification cost across all input / output pairs. expiresAtUnix is required — set it just past the expected inclusion window; if the payload sits in the mempool past that timestamp the call reverts and must be re-proved.
import { createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
const PRIVACY = "0x100000000000000000000000000000000000000b" as const;
const privacyAbi = [{
type: "function",
name: "singleProofBatchTransfer",
stateMutability: "nonpayable",
inputs: [{
name: "request",
type: "tuple",
components: [
{ name: "proof", type: "bytes" },
{ name: "root", type: "bytes" },
{ name: "nullifiers", type: "bytes[]" },
{ name: "outputs", type: "tuple[]", components: [
{ name: "commitment", type: "bytes" },
{ name: "ciphertext", type: "bytes" },
{ name: "viewTag", type: "bytes" },
{ name: "userPrivacyPolicy", type: "uint32" },
{ name: "userDisclosureMode", type: "uint8" },
{ name: "userDisclosureDigest", type: "bytes" },
{ name: "userDisclosureTargetPubkey", type: "bytes" },
{ name: "userDisclosurePayload", type: "bytes" },
{ name: "fullDisclosureDigest", type: "bytes" },
{ name: "auditDisclosurePayload", type: "bytes" },
{ name: "selfViewDisclosurePayload", type: "bytes" },
]},
{ name: "auditKeyId", type: "string" },
{ name: "auditKeyEpoch", type: "uint64" },
{ name: "auditDisclosureTargetPubkey", type: "bytes" },
{ name: "expiresAtUnix", type: "uint64" },
],
}],
outputs: [{ type: "bool" }],
}] as const;
const wallet = createWalletClient({
account: privateKeyToAccount(process.env.SENDER_KEY as `0x${string}`),
transport: http("https://rpc-testnet.maroo.io"),
});
// `request` is prepared by your off-chain prover; the fields below are
// placeholder shapes and must be replaced with real prover output.
const request = {
proof: "0x...",
root: "0x...",
nullifiers: ["0x...", "0x..."] as `0x${string}`[],
outputs: [
/* PrivacySingleProofBatchTransferOutput entries */
],
auditKeyId: "audit-key-1",
auditKeyEpoch: 1n,
auditDisclosureTargetPubkey: "0x...",
// TTL must be a future unix timestamp; a fresh prover run should set this.
expiresAtUnix: BigInt(Math.floor(Date.now() / 1000) + 300),
};
await wallet.writeContract({
address: PRIVACY,
abi: privacyAbi,
functionName: "singleProofBatchTransfer",
args: [request],
}); Decoding a string-revert failure
Do not attempt to decodeErrorResult on this precompile — it emits string reverts, not typed custom errors. Match on the verbatim reason substring for UX branching.
import { BaseError, ContractFunctionRevertedError } from "viem";
try {
await wallet.writeContract({
address: PRIVACY,
abi: privacyAbi,
functionName: "singleProofBatchTransfer",
args: [request],
});
} catch (err) {
if (err instanceof BaseError) {
const revert = err.walk(e => e instanceof ContractFunctionRevertedError);
if (revert instanceof ContractFunctionRevertedError) {
// The privacy precompile declares no typed custom errors, so
// `revert.reason` is a plain string such as:
// "privacy single-proof batch nullifier already spent"
// "single-proof batch transfer payload has expired"
console.error("privacy revert:", revert.reason);
}
}
throw err;
}