2.0 RC docsView 1.x docs
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.
Airdrop v2 · Types · 83@tokenops/sdk/fhe-airdrop

Airdrop v2 type surface.

No admin field at create time, the factory injects it. Params split by claim variant, plus new campaign and guard types.

Campaign params· 5

  • CommonAirdropParams{ token, startTime, endTime, canExtendClaimWindow, unwrappable, complianceAdmin, maxAcceptedGasFee }

    Fields shared by both variants at create time. Deliberately has no admin field - the factory sets admin = msg.sender; split roles afterward with grantInstanceRoles. maxAcceptedGasFee is required and has no default: the per-claim fee is frozen into the instance for its lifetime and the value that resolves is the factory fee manager's choice, so a default would hand that decision back to whoever is not making it. Pass 0n to accept only a zero-fee campaign, or UINT96_MAX to accept whatever resolves. complianceAdmin (zero means the creator) is both the compliance clone's admin and its first client delegate, with never-expiring decrypt rights over the instance from create.

  • EcdsaAirdropParams{ common: CommonAirdropParams, signer, dedupMode }

    createEcdsaAirdrop input. signer is the address that will hold SIGNER_ROLE; dedupMode is frozen at init. Read it back with readDedupMode() / useDedupMode.

  • MerkleAirdropParams{ common: CommonAirdropParams, merkleRoot, isMerkleRootMutable }

    createMerkleAirdrop input. A zero merkleRoot is only valid when isMerkleRootMutable is true.

    Concept primer →
  • DedupMode"perAddress" | "perDedupId" | "both" | "none"

    ECDSA replay-protection dimension, mirrors the DEDUP_MODE const object. Fixed at createEcdsaAirdrop time; readDedupMode() decodes the on-chain dedupMode() getter, and throws TOKENOPS_UNEXPECTED_CONTRACT_RESPONSE for an ordinal this SDK does not know.

  • DeploymentMode"clone" | "uups"

    Instance shell: an EIP-1167 clone (cheap, non-upgradeable) or a UUPS proxy (upgradeable, factory-gated). Merkle refuses uups outright.

Create + fund· 12

  • CreateAirdropArgs<P>{ params: P, mode, userSalt, expected? }

    Generic over EcdsaAirdropParams / MerkleAirdropParams. userSalt is combined with the deployer address to derive the CREATE2 salt. expected pins any create commitment instead of quoting it fresh at send time.

  • CreateMerkleAirdropArgsCreateAirdropArgs<MerkleAirdropParams> & { plan? }

    The Merkle creates. A plan pins expected.airdrop to plan.predictedAddress, requires params.merkleRoot to equal plan.root, and refuses the send once the Merkle implementation has moved.

    Concept primer →
  • CreateCommitments{ airdrop, complianceManagerImpl, complianceDelegate }

    What every create commits to. The factory reverts before consuming the salt when any of them drifted; the SDK quotes all three at send time unless pinned.

    Concept primer →
  • AirdropVariantParams{ variant: "ecdsa", params: EcdsaAirdropParams } | { variant: "merkle", params: MerkleAirdropParams }

    Input to factory.quoteCreateCommitments - ties the variant to its params type.

  • CreateAirdropResultMinedWrite & { hash, airdrop, complianceManager, managerImplementation, complianceDelegate }

    Factory deploy output. The compliance fields are read from the create receipt's ComplianceManagerCloned log, not a follow-up registry call.

  • PendingCreateAirdropResultPendingWrite & { hash, airdrop, complianceManager, managerImplementation, complianceDelegate }

    What a create returns with waitForReceipt: false (Safe signers). Every address is still set, built from the commitments, because the factory reverts unless it deploys exactly there.

  • ReceiptMode<W>{ waitForReceipt: false } | { waitForReceipt?: boolean }

    Mixed into each receipt-free-capable write as Args & ReceiptMode. The literal false selects the Pending overload; omitting it keeps the mined result type.

  • PendingWrite / MinedWrite{ pending: true } / { pending?: never }

    The shared discriminant on every receipt-free result. if (result.pending) narrows the same way in all three products.

  • FundInput{ amount, encryptor? } | { encryptedInput }

    Discriminated union guarded with never - supply exactly one of a plaintext amount (SDK encrypts) or a pre-encrypted input.

  • WriteAccountOverrideGasOverride & { account?: Account | Address }

    Mixed into nearly every write-method args type. Overrides the client's default signer for that one call, and carries the per-call gas.

  • GasHeadroomOption{ gasHeadroomPercent?: number }

    Accepted by every client config and hook. Percent added on top of each write's gas estimate; defaults to DEFAULT_GAS_HEADROOM_PERCENT, 0 sends the bare estimate, and the padded limit is clamped to MAX_TRANSACTION_GAS (the EIP-7825 cap, exported from the root).

    Concept primer →
  • GasOverride{ gas?: bigint }

    Per-call gas limit sent as is, skipping the estimate and headroom. A wallet or Safe that sets its own limit may ignore it.

    Concept primer →

