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

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