IPrivacy.withdraw
withdraw(PrivacyWithdrawRequest request) external returns (bool success) Unshields value from the privacy pool: consumes the note identified by nullifier and pays amount (as an aokrw coin string) to recipient in the clear. The precompile verifies a zero-knowledge proof that the nullifier corresponds to an unspent note under root whose value covers the withdrawn amount. Unlike earlier drafts of this API, withdraw no longer produces change back into the shielded pool — there is no newNoteCommitment or encryptedNote; any residual value must be handled by a separate shielded transfer beforehand.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
request.proof | bytes | ✓ | Zero-knowledge proof binding nullifier, root, and amount to an unspent note. Proof verification carries a fixed gas surcharge of 1,500,000. |
request.root | bytes | ✓ | Merkle root snapshot the proof is anchored against. Must be a root the chain still retains. |
request.nullifier | bytes | ✓ | Nullifier for the note being consumed. Must not already appear in the spent-set. |
request.amount | string | ✓ | Coin string being withdrawn (e.g. "10000000000000000000000000aokrw" for 10,000,000 OKRW). Must match the native privacy denom. |
request.recipient | address | ✓ | EOA or smart-account address receiving the unshielded funds. Must not be the zero address. |
request.chainId | string | ✓ | Chain identifier the payload is bound to. Must equal the chain's own id; mismatch reverts. |
request.expiresAtUnix | uint64 | ✓ | Unix-seconds deadline. A stale withdrawal reverts with "withdraw payload has expired". |
Returns
bool Returns true on successful unshielding. Failures revert with a plain string reason rather than returning false.
Errors
| Code | Name | Description |
|---|---|---|
string-revert | "withdraw payload has expired" | Reverts as a plain string when expiresAtUnix is at or before block time. |
string-revert | "expiresAtUnix is required" | Reverts as a plain string when expiresAtUnix is zero. |
string-revert | "privacy precompile only supports native denom %q, got %q" | Reverts as a plain string when the amount coin string uses a denom other than the chain's native privacy denom (aokrw on Maroo). |
string-revert | "invalid zero address" | Reverts as a plain string when recipient is the zero address. |
Examples
Unshield 10,000,000 OKRW to a recipient
Withdraw fully unshields the note identified by nullifier. If the note's value exceeds amount, split it in a prior shielded transfer — this call no longer produces change back into the pool.
import { createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
const PRIVACY = "0x100000000000000000000000000000000000000b" as const;
const privacyAbi = [{
type: "function",
name: "withdraw",
stateMutability: "nonpayable",
inputs: [{
name: "request",
type: "tuple",
components: [
{ name: "proof", type: "bytes" },
{ name: "root", type: "bytes" },
{ name: "nullifier", type: "bytes" },
{ name: "amount", type: "string" },
{ name: "recipient", type: "address" },
{ name: "chainId", type: "string" },
{ name: "expiresAtUnix", type: "uint64" },
],
}],
outputs: [{ type: "bool" }],
}] as const;
// TODO: replace with a real spender key before production.
const wallet = createWalletClient({
account: privateKeyToAccount(process.env.SPENDER_KEY as `0x${string}`),
transport: http("https://rpc-testnet.maroo.io"),
});
await wallet.writeContract({
address: PRIVACY,
abi: privacyAbi,
functionName: "withdraw",
args: [{
proof: "0x1b3e..." as `0x${string}`,
root: "0x0a91..." as `0x${string}`,
nullifier: "0xdead..." as `0x${string}`,
amount: "10000000000000000000000000aokrw", // 10,000,000 OKRW
// TODO: replace with the real recipient before production.
recipient: "0x5aB7c1e40b8dA46f9c7e29D3fA614e97b8f0Ac21",
chainId: "maroo-testnet",
expiresAtUnix: BigInt(Math.floor(Date.now() / 1000) + 300),
}],
}); Handling expired or malformed payloads
As with every IPrivacy method, failures arrive as raw string reverts — dispatch on the exact reason text, not on a typed error name.
import { BaseError, ContractFunctionRevertedError } from "viem";
try {
await wallet.writeContract({ /* …withdraw call from previous example… */ });
} catch (err) {
if (err instanceof BaseError) {
const revert = err.walk(e => e instanceof ContractFunctionRevertedError);
if (revert instanceof ContractFunctionRevertedError) {
console.error("privacy withdraw reverted:", revert.reason);
// e.g. "withdraw payload has expired"
// "invalid zero address"
}
}
throw err;
}