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 | Inherited from IPrecompile. Reverts when the ABI-encoded call does not carry exactly one argument. |
UnknownMethod | UnknownMethod | Inherited from IPrecompile. Reverts when the ABI selector does not match any function on the precompile. |
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 { privacyAbi } from "@maroo-chain/contracts/abi/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: privacyAbi,
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 { decodeErrorResult } from "viem";
import { privacyAbi } from "@maroo-chain/contracts/abi/IPrivacy";
try {
await wallet.writeContract({
address: PRIVACY,
abi: privacyAbi,
functionName: "transfer",
args: [request],
});
} catch (err: any) {
if (!err?.data) throw err;
const decoded = decodeErrorResult({ abi: privacyAbi, data: err.data });
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;
}
}