ConfidentialDisperseClient
One singleton per chain, on mainnet and Sepolia. register() deploys two ERC-1167 subwallets the singleton controls on your behalf; disperse() then pays up to one input proof of recipients in a single transaction, each ACL-granted its own amount.
Construct
Headless TS — non-React consumers (Node, Vite, server workers). React hosts use the per-hook surface; same method names, lazy encryptor.
import { createConfidentialDisperseClient } from "@tokenops/sdk/fhe-disperse";
const client = createConfidentialDisperseClient({
publicClient,
walletClient,
// address optional: resolved from publicClient.chain.id
});Methods
Setup · 3
- Deploy the caller's subwallet pair and approve it for token. Resolves { hash, wallets } from the caller's own UserRegistered event; waitForReceipt: false returns a PendingRegisterResult.
client.register()React:useRegister - Approve the singleton as an ERC-7984 operator over the caller's registered subwallets for a single token.
client.approveTokenOnWallets()React:useApproveTokenOnWallets - Revoke operator approval on the subwallets (rollback).
client.revokeTokenOnWallets()React:useRevokeTokenOnWallets
Disclose · ACL · 2
- Grant a party persistent ACL on one handle. Resolves a DisclosureResult; throws ReceiptEventNotFoundError / ReceiptEventAmbiguousError; accepts waitForReceipt: false.
client.discloseHandleToParty()React:useDiscloseHandleToParty - Batch ACL grant across many handles in one tx.
client.batchDiscloseHandlesToParty()React:useBatchDiscloseHandlesToParty
Read · 13
- Registration, the singleton approval (every mode), the subwallet approvals (wallet modes), fee vs ETH balance, batch and input-proof limits, per-recipient checks. Returns ready + blockerErrors.
client.preflightDisperse()React:usePreflightDisperse - Read the deployer's two registered subwallet addresses.
client.getWallets()React:useGetWallets - Compute the deterministic subwallet addresses for a deployer without registering.
client.predictWallets()React:usePredictWallets - Has this deployer already called register?
client.isRegistered()React:useIsRegistered - Check whether a user's registered subwallets have approved the singleton as an ERC-7984 operator for token.
client.hasApprovedSubwallets()React:useHasApprovedSubwallets - Quote the on-chain fee config for a given disperse batch shape (count + mode).
client.calculateFee()React:useCalculateFee - Read consolidated fee information for user — combines per-user resolved fees (getFeeAmounts) with the global feeConfig toggles in one read.
client.getFees()React:useGetFees client.getCustomFee()Read a user's raw CustomFee override; distinguishes 'no override set' from 'override disabled'. Client method only, no hook.- Read the singleton's { gasFeeEnabled, tokenFeeEnabled, defaultGasFee, defaultTokenFee } fee config.
client.getFeeConfig()React:useGetFeeConfig - Read the maximum batch size for each of the three disperse modes ("wallet", "direct", "wallet-token-fee").
client.getBatchLimits()React:useGetBatchLimits - Read the singleton's paused flag.
client.paused()React:useIsPaused - Read the block number at which the singleton was deployed — useful as the fromBlock argument to getLogs when subscribing to events.
client.deploymentBlockNumber()React:useDeploymentBlockNumber - Read the WALLET_IMPLEMENTATION address — the singleton clones this deterministically (Clones.cloneDeterministic) for each registered user to derive their wallet pair.
client.walletImplementation()React:useWalletImplementation
Configure · 6
- Admin: update the global fee configuration — enable/disable gas and token fees, set the defaults.
client.setFeeConfig()React:useSetFeeConfig - Admin: install a per-user fee override (gas fee in wei, token fee in BPS).
client.setCustomFee()React:useSetCustomFee - Admin: remove a per-user fee override, falling back to the global defaults.
client.disableCustomFee()React:useDisableCustomFee - Admin: set the maximum batch size for "wallet" mode disperses.
client.setMaxBatchSizeHolding()React:useSetMaxBatchSizeHolding - Admin: set the maximum batch size for "direct" mode disperses.
client.setMaxBatchSizeDirect()React:useSetMaxBatchSizeDirect - Admin: set the maximum batch size for "wallet-token-fee" mode disperses.
client.setMaxBatchSizeTokenFee()React:useSetMaxBatchSizeTokenFee
Roles · RBAC · 4
- Grant role to accountTarget. The hook takes { role, holder }.
client.grantRole()React:useGrantRole - Revoke role from accountTarget. The hook takes { role, holder }.
client.revokeRole()React:useRevokeRole client.renounceRole()Caller gives up one of their own roles (callerConfirmation must be the caller). Cannot be undone. Client method only, no hook.- Is address a member of role? The hook takes { role, holder }.
client.hasRole()React:useHasRole
Recovery · 8
- Admin: pause all disperse operations on the singleton.
client.pause()React:usePause - Admin: unpause the singleton.
client.unpause()React:useUnpause - Recover residual confidential ERC-7984 tokens from the caller's registered wallets to to.
client.recoverFromWallets()React:useRecoverFromWallets - Recover ERC-20 tokens accidentally sent to the caller's registered wallets.
client.recoverERC20FromWallets()React:useRecoverERC20FromWallets - Sweep stranded confidential tokens out of the singleton contract (admin-only).
client.rescueConfidentialTokens()React:useRescueConfidentialTokens - Sweep stranded plain ERC-20 tokens (e.g. someone sent the wrong token).
client.rescueERC20()React:useRescueERC20 - Fee-collector: withdraw amount wei of accumulated ETH gas fees from the singleton to to.
client.withdrawGasFee()React:useWithdrawGasFee - Withdraw encrypted token fees: plaintext amount (SDK encrypts) or encryptedInput, never both. Resolves { hash, transferredHandle }; throws ReceiptEventNotFoundError without a TokenFeeWithdrawn event.
client.withdrawTokenFee()React:useWithdrawTokenFee
Configuration
ConfidentialDisperseClientConfig extends GasHeadroomOption. A malformed address or a bad gasHeadroomPercent throws from the constructor; the hooks keep that error instead of throwing during render, so their queries stay disabled and their mutations reject with it.
| publicClient | viem public client for reads. Required. |
| walletClient? | viem wallet client for writes. Omit for a read-only client. |
| address? | Singleton override. Defaults to DEPLOYED_ADDRESSES.fheDisperse.disperseConfidentialSingleton[chainId], set on mainnet and Sepolia. |
| chainId? | Chain used for the default singleton and the FHEVM ACL address. Defaults to publicClient.chain?.id; a client built with chainId and a chain-less publicClient now resolves its ACL address from it. |
| encryptor? | Eager Encryptor or lazy () => Encryptor | undefined, used by disperse and withdrawTokenFee. A lazy factory must return an encryptor for the active account after a wallet switch. |
| aclAddress? | FHEVM ACL override, used by getEncryptedFeeReserve to find the granted handle. |
| gasHeadroomPercent? | Percent added to every write's gas estimate. Defaults to DEFAULT_GAS_HEADROOM_PERCENT (25); 0 sends the bare estimate. |
| telemetry? | Telemetry sink. Emits fhe-disperse.client.init and brackets every public write with a span. No-op by default. |
Receipt-free writes and gas
Each of these methods is overloaded on waitForReceipt. Omitted or true, it waits and returns the mined result with every field set; false returns on submission with a result carrying pending: true. getEncryptedFeeReserve has no receipt-free form. Every write that takes an argument object also accepts gas; the positional admin setters (setFeeConfig, setCustomFee, the batch-size setters, pause, the rescues, grantRole) use the client's gasHeadroomPercent.
| Method | Pending result | Receipt-derived fields |
|---|---|---|
| register | PendingRegisterResult | wallets: undefined - read the UserRegistered log |
| disperse | PendingDisperseResult | distributions: undefined - read the WalletDistribution / DirectDistribution logs |
| withdrawTokenFee | PendingWithdrawTokenFeeResult | transferredHandle: undefined - read the TokenFeeWithdrawn log |
| discloseHandleToParty | PendingDisclosureResult | discloser, party, disclosedHandles undefined - read the HandlesDisclosedToParty log |
| batchDiscloseHandlesToParty | PendingDisclosureResult | same as discloseHandleToParty |
import type { DisperseArgs, ReceiptMode } from "@tokenops/sdk/fhe-disperse";
// Literal false: the pending overload.
const pending = await client.register({ token, waitForReceipt: false });
pending.wallets; // undefined
// A runtime boolean (what a hook passes): the union, narrowed by `pending`.
async function send(args: DisperseArgs & ReceiptMode) {
const result = await client.disperse(args);
if (result.pending) return result.hash; // safeTxHash from a Safe
return result.distributions; // DisperseDistribution[], never empty
}
// Per-call gas limit, sent as is instead of the padded estimate.
await client.approveTokenOnWallets({ token, gas: 400_000n });Shared behaviour: Receipt-free writes, Gas headroom, Encryptors.