2.0 RC docsView 1.x docs
Concept · Error palette

Every error you can catch, by class and code

Every SDK error extends TokenOpsSdkError and carries a stable code and a typed context.

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.

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.

ClassCodeWhenRecovery
UnsupportedChainErrorTOKENOPS_UNSUPPORTED_CHAINThe 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.
DeploymentAddressUnavailableErrorTOKENOPS_DEPLOYMENT_ADDRESS_UNAVAILABLENo 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).
MissingPublicClientErrorTOKENOPS_MISSING_PUBLIC_CLIENTA 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.
MissingWalletClientErrorTOKENOPS_MISSING_WALLET_CLIENTA write ran on a client with no walletClient. context.method.Pass a walletClient, or wait for the wagmi wallet client before calling the mutation.
MissingAccountErrorTOKENOPS_MISSING_ACCOUNTA 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.
MissingEncryptorErrorTOKENOPS_MISSING_ENCRYPTORA 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.
MissingClientErrorTOKENOPS_MISSING_CLIENTA 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.
MissingPeerDependencyErrorTOKENOPS_MISSING_PEER_DEPENDENCYAn 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.
InvalidArgumentErrorTOKENOPS_INVALID_ARGUMENTAn 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.
TokenOpsValidationErrorTOKENOPS_INVALID_ARGUMENTA 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.

ClassCodeWhenRecovery
ReceiptEventNotFoundErrorTOKENOPS_RECEIPT_EVENT_NOT_FOUNDNo 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.
ReceiptEventAmbiguousErrorTOKENOPS_RECEIPT_EVENT_AMBIGUOUSMore 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.

ClassCodeWhenRecovery
ContractRevertErrorTOKENOPS_CONTRACT_REVERTA 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.
TokenOpsContractErrorTOKENOPS_CONTRACT_REVERTA 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.
PausedErrorTOKENOPS_PAUSEDThe contract is paused.Read paused() before retrying.
AccessDeniedErrorTOKENOPS_ACCESS_DENIEDOpenZeppelin 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.
OperatorNotApprovedErrorTOKENOPS_OPERATOR_NOT_APPROVEDThe 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.
InsufficientFeeErrorTOKENOPS_INSUFFICIENT_FEEThe 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.
InsufficientBalanceErrorTOKENOPS_INSUFFICIENT_BALANCEBalance 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.
BatchTooLargeErrorTOKENOPS_BATCH_TOO_LARGEA 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.
FeatureDisabledErrorTOKENOPS_FEATURE_DISABLEDA feature toggle baked into the clone is off. context.feature. Clone toggles are immutable.Deploy a new clone with the feature enabled.
TransferFailedErrorTOKENOPS_TRANSFER_FAILEDA native ETH or ERC-20 transfer failed at the contract layer. context.asset.Check that the recipient accepts the asset.
FheHandleNotAllowedErrorTOKENOPS_FHE_HANDLE_NOT_ALLOWEDThe 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.
AlreadyInitializedErrorTOKENOPS_ALREADY_INITIALIZEDinitialize() already ran.Nothing to do; the contract is initialized.
ReentrancyErrorTOKENOPS_REENTRANCYReentrancyGuardTransient blocked a nested call.Do not call back into the contract from a hook or receiver.
InvalidSignatureErrorTOKENOPS_INVALID_SIGNATUREAn 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.

ClassCodeWhenRecovery
WalletRejectedErrorTOKENOPS_WALLET_REJECTEDThe user rejected the transaction prompt (EIP-1193 code 4001).Safe to retry on user action.
WalletChainMismatchErrorTOKENOPS_WALLET_CHAIN_MISMATCHThe wallet's chain is not the public client's. Every SDK write checks this before estimating.Ask the user to switch networks, then retry.
NetworkErrorTOKENOPS_NETWORK_ERRORThe RPC endpoint failed (non-2xx, timeout, DNS, socket). context.statusCode when exposed.Retry, or move off a throttled public RPC.
InsufficientGasFundsErrorTOKENOPS_INSUFFICIENT_GAS_FUNDSThe 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.
UnknownWriteFailureErrorTOKENOPS_UNKNOWN_WRITE_FAILUREA 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.

