IPrivacy.transfer
transfer(PrivacyTransferRequest request) external returns (bool success) Executes a shielded transfer: consumes one or more nullifiers, appends new commitments to the Merkle tree, and records ciphertexts / view tags / disclosure payloads. PrivacyTransferRequest carries a mandatory expiresAtUnix deadline — the request is rejected with a plain string revert if it is zero, overflows int64, or the current block time is at or past that timestamp. Failures do NOT arrive as a typed custom error (IPrivacy declares no error types); the reason string is returned verbatim in the revert data and cannot be decoded with decodeErrorResult.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
request | PrivacyTransferRequest | ✓ | The shielded transfer request. See IPrivacy.sol for the full struct — key fields include proof, root, nullifiers, newCommitments, cipherTexts, viewTags, disclosure fields, and the required expiresAtUnix (unix seconds; must be strictly greater than the current block time and must fit in an int64). |
Returns
bool Returns true on success. Failures revert with a plain string reason.
Errors
| Code | Name | Description |
|---|---|---|
expiresAtUnix is required | expiresAtUnix is required | String revert (not a typed custom error). Returned when expiresAtUnix is zero. IPrivacy declares no error types, so the reason is the literal string and clients must match on it — decodeErrorResult cannot decode it. |
expiresAtUnix overflows int64 | expiresAtUnix overflows int64 | String revert. Returned when expiresAtUnix exceeds int64 range. |
transfer payload has expired | transfer payload has expired | String revert. Returned when the current block time is at or past expiresAtUnix. |
Examples
Build a transfer request with a bounded expiry
The expiresAtUnix field is now required. Setting it a few minutes in the future prevents replay of a stale payload if the transaction lingers in the mempool.
import { createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
const PRIVACY = "0x100000000000000000000000000000000000000b" as const;
const wallet = createWalletClient({
account: privateKeyToAccount(process.env.OPERATOR_KEY as `0x${string}`),
transport: http("https://rpc-testnet.maroo.io"),
});
// Expire 5 minutes into the future. Never send 0 or a past timestamp —
// the precompile now rejects both with a plain string revert.
const nowSec = Math.floor(Date.now() / 1000);
const expiresAtUnix = BigInt(nowSec + 300);
const request = {
proof: "0x...",
root: "0x...",
nullifiers: ["0x..."],
newCommitments: ["0x..."],
cipherTexts: ["0x..."],
viewTags: ["0x..."],
userPrivacyPolicy: 0,
userDisclosureDigest: "0x",
userDisclosureMode: 0,
userDisclosureTargetPubkey: "0x",
userDisclosurePayload: "0x",
auditDisclosureDigest: "0x",
auditDisclosureTargetPubkey: "0x",
auditDisclosurePayload: "0x",
selfViewDisclosureDigest: "0x",
selfViewDisclosurePayload: "0x",
expiresAtUnix,
} as const;
await wallet.writeContract({
address: PRIVACY,
abi: privacyAbi,
functionName: "transfer",
args: [request],
}); Handling expiry rejection on the client
Because IPrivacy declares no typed errors, expiry rejections must be matched against the verbatim reason string — decodeErrorResult will not decode them.
try {
await wallet.writeContract({
address: PRIVACY,
abi: privacyAbi,
functionName: "transfer",
args: [request],
});
} catch (err: any) {
// IPrivacy has no typed errors — the reason arrives as a plain string.
const reason: string = err?.shortMessage || err?.message || "";
if (reason.includes("transfer payload has expired")) {
// Re-sign the payload with a fresh expiresAtUnix and retry.
} else if (reason.includes("expiresAtUnix is required")) {
// Programmer error: caller forgot to set expiresAtUnix.
} else {
throw err;
}
}