Skip to main content

React Hooks

React hooks

Hooks live at @tokenops/sdk/fhe-vesting/react. The optional peer dependencies react, wagmi, and @tanstack/react-query must be installed at consumer-install time (they are not bundled). The encryptor source mirrors the headless EncryptorSource pattern — pass an eager Encryptor instance or a lazy () => Encryptor | undefined factory; both forms are accepted everywhere. @zama-fhe/react-sdk is an optional peer for FHE flows; the SDK never imports it at top level — consumers without FHE encryption flows pay no bundle cost.

Read-after-write race on load-balanced RPCs

Read hooks (useVestingInfo, usePendingVestingTransfer, and similar) issue plain eth_calls against the wagmi public client. On providers that fan reads across read-replicas (Alchemy, Infura, public-RPC pools), a query that fires immediately after a write tx's receipt confirms can return stale pre-write state — the write landed on one replica, but the read was served by a replica that hasn't caught up yet.

This is an RPC-layer artefact, not an SDK or contract bug. For post-mutation state, prefer parsing the relevant event from the write receipt (VestingCreated, VestingTransferInitiated, VestingTransferAccepted, etc.) and using that as the immediate truth; the cached query will catch up on the next render once the replica converges (typically < 1 block).

Hook catalogue

FamilyHooks
Factory readsuseConfidentialVestingFactoryImplementation, useFactoryDefaultGasFee, useFactoryDefaultTokenFee, useFactoryDefaultFeeType, useFactoryFeeCollector, useFactoryCustomFee, usePredictManagerAddress, useFactoryInitCodeHash
Factory writesuseCreateManager, useCreateManagerAndGetAddress, useSetDefaultGasFee, useSetDefaultTokenFee, useSetDefaultFeeType, useResetGasFee, useResetTokenFee, useSetCustomFee, useDisableCustomFee, useSetFeeCollector
Manager readsuseManagerToken, useManagerFeeType, useManagerFee, useManagerFeeInfo, useManagerDeploymentBlockNumber, useManagerIsSplitEnabled, useManagerIsPausable, useManagerPaused, useManagerMaxBatchSize, useManagerMaxRevokeBatchSize, useVestingInfo, useAllRecipients, useAllRecipientsLength, useAllRecipientsSliced, useIsRecipient, useRecipientVestings, useRecipientVestingsLength, useRecipientVestingsSliced, usePendingVestingTransfer, useHasRole, useRoleConstants
Manager encrypted viewsuseGetVestedAmount, useGetClaimableAmount, useGetTotalAllocation, useGetSettledAmount, useAdminGetVestedAmount, useAdminGetClaimableAmount, useAdminGetTotalAllocation, useAdminGetSettledAmount, useAdminGetTokenBalance
Manager writesuseCreateVesting, useBatchCreateVesting, useClaim, useAdminClaim, usePartialClaim, useAdminPartialClaim, useRevokeVesting, useBatchRevokeVesting, useSplitVesting, useInitiateVestingTransfer, useAcceptVestingTransfer, useCancelVestingTransfer, useDirectVestingTransfer, useDiscloseToParty, useBatchDiscloseToParty, useAdminDiscloseToParty, useAdminBatchDiscloseToParty, useDiscloseHandleToParty, useBatchDiscloseHandlesToParty, useWithdrawAdmin, useWithdrawOtherToken, useWithdrawOtherConfidentialToken, useWithdrawGasFee, useWithdrawTokenFee, useSetMaxBatchSize, useSetMaxRevokeBatchSize, useTransferFeeCollectorRole, usePause, useUnpause, useGrantRole, useRevokeRole, useRenounceRole

All 80 hooks are exported from @tokenops/sdk/fhe-vesting/react. The utility types EncryptorSource, Encryptor, VestingParams, VestingInfo, EncryptedViewResult, DisclosureType, FeeType, and scaleRatio are re-exported from the same path so component files need only one import.

useCreateManagerAndGetAddress quickstart

The canonical flow: deploy a manager clone and learn its address, then navigate to it or store it in state.

import { usePublicClient, useWalletClient } from "wagmi";
import { useQueryClient } from "@tanstack/react-query";
import { useCreateManagerAndGetAddress } from "@tokenops/sdk/fhe-vesting/react";

// Wagmi providers (`<WagmiProvider>` + `<QueryClientProvider>`) must wrap this component.
// No extra SDK provider is required for non-FHE factory hooks.