Claim args· 8

  • EcdsaClaimArgs{ to?, encryptedInput, dedupId, deadline, signer, signature, account? }

    EcdsaAirdropClient.claim / claimAndUnwrap input. The gas fee is attached automatically; no value field to set by hand.

  • EcdsaGetClaimAmountArgs{ encryptedInput, dedupId, deadline, signer, signature, account? }

    Preview an ECDSA claim amount. Aliased at the package root from the shared GetClaimAmountArgs name - does not consume replay state.

  • IsSignatureValidArgs{ recipient, encryptedAmountHandle, dedupId, deadline, signer, signature }

    Gas-free ECDSA claim validity preview. Simulated as an eth_call from recipient, so the wrong recipient always returns false.

  • MerkleClaimArgs{ to?, entry: CampaignEntry, account? }

    MerkleAirdropClient.claim input. entry.account is the claim identity and is paid unless it submits itself and sets a different to; anyone may submit. account? is the sending-account override, not the claim identity - never pass the recipient there on a relayed claim.

    Concept primer →
  • MerkleGetClaimAmountArgs{ entry: CampaignEntry, account? }

    Preview a Merkle claim amount. Grants the ACL handle to entry.account, not the caller - a relayer previewing learns nothing itself. account? is the sending-account override, not the claim identity.

    Concept primer →
  • UnwrapRequest{ claimant, beneficiary, claimKey, unwrapRequestId }

    Decoded ClaimedAndUnwrapInitiated event. Pass unwrapRequestId to the wrapper's finalizeUnwrap to release the underlying ERC-20.

  • UnwrapRequestSourcereceipt | receipt.logs

    What parseUnwrapRequest reads. Only this instance's event counts; two in one transaction throw ReceiptEventAmbiguousError.

  • CampaignEntry{ account, handle, inputProof, merkleProof }

    Everything merkle.claim needs for one recipient. account is the claim identity; the input is bound to the instance, so any address may submit it.

    Concept primer →

Client configs· 4

  • ConfidentialAirdropFactoryClientConfigGasHeadroomOption & { publicClient, walletClient?, address?, chainId?, encryptor?, telemetry? }

    Factory client constructor. address falls back to DEPLOYED_ADDRESSES on mainnet and Sepolia. encryptor is the default for fund-methods when passing a plaintext amount.

  • AirdropBaseClientConfigGasHeadroomOption & { publicClient, walletClient?, address, chainId?, aclAddress?, telemetry? }

    Shared constructor shape for both claim variants - address is the deployed instance, clone or UUPS proxy.

  • EcdsaAirdropClientConfigAirdropBaseClientConfig

    An alias: the ECDSA client takes no variant-specific config. The replay policy is read from the chain with readDedupMode().

  • ComplianceManagerClientConfig{ publicClient, walletClient?, address, chainId?, telemetry? }

    address is the ComplianceRoleManager clone, not the airdrop instance. Read it back with complianceManagerOf(airdrop).

