Skip to main content

API Reference

Exports

ExportKindDescription
ConfidentialDisperseClientclassTyped wrapper around the DisperseConfidential singleton. Methods: register, disperse, preflightDisperse, predictWallets, getWallets, getFees, getBatchLimits, calculateFee, hasApprovedSubwallets, approveTokenOnWallets, revokeTokenOnWallets, recoverFromWallets, recoverERC20FromWallets, getEncryptedFeeReserve, discloseHandleToParty, batchDiscloseHandlesToParty, role management, admin/fee writes
createConfidentialDisperseClientfunctionConvenience constructor; mirrors viem's create* convention
ConfidentialDisperseClientConfiginterfaceConstructor config
computeSubtotalsfunctionCompute { group0, group1 } subtotals using the contract's exact partition rule. Exported for callers who want to verify independently.
DisperseSubwalletNotFoundErrorclassThrown by register() when no UserRegistered event matching the singleton is found in the receipt
DisperseEncryptedReserveNotGrantedErrorclassThrown by getEncryptedFeeReserve() when no ACL Allowed event granting the caller access is found (reserve uninitialised)
DisperseModetype"wallet" | "wallet-token-fee" | "direct"
PreflightReportinterfaceComprehensive pre-flight result from preflightDisperse — the contract between SDK and any UI rendering a readiness checklist
PreflightDisperseArgsinterfaceArgs for preflightDisperse
RecipientCheckinterfacePer-recipient validation result inside PreflightReport
DisperseArgsinterfaceArgs for disperse
DisperseResultinterface{ hash: Hex } returned by disperse
RegisterResultinterface{ hash: Hex, wallets: [Address, Address] } returned by register
UserFeesinterfaceConsolidated fee state from getFees
FeeConfiginterfaceGlobal fee configuration from on-chain feeConfig struct
CalculatedFeeinterface{ ethValue: bigint, tokenFeeAmount?: bigint } from calculateFee
CalculateFeeArgsinterfaceArgs for calculateFee
BatchLimitsinterfacePer-mode batch size limits from getBatchLimits
SubwalletApprovalStateinterface{ wallet0, wallet1, both } from hasApprovedSubwallets
GetEncryptedFeeReserveArgsinterfaceArgs for getEncryptedFeeReserve
EncryptedViewResultinterface{ handle: Hex, hash: Hex } from getEncryptedFeeReserve
DiscloseHandleArgsinterfaceArgs for discloseHandleToParty
BatchDiscloseHandlesArgsinterfaceArgs for batchDiscloseHandlesToParty
EncryptedInputinterfaceSingle { handle: Hex, inputProof: Hex }
EncryptedInputsinterfaceBatch { handles: Hex[], inputProof: Hex }
encryptUint64functionEncrypt a single uint64 to an externalEuint64 handle + input proof
encryptUint64BatchfunctionEncrypt N uint64 values under a single input proof
resolveEncryptorfunctionNormalize an EncryptorSource to the live Encryptor
EncryptorinterfaceStructural interface for Zama RelayerWeb / RelayerNode / mock
EncryptorSourcetypeEncryptor | (() => Encryptor | undefined) — eager or lazy
EncryptUint64ArgsinterfaceArgs for encryptUint64
EncryptUint64BatchArgsinterfaceArgs for encryptUint64Batch
FheValueInputtypeDiscriminated union of FHE input types
disperseConfidentialAbiconstABI for the DisperseConfidential singleton
disperseWalletAbiconstABI for the per-user DisperseWallet clone

API reference

ConfidentialDisperseClient

Wraps the deployed DisperseConfidential singleton. Read methods use publicClient; write methods require walletClient.

Constructor / factory function

new ConfidentialDisperseClient(config: ConfidentialDisperseClientConfig)
createConfidentialDisperseClient(config: ConfidentialDisperseClientConfig)

The constructor throws immediately when neither config.address nor a recognized chain id resolves a singleton address. It also throws when no FHEVM ACL address is known for the chain — pass aclAddress explicitly for custom networks.

Registration

