2.0 RC docsView 1.x docs
Upgrading · Zama SDK 3.0 to 3.6

Upgrading the Zama peers to 3.6

RelayerNode, RelayerWeb and the *Config presets are gone upstream. A ZamaSDK now plugs straight into the SDK.

v2.0.0-rc.1 release candidate
The API is frozen and later candidates carry fixes only, except the receipt-free and Safe create surface of /fhe-airdrop, which is @beta. Published on the next dist-tag; latest stays on 1.6.0 until 2.0.0.

Who is affected: anyone who installs the Zama peers, passes a raw relayer or a custom encryptor, calls useDecryptedHandle, or tunes createSepoliaEncryptor. Every product client signature is unchanged, and so are the SDK's own encryptUint64 / encryptUint64Batch outputs. If you only call product clients with an encryptor built by createSepoliaEncryptor, createSepoliaEncryptorWeb or createMockEncryptor, the peer bump and the runtime notes are all that apply.

Peer ranges#

Package1.x and every 2.0 alpha2.0 release candidate
@zama-fhe/sdk^3.0.0~3.6.0
@zama-fhe/react-sdk^3.0.0~3.6.0
pnpm add @zama-fhe/sdk@~3.6.0
# React hooks:
pnpm add @zama-fhe/react-sdk@~3.6.0

Only 3.0.x ever worked under ^3.0.0: 3.1 dropped the root RelayerWeb, SepoliaConfig and MainnetConfig exports the 1.x /fhe helpers import. No single range covers 3.0 and 3.6, so both peers move in one jump. The range is ~, not ^, because upstream removes APIs in minor releases. 3.6 also drops HardhatConfig, so a local-test setup built on it moves to createConfig too.

wagmi v2 and v3#

The wagmi peer stays ^2.0.0 || ^3.0.0 and the TokenOps hooks build on both. If you build your ZamaProvider config with createConfig from @zama-fhe/react-sdk/wagmi, note that 3.6.0's adapter reads getConnection and watchConnection from wagmi/actions, which only wagmi v3 exports. On wagmi v2 a webpack build (next build on Next.js 15, next build --webpack on 16) fails with Attempted import error; Turbopack builds it. Move to wagmi v3, or build the config with @zama-fhe/sdk/viem.

Encryptor#

The Encryptor contract, one definition re-exported by /fhe, /fhe-vesting, /fhe-airdrop and /fhe-disperse, now mirrors ZamaSDK.encrypt:

// Before (3.0): mirrored RelayerSDK.encrypt
encrypt(params: {
  values: FheValueInput[];
  contractAddress: Address;
  userAddress: Address;
}): Promise<{ handles: Uint8Array[]; inputProof: Uint8Array }>;
// After (3.6): mirrors ZamaSDK.encrypt
encrypt(params: {
  readonly values: readonly FheValueInput[];
  contractAddress: Address;
  userAddress: Address;
}): Promise<{ readonly encryptedValues: readonly Hex[]; inputProof: Hex }>;

A custom encryptor returns one hex bytes32 handle per input value, in input order, plus the hex input proof:

lib/encryptor.ts
ts
import { bytesToHex, type Address } from "viem";
import type { Encryptor } from "@tokenops/sdk/fhe";

// A backend that still produces bytes: convert to one bytes32 hex handle per
// input value, in input order, plus the hex input proof.
export const encryptor: Encryptor = {
  async encrypt({ values, contractAddress, userAddress }) {
    const { handles, inputProof } = await myBackend.encrypt(values, contractAddress, userAddress);
    return {
      encryptedValues: handles.map((h: Uint8Array) => bytesToHex(h)),
      inputProof: bytesToHex(inputProof),
    };
  },
};

FheValueInput#

FheValueInput narrows to upstream's EncryptInput, and every field is readonly:

TypeValue
euint8, euint16, euint32, euint64, euint128, euint256bigint
eboolboolean | 1n | 0n
eaddressAddress
euint160, ebytes64, ebytes128, ebytes256Removed

ebool no longer takes an arbitrary bigint; pass a boolean, 1n or 0n.

Passing a ZamaSDK#

sdk.relayer no longer has encrypt, so the old React hint fails to compile. Pass the ZamaSDK itself, eagerly or lazily; it satisfies Encryptor with no cast:

const zamaSDK = useZamaSDK();

// Before (3.0)
const create = useCreateVesting({ address, encryptor: () => zamaSDK.relayer });

// After (3.6)
const create = useCreateVesting({ address, encryptor: () => zamaSDK });

Node: RelayerNode to createConfig#

// Before (3.0)
import { RelayerNode, SepoliaConfig } from "@zama-fhe/sdk";

const relayer = new RelayerNode({
  transports: { [SepoliaConfig.chainId]: /* ... */ },
});

Replace a hand-built RelayerNode with the SDK helper, or build your own ZamaSDK:

server/encryptor.ts
ts
// After: the SDK helper
import { createSepoliaEncryptor } from "@tokenops/sdk/fhe";

const encryptor = await createSepoliaEncryptor({ rpcUrl: process.env.RPC_URL! });
// ... product clients take { encryptor } as before
encryptor.terminate();
server/zama.ts
ts
// After: build your own ZamaSDK
import { createConfig, ZamaSDK } from "@zama-fhe/sdk";
import { node, sepolia } from "@zama-fhe/sdk/node";
import { ViemProvider } from "@zama-fhe/sdk/viem";
import { createConfidentialVestingManagerClient } from "@tokenops/sdk/fhe-vesting";

const sdk = new ZamaSDK(
  createConfig({
    chains: [{ ...sepolia, network: rpcUrl }],
    provider: new ViemProvider({ publicClient }),
    relayers: { [sepolia.id]: node() },
  }),
);