Merkle campaign tooling· 13

  • BuiltCampaign{ root, entries }

    Output of buildMerkleCampaign - a published root plus every recipient's CampaignEntry.

    Concept primer →
  • CampaignRecipient{ recipient, cumulativeTotal }

    Input roster row. cumulativeTotal is the recipient's running lifetime allocation, never a per-drop tranche.

    Concept primer →
  • BuildMerkleCampaignArgs{ instance, recipients, encryptor }

    Mutable-root path - the instance already exists, encrypt + build the tree against its real address.

    Concept primer →
  • PlanMerkleCampaignArgs{ factory, params, mode, creator, userSalt, recipients, encryptor }

    Immutable-root path - predicts the CREATE2 address first, then encrypts and builds against that predicted address.

    Concept primer →
  • PlannedCampaignBuiltCampaign & { predictedAddress, initCodeHashBefore, initCodeHashAfter }

    planMerkleCampaign's output. Two init-code-hash reads bracket the build to catch a mid-flight implementation swap. Pass it as plan to the Merkle create.

    Concept primer →
  • RotateMerkleRootArgs{ airdrop, recipients, encryptor } & WriteAccountOverride

    Rebuilds a live campaign's full tree and calls setMerkleRoot in one step. Root is published last, so a failed rebuild never touches the live campaign.

    Concept primer →
  • RotatedCampaignBuiltCampaign & { hash }

    rotateMerkleRoot's output - the new tree plus the setMerkleRoot transaction hash.

    Concept primer →
  • MerkleLeafInput{ account, handle }

    Minimum shape buildMerkleTree needs - the tree only commits (instance, account, handle). Extra fields ride through onto the entry.

    Concept primer →
  • LeafOfArgs{ instance, account, handle }

    Input to leafOf - computes keccak256(keccak256(abi.encode(instance, account, handle))), byte-for-byte matching the contract's _leafOf.

    Concept primer →
  • BuildMerkleTreeArgs<L>{ instance, leaves: readonly L[] }

    Input to buildMerkleTree. Pure - no relayer, no chain access. Generic over anything wider than MerkleLeafInput, so a caller's own fields ride along onto the entry.

    Concept primer →
  • BuiltMerkleTree<L>{ root, entries: readonly (L & { merkleProof }) [] }

    buildMerkleTree's output - the root to publish, plus every leaf with its Merkle proof attached, in input order.

    Concept primer →
  • EncryptCampaignAmountsArgs{ instance, recipients: readonly CampaignRecipient[], encryptor }

    Input to encryptCampaignAmounts - encrypts a roster against (instance, instance), 32 values per proof (MERKLE_BATCH_LIMIT), without building a tree. Mutable-root path only: instance must already be deployed.

    Concept primer →
  • EncryptedCampaignLeafMerkleLeafInput & { inputProof }

    encryptCampaignAmounts' per-recipient output - everything buildMerkleTree needs for one leaf, plus the input proof the tree itself doesn't touch.

    Concept primer →

CREATE2 address prediction· 2

  • PredictArgs<P>{ params: P, mode, deployer, userSalt }

    Input to factory.predictEcdsaAirdropAddress / predictMerkleAirdropAddress. Only mode, deployer, and userSalt affect the CREATE2 address - params travels along generically but isn't part of the salt.

    Concept primer →
  • InitCodeHashArgs<P>Pick<PredictArgs<P>, "params" | "mode">

    Input to getEcdsaInitCodeHash / getMerkleInitCodeHash - narrower than PredictArgs on purpose: the hash covers only the proxy init-code, so deployer and userSalt can't affect it.

    Concept primer →

ERC-7984 operator (funding prerequisite)· 2

  • SetOperatorArgsGasHeadroomOption & GasOverride & { publicClient, walletClient, account?, token, spender, deadline?, waitForReceipt?, telemetry? }

    Input to setOperator (@tokenops/sdk/fhe) - authorizes spender (the airdrop factory - funding always routes through it) to pull the caller's ERC-7984 tokens until deadline. Required before fundAirdrop / createAndFund*.

    Concept primer →
  • RevokeOperatorArgs{ publicClient, walletClient, account?, token, spender, waitForReceipt?, telemetry? }

    Input to revokeOperator - identical to SetOperatorArgs minus deadline; calls setOperator(spender, 0), the ERC-7984 revoke convention, for a stale or replaced clone.

    Concept primer →