ClassCodeWhenRecovery
UserRejectedSignatureErrorTOKENOPS_USER_REJECTEDThe user cancelled a signature prompt (the decryption permit). context.operation.Safe to retry on user action.
SigningFailedErrorTOKENOPS_SIGNING_FAILEDA signature failed for another reason (timeout, wallet crash, malformed request).Retry; check the wallet connection.
RelayerUnreachableErrorTOKENOPS_RELAYER_UNREACHABLEThe relayer call failed. context.statusCode when exposed; a mainnet 403 means no API key.Retry; check relayerUrl and, on mainnet, the relayer API key.
EncryptionFailedErrorTOKENOPS_ENCRYPTION_FAILEDInput 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.
DecryptionFailedErrorTOKENOPS_DECRYPTION_FAILEDUser or public decryption failed for an unclassified reason.Inspect error.cause; confirm the handle was granted to this signer.
AclNotPropagatedErrorTOKENOPS_ACL_NOT_PROPAGATEDThe 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.
UserDecryptNotAllowedErrorTOKENOPS_USER_DECRYPT_NOT_ALLOWEDThe 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 errorBecomes
DELEGATION_NOT_PROPAGATED, DelegationNotPropagatedError, or a 400 / 500 naming the propagation conditionAclNotPropagatedError
SIGNING_REJECTEDUserRejectedSignatureError
SIGNING_FAILEDSigningFailedError
RELAYER_REQUEST_FAILEDRelayerUnreachableError (with statusCode)
ENCRYPTION_FAILED, ENCRYPT_OFFLOAD_UNAVAILABLEEncryptionFailedError
DECRYPTION_FAILEDDecryptionFailedError
NOT_ENTITLEDUserDecryptNotAllowedError
CONFIGURATIONInvalidArgumentError (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 elseEncryptionFailedError 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.

ClassCodeSubpath
ClaimLockedErrorTOKENOPS_CLAIM_LOCKED/fhe-vesting
NoPendingTransferErrorTOKENOPS_NO_PENDING_TRANSFER/fhe-vesting
NotPendingRecipientErrorTOKENOPS_NOT_PENDING_RECIPIENT/fhe-vesting
NotVestingRecipientErrorTOKENOPS_NOT_RECIPIENT/fhe-vesting
TransferAlreadyPendingErrorTOKENOPS_TRANSFER_ALREADY_PENDING/fhe-vesting
TransferExpiredErrorTOKENOPS_TRANSFER_EXPIRED/fhe-vesting
VestingExpiredErrorTOKENOPS_VESTING_EXPIRED/fhe-vesting
VestingNotFoundErrorTOKENOPS_VESTING_NOT_FOUND/fhe-vesting
VestingNotRevocableErrorTOKENOPS_NOT_REVOCABLE/fhe-vesting
VestingRevokedErrorTOKENOPS_VESTING_REVOKED/fhe-vesting
AlreadyClaimedErrorTOKENOPS_ALREADY_CLAIMED/fhe-airdrop
ClaimNotStartedErrorTOKENOPS_CLAIM_NOT_STARTED/fhe-airdrop
ClaimWindowClosedErrorTOKENOPS_CLAIM_WINDOW_CLOSED/fhe-airdrop
ClawbackRequiresPauseErrorTOKENOPS_CLAWBACK_REQUIRES_PAUSE/fhe-airdrop
CreateCommitmentMismatchErrorTOKENOPS_CREATE_COMMITMENT_MISMATCH/fhe-airdrop
DedupIdConsumedErrorTOKENOPS_DEDUP_ID_CONSUMED/fhe-airdrop
FeeCollectorSelfAdministeredErrorTOKENOPS_FEE_COLLECTOR_SELF_ADMINISTERED/fhe-airdrop
GasFeeNotAcceptedErrorTOKENOPS_GAS_FEE_NOT_ACCEPTED/fhe-airdrop
MerkleUupsUnsupportedErrorTOKENOPS_MERKLE_UUPS_UNSUPPORTED/fhe-airdrop
NativeRescueRequiresZeroFeeErrorTOKENOPS_NATIVE_RESCUE_REQUIRES_ZERO_FEE/fhe-airdrop
NonStockWrapperErrorTOKENOPS_NON_STOCK_WRAPPER/fhe-airdrop
PredictionDriftErrorTOKENOPS_PREDICTION_DRIFT/fhe-airdrop
SaltCollisionErrorTOKENOPS_SALT_COLLISION/fhe-airdrop
SignatureExpiredErrorTOKENOPS_SIGNATURE_EXPIRED/fhe-airdrop
UnauthorizedRedirectErrorTOKENOPS_UNAUTHORIZED_REDIRECT/fhe-airdrop
UnrecognisedAirdropErrorTOKENOPS_UNRECOGNISED_AIRDROP/fhe-airdrop
UpgradeabilityNotAllowedErrorTOKENOPS_UPGRADEABILITY_NOT_ALLOWED/fhe-airdrop
AlreadyRegisteredErrorTOKENOPS_ALREADY_REGISTERED/fhe-disperse
DisperseEncryptedReserveNotGrantedErrorTOKENOPS_RECEIPT_EVENT_NOT_FOUND/fhe-disperse
DisperseSubwalletNotFoundErrorTOKENOPS_RECEIPT_EVENT_NOT_FOUND/fhe-disperse
NotRegisteredErrorTOKENOPS_NOT_REGISTERED/fhe-disperse
SingletonNotApprovedErrorTOKENOPS_SINGLETON_NOT_APPROVED/fhe-disperse
SubwalletsNotApprovedErrorTOKENOPS_SUBWALLETS_NOT_APPROVED/fhe-disperse
FaucetSupplyExhaustedErrorTOKENOPS_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.

CodeThrown as
TOKENOPS_UNSUPPORTED_CHAINUnsupportedChainError
TOKENOPS_DEPLOYMENT_ADDRESS_UNAVAILABLEDeploymentAddressUnavailableError
TOKENOPS_MISSING_PUBLIC_CLIENTMissingPublicClientError
TOKENOPS_MISSING_WALLET_CLIENTMissingWalletClientError
TOKENOPS_MISSING_ACCOUNTMissingAccountError
TOKENOPS_MISSING_ENCRYPTORMissingEncryptorError
TOKENOPS_MISSING_CLIENTMissingClientError
TOKENOPS_MISSING_PEER_DEPENDENCYMissingPeerDependencyError
TOKENOPS_INVALID_ARGUMENTInvalidArgumentError, TokenOpsValidationError
TOKENOPS_RECEIPT_EVENT_NOT_FOUNDReceiptEventNotFoundError, DisperseEncryptedReserveNotGrantedError, DisperseSubwalletNotFoundError
TOKENOPS_RECEIPT_EVENT_AMBIGUOUSReceiptEventAmbiguousError
TOKENOPS_CONTRACT_REVERTContractRevertError, TokenOpsContractError
TOKENOPS_PAUSEDPausedError
TOKENOPS_ACCESS_DENIEDAccessDeniedError
TOKENOPS_OPERATOR_NOT_APPROVEDOperatorNotApprovedError
TOKENOPS_INSUFFICIENT_FEEInsufficientFeeError
TOKENOPS_INSUFFICIENT_BALANCEInsufficientBalanceError
TOKENOPS_BATCH_TOO_LARGEBatchTooLargeError
TOKENOPS_FEATURE_DISABLEDFeatureDisabledError
TOKENOPS_TRANSFER_FAILEDTransferFailedError
TOKENOPS_FHE_HANDLE_NOT_ALLOWEDFheHandleNotAllowedError
TOKENOPS_ALREADY_INITIALIZEDAlreadyInitializedError
TOKENOPS_REENTRANCYReentrancyError
TOKENOPS_INVALID_SIGNATUREInvalidSignatureError
TOKENOPS_WALLET_REJECTEDWalletRejectedError
TOKENOPS_WALLET_CHAIN_MISMATCHWalletChainMismatchError
TOKENOPS_NETWORK_ERRORNetworkError
TOKENOPS_INSUFFICIENT_GAS_FUNDSInsufficientGasFundsError
TOKENOPS_UNKNOWN_WRITE_FAILUREUnknownWriteFailureError
TOKENOPS_UNEXPECTED_CONTRACT_RESPONSEThe base TokenOpsSdkError: a read succeeded but returned a value the SDK cannot interpret, such as an unknown dedup ordinal from readDedupMode().
TOKENOPS_USER_REJECTEDUserRejectedSignatureError
TOKENOPS_SIGNING_FAILEDSigningFailedError
TOKENOPS_RELAYER_UNREACHABLERelayerUnreachableError
TOKENOPS_ENCRYPTION_FAILEDEncryptionFailedError
TOKENOPS_DECRYPTION_FAILEDDecryptionFailedError
TOKENOPS_USER_DECRYPT_NOT_ALLOWEDUserDecryptNotAllowedError
TOKENOPS_VESTING_NOT_FOUNDVestingNotFoundError
TOKENOPS_NOT_RECIPIENTNotVestingRecipientError
TOKENOPS_CLAIM_LOCKEDClaimLockedError
TOKENOPS_VESTING_REVOKEDVestingRevokedError
TOKENOPS_NOT_REVOCABLEVestingNotRevocableError
TOKENOPS_VESTING_EXPIREDVestingExpiredError
TOKENOPS_TRANSFER_ALREADY_PENDINGTransferAlreadyPendingError
TOKENOPS_NO_PENDING_TRANSFERNoPendingTransferError
TOKENOPS_NOT_PENDING_RECIPIENTNotPendingRecipientError
TOKENOPS_TRANSFER_EXPIREDTransferExpiredError
TOKENOPS_ALREADY_CLAIMEDAlreadyClaimedError
TOKENOPS_CLAIM_NOT_STARTEDClaimNotStartedError
TOKENOPS_CLAIM_WINDOW_CLOSEDClaimWindowClosedError
TOKENOPS_NOT_REGISTEREDNotRegisteredError
TOKENOPS_ALREADY_REGISTEREDAlreadyRegisteredError
TOKENOPS_SUBWALLETS_NOT_APPROVEDSubwalletsNotApprovedError
TOKENOPS_SINGLETON_NOT_APPROVEDSingletonNotApprovedError
TOKENOPS_FAUCET_SUPPLY_EXHAUSTEDFaucetSupplyExhaustedError
TOKENOPS_MERKLE_UUPS_UNSUPPORTEDMerkleUupsUnsupportedError
TOKENOPS_NON_STOCK_WRAPPERNonStockWrapperError
TOKENOPS_UPGRADEABILITY_NOT_ALLOWEDUpgradeabilityNotAllowedError
TOKENOPS_FEE_COLLECTOR_SELF_ADMINISTEREDFeeCollectorSelfAdministeredError
TOKENOPS_DEDUP_ID_CONSUMEDDedupIdConsumedError
TOKENOPS_SIGNATURE_EXPIREDSignatureExpiredError
TOKENOPS_SALT_COLLISIONSaltCollisionError
TOKENOPS_CREATE_COMMITMENT_MISMATCHCreateCommitmentMismatchError
TOKENOPS_CLAWBACK_REQUIRES_PAUSEClawbackRequiresPauseError
TOKENOPS_NATIVE_RESCUE_REQUIRES_ZERO_FEENativeRescueRequiresZeroFeeError
TOKENOPS_PREDICTION_DRIFTPredictionDriftError
TOKENOPS_UNAUTHORIZED_REDIRECTUnauthorizedRedirectError
TOKENOPS_GAS_FEE_NOT_ACCEPTEDGasFeeNotAcceptedError
TOKENOPS_UNRECOGNISED_AIRDROPUnrecognisedAirdropError
TOKENOPS_ACL_NOT_PROPAGATEDAclNotPropagatedError

See also