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.This page also documents AirdropBaseClient
Every method below except claim, claimAndUnwrap, getClaimAmount, isSignatureValid, the EIP-712 identity reads, and SIGNER_ROLE lives on the shared AirdropBaseClient both EcdsaAirdropClient and MerkleAirdropClient extend. See MerkleAirdropClient for the Merkle-specific surface built on the same base.Airdrop v2 · Client@tokenops/sdk/fhe-airdrop
EcdsaAirdropClient
Extends AirdropBaseClient. The config takes no dedupMode - the policy is frozen at create, read it from the chain with readDedupMode(). Optional gasHeadroomPercent pads every write; each write object also takes a per-call gas. Recipients claim with a { encryptedInput, dedupId, deadline, signer, signature } payload over a 4-field EIP-712 Claim struct; the gas fee attaches automatically as msg.value.
Construct
Headless TS — non-React consumers (Node, Vite, server workers). React hosts use the per-hook surface; same method names, lazy encryptor.
@tokenops/sdk/fhe-airdrop
ts
import { createEcdsaAirdropClient } from "@tokenops/sdk/fhe-airdrop";
const client = createEcdsaAirdropClient({
publicClient,
walletClient,
// address optional: resolved from publicClient.chain.id
});Methods
Write · 4
- Redeem a signed encrypted allocation. A non-empty input proof is bound to (instance, claimant). An empty proof (inputProof: "0x") skips coprocessor verification: a zero handle becomes an encrypted zero, and a nonzero handle is accepted only if the claimant and instance already hold ACL on it (else FheHandleNotAllowedError, or a raw ContractRevertError for the ACL's SenderNotAllowed). The voucher still fixes identity, handle, dedupId and deadline. A campaign that may reissue a voucher for an already-paid entitlement should use dedupMode perDedupId or both. Pass to to redirect where the tokens land; the signature, replay dedup and a non-empty proof stay bound to the sending account. Throws ClaimNotStartedError, ClaimWindowClosedError, SignatureExpiredError, InvalidSignatureError, AlreadyClaimedError, or DedupIdConsumedError (context.dedupId names the consumed id).
client.claim()React:useEcdsaClaim - Same replay handling as claim, then routes straight into the ERC7984ERC20Wrapper unwrap. Reverts FeatureDisabledError on a non-unwrappable campaign. The amount is public from the claim transaction itself, not at finalizeUnwrap: the emitted unwrapRequestId is that amount's handle. Only a plain claim keeps the amount confidential.
client.claimAndUnwrap()React:useEcdsaClaimAndUnwrap - Decode the ClaimedAndUnwrapInitiated event a claimAndUnwrap transaction emitted into { claimant, beneficiary, claimKey, unwrapRequestId }. Pass unwrapRequestId to the wrapper's finalizeUnwrap (with Zama's SDK, sdk.createWrappedToken(wrapper).finalizeUnwrap(unwrapRequestId)). parseUnwrapRequest(receipt | logs) does the same without a fetch. Shared base method.
client.readUnwrapRequest()React:useUnwrapRequest - Point a UUPS instance's proxy at a new implementation. UPGRADER_ROLE. Inert (reverts at the proxy layer) on clone-mode instances - check deploymentMode() first.
client.upgradeToAndCall()React:useAirdropUpgradeToAndCall
Encrypted read · 6
- Preview a signed allocation as an encrypted handle without consuming either replay guard, for no fee. Repeatable, not unconditional: once the claim is no longer available this reverts with the same error the consuming claim would raise, in the same order - the dedup slot first, the EIP-712 digest second. Not a probe for "has this been claimed"; use isSignatureValid, which never reverts and answers that for free. Grants ACL to the caller, submits a transaction internally.
client.getClaimAmount()React:useAccessEcdsaClaimAmount - Re-grant the compliance clone on the instance's current pool balance and return the handle it was granted on. Permissionless by design: the token rotates the instance's balance handle on every incoming transfer, including a direct one no airdrop code observes, so without this a campaign can be left with a clone that cannot read the pool and no admin around to fix it. The handle is granted to the CLONE, not the caller. A snapshot, not a subscription - the next transfer in rotates the instance onto a handle the clone was never granted on, so call it again before each read.
client.refreshComplianceBalance()React:useRefreshComplianceBalance - Read the instance's remaining confidential pool balance as a handle granted to the caller. DISCLOSURE_ADMIN_ROLE. Also re-grants the compliance clone on the same handle, so an admin read doubles as a refreshComplianceBalance.
client.adminGetCurrentBalance()React:useAdminGetCurrentBalance - Same balance read, ACL granted to a named party instead of the caller. DISCLOSURE_ADMIN_ROLE.
client.adminDiscloseBalanceToParty()React:useAdminDiscloseBalanceToParty - One balance read fanned out to many parties as the same handle. DISCLOSURE_ADMIN_ROLE.
client.adminBatchDiscloseBalanceToParties()React:useAdminBatchDiscloseBalanceToParties - Grant ACL on a handle you already hold to a third party, e.g. an auditor. Gate #1 demands PERSISTENT user-decryption ACL in the (caller, instance) context - both legs - so a handle fresh out of an FHE op, which carries only a transient allowance, fails with FheHandleNotAllowedError. Disclose handles that came from the encrypted views on this client; their grants are persistent. DISCLOSURE_ADMIN_ROLE bypasses gate #1 only. Gate #2: the instance must be allowed on the handle.
client.discloseHandleToParty()React:useDiscloseHandleToParty
Disclose · ACL · 1
Read · 21
- ERC-7984 token this instance distributes.
client.token()React:useAirdropToken - 0 for ECDSA, 1 for Merkle. Bundled with token/unwrappable/complianceManager/gasFee in useAirdropConfig.
client.airdropType()React:useAirdropConfig - Claim window open time. Bundled with endTime and the derived flags in useAirdropWindow.
client.startTime()React:useAirdropWindow - Claim window close time, same bundle.
client.endTime()React:useAirdropWindow - Window open AND not paused - the field a claim button gates on.
client.isClaimWindowActive()React:useAirdropWindow - block.timestamp past startTime, ignoring pause.
client.hasClaimStarted()React:useAirdropWindow - block.timestamp past endTime, ignoring pause.
client.hasClaimEnded()React:useAirdropWindow - Whether claimAndUnwrap is available on this instance.
client.unwrappable()React:useAirdropConfig - This instance's own ComplianceManagerClient clone address.
client.complianceManager()React:useComplianceManager - Per-claim ETH fee attached automatically to claim/claimAndUnwrap.
client.gasFee()React:useAirdropGasFee - Raw pause flag, independent of the claim window.
client.paused()React:useAirdropPaused - Whether extendClaimWindow is allowed on this instance, fixed at create time. Also returned by useAirdropConfig.
client.canExtendClaimWindow()React:useAirdropConfig - The block the instance was initialized in - the lower bound for an event scan. Now a getter; AirdropInitialized no longer carries it.
client.deploymentBlockNumber()React:useAirdropConfig - The campaign's replay policy, read from the chain. ECDSA-only. Throws TOKENOPS_UNEXPECTED_CONTRACT_RESPONSE for an ordinal this SDK does not know.
client.readDedupMode()React:useDedupMode - "clone" or "uups" - read via the ERC-1967 storage slot directly, the only reliable discriminator (proxiableUUID and upgradeInterfaceVersion answer identically for both).
client.deploymentMode()React:useAirdropDeploymentMode client.proxiableUUID()OpenZeppelin's notDelegated-guarded constant. Reverts on both clone and UUPS instances when called through the proxy - not a mode discriminator.client.upgradeInterfaceVersion()Fixed constant string ("5.0.0"). Answers identically regardless of deployment mode.- Gas-free preview of whether a claim authorization would be accepted right now. Simulated as an eth_call from recipient, so the wrong recipient always returns false.
client.isSignatureValid()React:useIsSignatureValid - Live on-chain CLAIM_TYPEHASH. Bundled with domainSeparator and eip712Domain in useEcdsaDomain.
client.claimTypehash()React:useEcdsaDomain - EIP-712 domain separator, same bundle.
client.domainSeparator()React:useEcdsaDomain - ERC-5267 7-tuple domain descriptor, same bundle.
client.eip712Domain()React:useEcdsaDomain
Recovery · 7
- Halt claims. PAUSER_ROLE.
client.pause()React:useAirdropPause - Resume claims, same hook with paused: false. PAUSER_ROLE.
client.unpause()React:useAirdropPause - Sweep the entire remaining confidential pool to a recipient. TREASURY_ROLE. Refused with ClawbackRequiresPauseError while the campaign is unpaused inside its claim window - pause first.
client.withdrawConfidential()React:useWithdrawConfidential - Withdraw accrued per-claim ETH fee. FEE_COLLECTOR_ROLE; over-request reverts InsufficientBalanceError.
client.withdrawGasFee()React:useWithdrawGasFee - Sweep a stranded plain ERC-20 sent to the instance by mistake. RESCUER_ROLE.
client.rescueERC20()React:useRescueERC20 - Sweep a stranded ERC-7984 token that isn't the campaign's own. RESCUER_ROLE; rejects the campaign token itself.
client.rescueOtherConfidentialToken()React:useRescueOtherConfidentialToken - Sweep ETH from a zero-fee campaign to { recipient }. RESCUER_ROLE. On a fee-charging campaign the ETH is fee revenue, so this throws NativeRescueRequiresZeroFeeError - use withdrawGasFee there.
client.rescueNativeToken()React:useRescueNativeToken
Roles · RBAC · 17
- Role bytes32 for pause/unpause. Bundled with the other role constants in useAirdropRoleConstants.
client.PAUSER_ROLE()React:useAirdropRoleConstants - Role bytes32 for extendClaimWindow.
client.WINDOW_ADMIN_ROLE()React:useAirdropRoleConstants - Role bytes32 for withdrawConfidential.
client.TREASURY_ROLE()React:useAirdropRoleConstants - Role bytes32 for rescueERC20 / rescueOtherConfidentialToken.
client.RESCUER_ROLE()React:useAirdropRoleConstants - Role bytes32 for withdrawGasFee. Self-administered - re-parented to itself, not DEFAULT_ADMIN_ROLE.
client.FEE_COLLECTOR_ROLE()React:useAirdropRoleConstants - Role bytes32 for upgradeToAndCall.
client.UPGRADER_ROLE()React:useAirdropRoleConstants - Role bytes32 for the admin disclosure methods.
client.DISCLOSURE_ADMIN_ROLE()React:useAirdropRoleConstants - Administers every role above except the self-administered FEE_COLLECTOR_ROLE.
client.DEFAULT_ADMIN_ROLE()React:useAirdropRoleConstants - ECDSA-only role constant, included in useAirdropRoleConstants's optional fields for this variant.
client.SIGNER_ROLE()React:useAirdropRoleConstants - Check whether an address holds a given instance role.
client.hasRole()React:useAirdropHasRole - Which role administers a given role on this instance.
client.getRoleAdmin()React:useAirdropRoleAdmin - Number of holders of a role. Bundled with enumeration in useAirdropRoleMembers.
client.getRoleMemberCount()React:useAirdropRoleMembers - One role holder by enumeration index.
client.getRoleMember()React:useAirdropRoleMembers - Every current holder of a role - how you answer who can withdrawGasFee, since no feeCollectors() view exists.
client.getRoleMembers()React:useAirdropRoleMembers - Grant an instance role. Prefer grantInstanceRoles for splitting roles right after create.
client.grantRole()React:useAirdropGrantRole - Revoke an instance role. On-chain floors block revoking the last DEFAULT_ADMIN_ROLE holder and, once gasFee() > 0, the last FEE_COLLECTOR_ROLE holder.
client.revokeRole()React:useAirdropRevokeRole - Give up a role yourself - takes no holder argument; OpenZeppelin v5 requires the caller to confirm itself.
client.renounceRole()React:useAirdropRenounceRole