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.Disperse 2.0 · Errors · 17@tokenops/sdk/fhe-disperse
Disperse 2.0 errors you can catch by class.
Most of these arrive twice: once as an entry in preflightDisperse's blockerErrors, and again as the error the write would throw. Branch on error.code. Every class here is also re-exported from /fhe-disperse/react.
For the catch-ladder pattern + how SDK-level and generic-fallback errors fit alongside these, read Concepts › Typed errors + recovery.
| Class | When thrown | Recovery |
|---|---|---|
| OperatorNotApprovedError | TOKENOPS_OPERATOR_NOT_APPROVED. The sender has not made the singleton an ERC-7984 operator on the token. preflightDisperse checks it in every mode (hasApprovedSingleton: false); disperse() throws it for the ERC7984UnauthorizedSpender revert. The message names the holder, the spender and the token when known. | The sender calls setOperator({ token, spender }) or ensureOperator with spender set to the singleton, then re-runs the preflight. useEnsureOperator |
| SubwalletsNotApprovedError | TOKENOPS_SUBWALLETS_NOT_APPROVED. Wallet modes only: one or both subwallets have not approved the singleton for this token. context carries wallet0Approved and wallet1Approved. | approveTokenOnWallets({ token }) on the client, or useApproveTokenOnWallets in React. useApproveTokenOnWallets |
| NotRegisteredError | TOKENOPS_NOT_REGISTERED. A wallet-mode preflight, or a subwallet method, for a user who has not called register. Direct mode does not need registration. | Register once per user; useIsRegistered gates the whole subwallet UI. useRegister |
| AlreadyRegisteredError | TOKENOPS_ALREADY_REGISTERED. register called a second time; it is once per user, not per token. | Read the existing pair with useGetWallets and approve the new token with approveTokenOnWallets({ token }). useIsRegistered |
| InsufficientBalanceError | TOKENOPS_INSUFFICIENT_BALANCE with balanceKind: "eth". The sender's ETH balance is below the disperse fee; requested is the fee, available the balance. On 1.x this was InsufficientFeeError (TOKENOPS_INSUFFICIENT_FEE). | Fund the sender. The SDK always attaches the full fee, so lowering gasFeeOverride is not the fix. A gasFeeOverride that differs from the fee is a different error: see InsufficientFeeError. useCalculateFee |
| InvalidArgumentError | TOKENOPS_INVALID_ARGUMENT. More than 32 recipients in "direct" or 30 in the wallet modes (one input proof; batchOk: false), mismatched lengths, a zero address, a mixed-case address that fails its EIP-55 checksum in recipients, token or user (normalise with viem getAddress), an amount outside uint64, wallet-mode subtotals above uint64, all raised before any encryption. A token that is not ERC-7984 is caught by preflightDisperse only, as a blocker; disperse() does not check it. Also thrown when a disclosure names a transferred handle, which is scoped to the token: disclose those through the token, not the singleton. | Split the batch into calls of at most MAX_EUINT64_PER_INPUT_PROOF (32) direct or 30 wallet-mode recipients, or fix the argument the reason names. usePreflightDisperse |
| InsufficientFeeError | TOKENOPS_INSUFFICIENT_FEE. Write path only: a gasFeeOverride that does not equal the fee. The contract requires msg.value to match exactly, so an override above or below it reverts at simulate time. | Drop gasFeeOverride so the SDK reads the live fee, or set it to recipients.length * gasFeeWei. useCalculateFee |
| AccessDeniedError | TOKENOPS_ACCESS_DENIED. A role-gated write from an account without the role: fee withdrawals, getEncryptedFeeReserve, pause and unpause, the setters and the rescues. | Send from a holder of the role the call needs (see Roles), or have a DEFAULT_ADMIN_ROLE holder grant it. |
| BatchTooLargeError | TOKENOPS_BATCH_TOO_LARGE. recipients.length exceeds the on-chain cap for the mode (maxBatchSizeHolding, maxBatchSizeDirect or maxBatchSizeTokenFee; 0n means no cap). | Read the live caps with getBatchLimits and chunk to the smaller of the cap and the one-proof limit. useGetBatchLimits |
| PausedError | TOKENOPS_PAUSED. The singleton is paused; preflight reports it and writes revert until a PAUSER_ROLE holder unpauses. | Surface the paused state from useIsPaused instead of letting the write fail. useIsPaused |
| ReceiptEventNotFoundError | TOKENOPS_RECEIPT_EVENT_NOT_FOUND. The mined receipt lacks the event the result is built from: no distribution event for disperse(), no HandlesDisclosedToParty for a disclosure, no TokenFeeWithdrawn for withdrawTokenFee. In practice the transaction reverted; the hint says so when it did. | Inspect the transaction by hash. Do not treat it as a payout: on 1.x disperse() returned distributions: [] here. useDisperse |
| ReceiptEventAmbiguousError | TOKENOPS_RECEIPT_EVENT_AMBIGUOUS. The receipt carries more than one matching event: two UserRegistered events naming the caller for register, a repeated HandlesDisclosedToParty for a disclosure, or more than one TokenFeeWithdrawn for withdrawTokenFee. | Read the logs of that transaction directly and pick the one you need. |
| DisperseSubwalletNotFoundError | TOKENOPS_RECEIPT_EVENT_NOT_FOUND, the same code as ReceiptEventNotFoundError, so code branching on it catches both. register's receipt has no UserRegistered event from the singleton that names the caller. context carries txHash and singletonAddress. | Check the transaction and the singleton address, then retry. usePredictWallets shows the pair register should have deployed. usePredictWallets |
| DisperseEncryptedReserveNotGrantedError | TOKENOPS_RECEIPT_EVENT_NOT_FOUND, the same code as ReceiptEventNotFoundError, so code branching on it catches both. getEncryptedFeeReserve's receipt has no ACL Allowed event for the caller, usually because no token fees have accrued for that token yet. | Run a fee-charging disperse for the token first; the SDK will not return a zero handle the relayer cannot decrypt. useAccessEncryptedFeeReserve |
| MissingEncryptorError | TOKENOPS_MISSING_ENCRYPTOR. disperse or withdrawTokenFee ran with no encryptor on the client, the hook or (for withdrawTokenFee) the call. The message names the method. | Pass encryptor to the client, or to the hook as a lazy () => zamaSDK factory captured from useZamaSDK. useDisperse |
| DeploymentAddressUnavailableError | TOKENOPS_DEPLOYMENT_ADDRESS_UNAVAILABLE. No singleton or ACL address for the chain. Hooks keep it as their resolution error instead of throwing in render; queries stay disabled and mutations reject with it. | Connect to mainnet or Sepolia, or pass address (and aclAddress) for a custom network. |
| SingletonNotApprovedError | Deprecated. TOKENOPS_SINGLETON_NOT_APPROVED is no longer raised by anything; the same condition is OperatorNotApprovedError in every mode. | Branch on TOKENOPS_OPERATOR_NOT_APPROVED instead. The class is still exported and will be removed in a future major. |
TokenOpsSdkError and carries the offending values under err.context — render specific messages instead of generic "transaction failed."Read the catch ladder Wallet, relayer and decryption errors shared by every product: Error palette.