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.logsWhat 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.
EcdsaAirdropClientConfigAirdropBaseClientConfigAn 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 } & WriteAccountOverrideRebuilds 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 interfaceSatisfied 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-tupleeip712Domain()'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 ruleEvery /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 suppliesVariables of the four campaign-tooling hooks. Named Variables because the headless subpath exports BuildMerkleCampaignArgs and the rest.
DiscloseHandleToPartyArgs / BatchDiscloseHandlesToPartyArgs / AdminDiscloseBalanceToPartyArgs / AdminBatchDiscloseBalanceToPartiesArgsdisclosure variablesVariables of the four disclosure hooks, without the former Use prefix.
FundAirdropArgs / SetMerkleRootArgs / ExtendClaimWindowArgs / WithdrawConfidentialArgs / RescueNativeTokenArgswrite args & GasOverrideNamed 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 →