Skip to main content

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

FamilyHooks
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"] });