2.0 RC docsView 1.x docs
Disperse 2.0 · Flows

The four disperse flows.

Register, approve, disperse and recover, with the hooks that ship each step on the 2.0 line.

v2.0.0-rc.1 release candidate
The API is frozen and later candidates carry fixes only, except the receipt-free and Safe create surface of /fhe-airdrop, which is @beta. Published on the next dist-tag; latest stays on 1.6.0 until 2.0.0.
Prerequisite: the sender approves the singleton
Every disperse mode transfers from the sender with confidentialTransferFrom, so the sender must first make the singleton an ERC-7984 operator on the token: setOperator({ publicClient, walletClient, token, spender, deadline }) or ensureOperator from @tokenops/sdk/fhe, with spender set to the singleton address. Without it the transaction reverts with ERC7984UnauthorizedSpender, which the SDK surfaces as OperatorNotApprovedError. Do not route disperse through Multicall3: it becomes msg.sender and breaks both the operator grant and the subwallet lookup.
  1. 1

    Register (once per user)

    register({ token }) deploys two ERC-1167 subwallet clones for the caller under deterministic salts and has them approve the singleton as their ERC-7984 operator on token. Predict the pair before registering; read it back after.

    1. useIsRegistered({ user }) gates the UI; a second register reverts and surfaces as AlreadyRegisteredError
    2. useRegister().mutate({ token }) resolves { hash, wallets } from the caller's own UserRegistered event; more than one such event throws ReceiptEventAmbiguousError
    3. From a Safe, pass waitForReceipt: false: data is a PendingRegisterResult with wallets: undefined, so narrow with if (data.pending) and read the pair later with useGetWallets({ user })
    4. usePredictWallets({ user }) returns the same pair before registration; useWalletImplementation caches the clone implementation with staleTime: Infinity
  2. 2

    Approve the singleton and the subwallets

    Two different approvals. Every mode pulls from the sender, so the sender approves the singleton as an ERC-7984 operator on the token. The wallet modes also need both subwallets approved for that token, which register does for the token it was given.

    1. useIsOperator({ token, spender: singleton }) reads the sender's grant; useEnsureOperator sends setOperator only when it is missing
    2. Without it, preflightDisperse reports OperatorNotApprovedError (TOKENOPS_OPERATOR_NOT_APPROVED) in every mode, and hasApprovedSingleton is false
    3. useHasApprovedSubwallets({ user, token }) reads both subwallets; useApproveTokenOnWallets().mutate({ token }) approves them for another token, useRevokeTokenOnWallets rolls it back
    4. Missing subwallet approval surfaces as SubwalletsNotApprovedError, whose message names approveTokenOnWallets({ token })
  3. 3

    Disperse to N recipients

    One call, one input proof. The SDK validates the batch, splits the wallet-mode subtotals, encrypts amounts and subtotals together, attaches the ETH fee and routes by mode. A proof carries 32 values: 32 recipients direct, 30 in the wallet modes.

    1. usePreflightDisperse({ user, token, recipients, amounts, mode }) returns the PreflightReport: ready plus typed blockerErrors. Its query key holds a salted digest of amounts, never the plaintext
    2. Blockers to expect: OperatorNotApprovedError (any mode), NotRegisteredError and SubwalletsNotApprovedError (wallet modes), InsufficientBalanceError (ETH below the fee, balanceKind: "eth"), InvalidArgumentError (over the proof limit with batchOk: false, or a token that is not ERC-7984), BatchTooLargeError (over the on-chain cap), PausedError
    3. useDisperse({ encryptor: () => zamaSDK }).mutate({ token, mode, recipients, amounts }) resolves { hash, distributions }; a receipt with no distribution event throws ReceiptEventNotFoundError
    4. From a Safe, waitForReceipt: false resolves a PendingDisperseResult; read the WalletDistribution / DirectDistribution logs once the transaction executes
  4. 4

    Recover, collect fees, respond to incidents

    Users sweep their own subwallets. Role holders collect fees, read the encrypted fee reserve, disclose handles and pause the singleton.

    1. useRecoverFromWallets().mutate({ token, to }) moves residual confidential tokens out of the caller's subwallets; useRecoverERC20FromWallets does the same for stray ERC-20s
    2. FEE_COLLECTOR_ROLE: useWithdrawGasFee, and useWithdrawTokenFee({ encryptor: () => zamaSDK }).mutate({ token, to, amount }) resolving { hash, transferredHandle } (or a PendingWithdrawTokenFeeResult with waitForReceipt: false)
    3. useAccessEncryptedFeeReserve().mutate({ token }) is a write that grants the caller the reserve handle; it has no receipt-free form
    4. useDiscloseHandleToParty / useBatchDiscloseHandlesToParty resolve a DisclosureResult and accept waitForReceipt: false; usePause / useUnpause for incident response