2.0 RC docsView 1.x docs
Product · Disperse · v2.0.0-rc.1

Confidential disperse on the 2.0 line.

Same singleton and addresses as the 1.x line. Stricter preflight, one-proof batches, and writes a Safe can sign.

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.

Where to start

Deployed singletons

One DisperseConfidential singleton per chain, unchanged from the 1.x line. createConfidentialDisperseClient and every hook resolve it from DEPLOYED_ADDRESSES.fheDisperse.disperseConfidentialSingleton by chain id, so pass address only for a fork or a custom network. The two chains use different addresses.

ChainChain idSingleton
Ethereum mainnet10x4fC0d28cBe4B82D512Ad0B42F6787480Cc98cC70
Sepolia111551110x710dD9885Cc9986EfD234E7719483147a6d8DBb4

Every product's addresses, per chain: Deployments.

Changed since 1.x - breaking

The contracts and their ABI are unchanged. What changed is what the SDK checks before a write and what it does when a receipt does not say what it should. The @zama-fhe/sdk peer moves to ~3.6.0 across the line.

  • preflightDisperse checks the singleton approval in every mode

    Every entry point pulls from the sender, so the wallet modes need the approval too, as direct already did. A wallet-mode sender without it now gets an OperatorNotApprovedError blocker (TOKENOPS_OPERATOR_NOT_APPROVED) instead of ready: true and a revert; in direct the blocker changes from SingletonNotApprovedError to OperatorNotApprovedError. PreflightReport.hasApprovedSingleton narrows from boolean | null to boolean. SingletonNotApprovedError is deprecated and no longer raised.

    Operator approvals
  • PreflightReport.blockers is removed

    Read blockerErrors: TokenOpsSdkError[] and branch on error.code, or render error.message where the strings were shown. ready is unchanged.

  • An ETH balance below the fee is InsufficientBalanceError

    preflightDisperse reports it with balanceKind: "eth", requested set to the fee and available to the balance. It was InsufficientFeeError with "provided: 0 wei". Code that branched on TOKENOPS_INSUFFICIENT_FEE for this blocker needs TOKENOPS_INSUFFICIENT_BALANCE.

  • A reverted transaction no longer resolves as success

    disperse() throws ReceiptEventNotFoundError when the receipt has no distribution event, where it returned distributions: []. discloseHandleToParty and batchDiscloseHandlesToParty throw ReceiptEventNotFoundError or ReceiptEventAmbiguousError instead of echoing their inputs. withdrawTokenFee throws ReceiptEventNotFoundError without a TokenFeeWithdrawn event, so transferredHandle is always a Hex on a mined result.

    Disperse errors
  • useRegister, useDisperse and useWithdrawTokenFee resolve Mined | Pending

    They accept waitForReceipt, and a hook cannot overload on it, so receipt-derived fields on data type as X | undefined. Narrow with if (data.pending) return;. Headless calls that never pass the option keep the mined result type.

    Receipt-free writes
  • Role hooks name the role holder holder

    useHasRole({ role, holder }), and mutate({ role, holder }) on useGrantRole / useRevokeRole. account (read) and accountTarget (write) still work and are deprecated. accountTarget cannot be combined with holder on the write hooks; on useHasRole, holder wins over account when both are passed.

    Roles
  • The fee-reserve hook has one name: useAccessEncryptedFeeReserve

    The deprecated alias is gone from /fhe-disperse/react; the function behind it is the same. The root brand type for a disperse id and its cast helper are removed too: the singleton emits no bytes32 disperse id, so use Hex from viem for an identifier of your own.

    Upgrading from 1.x
  • Immutable reads are cached forever

    useWalletImplementation and useDeploymentBlockNumber default to staleTime: Infinity. Pass query: { staleTime: 0 } for the old behaviour.

    Read-hook query options

