sdk/js-sdk/notes/rfc/RFC001.md
Status: Implemented Authors: @Aurora Makovac @Alex B. Created: February 17, 2026 Updated: March 30, 2026 — updated to reflect current naming conventions and API surface
Target Repository: zama-ai/fhevm
Related Components: Hardhat plugin, Rust SDK
This RFC describes the architectural restructuring of the relayer-sdk into @fhevm/sdk — a layered, composable, library-agnostic SDK for building on FHEVM chains.
The restructuring has been implemented as a single package (@fhevm/sdk) with multiple entry points, a protocol-agnostic core, and thin adapters for ethers.js v6 and viem. The architecture follows a viem-inspired composable design with symbol-based security, lazy WASM loading, and strict module boundaries.
src/core/ contains all protocol logic with zero ethers/viem dependenciessrc/ethers/ and src/viem/ are ~200 LOC each, bridging native types to core abstractionsTrustedClient sealing, frozen objects, private #fields@fhevm/sdk/actions/* organize standalone functions by capability requirementThe core (src/core/):
ethers or viemTrustedClient, EthereumModule, FhevmRuntimeTrustedClient wrappersAdapters (src/ethers/, src/viem/) are thin wrappers that:
EthereumModule using their respective library (ABI encoding, contract reads, signature recovery)TrustedClientParameters type, ReturnType type)setFhevmRuntimeConfig().extend() decorator chaining*.test.ts alongside source)@fhevm/sdk/viem does NOT pull in ethers (and vice versa)"sideEffects": false@fhevm/sdk/actions/*ethers or viem (peer, not bundled)TrustedClient uses opaque sealing — core layer never sees native ethers/viem objectsPRIVATE_TOKEN prevents unauthorized CoreFhevmImpl instantiationE2eTransportKeypair hides TKMS private key behind #privateFields — never exposed to application code#field syntax for all internal stateObject.freeze() on chain definitions, runtime instances, client internalsRather than the originally proposed multi-package monorepo (fhevm-types, fhevm-core, fhevm-wasm-sdk, etc.), the SDK ships as a single package with multiple entry points. This decision was driven by:
@fhevm/sdk ← package name
├── @fhevm/sdk/ethers ← Ethers.js v6 adapter
├── @fhevm/sdk/viem ← Viem adapter
├── @fhevm/sdk/chains ← Chain definitions (framework-agnostic)
├── @fhevm/sdk/actions/base ← Base actions (publicDecrypt, ACL checks, input proofs)
├── @fhevm/sdk/actions/encrypt ← Encrypt actions (encrypt, generateZkProof)
├── @fhevm/sdk/actions/decrypt ← Decrypt actions (decrypt, generateE2eTransportKeypair)
├── @fhevm/sdk/actions/chain ← Chain utility actions (signDecryptionPermit, EIP-712, keypair ops)
├── @fhevm/sdk/actions/host ← Host contract read actions (resolveFhevmConfig, contract data)
Entry points are defined via subdirectory package.json files.
src/
├── core/ ← Protocol-agnostic business logic
│ ├── actions/ ← All action functions (organized by tier)
│ │ ├── base/ ← publicDecrypt, fetchVerifiedInputProof, ACL checks, signers
│ │ ├── chain/ ← signDecryptionPermit, createKmsUserDecryptEIP712, keypair ops
│ │ ├── decrypt/ ← decrypt, generateE2eTransportKeypair, decryptKmsSignedcryptedShares
│ │ ├── encrypt/ ← encrypt, generateZkProof
│ │ └── host/ ← Contract reads (ACL, InputVerifier, KMSVerifier, FhevmExecutor)
│ ├── base/ ← Primitives (address, bytes, object, errors, trustedValue)
│ ├── chains/
│ │ ├── definitions/ ← mainnet.ts, sepolia.ts
│ │ ├── utils.ts ← defineFhevmChain()
│ │ └── index.ts ← @fhevm/sdk/chains entry point
│ ├── clients/
│ │ └── decorators/ ← baseActions, encryptActions, decryptActions
│ ├── modules/
│ │ ├── encrypt/ ← EncryptModule (TFHE WASM bindings)
│ │ ├── decrypt/ ← DecryptModule (TKMS WASM bindings)
│ │ ├── ethereum/ ← EthereumModule interface + TrustedClient
│ │ └── relayer/ ← RelayerModule (HTTP client for relayer API)
│ ├── runtime/ ← CoreFhevm-p.ts (client), CoreFhevmRuntime-p.ts (runtime)
│ ├── types/ ← All shared types
│ ├── coprocessor/ ← Coprocessor logic
│ ├── handle/ ← EncryptedValue / handle parsing logic
│ ├── host-contracts/ ← ABI definitions
│ ├── kms/ ← E2eTransportKeypair, SignedDecryptionPermit, KMS EIP-712
│ └── errors/ ← Structured error types
│
├── ethers/ ← Ethers.js v6 adapter
│ ├── clients/ ← createFhevmClient, createFhevmEncryptClient, createFhevmDecryptClient, createFhevmBaseClient
│ ├── internal/ ← ethers-p.ts (runtime config, TrustedClient sealing), ethereum.ts (EthereumModule impl)
│ └── index.ts ← @fhevm/sdk/ethers entry point
│
├── viem/ ← Viem adapter
│ ├── clients/ ← (same pattern as ethers)
│ ├── internal/
│ └── index.ts ← @fhevm/sdk/viem entry point
│
└── wasm/ ← WASM bindings
├── tfhe/ ← TFHE WASM (~5MB, encryption)
├── tkms/ ← TKMS WASM (~600KB, decryption)
├── wasmBaseUrl.ts ← ESM: import.meta.url
└── wasmBaseUrl.cts ← CJS: __filename
| RFC001 Proposed Package | Implemented As |
|---|---|
fhevm-types | src/core/types/ — types are co-located, not a separate package |
fhevm-core | src/core/ (actions, base, chains, clients, handle, host-contracts, kms) |
fhevm-wasm-sdk | src/core/modules/encrypt/ + src/core/modules/decrypt/ + src/wasm/ |
core-relayer-sdk | src/core/modules/relayer/ + src/core/actions/ |
fhevm-ethers-wrapper | src/ethers/ (~200 LOC adapter) |
fhevm-viem-wrapper | src/viem/ (~200 LOC adapter) |
The single-package approach achieves the same separation of concerns through directory boundaries and TypeScript's module system, while avoiding the coordination overhead of 6+ packages.
┌─────────────────────────────────────────────────────┐
│ @fhevm/sdk/ethers @fhevm/sdk/viem │ Adapters
│ (setFhevmRuntimeConfig, (setFhevmRuntimeConfig, │ (thin wrappers)
│ createFhevmClient, ...) createFhevmClient, ...)│
├─────────────────────────────────────────────────────┤
│ src/core/clients/decorators/ │ Decorators
│ (baseActions, encryptActions, decryptActions) │ (curry actions → methods)
├─────────────────────────────────────────────────────┤
│ src/core/actions/ │ Actions
│ (encrypt, decrypt, publicDecrypt, │ (standalone functions)
│ signDecryptionPermit, fetchFheEncryptionKeyBytes) │
├─────────────────────────────────────────────────────┤
│ src/core/runtime/ │ Runtime
│ (CoreFhevm, FhevmRuntime, extend()) │ (client + module composition)
├─────────────────────────────────────────────────────┤
│ src/core/modules/ │ Modules
│ ├── encrypt/ (TFHE WASM) │ (WASM, HTTP, ABI)
│ ├── decrypt/ (TKMS WASM) │
│ ├── relayer/ (HTTP client) │
│ └── ethereum/ (TrustedClient, EthereumModule) │
├─────────────────────────────────────────────────────┤
│ src/core/base/ │ Base
│ (address, bytes, trustedValue, errors) │ (zero-dep primitives)
└─────────────────────────────────────────────────────┘
Dependency direction is strictly top-down. Core never imports from adapters. Actions never import from decorators. Modules never import from actions.
Clients are built by composing a base CoreFhevm with decorator actions via .extend():
// Inside createFhevmClient() — both ethers and viem adapters
const c = createCoreFhevm(PRIVATE_TOKEN, {
chain,
runtime: getAdapterRuntime(), // includes EthereumModule + RelayerModule
client: provider, // sealed as TrustedClient
});
return c
.extend(baseActions) // adds: publicDecrypt, signDecryptionPermit, parseE2eTransportKeypair, ...
.extend(decryptActions) // adds: decrypt, generateE2eTransportKeypair, createUserDecryptEIP712, ...
.extend(encryptActions); // adds: encrypt
Four pre-composed client variants:
| Factory | Decorators Applied | WASM Cost |
|---|---|---|
createFhevmClient() | base + decrypt + encrypt | ~5.6MB + 50MB key |
createFhevmEncryptClient() | base + encrypt | ~5MB + 50MB key |
createFhevmDecryptClient() | base + decrypt | ~600KB |
createFhevmBaseClient() | base | None |
The runtime itself is extended with modules:
EncryptModule — TFHE WASM operations (lazy-loaded on first encrypt())DecryptModule — TKMS WASM operations (lazy-loaded on first decrypt())RelayerModule — HTTP client for relayer API (always available)EthereumModule — ABI encoding, contract reads, signature recovery (adapter-specific)The original RFC presented two options: "Direct Wrapper" vs. "Explicit Layer Injection."
The implemented approach is Direct Wrapper — each adapter provides factory functions that handle all internal wiring:
// With viem
import { setFhevmRuntimeConfig, createFhevmClient } from "@fhevm/sdk/viem";
import { sepolia } from "@fhevm/sdk/chains";
setFhevmRuntimeConfig({});
const provider = createPublicClient({ chain: viemSepolia, transport: http() });
const client = createFhevmClient({ chain: sepolia, provider });
// With ethers
import { setFhevmRuntimeConfig, createFhevmClient } from "@fhevm/sdk/ethers";
import { sepolia } from "@fhevm/sdk/chains";
setFhevmRuntimeConfig({});
const provider = new ethers.BrowserProvider(window.ethereum);
const client = createFhevmClient({ chain: sepolia, provider });
Developers never need to manually inject modules, create runtimes, or compose layers. The factory handles everything. The .extend() mechanism remains available internally and for advanced use cases.
The SDK ships both CommonJS and ESM from a single TypeScript source:
src/_esm/ ← ESM output (module: esnext, "sideEffects": false)
src/_cjs/ ← CJS output (module: commonjs)
src/_types/ ← Declaration files (.d.ts + .d.ts.map)
WASM base URL resolution is the only CJS/ESM divergence:
wasmBaseUrl.ts): new URL("./file", import.meta.url)wasmBaseUrl.cts): require("node:url").pathToFileURL(__filename)package.json "imports" field: #wasm/baseUrl| Mechanism | Purpose |
|---|---|
TrustedClient (opaque sealed value) | Prevents native ethers/viem client from leaking into core |
PRIVATE_TOKEN (symbol) | Only adapter code can create CoreFhevm instances |
PRIVATE_VIEM_TOKEN / PRIVATE_ETHERS_TOKEN | Only the originating adapter can unseal its TrustedClient |
E2eTransportKeypair (#privateFields) | TKMS private key never exposed as raw bytes to user code |
#privateFields | ES2015 hard-private fields on all internal classes |
Object.freeze() + Object.defineProperty(..., { writable: false }) | Immutable chain definitions, frozen runtime, locked client properties |
defineFhevmChain() → simpleDeepFreeze() | Chain objects are recursively frozen at definition time |
@zama-fhe/relayer-sdk| Area | Old SDK | New SDK |
|---|---|---|
| Initialization | initSDK() + createInstance(config) | setFhevmRuntimeConfig() + createFhevmClient({ chain, provider }) |
| Encryption | Builder: createEncryptedInput().add32(42).encrypt() | Declarative: encrypt({ values: [...], contractAddress, userAddress }) |
| Encrypt result | { handles, inputProof } | { externalEncryptedValues, inputProof } |
| Key management | generateKeypair() → raw { publicKey, privateKey } | generateE2eTransportKeypair() → opaque E2eTransportKeypair |
| Permit + signing | createEIP712() + wallet sign + manual bundling | signDecryptionPermit({ signer, e2eTransportKeypair, ... }) — all-in-one |
| Decrypt | 9 positional params | Object: decrypt({ e2eTransportKeypair, encryptedValues, signedPermit }) |
| Decrypt result | Record<handle, value> | ClearValue[] with typed .value, .fheType, .encryptedValue |
| Public decrypt | publicDecrypt(handles) | publicDecrypt({ encryptedValues }) → PublicDecryptionProof |
| Type names | FhevmHandle, DecryptedFhevmHandle | EncryptedValue, ClearValue |
| Chain config | Flat SepoliaConfig object | FhevmChain with nested fhevm.contracts, fhevm.gateway |
| Entry points | @zama-fhe/relayer-sdk/web vs /node | @fhevm/sdk/ethers, @fhevm/sdk/viem, @fhevm/sdk/chains, @fhevm/sdk/actions/* |
Breaking changes were accepted as necessary for the architectural goals. The new API is object-based throughout (no positional param ambiguity), type-safe (wrong client variant = compile error), and secure by default.
The Hardhat plugin remains a separate package but is designed to consume @fhevm/sdk directly. The SDK's modular architecture supports this:
@fhevm/sdk/chains) can define hardhatLocal for local testingcreateFhevmClient() API works for both production and test environmentsThe core is chain-agnostic by design:
FhevmChain type is a plain data object (no EVM-specific methods)EthereumModule is an adapter interface — a SolanaModule would implement the same contract-read / signature-recovery interfaceTrustedClient sealing works with any native client typeA Solana adapter would follow the same pattern as src/viem/ or src/ethers/: implement EthereumModule (or a renamed ChainModule) and provide createFhevmClient() that seals the Solana client.
MockEncryptModule and MockDecryptModule that skip WASM entirely for local dev/testing.@fhevm/sdk instead of @zama-fhe/relayer-sdk.fhevm monorepo is still planned but not yet executed.@fhevm/sdk with subpath exports. The originally debated names (relayer-sdk, protocol-sdk, js-sdk) were not used.The restructuring achieved the original goals:
src/core/The result is a low-level, composable SDK that higher-level libraries (Hardhat plugin, React hooks, wallet SDKs) can safely build upon.