Skip to content

zunmax/fhevm-template

Repository files navigation

Confidential Counter · FHEVM Template

Zama · FHEVM · Confidential Counter Template

🔐 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.

FHEVM v0.11 Solidity 0.8.28 Node ≥ 20 MIT License Sepolia

Features

  • 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-plugin mock 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.

Quickstart

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()

Prerequisites

  • 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.

Execution tiers

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

Project layout

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

Root npm scripts

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.

Workflow

Test

npm run test

Runs against the @fhevm/hardhat-plugin mock network. Encryption is simulated, but the full ACL, handle lifecycle, and public/user-decrypt flows are exercised.

Deploy

npm run deploy:sepolia

contracts/scripts/deploy.ts performs four steps in one pass:

  1. Deploys the contract via direct ethers.
  2. Writes contracts/deployments/<network>/ConfidentialCounter.json (name, address, chainId, tx hash, deployer, full ABI).
  3. Regenerates frontend-react/src/generated/, frontend-next/src/generated/, and scripts-node/src/generated/ (any workspace whose package.json is missing is silently skipped). Each module exports {ContractName}Address, {ContractName}ChainId, and {ContractName}ABI as const.
  4. Best-effort Etherscan verify if ETHERSCAN_API_KEY is set.

The frontend and Node workspaces import the contract address directly from the generated module, so no env var for the address.

Frontend (/web)

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:5173

The 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 → encryptadd(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

Node CLI (/node)

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 counter

No initSDK(), no WASM worker, no COOP/COEP concerns. Useful for CI jobs, indexers, oracles, and server-side integrations.

Which SDK path?

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 /bundle build (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.

FHEVM patterns baked in

  • ZamaEthereumConfig inheritance (NOT SepoliaConfig, which was v0.8 and is removed).
  • FHE.fromExternal(encAmount, inputProof) to consume user inputs (NOT asEuint64, which converts Solidity literals).
  • No branching on encrypted values: sub() uses FHE.le + FHE.select instead of require.
  • ACL re-grant on every write: _store calls FHE.allowThis + FHE.allow(user) because ACL is per-handle and every computation yields a new handle.
  • _revealed invariant: _store clears 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-chain publicDecrypt (NOT requestDecryption, removed in v0.9).
  • Handle hex order: the frontend passes encrypted.handles[N] in the exact order of the addXX() calls. The SDK's encrypt() result guarantees positional matching.

Mainnet

Zama mainnet launched 2025-12-30. The mainnet relayer is authenticated; every encrypt / decrypt call needs an API key.

What's automatic

  • Chain-aware contract config. ConfidentialCounter already inherits ZamaEthereumConfig, which resolves the right ACL / coprocessor / KMSVerifier from block.chainid - same source on Sepolia and mainnet.
  • Chain-aware deploy. npm run deploy:mainnet (Path A) or npm 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-runs verify:verify on Etherscan if ETHERSCAN_API_KEY is set; for Path B, run npm run verify:mainnet -- <address> from the contracts/ workspace afterwards.
  • Chain-aware Node CLI. When the generated module records chainId === 1, scripts-node/src/common.ts switches to MainnetConfig, reads MAINNET_RPC_URL, and authenticates with auth: { __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, both frontend-react/src/fhe.ts and frontend-next/src/lib/fhe.ts switch to MainnetConfig and read VITE_RELAYER_PROXY_URL / NEXT_PUBLIC_RELAYER_PROXY_URL. wagmi.ts already lists mainnet, so RainbowKit shows it in the chain selector.
  • Backend relayer proxy (proxy/) included in the template. npm run proxy boots a hardened Express server (CORS allowlist, origin gate, rate limit) that injects x-api-key server-side. Deploy the same proxy/src/index.ts to your host of choice for production.

What you need to provide

  1. Zama Relayer API key - apply at https://forms.gle/jq84zEek1oiv3kBz9. Set as ZAMA_FHEVM_API_KEY in .env.
  2. ALLOWED_ORIGIN in .env - the dApp URL the proxy will accept (defaults to http://localhost:5173 for Vite dev).
  3. VITE_RELAYER_PROXY_URL in .env - the URL the browser will call. Path MUST include the upstream version segment (/v1 or /v2). For local dev this is http://localhost:8787/relayer/v1; in production it's your deployed proxy at https://your-app.example.com/api/relayer/v1. The frontend pairs this with relayerRouteVersion: 1 in createInstance().

Test the mainnet flow locally

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 Sepolia

Verify the wiring before clicking around:

  • Browser DevTools -> Network: every relayer call should hit localhost:8787/relayer/v1/*, never relayer.mainnet.zama.org directly.
  • Browser DevTools -> Sources -> Ctrl+Shift+F your key prefix: zero matches in the bundle.

Changing the contract

  1. Edit contracts/contracts/ConfidentialCounter.sol or add a new contract.
  2. Update tests in contracts/test/.
  3. If you added a new contract, update CONTRACT_NAME in contracts/scripts/deploy.ts, or turn the script into a loop over a list.
  4. Run npm run test && npm run deploy:sepolia.
  5. The frontend and Node workspaces pick up the new ABI automatically; no manual edits.

License

Released under the MIT License.

About

FHEVM v0.11 monorepo - encrypted Solidity state, dual frontends (Vite + Next 15), Node CLI, and a server-side relayer proxy.

Topics

Resources

License

Stars

2 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors