IPrivacy.withdraw

withdraw(PrivacyWithdrawRequest request) external returns (bool success)

Spends a shielded note and pays the underlying transparent OKRW to request.recipient. msg.sender is the effective sender for PCL policy purposes. Failures revert with typed custom errors declared on IPrivacy (request-expiry, native-denom mismatch, nullifier collision) or inherited from IPrecompile (InvalidAmount, InvalidAddress, etc.).

Parameters

Name Type Required Description
request PrivacyWithdrawRequest ✓ The withdraw payload — proof, Merkle root, nullifier, amount (SDK coin string, e.g. "15000000000000000000atokrw" for 15 OKRW-worth of atokrw — must use the native denom or revert with PrivacyNativeDenomMismatch), recipient, chain id, and 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. withdraw is not payable.
PrivacyRequestExpired PrivacyRequestExpired Reverts when request.expiresAtUnix is zero or has already passed.
PrivacyNativeDenomMismatch PrivacyNativeDenomMismatch Reverts when request.amount uses a denom other than the network's native denom (atokrw). Payload is (got, expected).
PrivacyNullifierAlreadySpent PrivacyNullifierAlreadySpent Reverts when the nullifier is already recorded on-chain.
InvalidAmount InvalidAmount Inherited from IPrecompile. Reverts when request.amount fails to parse as a coin string. Encoded as InvalidAmount(string amount) where the payload is the raw amount string.
InvalidAddress InvalidAddress Inherited from IPrecompile. Reverts when the caller cannot be converted to a valid account address. Encoded as InvalidAddress(string bad).
PrivacyTxLimitExceeded PrivacyTxLimitExceeded Reverts when the transaction has already exceeded the global stateful-privacy call limit.
PrivacySenderTxLimitExceeded PrivacySenderTxLimitExceeded Reverts when the effective sender has already exceeded the per-sender stateful-privacy call 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

Basic withdraw

The amount string must use atokrw — the network's native denom. Passing another denom reverts with PrivacyNativeDenomMismatch.

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.USER_KEY as `0x${string}`),
  transport: http("https://rpc-testnet.maroo.io"),
});

// The proof is bound to this request: its amount, recipient and expiresAtUnix are public inputs of the
// withdraw proof. Pass the values the proof was generated for — a new deadline needs a new proof.
// The single withdraw amount fits inside the 64-bit shielded cap.
// TODO: replace with the real payout recipient before production.
const request = {
  proof: proofBytes,
  root: merkleRoot,
  nullifier: noteNullifier,
  amount: "15000000000000000000atokrw", // 15 OKRW
  recipient: "0x8F3ac2B1d9E74c05A6B18FE27Dc4913e5A0F7b62" as const,
  // the Cosmos chain-id, exactly (maroo-testnet-1 on the testnet): read it from the node, GET
  // /cosmos/base/tendermint/v1beta1/node_info → default_node_info.network, or from the .env template
  chainId: process.env.MAROO_COSMOS_CHAIN_ID as string,
  expiresAtUnix: provedExpiresAtUnix, // the deadline the proof was generated for, a few minutes ahead
};

await wallet.writeContract({
  address: PRIVACY,
  abi: iPrivacyAbi,
  functionName: "withdraw",
  args: [request],
});

Handling denom / expiry rejections

A denom mismatch is fixed by rebuilding the request with the correct atokrw denom string; the proof stays valid. An expired request needs a new proof: expiresAtUnix is a public input of the withdraw proof and part of the spend intent the note owner signs, so a fresh deadline means a new proof and a new signature.

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: "withdraw",
    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 === "PrivacyNativeDenomMismatch") {
    const [got, expected] = decoded.args as [string, string];
    console.warn(`use denom ${expected}; got ${got}`);
  } else if (decoded.errorName === "PrivacyRequestExpired") {
    const [expiresAtUnix] = decoded.args as [bigint];
    console.warn(`withdraw request expired at ${expiresAtUnix} — generate a new proof and spend signature with a fresh deadline`);
  } else {
    throw err;
  }
}
ESC
Type to search