React Hooks
Hooks live at @tokenops/sdk/fhe-airdrop/react. The optional peer dependencies react, wagmi, and @tanstack/react-query must be installed. @zama-fhe/react-sdk is an optional peer for FHE write flows.
Wrap your app in wagmi's <WagmiProvider> + <QueryClientProvider> + @zama-fhe/react-sdk's <ZamaProvider> before rendering components that use encrypted hooks.
Hook catalogue
| Family | Hooks |
|---|---|
| Factory reads (6) | useConfidentialAirdropFactoryImplementation, useFactoryDefaultGasFee, useFactoryFeeCollector, useFactoryCustomFee, usePredictAirdropAddress, useFactoryInitCodeHash |
| Factory writes (8) | useCreateConfidentialAirdrop, useCreateConfidentialAirdropAndGetAddress, useCreateAndFundConfidentialAirdrop, useFundConfidentialAirdrop, useSetFeeCollector, useSetDefaultGasFee, useSetCustomFee, useDisableCustomFee |
| Airdrop clone reads (15) | useAirdropToken, useAirdropGasFee, useAirdropStartTime, useAirdropCanExtendClaimWindow, useAirdropEndTime, useAirdropIsPaused, useAirdropDeploymentBlockNumber, useAirdropDomainSeparator, useAirdropIsClaimWindowActive, useAirdropHasClaimStarted, useAirdropHasClaimEnded, useAirdropClaimedSignatures, useAirdropIsSignatureClaimed, useAirdropIsSignatureValid, useAirdropHasRole |
| Airdrop clone writes (10) | useClaim, useGetClaimAmount, useWithdraw, useSetPaused, useExtendClaimWindow, useWithdrawOtherToken, useWithdrawOtherConfidentialToken, useAirdropWithdrawGasFee, useAirdropGrantRole, useAirdropRevokeRole |
| Signing helper (1) | useSignClaimAuthorization |
The utility types EncryptorSource, Encryptor, AirdropParams, ClaimArgs, EncryptedViewResult, and signClaimAuthorization are re-exported from @tokenops/sdk/fhe-airdrop/react.
Deploy an airdrop
import { useQueryClient } from "@tanstack/react-query";
import { useCreateConfidentialAirdropAndGetAddress } from "@tokenops/sdk/fhe-airdrop/react";
function DeployAirdropButton({
token,
userSalt,
onDeployed,
}: {
token: `0x${string}`;
userSalt: `0x${string}`;
onDeployed: (airdrop: `0x${string}`) => void;
}) {
const queryClient = useQueryClient();
const create = useCreateConfidentialAirdropAndGetAddress();
const now = Math.floor(Date.now() / 1000);
return (
<button
onClick={() =>
create.mutate(
{
params: {
token,
startTimestamp: now + 60,
endTimestamp: now + 30 * 86400,
canExtendClaimWindow: false,
admin: "0xYourAdminAddress" as `0x${string}`,
},
userSalt,
},
{
onSuccess: ({ airdrop }) => {
queryClient.invalidateQueries({ queryKey: ["tokenops-sdk", "fhe-airdrop"] });
onDeployed(airdrop);
},
},
)
}
disabled={create.isPending}
>
{create.isPending ? "Deploying…" : "Deploy airdrop"}
</button>
);
}
Issue a claim authorization (admin)
The admin encrypts and signs the claim payload off-chain, then delivers it to the recipient. useSignClaimAuthorization is for admin dashboard UIs where the admin wallet is connected in-browser. For server-side flows, call signClaimAuthorization from @tokenops/sdk/fhe-airdrop directly with an explicit walletClient.
import { useZamaSDK } from "@zama-fhe/react-sdk";
import { useSignClaimAuthorization, encryptUint64 } from "@tokenops/sdk/fhe-airdrop/react";
function IssueClaimButton({
airdropAddress,
recipient,
amount,
onIssued,
}: {
airdropAddress: `0x${string}`;
recipient: `0x${string}`;
amount: bigint;
onIssued: (payload: {
encryptedInput: { handle: `0x${string}`; inputProof: `0x${string}` };
signature: `0x${string}`;
}) => void;
}) {
const zamaSDK = useZamaSDK();
const sign = useSignClaimAuthorization();
async function handleIssue() {
const encrypted = await encryptUint64({
encryptor: zamaSDK.relayer,
contractAddress: airdropAddress,
userAddress: recipient,
value: amount,
});
const signature = await sign.mutateAsync({
airdropAddress,
recipient,
encryptedAmountHandle: encrypted.handle,
});
onIssued({ encryptedInput: encrypted, signature });
}
return (
<button onClick={handleIssue} disabled={sign.isPending}>
Issue claim
</button>
);
}
Claim tokens (recipient)
The recipient passes the admin-issued { encryptedInput, signature } pair verbatim. useClaim does not encrypt — re-encrypting would produce a different handle and break the signature.
import { useQueryClient } from "@tanstack/react-query";
import { useClaim } from "@tokenops/sdk/fhe-airdrop/react";
function ClaimButton({
airdropAddress,
claimPayload,
}: {
airdropAddress: `0x${string}`;
claimPayload: {
encryptedInput: { handle: `0x${string}`; inputProof: `0x${string}` };
signature: `0x${string}`;
};
}) {
const queryClient = useQueryClient();
const claim = useClaim({ address: airdropAddress });
return (
<button
onClick={() =>
claim.mutate(claimPayload, {
onSuccess: () =>
queryClient.invalidateQueries({ queryKey: ["tokenops-sdk", "fhe-airdrop"] }),
})
}
disabled={claim.isPending}
>
{claim.isPending ? "Claiming…" : "Claim tokens"}
</button>
);
}
Pre-claim validation
Gate the claim button using three read hooks so the user never pays gas on a revertable claim:
import {
useAirdropIsClaimWindowActive,
useAirdropIsSignatureClaimed,
useAirdropIsSignatureValid,
} from "@tokenops/sdk/fhe-airdrop/react";
function CanClaim({
airdropAddress,
user,
encryptedAmountHandle,
signature,
}: {
airdropAddress: `0x${string}`;
user: `0x${string}`;
encryptedAmountHandle: `0x${string}`;
signature: `0x${string}`;
}) {
const windowActive = useAirdropIsClaimWindowActive({ address: airdropAddress });
const alreadyClaimed = useAirdropIsSignatureClaimed({ address: airdropAddress, user, encryptedAmountHandle });
const sigValid = useAirdropIsSignatureValid({ address: airdropAddress, encryptedAmountHandle, signature });
const ready =
windowActive.data === true && alreadyClaimed.data === false && sigValid.data === true;
return <button disabled={!ready}>{ready ? "Claim" : "Not ready"}</button>;
}
Encrypted view pattern
useGetClaimAmount submits a transaction — it is a useMutation instance, not a read query. The contract performs FHE.allow(handle, msg.sender) on-chain; the SDK extracts the handle from the receipt. Pass data.handle to Zama's userDecrypt to read the plaintext.
useGetClaimAmount does not consume the claim — the claimedSignatures bit is not set. Call useClaim when the user is ready to transfer tokens.
import { useGetClaimAmount } from "@tokenops/sdk/fhe-airdrop/react";
function RevealAmountButton({
airdropAddress,
claimPayload,
}: {
airdropAddress: `0x${string}`;
claimPayload: {
encryptedInput: { handle: `0x${string}`; inputProof: `0x${string}` };
signature: `0x${string}`;
};
}) {
const getClaimAmount = useGetClaimAmount({ address: airdropAddress });
async function handleReveal() {
const { handle } = await getClaimAmount.mutateAsync(claimPayload);
// Pass handle to zamaSDK.relayer.userDecrypt
console.log("encrypted claim handle:", handle);
}
return (
<button onClick={handleReveal} disabled={getClaimAmount.isPending}>
Reveal claim amount
</button>
);
}
Encryptor source
The following factory mutation hooks require an encryptor:
useCreateAndFundConfidentialAirdrop, useFundConfidentialAirdrop
The airdrop clone hooks useClaim and useGetClaimAmount take no encryptor — they submit the admin-issued pair verbatim.
const zamaSDK = useZamaSDK();
const create = useCreateAndFundConfidentialAirdrop({
encryptor: () => zamaSDK.relayer,
});
Query keys
["tokenops-sdk", "fhe-airdrop", "<methodName>", chainId, address?.toLowerCase(), ...primitiveArgs]
Invalidate everything for an airdrop after a write:
queryClient.invalidateQueries({ queryKey: ["tokenops-sdk", "fhe-airdrop"] });