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