sdk/js-sdk/docs/migration.md
@zama-fhe/relayer-sdkThis guide helps you migrate from the old SDK (@zama-fhe/relayer-sdk v0.4.x) to the new SDK (@fhevm/sdk). The new SDK is a complete rewrite with a different API, but the concepts are the same.
| What changed | Old SDK (@zama-fhe/relayer-sdk) | New SDK (@fhevm/sdk) |
|---|---|---|
| Package name | @zama-fhe/relayer-sdk | @fhevm/sdk |
| Entry points | /web, /node, /bundle | /ethers, /viem, /chains |
| Initialization | initSDK() + createInstance(config) | setFhevmRuntimeConfig() + createFhevmClient({ chain, provider }) |
| Configuration | Flat config object (SepoliaConfig) | Chain definitions (sepolia from @fhevm/sdk/chains) |
| Encryption | Builder pattern: .add32(42).encrypt() | Declarative: encrypt({ values: [{ type: "uint32", value: 42 }] }) |
| Key generation | generateKeypair() → raw { publicKey, privateKey } | generateE2eTransportKeypair() → opaque E2eTransportKeypair |
| Permit creation + signing | Separate: createEIP712() + wallet sign + manual bundling | Combined: signDecryptionPermit({ signer, e2eTransportKeypair, ... }) |
| Decrypt | 9 positional args | Object: decrypt({ e2eTransportKeypair, encryptedValues, signedPermit }) |
| Read public values | publicDecrypt(handles) → { clearValues } | publicDecrypt({ encryptedValues }) → PublicDecryptionProof with .orderedClearValues |
| Provider | Passed in config as network | Passed directly as provider |
| Framework support | Framework-agnostic (single entry) | Explicit adapters (/ethers, /viem) |
Before:
import { initSDK, createInstance, SepoliaConfig } from "@zama-fhe/relayer-sdk/web";
After:
import { setFhevmRuntimeConfig, createFhevmClient } from "@fhevm/sdk/ethers"; // or "@fhevm/sdk/viem"
import { sepolia } from "@fhevm/sdk/chains";
The new SDK has separate imports for ethers.js and viem. Choose the one matching your project. The APIs are identical — only the provider type differs.
Before:
await initSDK();
const instance = await createInstance({
...SepoliaConfig,
network: provider, // or a URL string
});
After:
setFhevmRuntimeConfig({ numberOfThreads: 4 }); // optional config
const client = createFhevmClient({
chain: sepolia,
provider, // ethers Provider or viem PublicClient
});
Key differences:
setFhevmRuntimeConfig() replaces initSDK(). It's synchronous and configures WASM threading/logging.createFhevmClient() replaces createInstance(). It's also synchronous — no await needed. WASM loads lazily on first use.sepolia, mainnet) instead of a flat config. No more chainId, gatewayChainId, aclContractAddress, etc. — the chain object has all of it.This is the biggest API change. The old builder pattern is replaced by a declarative object.
Before:
const input = instance.createEncryptedInput(contractAddress, userAddress);
input.add32(42);
input.add8(100);
input.addBool(true);
const { handles, inputProof } = await input.encrypt();
After:
const encrypted = await client.encrypt({
contractAddress,
userAddress,
values: [
{ type: "uint32", value: 42 },
{ type: "uint8", value: 100 },
{ type: "bool", value: true },
],
});
const handles = encrypted.externalEncryptedValues;
const inputProof = encrypted.inputProof;
Key differences:
encrypt() call. If you want to control when the ~50MB download happens (for example, behind a loading spinner), call await client.init() at app startup.{ type, value } objects instead of chained .add*() calls."uint32", "bool", "address" (not add32, addBool, addAddress).externalEncryptedValues (array of ExternalEncryptedValue) and inputProof (hex string).Before:
const { publicKey, privateKey } = instance.generateKeypair();
// publicKey and privateKey are hex strings
After:
const e2eTransportKeypair = await client.generateE2eTransportKeypair();
// privateKey is hidden inside the key pair — never exposed
The new SDK wraps the private key in an opaque E2eTransportKeypair object. You can't access the raw private key directly — this prevents accidental exposure. The key pair is what you pass to signDecryptionPermit() and decrypt().
The biggest workflow change: permit creation and signing are now a single step.
Before:
// 1. Create EIP-712 data
const eip712 = instance.createEIP712(
publicKey, // string
contractAddresses, // string[]
startTimestamp, // number
durationDays, // number
);
// 2. Sign with wallet
const signature = await signer.signTypedData(eip712.domain, eip712.types, eip712.message);
// 3. Bundle manually
const signedPermit = createSignedPermit(eip712, signature, userAddress);
After:
// All in one step — SDK creates EIP-712, signs it, and bundles the result
const signedPermit = await client.signDecryptionPermit({
contractAddresses: ["0xContractA..."],
startTimestamp: Math.floor(Date.now() / 1000),
durationDays: 7,
signerAddress: await signer.getAddress(),
signer,
e2eTransportKeypair,
});
Key differences:
signDecryptionPermit() handles everything.signTypedData internally.Before:
const results = await instance.userDecrypt(
handleContractPairs, // HandleContractPair[]
privateKey, // string
publicKey, // string
signature, // string
contractAddresses, // string[]
userAddress, // string
startTimestamp, // number
durationDays, // number
);
// results is a Record<handle, value>
After:
const results = await client.decrypt({
e2eTransportKeypair,
encryptedValues: [
{ encryptedValue: "0x...", contractAddress: "0xContractA..." },
],
signedPermit,
});
// results is ClearValue[] with typed values
Key differences:
e2eTransportKeypair object instead of separate privateKey + publicKey strings.signedPermit from signDecryptionPermit() instead of raw signature + permit params.ClearValue[]) instead of a plain Record. Each entry has .value (typed correctly as number, bigint, boolean, or string), .fheType, and .encryptedValue.Before:
const { clearValues, decryptionProof } = await instance.publicDecrypt(handles);
// clearValues is Record<handle, bigint | boolean | string>
After:
const result = await client.publicDecrypt({
encryptedValues: [handle1, handle2],
});
const values = result.orderedClearValues;
// values[0].value — typed correctly
// values[0].fheType — "euint32", "ebool", etc.
Key differences:
{ encryptedValues: [...] } parameter object.ClearValue[] via result.orderedClearValues instead of a handle-keyed record.Before:
const eip712 = instance.createDelegatedUserDecryptEIP712(
publicKey, contractAddresses, delegatorAddress, startTimestamp, durationDays,
);
const results = await instance.delegatedUserDecrypt(
handleContractPairs, privateKey, publicKey, signature,
contractAddresses, delegatorAddress, delegateAddress,
startTimestamp, durationDays,
);
After:
const signedPermit = await client.signDecryptionPermit({
contractAddresses: ["0xContract..."],
startTimestamp: Math.floor(Date.now() / 1000),
durationDays: 1,
signerAddress: await signer.getAddress(),
signer,
e2eTransportKeypair,
onBehalfOf: "0xDataOwnerAddress...",
});
Same flow as regular decryption — onBehalfOf replaces the separate function.
These old SDK APIs have no direct equivalent in the new SDK:
| Old API | What to do instead |
|---|---|
instance.getPublicKey() | Use client.fetchFheEncryptionKeyBytes() |
instance.getPublicParams(bits) | Use client.fetchFheEncryptionKeyBytes() |
instance.config | Access client.chain for chain info |
instance.requestZKProofVerification() | Built into client.encrypt() automatically |
initSDK({ tfheParams, kmsParams }) | Use setFhevmRuntimeConfig({ locateFile }) for custom WASM paths |
createSignedPermit() | Built into client.signDecryptionPermit() |
Before (old SDK):
import { initSDK, createInstance, SepoliaConfig } from "@zama-fhe/relayer-sdk/web";
await initSDK();
const instance = await createInstance({ ...SepoliaConfig, network: provider });
// Encrypt
const input = instance.createEncryptedInput(contractAddr, userAddr);
input.add32(42);
input.add8(100);
const { handles, inputProof } = await input.encrypt();
// User decrypt
const { publicKey, privateKey } = instance.generateKeypair();
const eip712 = instance.createEIP712(publicKey, [contractAddr], startTs, 7);
const sig = await signer.signTypedData(eip712.domain, eip712.types, eip712.message);
const results = await instance.userDecrypt(
[{ handle, contractAddress: contractAddr }],
privateKey, publicKey, sig,
[contractAddr], userAddr, startTs, 7,
);
After (new SDK):
import { setFhevmRuntimeConfig, createFhevmClient } from "@fhevm/sdk/ethers";
import { sepolia } from "@fhevm/sdk/chains";
setFhevmRuntimeConfig({});
const client = createFhevmClient({ chain: sepolia, provider });
// Encrypt
const encrypted = await client.encrypt({
contractAddress: contractAddr,
userAddress: userAddr,
values: [
{ type: "uint32", value: 42 },
{ type: "uint8", value: 100 },
],
});
// Decrypt
const e2eTransportKeypair = await client.generateE2eTransportKeypair();
const signedPermit = await client.signDecryptionPermit({
contractAddresses: [contractAddr],
startTimestamp: startTs,
durationDays: 7,
signerAddress: userAddr,
signer,
e2eTransportKeypair,
});
const results = await client.decrypt({
e2eTransportKeypair,
encryptedValues: [{ encryptedValue: handle, contractAddress: contractAddr }],
signedPermit,
});