On this page

Controllers and Claim Links

Require wallet approval for private spends and set recipient and refund windows for claim links.

Ownership and wallet approval#

To spend a controlled note, you need its private ownership proof and the controller's current approval. The note commitment includes that controller, and the spend relation reveals it during execution. Recovering a profile restores keys; the EOA, Safe or contract wallet still has to approve notes assigned to it.

The Pool computes an EIP-712 approval digest from the chain, Pool/domain, controller, operation kind, proof, all 39 signals, encoded request, sender capsule and verification-key hash. Sign this digest for the final transaction. A signature for a quote, recipient label or earlier proof cannot replace it.

Account access and recovery methods
Account access and recovery methods

Wallet signatures#

WalletAuthorization handles spend approvals and recovery-configuration approvals. EOAs use canonical low-s ECDSA with 64-byte compact or 65-byte signatures. Contracts use an ERC-1271 envelope or Safe-compatible checks. During static wallet calls, msg.sender remains the calling Pool or registry; wallets that inspect the caller must allow that address.

An ERC-1271 envelope starts with the four-byte FUYA magic 0x46555941, then scheme byte 0x01 and the wallet's signature. A contract without that envelope must return a positive initialized getThreshold() and accept checkSignatures for the supplied preimage and digest. If its chosen check fails, Fuyu rejects the approval without trying another scheme.

Fuyu uses the Safe's own owner count and threshold rules. The wallet checks its policy at execution time. If owners, threshold or ERC-1271 policy change while an operation is pending, check whether the approval still works before retrying.

solidity
function safeApprovalDigest(
    Proof calldata _proof, uint256[39] calldata _s,
    uint8 _operation, bytes32 _requestHash,
    bytes calldata _senderCapsule
) public view returns (bytes32);

// Contract-wallet envelope: FUYA || scheme 1 || signature
// 0x46555941 || 0x01 || <ERC-1271 signature bytes>

Claim before a deadline, refund afterward#

FuyuClaimController controls a private claim-link note. Its Pool/domain, recipient wallet, distinct refund signer and nonzero deadline are immutable. Before the deadline, only the recipient can approve a spend. At or after it, only the refund signer can. The windows never overlap and use Unix seconds.

The controller holds no assets or note keys. Spending needs both the link's private note material and the wallet approval for the current window. Having the link alone is insufficient. After a successful claim or refund, Pool nullifiers prevent another spend of those inputs.

The factory predicts the controller with CREATE2, and anyone can deploy it with those terms. You can issue the note before deploying the controller. Deployment reveals recipient, refund signer and deadline onchain, so a later claim can expose authorization details that were absent when the note was issued.

StateAuthorized walletEffect
Before deadlinerecipientApprove the controlled note's spend
At/after deadlinerefundSignerApprove a refund spend
Before controller deploymentNo hook code yetThe note can commit to the factory's predicted address
Controller deploymentAny caller who pays gasRecipient, refund signer and deadline become public
After successful spendNeither can reuse inputsThe Pool keeps their nullifiers spent permanently

Controller payload#

getThreshold() returns two as the claim controller's Safe-compatible hook marker. This does not require a user's ordinary Safe to have two owners. checkSignatures rebuilds the Pool digest from Authorization and checks the cosignature from the recipient or refund signer whose window is active.

Start the payload with 130 zero bytes, then abi.encode(Authorization). Authorization contains operation, proof, 39 signals, encoded request, capsule hash, verification-key hash, output-opening fields and cosignature. The controller ignores the output-opening fields for authorization. Signal 38 must identify this controller.

solidity
function predict(
    address _pool, address _recipient,
    address _refundSigner, uint64 _deadline
) public view returns (address);
function deploy(
    address _pool, address _recipient,
    address _refundSigner, uint64 _deadline
) external returns (address controller);

function getThreshold() external pure returns (uint256);
function checkSignatures(
    bytes32 _dataHash, bytes calldata, bytes calldata _signatures
) external view;

Public token escrow#

FuyuUnregisteredEscrow holds a public ERC-20 payment. open fixes token, recipient, refundTo, amount, deadline and clientRef. Only the recipient can claim before the deadline. Afterward, funds can be refunded to refundTo. The escrow moves through Missing, Pending, Claimed and Refunded states without creating private notes.

escrowId is derived from the sender and client reference. Retry with matching terms to identify the same escrow; different terms raise ConflictingRetry. This is a public escrow payment. Its claim state tells you nothing about whether a separate controlled private note has been spent.

Check a failed claim#

MalformedAuthorization means the header, payload layout or controller signal failed. DigestMismatch means Authorization rebuilds a different Pool digest. ClaimNotAuthorized and RefundNotAuthorized mean the active wallet rejected the signature. InvalidConfig points to invalid Pool, wallet or deadline terms.

Check the recipient, refund signer and deadline before issuing the note. Store its recovery material separately from signatures. After a timeout, inspect the original note nullifier and controller terms before making another claim or automatically signing a new proof.