IPrivacy.singleProofBatchTransferWithAuthorization
singleProofBatchTransferWithAuthorization(
PrivacySingleProofBatchTransferRequest request,
PrivacyActionAuthorization authorization
) external returns (bool success) Meta-transaction variant of singleProofBatchTransfer: a relayer submits the batch on behalf of authorization.effectiveSender, who signed an off-chain authorization binding the request hash to a specific executor, nonce, and deadline. Supports EOA signatures, ERC-1271 smart-account signatures, and EIP-7702 delegated-EOA signatures, chosen via authorization.authorizationKind. The request-hash domain is dedicated to the single-proof batch method — it will not collide with authorizations for other privacy calls. Failures revert with a typed custom error: IPrivacy declares its own, and inherits the shared protocol errors from IPrecompile, so a client decodes the 4-byte selector rather than matching reason text. One case is still a plain string — an EOA signature that is not exactly 65 bytes is rejected before any typed error is built.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
request | PrivacySingleProofBatchTransferRequest | ✓ | Same batch payload as singleProofBatchTransfer. Its hash — computed over the method ID and the ABI-encoded request tuple — is bound into the authorization signature. |
authorization | PrivacyActionAuthorization | ✓ | Off-chain authorization. Fields: effectiveSender (the account the policy engine treats as the actual sender), executor (must equal msg.sender — the relayer address), nonce (unique per effectiveSender; replay-protected), deadline (unix seconds; must be in the future), authorizationKind (0=EOA, 1=ERC-1271, 2=EIP-7702; other values revert), and signature (bytes; for EOA a 65-byte (r, s, v) blob). |
Returns
bool Returns true on success. Failures revert with a typed custom error — decode the 4-byte selector against the IPrivacy ABI, which includes the errors inherited from IPrecompile.
Errors
| Code | Name | Description |
|---|---|---|
PrivacyInvalidAuthorizationField | PrivacyInvalidAuthorizationField(string field) | Reverts when a required authorization field is missing or malformed. The payload names the field: effectiveSender, executor, nonce or deadline. |
RequesterIsNotMsgSender | RequesterIsNotMsgSender(address msgSender, address requester) | Reverts when the caller is not the executor the signature was bound to. Inherited from IPrecompile, so the selector is shared with the other precompiles. |
PrivacyAuthorizationExpired | PrivacyAuthorizationExpired(uint64 deadline) | Reverts when the block time has passed authorization.deadline. Payload is the deadline that was signed. |
PrivacyAuthorizationNonceUsed | PrivacyAuthorizationNonceUsed(address sender, uint256 nonce) | Reverts when this (effectiveSender, nonce) pair has already been consumed — the replay guard. |
PrivacyAuthorizationSignerMismatch | PrivacyAuthorizationSignerMismatch(address expected, address got) | Reverts when the address recovered from the signature is not effectiveSender. Applies to the EOA and EIP-7702 paths; the payload carries both addresses. |
PrivacyAuthorizationRejected | PrivacyAuthorizationRejected(uint8 authorizationKind) | Reverts when the account shape does not match the declared authorizationKind (EOA with code, ERC-1271 without code, EIP-7702 without delegated code), when an ERC-1271 verifier rejects, or when the kind is unknown. Payload is the kind that was tried. |
PrivacyInvalidAuthorizationMagic | PrivacyInvalidAuthorizationMagic(bytes4 magic) | Reverts when an ERC-1271 verifier returns a value other than the expected magic. Payload is what it returned. |
PrivacyRequestExpired | PrivacyRequestExpired(uint64 expiresAtUnix) | Reverts when request.expiresAtUnix is zero or already passed. This is the batch payload's own expiry, separate from the authorization deadline above. |
PrivacyDuplicateNullifier | PrivacyDuplicateNullifier() | Reverts when two items inside this batch spend the same nullifier. |
PrivacyNullifierAlreadySpent | PrivacyNullifierAlreadySpent() | Reverts when a nullifier in the batch is already recorded on-chain (double-spend). |
PrivacyDuplicateCommitment | PrivacyDuplicateCommitment() | Reverts when two items inside this batch produce the same output commitment. |
PrivacyCommitmentAlreadyExists | PrivacyCommitmentAlreadyExists() | Reverts when an output commitment in the batch already exists in the Merkle tree. |
PrivacyBatchSizeOutOfRange | PrivacyBatchSizeOutOfRange(uint256 count, uint256 max) | Reverts when the batch carries no items or more than the circuit supports. Payload carries the count and the maximum. |
privacy EOA authorization signature must be 65 bytes | privacy EOA authorization signature must be 65 bytes | The one PLAIN-STRING revert on this method: an EOA signature of the wrong length is rejected before a typed error is constructed, so match this one on text. |
Examples
Relayer submits an authorized single-proof batch
The relayer's own address is checked against authorization.executor; any mismatch reverts before the proof is evaluated. Pin the nonce off-chain (per-effectiveSender monotonic counter is fine).
import { createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
const PRIVACY = "0x100000000000000000000000000000000000000b" as const;
// ABI snippet — reuse the `PrivacySingleProofBatchTransferRequest` shape
// documented on `contract-privacy-single-proof-batch-transfer`.
const privacyAbi = [{
type: "function",
name: "singleProofBatchTransferWithAuthorization",
stateMutability: "nonpayable",
inputs: [
{ name: "request", type: "tuple", components: [ /* … */ ] },
{ name: "authorization", type: "tuple", components: [
{ name: "effectiveSender", type: "address" },
{ name: "executor", type: "address" },
{ name: "nonce", type: "uint256" },
{ name: "deadline", type: "uint64" },
{ name: "authorizationKind", type: "uint8" },
{ name: "signature", type: "bytes" },
]},
],
outputs: [{ type: "bool" }],
}] as const;
// The relayer's key. Must equal `authorization.executor`.
// TODO: replace with the real relayer address before production.
const relayer = createWalletClient({
account: privateKeyToAccount(process.env.RELAYER_KEY as `0x${string}`),
transport: http("https://rpc-testnet.maroo.io"),
});
// `request` and `authorization` come from the effective sender's wallet.
// `authorization.signature` is over the EIP-712 domain documented on
// `privacy-authorization-eip712-domain`, using the single-proof batch
// request-hash.
await relayer.writeContract({
address: PRIVACY,
abi: privacyAbi,
functionName: "singleProofBatchTransferWithAuthorization",
args: [request, authorization],
}); Handling authorization rejections
Failures decode off the ABI. Keep the errors IPrivacy inherits from IPrecompile in the ABI you hand to viem, or RequesterIsNotMsgSender arrives undecoded. One path has no typed error and is matched on text — see the default branch.
import { BaseError, ContractFunctionRevertedError } from "viem";
try {
await relayer.writeContract({
address: PRIVACY,
abi: privacyAbi, // must include IPrecompile's inherited errors
functionName: "singleProofBatchTransferWithAuthorization",
args: [request, authorization],
});
} catch (err) {
if (err instanceof BaseError) {
const revert = err.walk(e => e instanceof ContractFunctionRevertedError);
if (revert instanceof ContractFunctionRevertedError) {
switch (revert.data?.errorName) {
case "PrivacyAuthorizationExpired": {
const [deadline] = revert.data.args as [bigint];
// ask the effective sender to re-sign past `deadline`
break;
}
case "PrivacyAuthorizationNonceUsed": {
const [sender, nonce] = revert.data.args as [`0x${string}`, bigint];
// bump the nonce and re-sign
break;
}
case "RequesterIsNotMsgSender": {
// inherited from IPrecompile: the signature was bound to a different relayer
break;
}
case "PrivacyInvalidAuthorizationField": {
const [field] = revert.data.args as [string];
// "effectiveSender" | "executor" | "nonce" | "deadline"
break;
}
case "PrivacyAuthorizationSignerMismatch": {
const [expected, got] = revert.data.args as [`0x${string}`, `0x${string}`];
// the recovered signer is not the effective sender
break;
}
default: {
// The one plain-string path here: the length check runs before any typed
// error is built, so match it on PREFIX.
if ((revert.reason ?? "").startsWith("privacy EOA authorization signature must be 65 bytes")) {
// re-sign — the signature is the wrong length
}
}
}
}
}
throw err;
}