Added

  • One input proof per disperse, enforced before encryption

    Each entry point verifies every encrypted amount against a single inputProof, which carries at most MAX_EUINT64_PER_INPUT_PROOF (32) values. That caps "direct" at 32 recipients and "wallet" / "wallet-token-fee" at 30, since the two subtotals share the proof. A larger list throws InvalidArgumentError naming the limit; preflightDisperse reports it as a blocker on recipients and sets batchOk to false.

  • Receipt-free writes for Safe and multisig signers

    waitForReceipt: false on register, disperse, withdrawTokenFee, discloseHandleToParty and batchDiscloseHandlesToParty returns on submission with PendingRegisterResult, PendingDisperseResult, PendingWithdrawTokenFeeResult or PendingDisclosureResult, all carrying pending: true. getEncryptedFeeReserve has no receipt-free form: its handle is only useful to an account that can user-decrypt in-session.

    Receipt-free writes
  • Gas headroom on every write, and a per-call gas

    Each write sends its estimate plus DEFAULT_GAS_HEADROOM_PERCENT (25%) unless the client or hook sets gasHeadroomPercent. Writes that take an argument object (register, disperse, the subwallet methods, the disclosures, getEncryptedFeeReserve, withdrawTokenFee) accept gas, sent as is. The positional admin setters use the client's headroom.

    Gas headroom
  • telemetry and query on the hooks

    Every /fhe-disperse/react hook takes telemetry, forwarded into the headless client, and every write on ConfidentialDisperseClient is bracketed with a span. Read hooks take query (ReadHookQueryOptions): TanStack options minus queryKey / queryFn, where query.enabled can turn a ready query off but never an unready one on.

    Read-hook query options
  • New exports

    MAX_EUINT64_PER_INPUT_PROOF, DisclosureResult (the data type of both disclosure hooks), ReadHookQueryOptions, ReceiptMode, PendingWrite, MinedWrite, GasOverride, GasHeadroomOption and TokenOpsValidationError on /fhe-disperse/react; DEFAULT_GAS_HEADROOM_PERCENT and the Pending*Result types on /fhe-disperse.

    Disperse types

Fixed

  • usePreflightDisperse keeps plaintext amounts out of its query key

    The key holds a keccak256 digest of amounts, salted once per page or process, so React Query devtools, persisters and loggers never see them. Equal amounts in the same order still share an entry. Because the salt is per process, a preflight result is never restored through SSR hydration or a persisted cache.

  • register reads only the caller's UserRegistered event

    It throws ReceiptEventAmbiguousError on more than one, instead of taking the first, and DisperseSubwalletNotFoundError when none names the caller.

  • Preflight rejects a token that is not ERC-7984 as a blocker

    It is one InvalidArgumentError in blockerErrors, instead of a raw viem error rejecting the whole call.

  • Hooks no longer throw from render

    A client constructor error (a malformed address, a bad gasHeadroomPercent, an unsupported chain) is kept as the hook's resolution error: queries stay disabled and mutations reject with it.

  • The ACL address follows config.chainId

    A client built with chainId and a chain-less publicClient no longer throws DeploymentAddressUnavailableError on getEncryptedFeeReserve.

  • Error messages name the call you make

    AlreadyRegisteredError and SubwalletsNotApprovedError name approveTokenOnWallets({ token }) (useApproveTokenOnWallets), not the contract function. OperatorNotApprovedError names the holder, the spender, the token and the setOperator({ token, spender }) call when it knows them. MissingEncryptorError from withdrawTokenFee names the method, and the disperse one tells you to set the encryptor on the client. Range errors from encryptUint64 / encryptUint64Batch no longer copy the amount into context.value.

Upgrading: from 1.x or from a 2.0 alpha.

The shape of confidential disperse

One singleton, two subwallets per user

register({ token }) deploys two ERC-1167 clones for the caller and has them approve the singleton as their ERC-7984 operator on token. The singleton controls them; you act on them through it. Approve further tokens with approveTokenOnWallets({ token }).

Three modes, one call

disperse({ token, mode, recipients, amounts }) takes plaintext amounts, encrypts them (and, in the wallet modes, the two subtotals) under one input proof, and routes "wallet", "wallet-token-fee" or "direct" to the matching entry point. Each recipient can decrypt only its own handle.

Preflight before you send

preflightDisperse returns registration, both approvals, the fee against the sender's ETH balance, the batch and input-proof limits and per-recipient checks, with ready and typed blockerErrors.

Silent-zero transfers

An ERC-7984 transfer from an insufficient balance moves an encrypted zero instead of reverting, so a mined receipt does not prove value moved. Decrypt the transferred handles in distributions with the token as the contract address.

Reference