MethodDescription
register({ token, account? })Deploy the caller's dedicated wallet pair AND approve both wallets as ERC-7984 operators for token in the same tx. Returns { hash, wallets }. Throws DisperseSubwalletNotFoundError if no UserRegistered event is found. Can only be called once per user — use approveTokenOnWallets to add approvals for additional tokens later.
approveTokenOnWallets({ token, account? })Approve the caller's wallets as ERC-7984 operators for a different token after initial registration.
revokeTokenOnWallets({ token, account? })Revoke operator approval on token for the caller's wallets.

Reads

MethodReturnsDescription
isRegistered(user)booleanWhether the user has a registered wallet pair
getWallets(user)[Address, Address] | nullOn-chain wallet pair, or null if not registered
predictWallets(user)[Address, Address]Deterministic wallet addresses — works before registration
getFees(user)UserFeesConsolidated fee state: gas fee, token BPS, global toggles
getBatchLimits()BatchLimitsPer-mode batch size limits (0n = no limit)
calculateFee(args)CalculatedFeeCompute { ethValue, tokenFeeAmount? } without writing
hasApprovedSubwallets({ user, token })SubwalletApprovalStateApproval state of the user's registered wallets for the token
paused()booleanWhether the singleton is paused
deploymentBlockNumber()bigintBlock at which the singleton was deployed
walletImplementation()AddressWALLET_IMPLEMENTATION — the ERC-1167 clone target
hasRole(role, address)booleanCheck role membership

Preflight

MethodReturnsDescription
preflightDisperse(args)PreflightReportAll readiness checks in one call. Check report.ready before calling disperse.

Disperse

MethodReturnsDescription
disperse(args)DisperseResultEncrypt amounts + subtotals, compute fee, dispatch to the correct mode's contract function.

Recovery

MethodDescription
recoverFromWallets({ token, to, account? })Sweep residual confidential tokens from the caller's wallets to to. Use after a subtotal-mismatch disperse.
recoverERC20FromWallets({ token, to, account? })Sweep accidentally-sent ERC-20 tokens from the caller's wallets.

Encrypted view (admin / fee collector)

MethodReturnsDescription
getEncryptedFeeReserve(args)EncryptedViewResultWrite transaction. Grant caller FHE ACL on the encrypted fee reserve handle and return it. Throws DisperseEncryptedReserveNotGrantedError if reserve is uninitialised.

Disclosure

MethodDescription
discloseHandleToParty({ handle, party, account? })Grant party persistent FHE ACL on a single handle. Caller must already hold ACL on handle. ACL is append-only — cannot be revoked.
batchDiscloseHandlesToParty({ handles, party, account? })Atomically grant party ACL on all handles. Reverts on the first handle that fails ACL checks.

Admin / fee-manager writes

MethodDescription
pause(account?)Pause all disperses. Requires PAUSER_ROLE.
unpause(account?)Unpause. Requires PAUSER_ROLE.
setFeeConfig(config, account?)Update global fee toggles and defaults. Requires FEE_MANAGER_ROLE.
setCustomFee(user, gasFee, tokenFee, account?)Set per-user fee override. Requires FEE_MANAGER_ROLE.
disableCustomFee(user, account?)Remove per-user fee override. Requires FEE_MANAGER_ROLE.
setMaxBatchSizeHolding(size, account?)Set wallet-mode batch limit. 0n = no limit. Requires FEE_MANAGER_ROLE.
setMaxBatchSizeDirect(size, account?)Set direct-mode batch limit. Requires FEE_MANAGER_ROLE.
setMaxBatchSizeTokenFee(size, account?)Set token-fee-mode batch limit. Requires FEE_MANAGER_ROLE.
withdrawGasFee(to, amount, account?)Withdraw accumulated ETH gas fees. Requires FEE_COLLECTOR_ROLE.
withdrawTokenFee(args: WithdrawTokenFeeArgs)Withdraw accumulated encrypted token fees. Requires FEE_COLLECTOR_ROLE. WithdrawTokenFeeArgs is a discriminated union: pass either { token, to, amount, encryptor?, account? } (SDK encrypts amount) or { token, to, encryptedInput, account? } (pre-encrypted), never both — passing both is a compile error.
rescueConfidentialTokens(token, to, account?)Rescue accidentally-sent ERC-7984 tokens. Requires DEFAULT_ADMIN_ROLE.
rescueERC20(token, to, account?)Rescue accidentally-sent ERC-20 tokens. Requires DEFAULT_ADMIN_ROLE.
grantRole(role, accountTarget, account?)Grant role (admin only).
revokeRole(role, accountTarget, account?)Revoke role (admin only).