// A ZamaSDK satisfies Encryptor with no cast.
const vesting = createConfidentialVestingManagerClient({ publicClient, walletClient, address, encryptor: sdk });

UserDecryptor and useDecryptedHandle#

UserDecryptor (/fhe/react) mirrors Decryption.decryptValues instead of userDecrypt(params). The ZamaSDKassembles the EIP-712 permit and keypair through its own signer, so the hook's relayerParams option is removed.

interface UserDecryptor {
  decryptValues(
    encryptedInput: { encryptedValue: Hex; contractAddress: Address }[],
    options?: { signal?: AbortSignal; timeout?: number },
  ): Promise<Record<Hex, boolean | number | bigint | string | undefined>>;
}
const sdk = useZamaSDK();

// Before (3.0)
const before = useDecryptedHandle({
  handle,
  contractAddress,
  userDecryptor: () => sdk.relayer,
  relayerParams: { ...keypair, requestValidity, contractsChainId },
});

// After (3.6)
const { status, value } = useDecryptedHandle({
  handle,
  contractAddress,
  userDecryptor: () => sdk.decryption,
  account: address, // useConnection().address on wagmi v3, useAccount().address on v2
});
  • Upstream decrypts euint8 to euint32 to a number. The hook still resolves a bigint and reports any other value type, or a missing entry, as TokenOpsValidationError.
  • Decryptor failures go through the SDK's Zama error mapping: NOT_ENTITLED surfaces as UserDecryptNotAllowedError, re-exported from /fhe/react with DecryptionFailedError.
  • Pass account. A ZamaSDK keeps the same decryptionobject across wallet switches, so without it the hook keeps showing the previous account's value while handle and contractAddress stay the same.
  • Pass a stable decryptor: sdk.decryption or () => sdk.decryption. An object literal built inline is a new decryptor every render, and the hook then fails closed with a TokenOpsValidationError until it keeps its identity.
  • The hook aborts its in-flight request when its inputs change. Outside React, call sdk.decryption.decryptValues(...) directly; a ZamaSDK used for decryption needs a signer.

createSepoliaEncryptor and createSepoliaEncryptorWeb#

The names, the return shape (an Encryptor plus instance, chainId and terminate) and onPhase are unchanged. The browser helper moved to its own subpath:

// Before: the browser helper lived in the /fhe subpath.
// After:
import { createSepoliaEncryptorWeb } from "@tokenops/sdk/fhe/web";

That move is what lets a browser app bundle @tokenops/sdk/fhe without the optional @zama-fhe/sdk peer; see Browser encryptor. The Node helper drops its pool and artifact-cache options:

// Before (1.x)
const encryptor = await createSepoliaEncryptor({
  poolSize: 4,
  fheArtifactStorage: myStorage,
  fheArtifactCacheTTL: 86_400,
  relayerUrl: "https://relayer.testnet.zama.org/v2",
});

// After (2.0): relayerUrl defaults to https://relayer.testnet.zama.org (no /v2)
const encryptor = await createSepoliaEncryptor({
  rpcUrl: process.env.RPC_URL!,
});
ChangeDetail
instance is a ZamaSDKStill typed unknown; cast to import("@zama-fhe/sdk").ZamaSDK. The Node helper builds it without a signer, so user decryption through it throws.
New rpcUrl (Node)Sets the chain's network and the read provider. The preset's default public RPC is shared and rate-limited, so pass your own.
Removed poolSize, fheArtifactStorage, fheArtifactCacheTTLnode() has no worker pool or artifact cache. SepoliaEncryptorStorage is removed too.
chainId is validatedAnything other than 1 or 11155111 throws InvalidArgumentError instead of silently using the Sepolia config.
Web helper uses its clientscreateSepoliaEncryptorWeb builds the ZamaSDK from the /viem createConfig with publicClient and walletClient; the wallet client is the signer.
loggerForwarded to createConfig; any subset of debug / info / warn / error works.
New auth (both), relayer API keySet as the chain's relayer auth (RelayerAuth). The Zama-hosted mainnet relayer requires an API key; pass it from server code only.
New offload* (web), bundlingoffloadEncrypt, offloadWorker and offloadTimeouts forward to web(), for bundlers that do not emit the encrypt worker or CSPs that restrict worker sources.

Runtime behaviour#

  • Relayer URL. The Sepolia preset's relayerUrl is https://relayer.testnet.zama.org, with no /v2 suffix. Drop the suffix if you pass it explicitly.
  • node() encrypts on the calling thread. 3.0's RelayerNode ran encryption in a worker pool sized by poolSize; 3.6 has no pool, so each encrypt blocks the event loop while it runs. A server that encrypts per request should scale out across processes or move encryption to a queue of its own.
  • A failed init does not recover. If the first encrypt fails to load the WASM or fetch the key material, the ZamaSDK keeps the failure: call terminate() and build a new encryptor.
  • runtime.numberOfThreads is process-global. createSepoliaEncryptorWeb({ threads }) maps to it. It is set once per page; setting a different value later throws.

Local testing with createMockEncryptor#

@fhevm/mock-utils@0.4.2 imports @zama-fhe/relayer-sdk at runtime, and @zama-fhe/sdk 3.6 no longer installs it. Pin it yourself next to the mock; without it, createMockEncryptor throws MissingPeerDependencyError naming the package.

pnpm add -D @fhevm/mock-utils@^0.4.2 @zama-fhe/relayer-sdk@0.4.1 @zama-fhe/sdk@~3.6.0 ethers
Product clients are unchanged
Every /fhe-vesting, /fhe-airdrop and /fhe-disperse client keeps its signature. Once the encryptor and decryptor are on the 3.6 shape, the product code around them compiles as before.

Related guides