error.code is the machine contract: match on it in onError handlers and UI. Messages are human-readable and may change between versions. context carries the offending values, and the standard cause chain holds the underlying viem or Zama error. This page enumerates the installed build: 72 exported classes (38 on the root, 34 product-specific) and 69 codes.
Narrow with isTokenOpsSdkError#
isTokenOpsSdkError(error) checks a realm-global brand, so it still works when the SDK is duplicated in your bundle graph (hoisting issues, ESM and CJS loaded together, a route handler and a client bundle). instanceof works for the common case.
import { isTokenOpsSdkError } from "@tokenops/sdk";
function messageFor(err: unknown): string {
if (!isTokenOpsSdkError(err)) return "Something went wrong.";
switch (err.code) {
case "TOKENOPS_WALLET_REJECTED":
case "TOKENOPS_USER_REJECTED":
return "Cancelled in the wallet.";
case "TOKENOPS_WALLET_CHAIN_MISMATCH":
return "Switch your wallet to the app's network.";
case "TOKENOPS_OPERATOR_NOT_APPROVED":
return "Approve the contract as an operator on your token first.";
case "TOKENOPS_ACL_NOT_PROPAGATED":
return "Access is still propagating. Retrying...";
default:
// A switch made exhaustive with a never check needs a case for every
// code in the union, including the ones added on this line.
return err.message; // human-readable; may change between versions
}
}The core palette#
Exported from the root @tokenops/sdk and re-exported by every product subpath, except MissingPeerDependencyError, which only the root and /fhe export. The code column is read from each class in the installed build.
Configuration and inputs#
Thrown before anything is sent: a client or call is missing something, or an argument fails a precondition.
| Class | Code | When | Recovery |
|---|---|---|---|
UnsupportedChainError | TOKENOPS_UNSUPPORTED_CHAIN | The chain is not one the called surface runs on. context.chainId; context.method and context.hint are set when the surface is narrower than the SDK (the testnet faucet runs on Sepolia only), and the message then names the surface and the fix. | Switch the client to a supported chain. Render context.hint when it is present. |
DeploymentAddressUnavailableError | TOKENOPS_DEPLOYMENT_ADDRESS_UNAVAILABLE | No registry address for the chain. context.reason: chain-id-missing, registry-not-deployed (an explicit null) or registry-unknown-chain (no entry). context.overrideName names the field that bypasses the registry. | Pass chainId, switch chains, or set the overrideName field (usually address, aclAddress for the ACL). |
MissingPublicClientError | TOKENOPS_MISSING_PUBLIC_CLIENT | A method that reads the chain ran on a client built without a publicClient. context.method. | Construct the client with a viem PublicClient for the target chain. |
MissingWalletClientError | TOKENOPS_MISSING_WALLET_CLIENT | A write ran on a client with no walletClient. context.method. | Pass a walletClient, or wait for the wagmi wallet client before calling the mutation. |
MissingAccountError | TOKENOPS_MISSING_ACCOUNT | A write needs a signer and none resolved: no account argument and no walletClient.account. context.method. | Pass account (a viem Account signs locally), or connect a wallet client with a default account. |
MissingEncryptorError | TOKENOPS_MISSING_ENCRYPTOR | A method that encrypts a plaintext found no encryptor at call or client level. context.method names the public method; context.hint, when set, says where to configure it. | Pass encryptor (a ZamaSDK, a createSepoliaEncryptor result, or a lazy () => sdk) to the client or hook. |
MissingClientError | TOKENOPS_MISSING_CLIENT | A React hook's headless client could not be built, usually because the wagmi public client is not ready or the chain has no address. context.hook and context.clientKind. Query hooks gate on the client, so this fires on the mutation path. | Disable the action until the public client and chain are ready. |
MissingPeerDependencyError | TOKENOPS_MISSING_PEER_DEPENDENCY | An optional peer the helper loads at call time (such as @zama-fhe/sdk for the encryptor factories) is not installed. context.method, packageName and installHint; the import failure is the cause. | Run context.installHint. |
InvalidArgumentError | TOKENOPS_INVALID_ARGUMENT | An argument failed an SDK-side precondition (range, length, address, batch over one input proof). context.method, argument, reason, and value where it is public. | Fix the named argument. Confidential amounts are never copied into context.value. |
TokenOpsValidationError | TOKENOPS_INVALID_ARGUMENT | A free-form validation failure without a dedicated class, such as a malformed address or an unusable decrypted value. Shares TOKENOPS_INVALID_ARGUMENT. | Read the message; branch on name or instanceof to tell it from InvalidArgumentError. |
Receipt parsing#
The transaction mined, but its receipt did not carry exactly the event the SDK reads its result from.
| Class | Code | When | Recovery |
|---|---|---|---|
ReceiptEventNotFoundError | TOKENOPS_RECEIPT_EVENT_NOT_FOUND | No matching event in the receipt: often a mined-but-reverted transaction (the hint then says so), or an ACL Allowed grant that did not fire. context.method, eventName, contractAddress, txHash. | Inspect txHash. Encrypted views read the handle from the receipt, never from a simulation. |
ReceiptEventAmbiguousError | TOKENOPS_RECEIPT_EVENT_AMBIGUOUS | More than one matching event, so the SDK refuses to guess which is authoritative. context.count. | Not a retry candidate. File a bug with context.txHash. |
On-chain reverts#
The contract reverted, during simulation, estimation or execution, and the SDK decoded the revert.
| Class | Code | When | Recovery |
|---|---|---|---|
ContractRevertError | TOKENOPS_CONTRACT_REVERT | A decoded revert without a more specific class. context.revertName, revertSelector and revertArgs, or rawRevertData when the revert could not be decoded. | Branch on context.revertName. Raw viem errors never escape a write. |
TokenOpsContractError | TOKENOPS_CONTRACT_REVERT | A failed receipt wait or read in setOperator, isOperator and mintMockERC7984, or a mined transaction that reverted in those helpers and the faucet mints. Shares TOKENOPS_CONTRACT_REVERT, even for an RPC failure. | Tell it from ContractRevertError with instanceof or name; the viem error is the cause. |
PausedError | TOKENOPS_PAUSED | The contract is paused. | Read paused() before retrying. |
AccessDeniedError | TOKENOPS_ACCESS_DENIED | OpenZeppelin AccessControl rejected the caller. context.account and context.role when decodable. | Map context.role back with the client's role constants, then retry with the right signer. |
OperatorNotApprovedError | TOKENOPS_OPERATOR_NOT_APPROVED | The ERC-7984 token refused the product contract as a spender (ERC7984UnauthorizedSpender). Also a preflight blocker. context.tokenAddress, holder, spender when known. | The holder calls setOperator({ token, spender }) or ensureOperator, then retries. |
InsufficientFeeError | TOKENOPS_INSUFFICIENT_FEE | The ETH fee (feeKind gas) or encrypted token fee (feeKind token) did not match. required / provided only for gas. | Read the fee fresh. The SDK attaches the right value itself, so this usually means a hand-rolled call. |
InsufficientBalanceError | TOKENOPS_INSUFFICIENT_BALANCE | Balance too low. balanceKind eth, erc20 or confidential; requested / available stay unset for confidential balances. Preflights report an ETH balance below the fee this way. | Top up the named asset. |
BatchTooLargeError | TOKENOPS_BATCH_TOO_LARGE | A batch exceeded the contract's configured maximum. context.requested, and context.max as a bigint. | Split the batch; call Number(err.context.max) before interpolating. |
FeatureDisabledError | TOKENOPS_FEATURE_DISABLED | A feature toggle baked into the clone is off. context.feature. Clone toggles are immutable. | Deploy a new clone with the feature enabled. |
TransferFailedError | TOKENOPS_TRANSFER_FAILED | A native ETH or ERC-20 transfer failed at the contract layer. context.asset. | Check that the recipient accepts the asset. |
FheHandleNotAllowedError | TOKENOPS_FHE_HANDLE_NOT_ALLOWED | The caller has no ACL grant on the handle: FHE.isSenderAllowed reverted on-chain. context.handle and account when extractable. | Use a handle granted to this caller. The off-chain sibling is UserDecryptNotAllowedError. |
AlreadyInitializedError | TOKENOPS_ALREADY_INITIALIZED | initialize() already ran. | Nothing to do; the contract is initialized. |
ReentrancyError | TOKENOPS_REENTRANCY | ReentrancyGuardTransient blocked a nested call. | Do not call back into the contract from a hook or receiver. |
InvalidSignatureError | TOKENOPS_INVALID_SIGNATURE | An EIP-712 signature was malformed or did not recover to an authorized signer (ECDSA airdrop claims). | Re-sign with a current SIGNER_ROLE holder. |
Wallet and network#
The write was refused or failed on its way to the chain. No on-chain state changed.
| Class | Code | When | Recovery |
|---|---|---|---|
WalletRejectedError | TOKENOPS_WALLET_REJECTED | The user rejected the transaction prompt (EIP-1193 code 4001). | Safe to retry on user action. |
WalletChainMismatchError | TOKENOPS_WALLET_CHAIN_MISMATCH | The wallet's chain is not the public client's. Every SDK write checks this before estimating. | Ask the user to switch networks, then retry. |
NetworkError | TOKENOPS_NETWORK_ERROR | The RPC endpoint failed (non-2xx, timeout, DNS, socket). context.statusCode when exposed. | Retry, or move off a throttled public RPC. |
InsufficientGasFundsError | TOKENOPS_INSUFFICIENT_GAS_FUNDS | The account cannot cover the gas. Distinct from InsufficientFeeError, which the contract enforces. | Fund the account; it must cover the full padded limit, not only the gas used. |
UnknownWriteFailureError | TOKENOPS_UNKNOWN_WRITE_FAILURE | A write failure the SDK could not classify. | Inspect error.cause; this is the case to file a bug for. |
Relayer, encryption and decryption#
Errors from @zama-fhe/sdk, mapped to SDK classes. The upstream error is always the cause.
| Class | Code | When | Recovery |
|---|---|---|---|
UserRejectedSignatureError | TOKENOPS_USER_REJECTED | The user cancelled a signature prompt (the decryption permit). context.operation. | Safe to retry on user action. |
SigningFailedError | TOKENOPS_SIGNING_FAILED | A signature failed for another reason (timeout, wallet crash, malformed request). | Retry; check the wallet connection. |
RelayerUnreachableError | TOKENOPS_RELAYER_UNREACHABLE | The relayer call failed. context.statusCode when exposed; a mainnet 403 means no API key. | Retry; check relayerUrl and, on mainnet, the relayer API key. |
EncryptionFailedError | TOKENOPS_ENCRYPTION_FAILED | Input proof generation failed, including an unavailable encrypt worker under offloadEncrypt: true. | Inspect error.cause. A failed first init does not recover: terminate() and rebuild the encryptor. |
DecryptionFailedError | TOKENOPS_DECRYPTION_FAILED | User or public decryption failed for an unclassified reason. | Inspect error.cause; confirm the handle was granted to this signer. |
AclNotPropagatedError | TOKENOPS_ACL_NOT_PROPAGATED | The gateway has not yet observed an ACL change that is already effective on-chain. Transient. context.statusCode when known. | Retry with backoff, seconds up to a couple of minutes. |
UserDecryptNotAllowedError | TOKENOPS_USER_DECRYPT_NOT_ALLOWED | The relayer refused to decrypt for this signer: no FHE.allow(handle, signer). No transaction was sent. context.handle and userAddress when known. | Decrypt as the grantee, or request a fresh grant. |
The Zama error mapping#
Wherever the SDK calls @zama-fhe/sdk (product writes that encrypt, useDecryptedHandle, the airdrop campaign builders), it maps the thrown error to an SDK class and keeps the original as cause. Errors that are already TokenOpsSdkErrors pass through unchanged.
| Zama error | Becomes |
|---|---|
| DELEGATION_NOT_PROPAGATED, DelegationNotPropagatedError, or a 400 / 500 naming the propagation condition | AclNotPropagatedError |
| SIGNING_REJECTED | UserRejectedSignatureError |
| SIGNING_FAILED | SigningFailedError |
| RELAYER_REQUEST_FAILED | RelayerUnreachableError (with statusCode) |
| ENCRYPTION_FAILED, ENCRYPT_OFFLOAD_UNAVAILABLE | EncryptionFailedError |
| DECRYPTION_FAILED | DecryptionFailedError |
| NOT_ENTITLED | UserDecryptNotAllowedError |
| CONFIGURATION | InvalidArgumentError (argument config) |
| No code, but code: 4001 on the error or its cause (EIP-1193) | UserRejectedSignatureError |
| No code, a message rejecting the input value (invalid unsigned integer or address, exceeds the maximum, chainId exceeds maximum, packing too many values, fhevm assertion failed) | InvalidArgumentError (argument input) |
| Anything else | EncryptionFailedError when the operation is encrypt, DecryptionFailedError otherwise (including request-zk-proof) |
WalletChainMismatchError on every write#
Every SDK write, across all products and the /fhehelpers, refuses a wallet whose chain is not the public client's, before estimating gas and sending. The airdrop and faucet writes run their simulateContract preflight first, so a revert can surface before the chain check. The check is skipped when neither the request nor the wallet client carries a chain, or when the public client's chain id cannot be read. Since the airdrop contracts share their addresses across mainnet and Sepolia, a mismatched pair would otherwise simulate on one chain and send on the other. See Gas headroom for what happens after the check.
UnsupportedChainError: method and hint#
UnsupportedChainError takes optional method and hint, also set on its context, for surfaces narrower than the SDK. TestnetFaucetClient on mainnet says the faucet runs on Sepolia only and how to point the client there, instead of calling the chain unsupported by the whole SDK.
import { UnsupportedChainError } from "@tokenops/sdk";
if (err instanceof UnsupportedChainError) {
// context.method and context.hint are set when the surface is narrower than the SDK,
// for example the testnet faucet on mainnet.
showBanner(err.context.hint ?? `Chain ${err.context.chainId} is not supported.`);
}Product-specific classes#
Exported by one product subpath each. Their product error pages describe when each fires.
| Class | Code | Subpath |
|---|---|---|
ClaimLockedError | TOKENOPS_CLAIM_LOCKED | /fhe-vesting |
NoPendingTransferError | TOKENOPS_NO_PENDING_TRANSFER | /fhe-vesting |
NotPendingRecipientError | TOKENOPS_NOT_PENDING_RECIPIENT | /fhe-vesting |
NotVestingRecipientError | TOKENOPS_NOT_RECIPIENT | /fhe-vesting |
TransferAlreadyPendingError | TOKENOPS_TRANSFER_ALREADY_PENDING | /fhe-vesting |
TransferExpiredError | TOKENOPS_TRANSFER_EXPIRED | /fhe-vesting |
VestingExpiredError | TOKENOPS_VESTING_EXPIRED | /fhe-vesting |
VestingNotFoundError | TOKENOPS_VESTING_NOT_FOUND | /fhe-vesting |
VestingNotRevocableError | TOKENOPS_NOT_REVOCABLE | /fhe-vesting |
VestingRevokedError | TOKENOPS_VESTING_REVOKED | /fhe-vesting |
AlreadyClaimedError | TOKENOPS_ALREADY_CLAIMED | /fhe-airdrop |
ClaimNotStartedError | TOKENOPS_CLAIM_NOT_STARTED | /fhe-airdrop |
ClaimWindowClosedError | TOKENOPS_CLAIM_WINDOW_CLOSED | /fhe-airdrop |
ClawbackRequiresPauseError | TOKENOPS_CLAWBACK_REQUIRES_PAUSE | /fhe-airdrop |
CreateCommitmentMismatchError | TOKENOPS_CREATE_COMMITMENT_MISMATCH | /fhe-airdrop |
DedupIdConsumedError | TOKENOPS_DEDUP_ID_CONSUMED | /fhe-airdrop |
FeeCollectorSelfAdministeredError | TOKENOPS_FEE_COLLECTOR_SELF_ADMINISTERED | /fhe-airdrop |
GasFeeNotAcceptedError | TOKENOPS_GAS_FEE_NOT_ACCEPTED | /fhe-airdrop |
MerkleUupsUnsupportedError | TOKENOPS_MERKLE_UUPS_UNSUPPORTED | /fhe-airdrop |
NativeRescueRequiresZeroFeeError | TOKENOPS_NATIVE_RESCUE_REQUIRES_ZERO_FEE | /fhe-airdrop |
NonStockWrapperError | TOKENOPS_NON_STOCK_WRAPPER | /fhe-airdrop |
PredictionDriftError | TOKENOPS_PREDICTION_DRIFT | /fhe-airdrop |
SaltCollisionError | TOKENOPS_SALT_COLLISION | /fhe-airdrop |
SignatureExpiredError | TOKENOPS_SIGNATURE_EXPIRED | /fhe-airdrop |
UnauthorizedRedirectError | TOKENOPS_UNAUTHORIZED_REDIRECT | /fhe-airdrop |
UnrecognisedAirdropError | TOKENOPS_UNRECOGNISED_AIRDROP | /fhe-airdrop |
UpgradeabilityNotAllowedError | TOKENOPS_UPGRADEABILITY_NOT_ALLOWED | /fhe-airdrop |
AlreadyRegisteredError | TOKENOPS_ALREADY_REGISTERED | /fhe-disperse |
DisperseEncryptedReserveNotGrantedError | TOKENOPS_RECEIPT_EVENT_NOT_FOUND | /fhe-disperse |
DisperseSubwalletNotFoundError | TOKENOPS_RECEIPT_EVENT_NOT_FOUND | /fhe-disperse |
NotRegisteredError | TOKENOPS_NOT_REGISTERED | /fhe-disperse |
SingletonNotApprovedError | TOKENOPS_SINGLETON_NOT_APPROVED | /fhe-disperse |
SubwalletsNotApprovedError | TOKENOPS_SUBWALLETS_NOT_APPROVED | /fhe-disperse |
FaucetSupplyExhaustedError | TOKENOPS_FAUCET_SUPPLY_EXHAUSTED | /testnet-faucet |
SingletonNotApprovedError (/fhe-disperse) is deprecated: nothing raises it any more, and a missing singleton approval is an OperatorNotApprovedError.
Every code#
The full TokenOpsSdkErrorCode union. A code with several classes is shared on purpose; branch on name or instanceof when you need to tell them apart.
| Code | Thrown as |
|---|---|
| TOKENOPS_UNSUPPORTED_CHAIN | UnsupportedChainError |
| TOKENOPS_DEPLOYMENT_ADDRESS_UNAVAILABLE | DeploymentAddressUnavailableError |
| TOKENOPS_MISSING_PUBLIC_CLIENT | MissingPublicClientError |
| TOKENOPS_MISSING_WALLET_CLIENT | MissingWalletClientError |
| TOKENOPS_MISSING_ACCOUNT | MissingAccountError |
| TOKENOPS_MISSING_ENCRYPTOR | MissingEncryptorError |
| TOKENOPS_MISSING_CLIENT | MissingClientError |
| TOKENOPS_MISSING_PEER_DEPENDENCY | MissingPeerDependencyError |
| TOKENOPS_INVALID_ARGUMENT | InvalidArgumentError, TokenOpsValidationError |
| TOKENOPS_RECEIPT_EVENT_NOT_FOUND | ReceiptEventNotFoundError, DisperseEncryptedReserveNotGrantedError, DisperseSubwalletNotFoundError |
| TOKENOPS_RECEIPT_EVENT_AMBIGUOUS | ReceiptEventAmbiguousError |
| TOKENOPS_CONTRACT_REVERT | ContractRevertError, TokenOpsContractError |
| TOKENOPS_PAUSED | PausedError |
| TOKENOPS_ACCESS_DENIED | AccessDeniedError |
| TOKENOPS_OPERATOR_NOT_APPROVED | OperatorNotApprovedError |
| TOKENOPS_INSUFFICIENT_FEE | InsufficientFeeError |
| TOKENOPS_INSUFFICIENT_BALANCE | InsufficientBalanceError |
| TOKENOPS_BATCH_TOO_LARGE | BatchTooLargeError |
| TOKENOPS_FEATURE_DISABLED | FeatureDisabledError |
| TOKENOPS_TRANSFER_FAILED | TransferFailedError |
| TOKENOPS_FHE_HANDLE_NOT_ALLOWED | FheHandleNotAllowedError |
| TOKENOPS_ALREADY_INITIALIZED | AlreadyInitializedError |
| TOKENOPS_REENTRANCY | ReentrancyError |
| TOKENOPS_INVALID_SIGNATURE | InvalidSignatureError |
| TOKENOPS_WALLET_REJECTED | WalletRejectedError |
| TOKENOPS_WALLET_CHAIN_MISMATCH | WalletChainMismatchError |
| TOKENOPS_NETWORK_ERROR | NetworkError |
| TOKENOPS_INSUFFICIENT_GAS_FUNDS | InsufficientGasFundsError |
| TOKENOPS_UNKNOWN_WRITE_FAILURE | UnknownWriteFailureError |
| TOKENOPS_UNEXPECTED_CONTRACT_RESPONSE | The base TokenOpsSdkError: a read succeeded but returned a value the SDK cannot interpret, such as an unknown dedup ordinal from readDedupMode(). |
| TOKENOPS_USER_REJECTED | UserRejectedSignatureError |
| TOKENOPS_SIGNING_FAILED | SigningFailedError |
| TOKENOPS_RELAYER_UNREACHABLE | RelayerUnreachableError |
| TOKENOPS_ENCRYPTION_FAILED | EncryptionFailedError |
| TOKENOPS_DECRYPTION_FAILED | DecryptionFailedError |
| TOKENOPS_USER_DECRYPT_NOT_ALLOWED | UserDecryptNotAllowedError |
| TOKENOPS_VESTING_NOT_FOUND | VestingNotFoundError |
| TOKENOPS_NOT_RECIPIENT | NotVestingRecipientError |
| TOKENOPS_CLAIM_LOCKED | ClaimLockedError |
| TOKENOPS_VESTING_REVOKED | VestingRevokedError |
| TOKENOPS_NOT_REVOCABLE | VestingNotRevocableError |
| TOKENOPS_VESTING_EXPIRED | VestingExpiredError |
| TOKENOPS_TRANSFER_ALREADY_PENDING | TransferAlreadyPendingError |
| TOKENOPS_NO_PENDING_TRANSFER | NoPendingTransferError |
| TOKENOPS_NOT_PENDING_RECIPIENT | NotPendingRecipientError |
| TOKENOPS_TRANSFER_EXPIRED | TransferExpiredError |
| TOKENOPS_ALREADY_CLAIMED | AlreadyClaimedError |
| TOKENOPS_CLAIM_NOT_STARTED | ClaimNotStartedError |
| TOKENOPS_CLAIM_WINDOW_CLOSED | ClaimWindowClosedError |
| TOKENOPS_NOT_REGISTERED | NotRegisteredError |
| TOKENOPS_ALREADY_REGISTERED | AlreadyRegisteredError |
| TOKENOPS_SUBWALLETS_NOT_APPROVED | SubwalletsNotApprovedError |
| TOKENOPS_SINGLETON_NOT_APPROVED | SingletonNotApprovedError |
| TOKENOPS_FAUCET_SUPPLY_EXHAUSTED | FaucetSupplyExhaustedError |
| TOKENOPS_MERKLE_UUPS_UNSUPPORTED | MerkleUupsUnsupportedError |
| TOKENOPS_NON_STOCK_WRAPPER | NonStockWrapperError |
| TOKENOPS_UPGRADEABILITY_NOT_ALLOWED | UpgradeabilityNotAllowedError |
| TOKENOPS_FEE_COLLECTOR_SELF_ADMINISTERED | FeeCollectorSelfAdministeredError |
| TOKENOPS_DEDUP_ID_CONSUMED | DedupIdConsumedError |
| TOKENOPS_SIGNATURE_EXPIRED | SignatureExpiredError |
| TOKENOPS_SALT_COLLISION | SaltCollisionError |
| TOKENOPS_CREATE_COMMITMENT_MISMATCH | CreateCommitmentMismatchError |
| TOKENOPS_CLAWBACK_REQUIRES_PAUSE | ClawbackRequiresPauseError |
| TOKENOPS_NATIVE_RESCUE_REQUIRES_ZERO_FEE | NativeRescueRequiresZeroFeeError |
| TOKENOPS_PREDICTION_DRIFT | PredictionDriftError |
| TOKENOPS_UNAUTHORIZED_REDIRECT | UnauthorizedRedirectError |
| TOKENOPS_GAS_FEE_NOT_ACCEPTED | GasFeeNotAcceptedError |
| TOKENOPS_UNRECOGNISED_AIRDROP | UnrecognisedAirdropError |
| TOKENOPS_ACL_NOT_PROPAGATED | AclNotPropagatedError |