🔐 End-to-end template for confidential dApps on FHEVM v0.11. Solidity contract, React + Next.js dApps, Node CLI, and a hardened mainnet relayer proxy in one repo.
- Solidity contract with encrypted state, ACL-gated reads, public reveal, and handle rehiding.
- React + Vite + wagmi frontend showcasing every decrypt scenario (user-decrypt via EIP-712 and public-decrypt) in labelled panels.
- Two SDK loading patterns:
/web(Vite bundler) and/node(Node CLI). - Hardhat tests on the
@fhevm/hardhat-pluginmock network covering ACL, reveal, and rehide semantics. - One deploy script that deploys, persists a deployment record, and auto-syncs ABI + address into the frontend and Node workspaces. No hand-edits required.
- Single root
.env: Hardhat,scripts-node, and Vite all read from it.
npm run setup # install all workspaces + compile contracts
cp .env.example .env # fill PRIVATE_KEY, RPC_URL, ETHERSCAN_API_KEY, VITE_WALLETCONNECT_PROJECT_ID
npm run test # mock-network test suite (no env required)
npm run deploy:sepolia # deploy + sync ABI/address into both frontends + scripts-node
npm run web:react # Vite + React dApp on http://localhost:5173
npm run web:next # Next.js 15 App Router dApp on http://localhost:5173
npm run node:encrypt -- 42 # CLI path: encrypt 42 and call add()- Node ≥ 20
- A Sepolia RPC URL and a funded deployer private key (the deploy script aborts on 0 ETH).
- Optional: a WalletConnect Cloud project ID for the frontend wallet connectors, and an Etherscan API key for contract verification.
Environment variables live in a single root .env file. Only VITE_*-prefixed variables are exposed to the browser bundle; secrets stay server-side.
The template supports every stage of an FHEVM project lifecycle:
| Tier | Command | Network | FHE ops? | Persists? | Purpose |
|---|---|---|---|---|---|
| 1. Mock tests | npm run test |
in-process hardhat |
✔ via @fhevm/hardhat-plugin mock |
ephemeral | Unit + integration tests with simulated encryption |
| 2. Local dry-run | npm run deploy:local |
in-process hardhat |
✔ mock | ephemeral | Sanity-check the deploy script, no env required, no files clobbered |
| 3. Testnet | npm run deploy:sepolia, then npm run web:react (or web:next) / npm run node:* |
Sepolia | ✔ real coprocessor | ✔ on-chain | Interactive dApp + server-side CLI against the live network |
| 4. Mainnet | npm run deploy:mainnet, then run with mainnet env vars set |
Ethereum mainnet (chain 1) | ✔ via authenticated relayer | ✔ on-chain | Production. Requires Zama Relayer API key (server-side) + a backend proxy for the browser |
zama-project/
├── contracts/ Hardhat workspace
│ ├── contracts/
│ │ └── ConfidentialCounter.sol
│ ├── scripts/
│ │ ├── deploy.ts deploy + auto-sync ABI to frontend and scripts-node
│ │ └── check-balance.ts
│ ├── test/
│ │ └── ConfidentialCounter.test.ts
│ └── hardhat.config.ts
├── frontend-react/ Vite + React dApp (`/web` SDK path)
│ ├── src/
│ │ ├── generated/ auto-written ABI + address (.gitignored)
│ │ ├── fhe.ts @zama-fhe/relayer-sdk/web helpers
│ │ ├── contract.ts
│ │ ├── components/
│ │ └── main.tsx
│ └── vite.config.ts COOP/COEP + optimizeDeps exclude
├── frontend-next/ Next.js 15 App Router dApp (`/web` SDK path)
│ ├── src/
│ │ ├── generated/ auto-written ABI + address (.gitignored)
│ │ ├── lib/ fhe.ts / wagmi.ts / contract.ts
│ │ ├── components/ same UI components, marked `'use client'`
│ │ └── app/ layout.tsx + providers.tsx + page.tsx
│ └── next.config.js COOP/COEP headers + WASM webpack config
├── scripts-node/ Node-side CLI (`/node` SDK path, ESM via tsx)
│ ├── src/
│ │ ├── common.ts @zama-fhe/relayer-sdk/node helpers
│ │ ├── encrypt-and-add.ts
│ │ ├── user-decrypt.ts
│ │ └── public-decrypt.ts
│ └── tsconfig.json
├── proxy/ Relayer API-key proxy (mainnet browser flow)
│ ├── src/
│ │ └── index.ts Express server: CORS, origin gate, rate limit, key injection
│ ├── package.json
│ └── tsconfig.json
├── scripts/
│ ├── setup.mjs installs all workspaces, compiles contracts
│ ├── run-node.mjs forwards args to a scripts-node script
│ └── web.mjs dispatches dev/build to frontend-react or frontend-next
├── assets/
│ └── banner.svg
├── package.json root npm scripts
├── .env.example single env file for every workspace
└── README.md
| Script | Purpose |
|---|---|
npm run setup |
Install contracts + frontend + scripts-node deps, compile contracts. |
npm run clean |
hardhat clean. |
npm run compile |
hardhat compile. |
npm run test |
hardhat test on the mock FHEVM network. |
npm run coverage |
solidity-coverage report. |
npm run balance |
Print deployer address + Sepolia ETH balance. |
npm run deploy:local |
Dry-run deploy on the in-process mock (no file sync). |
npm run deploy:sepolia |
Real deploy + sync ABI/address into frontend and scripts-node. |
npm run deploy:mainnet |
Mainnet deploy + sync (Path A). Requires MAINNET_RPC_URL and a funded PRIVATE_KEY. |
npm run deploy-b:localhost / :sepolia / :mainnet |
Path B: same flows via the hardhat-deploy plugin. |
npm run web:react |
Launch the Vite + React dev server. |
npm run web:next |
Launch the Next.js dev server. |
npm run build:react / build:next |
Frontend production build for the chosen framework. |
npm run node:init |
Call initialize() once per address; idempotent. |
npm run node:encrypt -- <amount> |
Encrypt <amount> and call add(). Auto-initializes on first use. |
npm run node:user-decrypt |
EIP-712 user-decrypt the caller's counter. |
npm run node:public-decrypt -- <address> |
Publicly decrypt any revealed counter. |
npm run proxy |
Start the relayer API-key proxy on http://localhost:8787 (mainnet browser flow). |
npm run proxy:dev |
Same proxy with file-watch reload. |
npm run testRuns against the @fhevm/hardhat-plugin mock network. Encryption is simulated, but the full ACL, handle lifecycle, and public/user-decrypt flows are exercised.
npm run deploy:sepoliacontracts/scripts/deploy.ts performs four steps in one pass:
- Deploys the contract via direct
ethers. - Writes
contracts/deployments/<network>/ConfidentialCounter.json(name, address, chainId, tx hash, deployer, full ABI). - Regenerates
frontend-react/src/generated/,frontend-next/src/generated/, andscripts-node/src/generated/(any workspace whosepackage.jsonis missing is silently skipped). Each module exports{ContractName}Address,{ContractName}ChainId, and{ContractName}ABI as const. - Best-effort Etherscan verify if
ETHERSCAN_API_KEYis set.
The frontend and Node workspaces import the contract address directly from the generated module, so no env var for the address.
Pick a framework. Both share lib/, components/, and the auto-synced generated/ module, and both read the same root .env. The Next.js variant maps VITE_* env vars to NEXT_PUBLIC_* automatically so a single .env works for both.
npm run web:react # Vite + React 18, served on http://localhost:5173
npm run web:next # Next.js 15 App Router, served on http://localhost:5173The dashboard shows six numbered panels:
| # | Panel | What it demonstrates |
|---|---|---|
| 1 | State | Connection, chain match, contract address, current encrypted handle, reveal flag |
| 2 | Initialize | One-off initialize() that creates an encrypted zero |
| 3 | Mutate | createEncryptedInput → add64 → encrypt → add(handle, proof) / sub(...) |
| 4 | Reveal / Rehide | requestReveal (makePubliclyDecryptable, one-way) and rehide (rotate handle) |
| 5 | User Decrypt (private) | generateKeypair → createEIP712 → signTypedData → userDecrypt, no on-chain tx |
| 6 | Public Decrypt | publicDecrypt([handle]). Works only after panel 4 has been used |
npm run node:init # one-time: create the encrypted-zero counter
npm run node:encrypt -- 42 # encrypt 42, submit add() (auto-inits if needed)
npm run node:user-decrypt # EIP-712 sign, decrypt your own counter
npm run node:public-decrypt -- 0xAbC...123 # publicly decrypt any revealed counterNo initSDK(), no WASM worker, no COOP/COEP concerns. Useful for CI jobs, indexers, oracles, and server-side integrations.
| Path | Use when |
|---|---|
/web |
You're building a bundled SPA (Vite, Next.js, webpack, Rollup). |
/node |
Scripts, CLIs, indexers, oracles, serverless handlers, integration tests. |
The SDK also ships a
/bundlebuild (CDN<script>tag, no bundler) for plain HTML / CMS widgets. It is not demonstrated in this template; see the Zama relayer-sdk docs if you need it.
ZamaEthereumConfiginheritance (NOTSepoliaConfig, which was v0.8 and is removed).FHE.fromExternal(encAmount, inputProof)to consume user inputs (NOTasEuint64, which converts Solidity literals).- No branching on encrypted values:
sub()usesFHE.le+FHE.selectinstead ofrequire. - ACL re-grant on every write:
_storecallsFHE.allowThis+FHE.allow(user)because ACL is per-handle and every computation yields a new handle. _revealedinvariant:_storeclears the flag so any add/sub/rehide automatically rotates the counter to a private handle. The old handle stays public forever (cryptographic reality), but the current on-chain state does not.- 3-step public decryption via
FHE.makePubliclyDecryptable+ off-chainpublicDecrypt(NOTrequestDecryption, removed in v0.9). - Handle hex order: the frontend passes
encrypted.handles[N]in the exact order of theaddXX()calls. The SDK'sencrypt()result guarantees positional matching.
Zama mainnet launched 2025-12-30. The mainnet relayer is authenticated; every encrypt / decrypt call needs an API key.
- Chain-aware contract config.
ConfidentialCounteralready inheritsZamaEthereumConfig, which resolves the right ACL / coprocessor / KMSVerifier fromblock.chainid- same source on Sepolia and mainnet. - Chain-aware deploy.
npm run deploy:mainnet(Path A) ornpm run deploy-b:mainnet(Path B) deploys, persists the deployment record, and syncs the generated ABI/address to both frontend and Node workspaces. Path A also auto-runsverify:verifyon Etherscan ifETHERSCAN_API_KEYis set; for Path B, runnpm run verify:mainnet -- <address>from thecontracts/workspace afterwards. - Chain-aware Node CLI. When the generated module records
chainId === 1,scripts-node/src/common.tsswitches toMainnetConfig, readsMAINNET_RPC_URL, and authenticates withauth: { __type: "ApiKeyHeader", value: ZAMA_FHEVM_API_KEY }. The key never leaves the machine running the CLI. - Chain-aware frontend. When the generated module records
chainId === 1, bothfrontend-react/src/fhe.tsandfrontend-next/src/lib/fhe.tsswitch toMainnetConfigand readVITE_RELAYER_PROXY_URL/NEXT_PUBLIC_RELAYER_PROXY_URL.wagmi.tsalready listsmainnet, so RainbowKit shows it in the chain selector. - Backend relayer proxy (
proxy/) included in the template.npm run proxyboots a hardened Express server (CORS allowlist, origin gate, rate limit) that injectsx-api-keyserver-side. Deploy the sameproxy/src/index.tsto your host of choice for production.
- Zama Relayer API key - apply at https://forms.gle/jq84zEek1oiv3kBz9. Set as
ZAMA_FHEVM_API_KEYin.env. ALLOWED_ORIGINin.env- the dApp URL the proxy will accept (defaults tohttp://localhost:5173for Vite dev).VITE_RELAYER_PROXY_URLin.env- the URL the browser will call. Path MUST include the upstream version segment (/v1or/v2). For local dev this ishttp://localhost:8787/relayer/v1; in production it's your deployed proxy athttps://your-app.example.com/api/relayer/v1. The frontend pairs this withrelayerRouteVersion: 1increateInstance().
After Sepolia E2E passes:
# 1. deploy to mainnet (rewrites generated/ to chainId 1)
npm run deploy:mainnet
# 2. verify the Node CLI side - this is the cheapest way to confirm your key works
npm run node:init
npm run node:encrypt -- 7
npm run node:user-decrypt
# 3. browser flow - run proxy and dApp in two shells
npm run proxy # shell 1: starts http://localhost:8787, forwards /relayer/v1/* with x-api-key
npm run web:react # shell 2: dev server on http://localhost:5173 (or `web:next` for Next.js, also on :5173)
# connect MetaMask to Ethereum Mainnet, panels work as on SepoliaVerify the wiring before clicking around:
- Browser DevTools -> Network: every relayer call should hit
localhost:8787/relayer/v1/*, neverrelayer.mainnet.zama.orgdirectly. - Browser DevTools -> Sources -> Ctrl+Shift+F your key prefix: zero matches in the bundle.
- Edit
contracts/contracts/ConfidentialCounter.solor add a new contract. - Update tests in
contracts/test/. - If you added a new contract, update
CONTRACT_NAMEincontracts/scripts/deploy.ts, or turn the script into a loop over a list. - Run
npm run test && npm run deploy:sepolia. - The frontend and Node workspaces pick up the new ABI automatically; no manual edits.
Released under the MIT License.