MerkleAirdropClient extends the same AirdropBaseClient as the ECDSA variant: pause, extendClaimWindow, withdrawConfidential, rescueERC20, refreshComplianceBalance, the balance disclosure methods, and the full roles surface all apply here too. See EcdsaAirdropClient for that full listing rather than repeating it here.MerkleAirdropClient
Extends AirdropBaseClient with no extra config field - dedup/signer concepts don't apply to proof-authorized claims. A leaf commits (instance, account, handle), where handle encrypts the account's CUMULATIVE allocation, never a per-drop tranche; a claim pays total minus whatever was already delivered.
Construct
Headless TS — non-React consumers (Node, Vite, server workers). React hosts use the per-hook surface; same method names, lazy encryptor.
import { createMerkleAirdropClient } from "@tokenops/sdk/fhe-airdrop";
const client = createMerkleAirdropClient({
publicClient,
walletClient,
// address optional: resolved from publicClient.chain.id
});Methods
Write · 2
- Redeem the outstanding amount for entry.account. Anyone may submit - the input is bound to the instance, not the sender - and the contract always pays entry.account. to defaults to entry.account and only that account may redirect it (UnauthorizedRedirectError otherwise). A leaf someone already settled pays an encrypted zero and still charges the fee, so check getClaimedAmount first.
client.claim()React:useMerkleClaim - Same accounting as claim, then routes into the wrapper's unwrap. Self-only - entry.account must be the sender, or the SDK refuses client-side before sending. Returns the hash; read the started request with readUnwrapRequest(hash) and pass its unwrapRequestId to the wrapper's finalizeUnwrap. The amount is public from the claim transaction itself - only a plain claim keeps it confidential. Merkle claims always reject an empty input proof.
client.claimAndUnwrap()React:useMerkleClaimAndUnwrap
Read · 3
- An account's cumulative delivered total as an encrypted handle. Returns the zero handle if the account never claimed. Takes a bare address - claim, claimAndUnwrap and getClaimAmount all take an args object, this one does not.
client.getClaimedAmount()React:useClaimedAmount - Currently published root.
client.merkleRoot()React:useMerkleRoot - Fixed at create time. A zero root at create is only valid when this is true.
client.isMerkleRootMutable()React:useIsMerkleRootMutable
Root rotation is a top-up, never a clawback
rotateMerkleRoot (from @tokenops/sdk/fhe-airdrop, not a client method) rebuilds the full campaign against a live MerkleAirdropClient and calls setMerkleRoot in one step, publishing the new root last so a failed rebuild never touches the live campaign. claimedAmount is keyed by account, not by root, so it survives rotation - a claim against a new root only pays the increase over what was already delivered, and a total lower than what a recipient already received pays nothing. One leaf per recipient per root: two leaves for the same recipient pay the max of the two totals, never their sum.
import { rotateMerkleRoot } from "@tokenops/sdk/fhe-airdrop";
const rotated = await rotateMerkleRoot({
airdrop, // a MerkleAirdropClient
recipients, // full updated roster - cumulative totals, not deltas
encryptor,
});
// rotated.root is already published on-chain when this resolvesReact callers use useRotateMerkleRoot. Building a campaign for an instance that doesn't exist yet (root baked into createMerkleAirdrop) goes through planMerkleCampaign instead - see ConfidentialAirdropFactoryClient for address prediction.