sdk/js-sdk/docs/getting-started.md
This guide takes you from an empty project to a working end-to-end flow: encrypt a value, send it to a confidential contract, and decrypt a result — all without plaintext ever touching the chain.
By the end you will have run, in order:
@fhevm/sdk ships an adapter for each and treats both as
optional peer dependencies — install the one you already use.FHECounter
example already deployed on Sepolia.npm install @fhevm/sdk
# plus your Ethereum library of choice:
npm install ethers # or: npm install viem
Every import in this guide comes from one of two adapter entry points —
@fhevm/sdk/ethers or @fhevm/sdk/viem. Pick one and use it consistently. The
method names and shapes are identical across both.
Call setFhevmRuntimeConfig once, at startup, before you create any client. An
empty object accepts every default, which is the right starting point.
import { setFhevmRuntimeConfig } from '@fhevm/sdk/ethers';
setFhevmRuntimeConfig({});
The runtime controls how the WebAssembly cryptography modules are loaded — thread count, asset location, and version pinning. See Runtime configuration for every option. Calling it twice with the same config is safe; calling it with a different config throws, so keep it in one place.
A client bundles a chain definition with a read connection to the network. The
default createFhevmClient factory can both encrypt and decrypt.
{% tabs %} {% tab title="ethers.js" %}
import { createFhevmClient } from '@fhevm/sdk/ethers';
import { sepolia } from '@fhevm/sdk/chains';
import { ethers } from 'ethers';
const provider = new ethers.JsonRpcProvider('https://ethereum-sepolia-rpc.publicnode.com');
const client = createFhevmClient({ chain: sepolia, provider });
{% endtab %} {% tab title="viem" %}
import { createFhevmClient } from '@fhevm/sdk/viem';
import { sepolia } from '@fhevm/sdk/chains';
import { createPublicClient, http } from 'viem';
import { sepolia as viemSepolia } from 'viem/chains';
const publicClient = createPublicClient({
chain: viemSepolia,
transport: http('https://ethereum-sepolia-rpc.publicnode.com'),
});
const client = createFhevmClient({ chain: sepolia, publicClient });
{% endtab %} {% endtabs %}
Constructing a client is synchronous and does no network or WASM work. Before you encrypt or decrypt, resolve the protocol versions and load the WASM once:
await client.ready; // alias for `await client.init()`
ready is idempotent — awaiting it again returns the same cached promise. If you
only need to encrypt or only need to decrypt, use a lighter factory to download
less WASM — see Clients.
encryptValues takes a batch of plaintext values, encrypts them client-side, and
returns opaque encrypted values plus a single inputProof that proves to the
contract they were encrypted correctly.
const userAddress = '0xYourWalletAddress';
const contractAddress = '0xef6c6230bF565015f8B37f2966d200C8804b409a';
const encrypted = await client.encryptValues({
contractAddress,
userAddress,
values: [
{ type: 'uint32', value: 42 },
{ type: 'bool', value: true },
],
});
encrypted.encryptedValues; // [externalEuint32, externalEbool] — one per input
encrypted.inputProof; // one shared proof for the whole batch
Two rules make encryption work:
type field uses Solidity names ('uint32', 'bool', 'address'),
not Fully Homomorphic Encryption (FHE) names. Values can be number,
bigint, boolean, or an address string depending on the type.contractAddress and userAddress. The resulting
proof is only valid when that exact user submits it to that exact contract.{% hint style="warning" %}
The input proof is bound to both contractAddress and userAddress. If either differs when you submit the transaction, on-chain verification fails. Encrypt with the exact contract and sender you will transact with.
{% endhint %}
Pass the results straight to your contract call — each encrypted value maps to an
externalEuintXX / externalEbool argument, and the proof is the trailing
bytes argument:
await contract.increment(
encrypted.encryptedValues[0], // externalEuint32
encrypted.inputProof, // bytes
);
See Encryption for the full type table and single-value
encryptValue.
Reading an encrypted value back requires proving you are allowed to see it. That takes two pieces:
// A signer that can produce EIP-712 signatures (ethers Wallet / Signer,
// or a viem Account / WalletClient).
const signerAddress = '0xYourWalletAddress';
// 1. Generate the transport key pair.
const transportKeyPair = await client.generateTransportKeyPair();
// 2. Build and sign the permit in one step.
const signedPermit = await client.signDecryptionPermit({
transportKeyPair,
contractAddresses: [contractAddress],
startTimestamp: Math.floor(Date.now() / 1000),
durationSeconds: 7 * 24 * 60 * 60, // valid for 7 days
signerAddress,
signer, // ethers signer or viem account/wallet client
});
// 3. Decrypt an encrypted value the contract returned to you.
const decrypted = await client.decryptValue({
transportKeyPair,
encryptedValue, // a bytes32 handle read from the contract
contractAddress,
signedPermit,
});
decrypted.value; // 42 (number), 1000n (bigint), true, or "0x…" (address)
decrypted.type; // "uint32", "bool", "address", … (Solidity value-type names)
The result is a TypedValue — { type, value } in the same value-domain
vocabulary you encrypt with. uint8/uint16/uint32 come back as number,
uint64/uint128/uint256 as bigint, bool as boolean, and address as
a checksummed string.
The plaintext is reconstructed locally from KMS shares — it is never sent in the clear over the network. To decrypt several values at once, or to read values a contract has made public, see Decryption.
{% hint style="success" %} That's the full loop: you encrypted a value, sent it to a contract, and decrypted a private result. Everything else in these docs builds on these three calls. {% endhint %}
The repository ships runnable end-to-end scripts for both adapters. They encrypt, read public values, and privately decrypt a live Sepolia contract:
npx tsx ./examples/node-ethers/test-run.ts
@zama-fhe/relayer-sdk.