function DeployManagerButton({
token,
userSalt,
onDeployed,
}: {
token: `0x${string}`;
userSalt: `0x${string}`;
onDeployed: (manager: `0x${string}`) => void;
}) {
const queryClient = useQueryClient();

// Pass no options — the hook resolves the Sepolia factory address from
// DEPLOYED_ADDRESSES using the connected chain. Pass `address` to override.
const create = useCreateManagerAndGetAddress();

function handleDeploy() {
create.mutate(
{ token, userSalt },
{
onSuccess: ({ manager }) => {
// Blast all fhe-vesting queries so any downstream reads refresh.
queryClient.invalidateQueries({ queryKey: ["tokenops-sdk", "fhe-vesting"] });
onDeployed(manager);
},
},
);
}

return (
<button onClick={handleDeploy} disabled={create.isPending}>
{create.isPending ? "Deploying…" : "Deploy manager"}
</button>
);
}

create.data is typed as { hash: Hex; manager: Address } after success.

useCreateVesting quickstart

For any hook that submits encrypted inputs, pass encryptor as a lazy factory so the hook always picks up the live Zama React SDK context:

import { useZamaSDK } from "@zama-fhe/react-sdk";
import { useQueryClient } from "@tanstack/react-query";
import { useCreateVesting, type VestingParams } from "@tokenops/sdk/fhe-vesting/react";

// Install @zama-fhe/react-sdk and wrap your app in <ZamaProvider> from that package.

function CreateVestingForm({ managerAddress }: { managerAddress: `0x${string}` }) {
const queryClient = useQueryClient();

// encryptor is a lazy factory — the SDK calls it per-encryption, picking up
// the live ZamaSDK instance from React context at submit time.
// This avoids the stale-capture bug described in CLAUDE.md Pitfall #3.
//
// `useZamaSDK()` returns `ZamaSDK`; the encrypt-capable interface lives on
// `.relayer` (a `RelayerSDK` — the structural shape `Encryptor` mirrors).
const zamaSDK = useZamaSDK();
const create = useCreateVesting({
address: managerAddress,
encryptor: () => zamaSDK.relayer,
});

function handleSubmit(params: VestingParams, amount: bigint) {
// amount is plaintext — the SDK encrypts it to an externalEuint64 before
// submitting the transaction.
create.mutate(
{ params, amount },
{
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["tokenops-sdk", "fhe-vesting"] });
},
},
);
}

return (
<button
onClick={() =>
handleSubmit(
{
recipient: "0x0000000000000000000000000000000000000001" as `0x${string}`,
startTimestamp: Math.floor(Date.now() / 1000),
endTimestamp: Math.floor(Date.now() / 1000) + 365 * 86400,
cliffSeconds: 0,
releaseIntervalSecs: 86400,
timelockSeconds: 0,
initialUnlockBps: 0,
cliffAmountBps: 0,
isRevocable: false,
},
1_000_000n,
)
}
disabled={create.isPending}
>
Create vesting
</button>
);
}

useGetClaimableAmount — encrypted view pattern

Encrypted view hooks submit a transaction — they are useMutation instances, not TanStack read queries. The contract performs FHE.allow(handle, msg.sender) on-chain; the SDK extracts the granted handle from the receipt's ACL.Allowed event. Pass data.handle to the Zama relayer's userDecrypt to obtain the plaintext.

import { useZamaSDK } from "@zama-fhe/react-sdk";
import { useGetClaimableAmount } from "@tokenops/sdk/fhe-vesting/react";

function ClaimableAmountButton({
managerAddress,
vestingId,
}: {
managerAddress: `0x${string}`;
vestingId: `0x${string}`;
}) {
const zamaSDK = useZamaSDK();
const getClaimable = useGetClaimableAmount({ address: managerAddress });

async function handleReveal() {
const { handle } = await getClaimable.mutateAsync({ vestingId });

// `handle` is a euint128 ciphertext bound to the caller. Decrypting it
// requires a signed EIP-712 keypair credential — see Zama's relayer
// user-decrypt flow:
// https://docs.zama.ai/protocol/relayer-sdk-guides/fhevm-relayer/user-decrypt
//
// For React, `@zama-fhe/react-sdk` exposes `useUserDecrypt` /
// `userDecryptQueryOptions` that wrap the EIP-712 signing + caching for
// you. The raw `zamaSDK.relayer.userDecrypt({ handles, ... })` takes a
// `UserDecryptParams` object containing the keypair, signature, and
// signed contract addresses.
void zamaSDK; // suppress unused-var while you wire userDecrypt.
console.log("encrypted claimable handle:", handle);
}

return (
<button onClick={handleReveal} disabled={getClaimable.isPending}>
Reveal claimable amount
</button>
);
}

