Skip to main content

React Hooks

Hooks live at @tokenops/sdk/fhe-disperse/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
Reads (11)useIsRegistered, useGetWallets, usePredictWallets, useGetFees, useGetBatchLimits, useCalculateFee, useHasApprovedSubwallets, useHasRole, useIsPaused, useDeploymentBlockNumber, useWalletImplementation
Preflight (1)usePreflightDisperse
User-flow writes (5)useRegister, useApproveTokenOnWallets, useRevokeTokenOnWallets, useRecoverFromWallets, useRecoverERC20FromWallets
Core disperse (1)useDisperse
Encrypted views + disclosure (3)useGetEncryptedFeeReserve, useDiscloseHandleToParty, useBatchDiscloseHandlesToParty
Admin / fee-manager / fee-collector (14)usePause, useUnpause, useSetFeeConfig, useSetCustomFee, useDisableCustomFee, useSetMaxBatchSizeHolding, useSetMaxBatchSizeDirect, useSetMaxBatchSizeTokenFee, useWithdrawGasFee, useWithdrawTokenFee, useRescueConfidentialTokens, useRescueERC20, useGrantRole, useRevokeRole

All utility types are re-exported from @tokenops/sdk/fhe-disperse/react, including PreflightReport, DisperseMode, DisperseArgs, RegisterResult, computeSubtotals, encryptUint64, and all error classes.


Register (one-time per user)

useRegister deploys the user's dedicated wallet pair and approves both wallets as ERC-7984 operators in a single transaction. Use useIsRegistered to gate the button.

import { useQueryClient } from "@tanstack/react-query";
import { useIsRegistered, useRegister } from "@tokenops/sdk/fhe-disperse/react";

function RegisterButton({
singletonAddress,
token,
user,
}: {
singletonAddress: `0x${string}`;
token: `0x${string}`;
user: `0x${string}`;
}) {
const queryClient = useQueryClient();
const { data: isRegistered } = useIsRegistered({ address: singletonAddress, user });
const register = useRegister({ address: singletonAddress });

return (
<button
onClick={() =>
register.mutate(
{ token },
{
onSuccess: () =>
queryClient.invalidateQueries({ queryKey: ["tokenops-sdk", "fhe-disperse"] }),
},
)
}
disabled={isRegistered === true || register.isPending}
>
{isRegistered ? "Already registered" : register.isPending ? "Registering…" : "Register"}
</button>
);
}

register.data is typed as { hash: Hex; wallets: [Address, Address] } after success.


Preflight gating

Use usePreflightDisperse before rendering the disperse button. data.ready gates the button; data.blockerErrors (a typed TokenOpsSdkError[]) drives an inline diagnostic list:

import { usePreflightDisperse } from "@tokenops/sdk/fhe-disperse/react";

function DisperseReadinessPanel({
singletonAddress,
token,
recipients,
amounts,
user,
onReady,
}: {
singletonAddress: `0x${string}`;
token: `0x${string}`;
recipients: `0x${string}`[];
amounts: bigint[];
user: `0x${string}`;
onReady: () => void;
}) {
const { data: report, isLoading } = usePreflightDisperse({
address: singletonAddress,
user,
token,
recipients,
amounts,
mode: "wallet",
});

if (isLoading || !report) return <p>Checking readiness…</p>;

return (
<div>
{!report.ready && (
<ul>
{report.blockerErrors.map((err) => (
<li key={err.code + err.message}>{err.message}</li>
))}
</ul>
)}
<button onClick={onReady} disabled={!report.ready}>
{report.ready ? "Disperse" : "Not ready"}
</button>
</div>
);
}

For live "approve / register / disperse" gating UIs, set refetchInterval globally via queryClient.setQueryDefaults(["tokenops-sdk", "fhe-disperse"], { refetchInterval: 5_000 }) so the report re-fetches across the component tree.


Disperse

useDisperse is the core encrypted-write hook. Pass encryptor as a lazy factory so the SDK always reads the live Zama React SDK context at submit time.

import { useQueryClient } from "@tanstack/react-query";
import { useZamaSDK } from "@zama-fhe/react-sdk";
import { useCalculateFee, useDisperse } from "@tokenops/sdk/fhe-disperse/react";

function DisperseButton({
singletonAddress,
token,
recipients,
amounts,
user,
}: {
singletonAddress: `0x${string}`;
token: `0x${string}`;
recipients: `0x${string}`[];
amounts: bigint[];
user: `0x${string}`;
}) {
const queryClient = useQueryClient();
const zamaSDK = useZamaSDK();

const { data: fee } = useCalculateFee({
address: singletonAddress,
user,
recipients: recipients.length,
mode: "wallet",
});

const disperse = useDisperse({
address: singletonAddress,
encryptor: () => zamaSDK.relayer,
});

return (
<div>
{fee && <p>ETH fee: {fee.ethValue.toString()} wei</p>}
<button
onClick={() =>
disperse.mutate(
{ token, mode: "wallet", recipients, amounts },
{
onSuccess: () =>
queryClient.invalidateQueries({ queryKey: ["tokenops-sdk", "fhe-disperse"] }),
},
)
}
disabled={disperse.isPending}
>
{disperse.isPending ? "Dispersing…" : "Disperse tokens"}
</button>
</div>
);
}

Encrypted fee reserve (admin)

useGetEncryptedFeeReserve submits a transaction — it is a useMutation instance. The contract calls FHE.allow(handle, msg.sender) on-chain; the SDK extracts the handle from the receipt. Requires FEE_COLLECTOR_ROLE.

import { useGetEncryptedFeeReserve } from "@tokenops/sdk/fhe-disperse/react";

function FeeReserveDashboard({
singletonAddress,
token,
}: {
singletonAddress: `0x${string}`;
token: `0x${string}`;
}) {
const view = useGetEncryptedFeeReserve({ address: singletonAddress });

async function handleReveal() {
const { handle } = await view.mutateAsync({ token });
// Pass handle to zamaSDK.relayer.userDecrypt to obtain the plaintext reserve
console.log("encrypted fee reserve handle:", handle);
}

return (
<button onClick={handleReveal} disabled={view.isPending}>
{view.isPending ? "Fetching handle…" : "Reveal fee reserve"}
</button>
);
}

Do not call useGetEncryptedFeeReserve speculatively — ACL grants are append-only.


Address override

Every hook accepts { address?, chainId? }. When both are omitted, the hook resolves the singleton address via DEPLOYED_ADDRESSES using the connected chain. Pass address explicitly for forks or redeployments:

const register = useRegister({ address: "0xYourSingletonAddress" });

Encryptor source

The following mutation hooks require an encryptor:

useDisperse, useWithdrawTokenFee

const zamaSDK = useZamaSDK();

const disperse = useDisperse({
address: singletonAddress,
encryptor: () => zamaSDK.relayer,
});

Query keys

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

Invalidate everything after a write:

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