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

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