The same pattern applies to all nine encrypted-view hooks: useGetVestedAmount, useGetClaimableAmount, useGetTotalAllocation, useGetSettledAmount, and their useAdmin* counterparts. None can be swapped for useQuery — simulation diverges from execution on encrypted handles.

Address override

Every factory hook accepts { address?, chainId? } matching the headless BaseHookOptions. When both are omitted, the hook resolves the factory address from DEPLOYED_ADDRESSES.fheVesting.confidentialVestingFactory[chainId] using the chain id from usePublicClient().chain.id. If no address resolves (e.g. the chain is not Sepolia, and no override was passed), mutations throw DeploymentAddressUnavailableError (code: "TOKENOPS_DEPLOYMENT_ADDRESS_UNAVAILABLE"); error.context names the client (clientLabel: "ConfidentialVestingFactoryClient"), the unknown chain id, and the override knob (overrideName: "address").

Manager hooks require address — manager clones are per-user and have no deployed default:

// Factory hook — address resolves automatically on Sepolia.
const defaultGasFee = useFactoryDefaultGasFee();

// Factory hook — address override for a custom deployment.
const customFee = useFactoryCustomFee({ address: "0xMyFactory", creator: account });

// Manager hook — address is always required.
const token = useManagerToken({ address: managerAddress });

Encryptor source

The following mutation hooks require an encryptor to be available at call time:

useCreateVesting, useBatchCreateVesting, usePartialClaim, useAdminPartialClaim, useSplitVesting, useWithdrawAdmin, useWithdrawTokenFee

Pass encryptor at the hook level in ManagerHookOptions to wire it once for the component:

const zamaSDK = useZamaSDK();

const create = useCreateVesting({
address: managerAddress,
// Lazy — called per-encryption, not at hook construction. `useZamaSDK()`
// returns `ZamaSDK`; the encrypt-capable interface lives on `.relayer`.
encryptor: () => zamaSDK.relayer,
});

The lazy form (() => Encryptor | undefined) is recommended in React so the SDK always reads the live context value rather than a stale capture from mount time (CLAUDE.md Pitfall #3). An eager Encryptor instance also type-checks and is appropriate for server-side or test code where the encryptor lifetime matches the component.

If encryptor is absent at both hook level and call time, the SDK throws: Missing encryptor — pass encryptor to the hook config (e.g. encryptor: () => useZamaSDK().relayer) or to the mutation args.

If @zama-fhe/react-sdk is not installed and no encryptor is passed, the SDK's dynamic-import fallback throws: Missing optional peer dependency @zama-fhe/react-sdk. Either pnpm add @zama-fhe/react-sdk (and wrap your app in <ZamaProvider>) or pass an explicit encryptor to the hook.

Query keys

The key shape for every hook in this subpath is:

["tokenops-sdk", "fhe-vesting", "<methodName>", chainId, address?.toLowerCase(), ...primitiveArgs]

bigint args are stringified to base-10 strings. Hex addresses are lowercased. This means structural equality holds across renders even when viem returns checksummed addresses.

Cache behavior. All read hooks ship with staleTime: 0 — chain state is treated as live, so a focus-revalidation re-fires the underlying RPC call. This is the safest default for vesting reads that change as time progresses (useGetClaimableAmount is the obvious one) but it's aggressive for chain constants like useFactoryDefaultGasFee and the manager's immutable reads (useManagerFeeInfo, useManagerToken). Override staleTime per hook if you want to dial cache aggression up or down:

// Refresh manager fee info no more than once per minute — it's immutable, so
// in practice once-ever is fine.
const feeInfo = useManagerFeeInfo({
address: managerAddress,
// TanStack passes any unknown options through to useQuery (cast-through type;
// adapt per your wagmi/TanStack version if the inference complains).
} as never);

The current hook surface does not expose per-hook staleTime overrides — useQuery consumers who need them can build a thin wrapper. A future minor release may add { queryOptions?: Partial<UseQueryOptions> } pass-through.

Invalidate everything for a manager after a write:

queryClient.invalidateQueries({ queryKey: ["tokenops-sdk", "fhe-vesting"] });

Narrow to a specific method and manager address:

queryClient.invalidateQueries({
queryKey: ["tokenops-sdk", "fhe-vesting", "manager:token", chainId, managerAddress.toLowerCase()],
});