Roles + policy· 9

  • InstanceRoleAssignment{ pauser?, windowAdmin?, treasury?, rescuer?, disclosureAdmin?, upgrader?, merkleAdmin?, signer?, feeCollector?, admin? }

    Target holders for planInstanceRoleSplit / grantInstanceRoles - instances are create-then-grant, never atomic.

  • RoleGrantStep{ kind: "grant" | "revoke", role: RoleName, holder }

    One ordered step in a role-split plan. holder is the grantee (account means the sender everywhere else in the module). An admin reassignment always emits grant-then-revoke, in that order; planInstanceRoleSplit refuses assignment.admin equal to the caller with InvalidArgumentError - omit admin to keep it.

  • RoleGrantOutcome{ role: RoleName, holder, hash?, error? }

    Per-step result from grantInstanceRoles - exactly one of hash or error is set. Never collapsed into a single status.

  • InstanceRoleCall{ to, data, value: 0n }

    One grantRole / revokeRole call from encodeInstanceRoleSplit, in plan order - what a Safe submits as one MultiSend batch.

  • InstanceRoleConstantsPartial<Record<RoleName, Hex>>

    The bytes32 role constants encodeInstanceRoleSplit encodes, keyed by role name. Read them from the instance once.

  • RoleName"DEFAULT_ADMIN_ROLE" | "PAUSER_ROLE" | "WINDOW_ADMIN_ROLE" | "TREASURY_ROLE" | "RESCUER_ROLE" | "DISCLOSURE_ADMIN_ROLE" | "UPGRADER_ROLE" | "MERKLE_ADMIN_ROLE" | "SIGNER_ROLE"

    The 9-member union planInstanceRoleSplit accepts. FEE_COLLECTOR_ROLE is deliberately excluded - it is self-administered, not grantable from an admin role.

  • CompliancePolicy{ overridden, delegateToCompliance }

    Per-creator override state on the factory. overridden distinguishes an explicit choice from unset-use-the-default.

  • UpgradeabilityPolicy{ overridden, allowed }

    Same override-over-default shape, gating whether a creator may pass mode: "uups" at all.

  • CustomFee{ enabled, gasFee }

    Per-creator gas-fee override read from the factory. Moved here from a shared types module in v1 - same shape, now declared in factory.ts.

Preflight + guards· 9

  • PreflightCreateArgs{ factory, variant, params, mode, creator, userSalt, expectedInitCodeHash?, expected?, plan? }

    Input to the free function preflightCreate. plan is Merkle-only. A pinned expected field that differs from the fresh quote is a CreateCommitmentMismatchError blocker.

    Concept primer →
  • PreflightCreateResultPreflightResult & { commitments? }

    What preflightCreate returns. commitments is exactly what the create would be sent with.

    Concept primer →
  • PreflightClaimArgs{ airdrop, claimant, account?, to?, entrypoint?, signature?, factory? }

    Input to preflightClaim. Takes a structural ClaimPreflightTarget rather than a concrete client class to avoid an import cycle. Supplying factory turns on the genuineness check. Blockers carry the entrypoint they would stop (default "claim") as method, and a claimant whose ETH is below the gas fee is an InsufficientBalanceError (balanceKind: "eth"), not an InsufficientFeeError.

    Concept primer →
  • AirdropRegistry{ address, chainId, isAirdrop(candidate) }

    The slice of ConfidentialAirdropFactoryClient the genuineness check reads. chainId is checked against the instance's own chain before the registry is consulted - a registry answer from the wrong chain is worse than no answer, because it reads as a clean true.

    Concept primer →
  • PreflightResult{ ready, blockers }

    Generic cross-product report re-exported from core. Replaces the bespoke CreateAirdropPreflightReport v1 used to return.

    Concept primer →
  • ClaimPreflightTargetstructural client interface

    Satisfied by AirdropBaseClient, EcdsaAirdropClient, or MerkleAirdropClient - whatever preflightClaim is checking against.

    Concept primer →
  • EcdsaClaimPreflightSignature{ encryptedAmountHandle, dedupId, deadline, signer, signature }

    The signature fields preflightClaim needs to run isSignatureValid as part of an ECDSA claim preview.

    Concept primer →
  • AssertPredictionFreshArgs{ variant, initCodeHashBefore, initCodeHashAfter, predictedAddress?, method }

    Input to assertPredictionFresh - a pure comparison of two init-code-hash reads bracketing a prediction's use. Mismatch means an implementation rotation landed mid-flight.

    Concept primer →
  • AssertSaltAvailableArgs{ publicClient, predictedAddress, variant, mode, deployer, userSalt, method }

    Input to assertSaltAvailable - one eth_getCode check on the predicted address before create; non-empty code throws SaltCollisionError.

    Concept primer →

