Concept · Encryptor

The encryptor source pattern

The SDK never imports @zama-fhe at the top level. You inject the encryptor instead — eager for Node and tests, lazy for React, Vue, and signals. Wrong wiring is the #1 onboarding bug; the pattern below sidesteps it.

Every SDK call that submits an encrypted input takes an encryptor option. The option accepts either:

  • An eager instance: Encryptor
  • A lazy factory: () => Encryptor | undefined

React: always use a lazy factory#

React renders the SDK client at mount, but the wallet context that drives the encryptor only fully resolves later. Eager passes capture a stale or undefined encryptor.

components/CreateButton.tsx
tsx
import { useZamaSDK } from "@zama-fhe/react-sdk";
import { useCreateVesting } from "@tokenops/sdk/fhe-vesting/react";

export function CreateButton({ managerAddress }) {
  const zamaSDK = useZamaSDK();
  const create = useCreateVesting({
    address: managerAddress,
    // Lazy factory — the SDK calls this AT submit time, not at mount time.
    encryptor: () => zamaSDK.relayer,
  });
  return <button onClick={() => create.mutate(args)}>Create</button>;
}

The arrow function defers resolution. When the user clicks the button, the SDK calls encryptor(), which now returns the live relayer from zamaSDK.relayer. Same pattern works for Vue (read a ref) and signals (read a subscription).

Node + tests: eager is correct#

Server-side code controls its own dependency graph. The encryptor is created at startup; the SDK client is created right after; both live the lifetime of the process. Lazy resolution has no value here.

server/create.ts
ts
import { createSepoliaEncryptor } from "@tokenops/sdk/fhe";
import { createConfidentialVestingManagerClient } from "@tokenops/sdk/fhe-vesting";

// Server-side: the encryptor's lifetime matches the SDK client's. Eager is fine.
const encryptor = await createSepoliaEncryptor({ privateKey, rpcUrl });
const manager = createConfidentialVestingManagerClient({
  publicClient,
  walletClient,
  address: managerAddress,
  encryptor,
});

Available encryptor flavours#

Three drop-in adapters ship in @tokenops/sdk/fhe:

createSepoliaEncryptorWeb — the browser flavour. Wraps Zama's RelayerWeb. Takes the user's wallet client to sign the per-encrypt authorization. The story flows on this site use it.

createSepoliaEncryptor — Node flavour. Takes a private key + RPC URL. Same interface, no wallet client.

createMockEncryptor — the test flavour. Bound to a deterministic local FHEVM (Anvil + forge-fhevm). Use under describeMockFheVesting and friends — never in production code.

See also