# 从零到可提交：Zama FHEVM dApp 部署完整教程

By [pangdong](https://paragraph.com/@pangdong) · 2025-09-17

---

> 目标：**30–60 分钟**做出一个可提交到 **Zama Builder Track** 的 dApp：
> 
> *   合约部署到 **Sepolia**（带 FHE 接口 + 演示接口）
>     
> *   前端（Next.js + ethers）可“连接钱包 / 加一 / 减一 / 读取”
>     
> *   一键发布到 **Vercel**（免费 HTTPS 域名）
>     
> *   附：提交表单文案要点 & 常见报错速查
>     

* * *

0\. 环境准备（Ubuntu/Debian）
-----------------------

    sudo apt update && sudo apt upgrade -y && \
    sudo apt install -y git curl jq build-essential make gcc \
        nodejs npm && \
    sudo npm install -g yarn hardhat
    node -v && yarn -v
    

> 其他系统：可用 WSL/容器；`jq` 用于处理 ABI。

* * *

1\. 获取 FHEVM 模板 + 配置网络与私钥
-------------------------

    # 1) 克隆模板
    git clone https://github.com/zama-ai/fhevm-hardhat-template
    cd fhevm-hardhat-template
    npm install
    
    # 2) （可选）使用现成的 hardhat 配置
    curl -o hardhat.config.ts https://raw.githubusercontent.com/0xmoei/zama-fhe/refs/heads/main/hardhat.config.ts
    
    # 3) 配置 RPC 与私钥（注意 PRIVATE_KEY 不要带 0x 前缀）
    npx hardhat vars set SEPOLIA_RPC_URL https://eth-sepolia.public.blastapi.io
    npx hardhat vars set PRIVATE_KEY <你的私钥不带0x>
    
    # 4) 验证账户
    npx hardhat accounts --network sepolia
    # 能打印你的钱包地址即成功
    

> ⚠️ 常见报错：**“private key too long, expected 32 bytes”** → 去掉 `0x` 前缀后重设： `npx hardhat vars delete PRIVATE_KEY && npx hardhat vars set PRIVATE_KEY <不带0x的64位hex>`

* * *

2\. 给模板加一个部署脚本（deploy.ts）
-------------------------

    mkdir -p scripts
    cat > scripts/deploy.ts <<'TS'
    import { ethers } from "hardhat";
    import { writeFileSync, mkdirSync } from "fs";
    import path from "path";
    
    async function main() {
      const contractName = process.env.CONTRACT || "FHECounter";
      const Factory = await ethers.getContractFactory(contractName);
      const c = await Factory.deploy();
      await c.waitForDeployment();
      const address = await c.getAddress();
      console.log(`✅ Deployed ${contractName} at: ${address}`);
    
      const outDir = path.join("deployments", "sepolia");
      mkdirSync(outDir, { recursive: true });
    
      const artifact = require(path.resolve(
        "artifacts",
        "contracts",
        `${contractName}.sol`,
        `${contractName}.json`
      ));
      writeFileSync(
        path.join(outDir, `${contractName}.json`),
        JSON.stringify({ network: "sepolia", address, contract: contractName, abi: artifact.abi }, null, 2)
      );
      console.log(`📝 Wrote deployment info to: deployments/sepolia/${contractName}.json`);
    }
    
    main().catch((e) => { console.error(e); process.exit(1); });
    TS
    

编译测试：

    npx hardhat compile
    

* * *

3\. 修改合约：保留 FHE 接口 + 新增“演示接口”（无参 & 明文返回）
----------------------------------------

> 模板的 `FHECounter` 只有 FHE 函数（需要密文和证明），前端直接调会报 **no matching fragment**。 我们**新增** 3 个“演示接口”，便于新手和评审快速体验：`incrementPlain / decrementPlain / getCountPlain`。

打开并**替换** `contracts/FHECounter.sol` 为下列完整内容（仅在原版基础上新增了 `mirror` 与 3 个函数；原 FHE 接口保留不变）：

    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.24;
    
    import {FHE, euint32, externalEuint32} from "@fhevm/solidity/lib/FHE.sol";
    import {SepoliaConfig} from "@fhevm/solidity/config/ZamaConfig.sol";
    
    /// @title A simple FHE counter contract (with plain demo interfaces)
    contract FHECounter is SepoliaConfig {
        euint32 private _count;
    
        // ✅ 新增：明文镜像，便于演示
        uint256 public mirror;
    
        /// @notice Returns the current encrypted count
        function getCount() external view returns (euint32) {
            return _count;
        }
    
        /// @notice FHE increment (ciphertext + proof)
        function increment(externalEuint32 inputEuint32, bytes calldata inputProof) external {
            euint32 encryptedEuint32 = FHE.fromExternal(inputEuint32, inputProof);
            _count = FHE.add(_count, encryptedEuint32);
            FHE.allowThis(_count);
            FHE.allow(_count, msg.sender);
        }
    
        /// @notice FHE decrement (ciphertext + proof)
        function decrement(externalEuint32 inputEuint32, bytes calldata inputProof) external {
            euint32 encryptedEuint32 = FHE.fromExternal(inputEuint32, inputProof);
            _count = FHE.sub(_count, encryptedEuint32);
            FHE.allowThis(_count);
            FHE.allow(_count, msg.sender);
        }
    
        // ✅ 新增：无参演示接口（交易会弹钱包）
        function incrementPlain() external { mirror += 1; }
    
        function decrementPlain() external {
            require(mirror > 0, "underflow");
            mirror -= 1;
        }
    
        function getCountPlain() external view returns (uint256) {
            return mirror;
        }
    }
    

重新编译 + 部署：

    npx hardhat compile
    npx hardhat run scripts/deploy.ts --network sepolia
    # 记录输出的合约地址，例如：
    # 0xa0402615e790d92d43e2D6644f95fb196Ae123458
    

* * *

4\. 创建前端（Next.js + ethers v6）
-----------------------------

    cd ~
    npx create-next-app@latest fhevm-dapp --ts --eslint --app --src-dir --use-npm
    cd fhevm-dapp
    npm i ethers
    

### 4.1 复制 ABI 到前端

    mkdir -p src/abi
    jq '{abi: .abi}' ~/fhevm-hardhat-template/deployments/sepolia/FHECounter.json > src/abi/FHECounter.json
    

### 4.2 设置环境变量

    cat > .env.local <<'ENV'
    NEXT_PUBLIC_CONTRACT_ADDRESS=改为你刚才的合约地址
    NEXT_PUBLIC_RPC_URL=https://eth-sepolia.public.blastapi.io
    ENV
    

### 4.3 合约封装（`src/lib/contract.ts`）

    /* eslint-disable @typescript-eslint/no-explicit-any */
    import { BrowserProvider, Contract } from "ethers";
    import abi from "@/abi/FHECounter.json";
    
    export async function getContract() {
      if (!(window as any).ethereum) throw new Error("No wallet found. Please install MetaMask.");
      const provider = new BrowserProvider((window as any).ethereum);
      const signer = await provider.getSigner();
      const address = process.env.NEXT_PUBLIC_CONTRACT_ADDRESS!;
      const theAbi: any = (abi as any).abi ?? abi;
      return new Contract(address, theAbi, signer);
    }
    

### 4.4 首页交互（`src/app/page.tsx`）

    /* eslint-disable @typescript-eslint/no-explicit-any */
    "use client";
    
    import { useState } from "react";
    import { getContract } from "@/lib/contract";
    
    export default function Home() {
      const [status, setStatus] = useState("Idle");
      const [count, setCount] = useState<string>("?");
    
      async function connect() {
        await (window as any).ethereum.request({ method: "eth_requestAccounts" });
        setStatus("Wallet connected");
      }
    
      async function switchToSepolia() {
        const ethereum = (window as any).ethereum;
        try {
          await ethereum.request({ method: "wallet_switchEthereumChain", params: [{ chainId: "0xaa36a7" }] });
          setStatus("Switched to Sepolia");
        } catch (e: any) {
          if (e.code === 4902) {
            await ethereum.request({
              method: "wallet_addEthereumChain",
              params: [{
                chainId: "0xaa36a7",
                chainName: "Sepolia",
                nativeCurrency: { name: "SepoliaETH", symbol: "ETH", decimals: 18 },
                rpcUrls: [process.env.NEXT_PUBLIC_RPC_URL],
                blockExplorerUrls: ["https://sepolia.etherscan.io/"]
              }]
            });
            setStatus("Sepolia added");
          } else {
            setStatus("Switch failed: " + e.message);
          }
        }
      }
    
      async function increment() {
        try {
          const c = await getContract();
          const tx = await c.incrementPlain();       // ✅ 演示接口：无参
          setStatus("Pending: " + tx.hash);
          await tx.wait();
          setStatus("Confirmed");
        } catch (e: any) { setStatus("Error: " + (e?.shortMessage || e?.message)); }
      }
    
      async function decrement() {
        try {
          const c = await getContract();
          const tx = await c.decrementPlain();       // ✅ 演示接口：无参
          setStatus("Pending: " + tx.hash);
          await tx.wait();
          setStatus("Confirmed");
        } catch (e: any) { setStatus("Error: " + (e?.shortMessage || e?.message)); }
      }
    
      async function readCount() {
        try {
          const c = await getContract();
          const v = await c.getCountPlain();         // ✅ 演示接口：返回明文
          setCount(v?.toString?.() ?? String(v));
          setStatus("Read success");
        } catch (e: any) { setStatus("Error: " + (e?.shortMessage || e?.message)); }
      }
    
      return (
        <main className="max-w-2xl mx-auto p-6">
          <h1 className="text-2xl font-bold">Zama FHEVM Demo</h1>
          <p className="text-sm opacity-70 mt-1">Contract: {process.env.NEXT_PUBLIC_CONTRACT_ADDRESS}</p>
    
          <div className="flex gap-2 mt-4">
            <button className="px-3 py-2 border rounded" onClick={connect}>Connect Wallet</button>
            <button className="px-3 py-2 border rounded" onClick={switchToSepolia}>Switch to Sepolia</button>
          </div>
    
          <div className="flex gap-2 mt-4">
            <button className="px-3 py-2 border rounded" onClick={increment}>Increment</button>
            <button className="px-3 py-2 border rounded" onClick={decrement}>Decrement</button>
            <button className="px-3 py-2 border rounded" onClick={readCount}>Read Count</button>
          </div>
    
          <p className="mt-4">Count: <b>{count}</b></p>
          <p className="mt-2">Status: {status}</p>
          <p className="mt-6 text-sm opacity-70">Network: Sepolia</p>
        </main>
      );
    }
    

> 如果 Vercel 构建被 ESLint 拦住（`no-explicit-any`），可在 `next.config.ts` 增加：
> 
>     export default { eslint: { ignoreDuringBuilds: true } }
>     

### 4.5 本地试跑

    npm run dev -- -H 0.0.0.0 -p 3000
    # 浏览器： http://<服务器IP>:3000
    # Connect → Switch → Increment → Read
    

* * *

5\. 一键上线（Vercel）
----------------

### 5.1 推到 GitHub

    cd ~/fhevm-dapp
    echo -e "node_modules/\n.next/\nout/\n.env.local" >> .gitignore
    git init
    git add .
    git commit -m "init dapp"
    git branch -M main
    # 先在 GitHub 网页创建仓库 你的用户名/fhevm-dapp
    git remote add origin https://github.com/你的用户名/fhevm-dapp.git
    git push -u origin main
    # 若提示密码：使用 GitHub Personal Access Token（Settings → Developer settings → Tokens）
    

### 5.2 Vercel 绑定仓库 → 自动部署

*   登录 [https://vercel.com](https://vercel.com) → **New Project → Import Git Repository**
    
*   选 `你的用户名/fhevm-dapp`
    
*   **Environment Variables** 添加：
    
    *   `NEXT_PUBLIC_CONTRACT_ADDRESS` = `你的合约地址`
        
    *   `NEXT_PUBLIC_RPC_URL` = `https://eth-sepolia.public.blastapi.io`
        
*   点击 **Deploy**
    
*   获得 `https://<project>.vercel.app`（免费 HTTPS）
    

线上自测：**Connect → Switch → Increment → Read**，数字应递增。

* * *

6\. （可选）README 模板要点
-------------------

    # Zama FHEVM Demo – Encrypted Counter
    
    - Network: Sepolia
    - Contract: 0xa0402615e790d92d43e2D6644f95fb196Ae95199
    - Demo: https://<your-project>.vercel.app
    
    ## Features
    - FHE: increment/decrement (ciphertext + proof), getCount() returns ciphertext.
    - Demo: incrementPlain/decrementPlain/getCountPlain (no params, plain number).
    
    ## Run Frontend
    npm install
    # .env.local: NEXT_PUBLIC_CONTRACT_ADDRESS=..., NEXT_PUBLIC_RPC_URL=...
    npm run dev
    
    ## Deploy Contract
    npx hardhat compile
    npx hardhat run scripts/deploy.ts --network sepolia
    

* * *

7\. 提交 Builder Track 表单（示例文案）
-----------------------------

*   **GitHub repo**：`https://github.com/你的用户名/fhevm-dapp`
    
*   **Deployed demo**：`https://<your-project>.vercel.app`
    
*   **Project description**（可直接贴）：
    
        Zama FHEVM Encrypted Counter (Builder Track)
        
        • What it does
        A minimal dApp demonstrating homomorphic computation on Zama FHEVM.
        It ships both FHE interfaces (encrypted inputs + proof, ciphertext state)
        and plain demo interfaces for quick judging (incrementPlain/decrementPlain/getCountPlain).
        
        • Why FHEVM
        Secure updates use externalEuint32 + inputProof to modify an euint32 state on-chain,
        enabling privacy-preserving computation without revealing raw inputs.
        
        • Implementation
        Solidity (Zama FHEVM libs) + Hardhat on Sepolia, Next.js + ethers v6 frontend, Vercel deployment.
        The current UI uses the plain interfaces for UX; FHE read-permit/decrypt will be added in v2.
        
    

* * *

8\. 常见问题速查
----------

*   **HH8: private key too long** → `PRIVATE_KEY` 不要带 `0x`。
    
*   **no matching fragment** → 你在前端调用了无参版本，但合约是带参 FHE 函数；使用 `incrementPlain/decrementPlain` 或按 FHE 流程传 `bytes32 + bytes`。
    
*   **Read 显示 0x0000…** → 这是密文；使用 `getCountPlain()` 显示明文演示值。
    
*   **Vercel 构建被 ESLint 拦住** → 文件头加 `/* eslint-disable @typescript-eslint/no-explicit-any */`，或 `next.config.ts` 中 `eslint.ignoreDuringBuilds=true`。
    
*   **钱包不弹窗** → 函数名不对 / ABI 未更新 / 钱包不在 Sepolia。
    
*   **交易 Pending** → 换 RPC（Blast/Alchemy/Infura）或提高 Gas。
    

* * *

---

*Originally published on [pangdong](https://paragraph.com/@pangdong/zama-fhevm-dapp)*