Encryption + misc· 10

  • Encryptor{ encrypt({ values, contractAddress, userAddress }): Promise<{ encryptedValues: Hex[], inputProof: Hex }> }

    Mirrors ZamaSDK.encrypt, so a ZamaSDK, createSepoliaEncryptor (@tokenops/sdk/fhe) or createSepoliaEncryptorWeb (@tokenops/sdk/fhe/web) result is assignable without a cast. resolveEncryptor unwraps an EncryptorSource into one.

    Concept primer →
  • EncryptorSourceEncryptor | (() => Encryptor | undefined)

    Eager-or-lazy encryptor input accepted by client configs and campaign-building functions.

  • EncryptUint64Args{ encryptor, contractAddress, userAddress, value }

    One euint64 in, one { handle, inputProof } out, bound to the (contractAddress, userAddress) pair.

  • EncryptUint64BatchArgs{ encryptor, contractAddress, userAddress, values: bigint[] }

    Input to encryptUint64Batch - N euint64 values encrypted under one shared KMS input proof instead of N independent ones.

  • FheValueInput{ value: boolean | bigint, type: "ebool" } | { value: Address, type: "eaddress" } | { value: bigint, type: "euint8" | ... }

    Discriminated FHE input mirroring upstream EncryptInput: the euint8 to euint256 types take a bigint, ebool takes boolean | 1n | 0n, eaddress takes an Address.

  • EncryptedInput{ handle, inputProof }

    One euint64 ciphertext + its KMS input proof - encryptUint64's return shape, and FundInput's encryptedInput branch.

  • EncryptedInputs{ handles: Hex[], inputProof }

    N ciphertexts sharing one input proof - encryptUint64Batch's return shape. handles[i] corresponds to the i-th plaintext value passed in.

  • Eip712DomainERC-5267 7-tuple

    eip712Domain()'s return shape on EcdsaAirdropClient - fields, name, version, chainId, verifyingContract, salt, extensions.

  • EncryptedViewResult{ handle, hash }

    Returned by every disclosure/claim-amount method. Decrypt handle with ZamaSDK decryption.decryptValues, or useDecryptedHandle in React.

  • AirdropVariant"ecdsa" | "merkle"

    Labels which claim variant a prediction guard or preflight result is evaluating.

React mutation variables (/fhe-airdrop/react)· 7

  • <Action>Args / <Action>Variablesnaming rule

    Every /fhe-airdrop/react mutation names its variables <Action>Args, or <Action>Variables where the headless subpath already exports <Action>Args. The old Use* names are gone; see the from-alpha migration for the rename table.

    Concept primer →
  • CreateEcdsaAirdropArgs / CreateMerkleAirdropVariablescreate args & ReceiptMode (beta)

    Variables of useCreateEcdsaAirdrop / useCreateMerkleAirdrop. waitForReceipt is accepted, so data resolves CreateAirdropResult | PendingCreateAirdropResult - narrow with if (data.pending) return; before reading receipt-derived fields.

    Concept primer →
  • CreateAndFundEcdsaAirdropArgs / CreateAndFundMerkleAirdropArgscreate args & FundInput & ReceiptMode (beta)

    Variables of the two createAndFund hooks, same Mined | Pending result.

  • BuildMerkleCampaignVariables / EncryptCampaignAmountsVariables / PlanMerkleCampaignVariables / RotateMerkleRootVariablesheadless args minus what the hook supplies

    Variables of the four campaign-tooling hooks. Named Variables because the headless subpath exports BuildMerkleCampaignArgs and the rest.

  • DiscloseHandleToPartyArgs / BatchDiscloseHandlesToPartyArgs / AdminDiscloseBalanceToPartyArgs / AdminBatchDiscloseBalanceToPartiesArgsdisclosure variables

    Variables of the four disclosure hooks, without the former Use prefix.

  • FundAirdropArgs / SetMerkleRootArgs / ExtendClaimWindowArgs / WithdrawConfidentialArgs / RescueNativeTokenArgswrite args & GasOverride

    Named variable types for hooks that typed them inline before. Each accepts a per-call gas.

  • SignClaimAuthorizationArgs{ recipient, encryptedAmountHandle, dedupId, deadline, account? }

    Variables of useSignClaimAuthorization; the hook supplies airdrop and chainId from its options. No gas field: signing sends no transaction.

Error support types· 2

  • DeploymentAddressUnavailableReason"chain-id-missing" | "registry-not-deployed" | "registry-unknown-chain"

    DeploymentAddressUnavailableError.context.reason's three values - branch UI copy on which one fired rather than parsing the message.

    Concept primer →
  • TokenOpsSdkErrorCode"TOKENOPS_UNSUPPORTED_CHAIN" | "TOKENOPS_PAUSED" | "TOKENOPS_MERKLE_UUPS_UNSUPPORTED" | ... (every TokenOpsSdkError code, shared across products)

    The stable string-literal union every TokenOpsSdkError.code takes. Match on error.code in onError handlers - messages may evolve between SDK versions, codes don't.

    Concept primer →