IPrivacy.transfer

transfer(PrivacyTransferRequest request) external returns (bool success)

Spends one or more shielded notes and creates new notes under a Groth16 proof. msg.sender is the effective sender — PCL evaluates policies against the caller directly. Failures revert with typed custom errors declared on IPrivacy (request-expiry, nullifier/commitment collisions, Merkle capacity, per-transaction limits) or inherited from IPrecompile. PCL policy failures surface as PCL ReasonCodes from IPcl instead.

Parameters

Name Type Required Description
request PrivacyTransferRequest ✓ The shielded transfer payload — proof, Merkle root, nullifiers, new commitments, ciphertexts, view tags, disclosure fields, and expiresAtUnix. expiresAtUnix is required and must be greater than the current block time; a zero or past value reverts with PrivacyRequestExpired(expiresAtUnix).

Returns

Type: bool

Returns true on success. Failures revert with a typed custom error rather than returning false.

Errors

Code Name Description
PrivacyNonPayable PrivacyNonPayable Reverts when msg.value is non-zero. Only deposit accepts value. Payload is the method name.
PrivacyRequestExpired PrivacyRequestExpired Reverts when request.expiresAtUnix is zero or has already passed. Payload carries the unix timestamp.
PrivacyNullifierAlreadySpent PrivacyNullifierAlreadySpent Reverts when a nullifier in the request is already recorded on-chain (double-spend attempt).
PrivacyCommitmentAlreadyExists PrivacyCommitmentAlreadyExists Reverts when a new commitment in the request already exists in the Merkle tree.
PrivacyMerkleCapacityExceeded PrivacyMerkleCapacityExceeded Reverts when appending the new commitments would exceed the shielded-pool Merkle tree capacity. Payload is the number of outputs.
PrivacyTxLimitExceeded PrivacyTxLimitExceeded Reverts when the current transaction has already made too many stateful privacy calls (global cap).
PrivacySenderTxLimitExceeded PrivacySenderTxLimitExceeded Reverts when the effective sender has already made too many stateful privacy calls within this transaction. Payload is (sender, limit).
InvalidNumberOfArgs InvalidNumberOfArgs Declared in the shared IPrecompile error set as InvalidNumberOfArgs(uint256 expected, uint256 got), but this function does not revert with it for a wrong argument count: calldata too short for the request reverts with ABISetupFailed, and bytes after the request are ignored.
ABISetupFailed ABISetupFailed Inherited from IPrecompile as ABISetupFailed(string reason). Reverts when the calldata cannot be decoded against the precompile's ABI: an unknown 4-byte selector gives the reason no method with id: 0x… (usually a stale ABI), and calldata too short for the arguments gives the decoder's message, for example abi: cannot marshal in to go type: length insufficient 32 require 64. An unknown selector does not revert with UnknownMethod, although the precompile declares it.

Examples

Direct shielded transfer

msg.sender is treated as the effective sender for PCL and per-sender rate-limit accounting.

import { createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { iPrivacyAbi } from "@maroo-chain/contracts/abi/precompiles/privacy/IPrivacy";

const PRIVACY = "0x100000000000000000000000000000000000000b" as const;

const wallet = createWalletClient({
  account: privateKeyToAccount(process.env.SENDER_KEY as `0x${string}`),
  transport: http("https://rpc-testnet.maroo.io"),
});

// `request` is prepared off-chain; expiresAtUnix must be in the future.
await wallet.writeContract({
  address: PRIVACY,
  abi: iPrivacyAbi,
  functionName: "transfer",
  args: [request],
});

Decoding a shielded-pool collision

Both errors mean the client's note set is stale relative to the on-chain tree — refresh before retrying.

import { BaseError, ContractFunctionRevertedError, decodeErrorResult } from "viem";
import { iPrivacyAbi } from "@maroo-chain/contracts/abi/precompiles/privacy/IPrivacy";

try {
  await wallet.writeContract({
    address: PRIVACY,
    abi: iPrivacyAbi,
    functionName: "transfer",
    args: [request],
  });
} catch (err) {
  // viem keeps the revert data on the ContractFunctionRevertedError in the cause chain, not on err.
  const revert = err instanceof BaseError ? err.walk((e) => e instanceof ContractFunctionRevertedError) : null;
  if (!(revert instanceof ContractFunctionRevertedError) || !revert.raw) throw err;
  const decoded = decodeErrorResult({ abi: iPrivacyAbi, data: revert.raw });
  if (decoded.errorName === "PrivacyNullifierAlreadySpent") {
    // The user tried to spend a note that is already gone — refresh their note
    // set from the indexer and rebuild the proof.
    console.warn("note already spent — reload note set");
  } else if (decoded.errorName === "PrivacyMerkleCapacityExceeded") {
    const [count] = decoded.args as [bigint];
    console.warn(`shielded tree is full (${count} outputs requested)`);
  } else {
    throw err;
  }
}
ESC
Type to search