IPrivacy.withdraw
withdraw(PrivacyWithdrawRequest request) external returns (bool success) 셸디드 노트를 소비하고 대응하는 공개된 OKRW를 request.recipient에게 지급합니다. PCL 정책 목적에서 msg.sender가 effective sender입니다. 실패는 IPrivacy에 선언된 타입 지정 커스텀 오류(요청 만료, 네이티브 denom 불일치, nullifier 충돌) 또는 IPrecompile의 상속 오류(InvalidAmount, InvalidAddress 등)로 revert됩니다.
파라미터
| 이름 | 타입 | 필수 | 설명 |
|---|---|---|---|
request | PrivacyWithdrawRequest | ✓ | withdraw 페이로드입니다. 증명, Merkle 루트, nullifier, 금액(SDK coin 문자열, 예: 15 OKRW에 해당하는 atokrw는 "15000000000000000000atokrw" — 네이티브 denom을 사용해야 하며 그렇지 않으면 PrivacyNativeDenomMismatch로 revert), 수신자, 체인 ID, expiresAtUnix를 포함합니다. |
반환값
타입:
bool 성공 시 true를 반환합니다. 실패는 false 반환이 아니라 타입 지정 커스텀 오류로 revert됩니다.
에러
| 코드 | 이름 | 설명 |
|---|---|---|
PrivacyNonPayable | PrivacyNonPayable | msg.value가 0이 아닐 때 revert됩니다. withdraw는 payable이 아닙니다. |
PrivacyRequestExpired | PrivacyRequestExpired | request.expiresAtUnix가 0이거나 이미 지났을 때 revert됩니다. |
PrivacyNativeDenomMismatch | PrivacyNativeDenomMismatch | request.amount가 네트워크의 네이티브 denom(atokrw)이 아닌 denom을 사용할 때 revert됩니다. 페이로드는 (got, expected)입니다. |
PrivacyNullifierAlreadySpent | PrivacyNullifierAlreadySpent | nullifier가 이미 온체인에 기록되어 있을 때 revert됩니다. |
InvalidAmount | InvalidAmount | IPrecompile에서 상속됩니다. request.amount를 coin 문자열로 파싱할 수 없을 때 revert됩니다. InvalidAmount(string amount)로 인코딩되며 페이로드는 원본 amount 문자열입니다. |
InvalidAddress | InvalidAddress | IPrecompile에서 상속됩니다. 호출자를 유효한 계정 주소로 변환할 수 없을 때 revert됩니다. InvalidAddress(string bad)로 인코딩됩니다. |
PrivacyTxLimitExceeded | PrivacyTxLimitExceeded | 트랜잭션이 이미 전역 stateful-privacy 호출 한도를 초과했을 때 revert됩니다. |
PrivacySenderTxLimitExceeded | PrivacySenderTxLimitExceeded | effective sender가 이미 sender별 stateful-privacy 호출 한도를 초과했을 때 revert됩니다. |
InvalidNumberOfArgs | InvalidNumberOfArgs | 공유 IPrecompile 오류 집합에 InvalidNumberOfArgs(uint256 expected, uint256 got)로 선언되어 있지만, 이 함수는 인자 개수가 틀려도 이 오류로 revert되지 않습니다. calldata가 요청을 담기에 짧으면 ABISetupFailed로 revert되고, 요청 뒤에 붙은 바이트는 무시됩니다. |
ABISetupFailed | ABISetupFailed | IPrecompile의 ABISetupFailed(string reason)을 상속합니다. calldata를 프리컴파일 ABI로 디코딩할 수 없을 때 revert됩니다. 알 수 없는 4바이트 selector이면 사유가 no method with id: 0x…이고(보통 ABI가 오래된 경우), calldata가 인자를 담기에 짧으면 디코더의 메시지(예: abi: cannot marshal in to go type: length insufficient 32 require 64)가 사유가 됩니다. 프리컴파일에 UnknownMethod도 선언되어 있지만, 알 수 없는 selector는 이 오류로 revert되지 않습니다. |
예제
기본 withdraw
amount 문자열은 네트워크의 네이티브 denom인 atokrw를 사용해야 합니다. 다른 denom을 전달하면 PrivacyNativeDenomMismatch로 revert됩니다.
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],
}); denom / 만료 거절 처리
denom 불일치는 올바른 atokrw denom 문자열로 요청을 다시 만들면 되며, 증명은 그대로 유효합니다. 만료된 요청에는 새 증명이 필요합니다. expiresAtUnix는 출금 증명의 공개 입력이자 노트 소유자가 서명하는 지출 의도에 포함되므로, 마감을 새로 정하려면 증명과 서명을 다시 만들어야 합니다.
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;
}
}