computeSubtotals

import { computeSubtotals } from "@tokenops/sdk/fhe-disperse";

const { group0, group1 } = computeSubtotals(amounts);

Computes the two group subtotals matching the contract's _distributeFromWallets partition:

  • group0 = sum of the first ceil(n/2) amounts (wallet0's group)
  • group1 = sum of the remaining floor(n/2) amounts (wallet1's group)

Called internally by disperse(). Exported for callers who need to verify the values before encrypting.


Error catalogue

SDK typed errors

All SDK errors extend TokenOpsSdkError and carry a stable code: TokenOpsSdkErrorCode you can match on in onError handlers. Use isTokenOpsSdkError(error) for cross-realm-safe branding (preferred over instanceof in monorepos with hoisting). Every typed error preserves the original viem / Zama error as error.cause.

Error classcodeTriggered byHow to recover
DisperseSubwalletNotFoundErrorTOKENOPS_RECEIPT_EVENT_NOT_FOUNDregister() — no UserRegistered event from the singleton in the receipt. Indicates the tx reverted silently or the singleton address is wrong.Verify client.address points to the deployed singleton. Check isRegistered — user may already be registered.
DisperseEncryptedReserveNotGrantedErrorTOKENOPS_RECEIPT_EVENT_NOT_FOUNDgetEncryptedFeeReserve() — no ACL Allowed event granting the caller access to the fee reserve. Reserve is uninitialised.No token fees have accrued for this token yet. Check that a "wallet-token-fee" disperse has been successfully submitted first.
NotRegisteredErrorTOKENOPS_NOT_REGISTEREDdisperse() / approveUserWalletsForToken() / recoverFromWallets() — caller has not called register(token) yet.Call register(token) first.
AlreadyRegisteredErrorTOKENOPS_ALREADY_REGISTEREDregister() — caller is already registered.Use approveUserWalletsForToken(token) to whitelist a new token against the existing wallet pair.
PausedErrorTOKENOPS_PAUSEDAny user-facing write while the singleton is paused.Read paused() before retrying.
AccessDeniedErrorTOKENOPS_ACCESS_DENIEDRole-gated admin / fee-manager / fee-collector writes by a caller who lacks the role. context.role is the bytes32 role selector.Call hasRole(role, account) to confirm; resolve the bytes32 selector via the on-chain role constants.
InsufficientFeeErrorTOKENOPS_INSUFFICIENT_FEEdisperse() with gasFeeOverride not matching the configured gas fee. feeKind: "gas".Read the live fee with getFees(user) or call preflightDisperse and use its feeEth.
BatchTooLargeErrorTOKENOPS_BATCH_TOO_LARGEdisperse() with recipients.length > maxBatchSize[mode]. context.requested / context.max carry the numbers.Check getBatchLimits() or preflightDisperse.batchOk before submitting.
FheHandleNotAllowedErrorTOKENOPS_FHE_HANDLE_NOT_ALLOWEDdiscloseHandleToParty(...) with a handle the caller does not hold ACL on.Confirm the caller received an ACL grant for the handle (parse the Allowed event of the tx that produced it).
ContractRevertErrorTOKENOPS_CONTRACT_REVERTAny other on-chain revert the SDK decodes but does not have a more specific typed subclass for. context.revertName + context.revertArgs carry the decoded payload.Read context.revertName for the contract-side error name; consult the contract source if no SDK guidance applies.

Zama relayer / encryption errors are also wrapped:

Error classcodeTriggered by
UserRejectedSignatureErrorTOKENOPS_USER_REJECTEDUser cancelled a wallet signature prompt (e.g. for user-decrypt).
SigningFailedErrorTOKENOPS_SIGNING_FAILEDWallet signature failed for a reason other than user rejection (timeout, wallet crash, malformed request).
RelayerUnreachableErrorTOKENOPS_RELAYER_UNREACHABLENetwork call to the Zama relayer failed; context.statusCode? is populated when the underlying HTTP error exposed it.
EncryptionFailedErrorTOKENOPS_ENCRYPTION_FAILEDEncryption flow (input proof generation) failed.
DecryptionFailedErrorTOKENOPS_DECRYPTION_FAILEDDecryption flow failed (user-decrypt / public-decrypt).
UserDecryptNotAllowedErrorTOKENOPS_USER_DECRYPT_NOT_ALLOWEDZama ACL refused to grant decryption for the caller on the requested handle. Distinct from FheHandleNotAllowedError (which is the on-chain revert for FHE.isSenderAllowed).

Contract named errors

ErrorCause
EmptyRecipientsrecipients array is empty.
ArrayLengthMismatchrecipients.length !== amounts.length. The SDK validates this before submitting.
BatchTooLarge(requested, max)Recipient count exceeds the configured batch limit. Check preflightDisperse.batchOk.
ZeroAddressRecipientA recipient is the zero address. The SDK validates this before submitting.
InsufficientAmount(sent, required)msg.value does not exactly match the required gas fee. The SDK computes this automatically; may arise from a stale fee read if the fee config changed between preflight and disperse.
TokenFeeTooHighBPS fee configured above 10000 (100%). Admin configuration error.
InvalidAddressA zero address was passed where a non-zero address is required (e.g. to in recover).
EmptyBatchhandles array is empty in a batch disclosure call.
TransferFailedA native ETH transfer (gas fee withdrawal) failed.
CustomFeeNotSetdisableCustomFee called for a user with no custom fee entry.
NotControllerWallet method called by an address that is not the wallet controller (the singleton).
InvalidWalletIndexWallet index other than 0 or 1 passed to getUserWallet.
UserAlreadyRegisteredregister() called a second time for the same user. Check isRegistered first.
UserNotRegisteredA wallet-mode operation attempted on an unregistered user. Call register() first.
HandleNotAlloweddiscloseHandleToParty called with a handle the caller does not hold ACL on.
ContractNotAlloweddiscloseHandleToParty called with a handle the singleton itself does not hold ACL on — the handle was never disclosed to the contract.

FHE and ACL pitfalls

getEncryptedFeeReserve is a write transaction, not a free view. The contract calls FHE.allow(handle, msg.sender) inside the tx. The handle value is determined by an on-chain counter at execution time — simulation runs against a snapshot that diverges from the executed counter, so the simulated handle never receives an ACL grant. The SDK extracts the correct handle from the receipt's ACL.Allowed event.

ACL grants are append-only. Once getEncryptedFeeReserve grants a handle to an address, that access cannot be revoked. Do not call it speculatively.

Deflated subtotals cause silent partial failures. The contract uses FHE.min(requested, walletBalance) to cap each transfer. If the subtotals passed to the contract are lower than the true sum of the corresponding group's amounts, each recipient gets less than intended with no revert. Always use the subtotals returned by computeSubtotals or computed internally by disperse().

euint64 overflow. Plaintext amount values must fit in uint64 (max 2^64 - 1). For ERC-7984 tokens (6 decimals), this is approximately 1.8 × 10^13 tokens — far above any realistic single-disperse amount. The SDK validates each value and throws if it exceeds the range.

For the full FHE ACL specification and userDecrypt flow, see docs.zama.ai/fhevm.

Tested on

  • Local FHEVM (Anvil + forge-fhevm host contracts + @fhevm/mock-utils): all three disperse modes happy path, operator state reads, registration flow, recovery.
  • Sepolia: read smokes (fee reads, batch limits, isRegistered). Write smokes pending singleton deployment.