SDK 2.1: platform toolkit + vesting engine
1.x: critical fixes onlyView 2.x docs
Recipe · Integration · Next 14+

Next.js App Router integration

The whole thing fits in 30 lines: provider tree at the root, lazy-factory encryptor at each call site.

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
tsx
// 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
tsx
// 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>
  );
}
Interactive · createVesting on Sepolia
Loading editor…
Edit any input above, then press ⌘↵ to run.
Console0 lines
No output yet. Run the snippet to see logs stream in.

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.

app/providers.tsx
tsx
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
js
// 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 > 1 but 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.

See also