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;
}
}