1. Provider tree#
WagmiProvider → QueryClientProvider → ZamaProvider. Mount these in a "use client" file at the root of your app's layout. Order matters: wagmi's hooks need QueryClient; Zama's relayer needs wagmi's wallet state.
// app/providers.tsx
"use client";
import { WagmiProvider } from "wagmi";
import { sepolia } from "wagmi/chains";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ZamaProvider, RelayerWeb, indexedDBStorage } from "@zama-fhe/react-sdk";
import { WagmiSigner } from "@zama-fhe/react-sdk/wagmi";
import { wagmiConfig } from "@/lib/wagmi";
const queryClient = new QueryClient();
// Construct the signer + relayer at module level — safe here because this
// file is "use client". Never build them in a server component.
const signer = new WagmiSigner({ config: wagmiConfig });
const relayer = new RelayerWeb({
getChainId: () => signer.getChainId(),
transports: {
[sepolia.id]: {
relayerUrl: "https://your-app.com/api/relayer/11155111",
network: "https://sepolia.infura.io/v3/YOUR_KEY",
},
},
});
export function Providers({ children }: { children: React.ReactNode }) {
return (
<WagmiProvider config={wagmiConfig}>
<QueryClientProvider client={queryClient}>
<ZamaProvider relayer={relayer} signer={signer} storage={indexedDBStorage}>
{children}
</ZamaProvider>
</QueryClientProvider>
</WagmiProvider>
);
}In app/layout.tsx, wrap {children} in <Providers>.
2. Use SDK hooks in client components#
Hooks live at @tokenops/sdk/<product>/react. Every hook is a TanStack Query primitive, mutations are useMutation, reads are useQuery. Use them like any other wagmi or viem hook.
// components/CreateVestingButton.tsx
"use client";
import { useZamaSDK } from "@zama-fhe/react-sdk";
import { useCreateVesting } from "@tokenops/sdk/fhe-vesting/react";
import type { Address } from "viem";
export function CreateVestingButton({
managerAddress,
recipient,
}: {
managerAddress: Address;
recipient: Address;
}) {
const zamaSDK = useZamaSDK();
const create = useCreateVesting({
address: managerAddress,
// Lazy factory: resolves the encryptor at submit time, not at mount.
encryptor: () => zamaSDK.relayer,
});
return (
<button
disabled={create.isPending}
onClick={() =>
create.mutate({
params: {
recipient,
startTimestamp: Math.floor(Date.now() / 1000),
endTimestamp: Math.floor(Date.now() / 1000) + 365 * 24 * 3600,
cliffSeconds: 0,
releaseIntervalSecs: 86_400,
timelockSeconds: 0,
initialUnlockBps: 0,
cliffAmountBps: 0,
isRevocable: false,
},
amount: 100_000n,
})
}
>
{create.isPending ? "Submitting…" : "Create vesting"}
</button>
);
}What server components can do#
Plenty, just not the encrypted writes. The headless client classes work fine on the server for reads: balance lookups, deployment metadata, vesting schedule shapes, address registry access. Anything that doesn't submit a tx is fair game in a server component or server action.
Optional: 2–3× faster encryption#
Want 2–3× faster encryption? Set threads: 4–8 on Zama's RelayerWeb — this requires cross-origin isolation, because multi-threaded WASM runs on SharedArrayBuffer, which browsers only enable on cross-origin-isolated pages.
const relayer = new RelayerWeb({
getChainId: () => signer.getChainId(),
// Opt-in: 4-8 threads is the sweet spot; needs the COOP/COEP
// headers below. Omit for the default single-threaded worker.
threads: 8,
transports: {
/* unchanged */
},
});// next.config.mjs — cross-origin isolation for threads > 1
/** @type {import('next').NextConfig} */
const nextConfig = {
async headers() {
return [
{
source: "/:path*",
headers: [
{ key: "Cross-Origin-Opener-Policy", value: "same-origin" },
{ key: "Cross-Origin-Embedder-Policy", value: "require-corp" },
],
},
];
},
};
export default nextConfig;Two things to know before flipping the switch. Cross-origin isolation is page-wide: Cross-Origin-Embedder-Policy: require-corp blocks cross-origin iframes, scripts, and images that do not send CORP or CORS headers, so audit your embeds first. And the setting fails soft: with threads > 1but no isolation, Zama's SDK logs a warning and encryption keeps working single-threaded — nothing breaks, it is just not faster. React Native is a separate story (no browser worker at all): see the React Native recipe.