前言

在 Web3 开发领域,与以太坊等区块链网络进行交互是构建去中心化应用(DApp)的核心环节。传统的 Web3 开发框架如 wagmi 为开发者提供了便利的 React Hooks,但有时我们也需要更底层、更灵活的控制。

本文将介绍一个纯 Viem 脚手架项目,详细分析如何使用 Viem 库直接与 MetaMask 钱包和智能合约进行交互,不依赖 wagmi 等高级抽象库,让开发者更好地理解底层交互逻辑。Viem 作为下一代以太坊开发工具,相比传统的 ethers.js 和 web3.js,提供了更现代化的 TypeScript 接口和更轻量级的实现。

项目概述

这是一个使用 Next.js 和 Viem 构建的简单 DApp 脚手架项目,主要功能包括连接钱包、获取账户信息、读写智能合约以及监听钱包状态变化。该项目采用纯 Viem 实现,没有使用 wagmi 等第三方状态管理库。

为什么选择纯 Viem?

纯 Viem 方案在 Web3 开发中因其轻量级设计和底层控制能力,逐渐成为开发者调查中的新趋势,尤其适合追求灵活性和性能的场景。相较于 wagmi 等高级抽象库,纯 Viem 提供了以下显著优势:

  • 更细粒度的控制

    开发者可以直接操作每个链上请求,深入理解底层通信逻辑,便于调试和优化,无需受抽象层限制。

  • 轻量级实现

    不依赖额外的状态管理库,项目体积大幅减少(仅 viem 比 wagmi 全家桶少 70%+),加载速度显著提升。

  • 灵活性更高

    根据项目需求自由定制交互逻辑,不被高级库的预设框架束缚,适合复杂或定制化场景。

  • 更好的 TypeScript 支持

    Viem 的原生类型推断确保合约交互类型安全,降低运行时错误风险,成为开发者信赖的核心。

  • 更直观的 API 设计

    API 贴近以太坊原生操作,易于掌握区块链交互本质,减少学习曲线。

  • 调试友好与未来趋势

    出错时直接面对 Viem 的原始错误信息,无需解构 hook 问题,调试效率翻倍。同时,wagmi v2 已全面转向 Viem,纯 Viem 方案是未来 Web3 开发的标杆。

这种架构不仅适合初学者快速上手,也为高级开发者提供无限扩展空间,是链上开发的理想选择。

项目创建步骤

项目初始化

 1# 1. 创建 Next.js 项目(App Router)
 2npx create-next-app@latest simple-viem --typescript --tailwind --eslint --app --import-alias "@/*"
 3
 4cd simple-viem
 5
 6# 2. 安装核心依赖
 7pnpm install \
 8  viem@latest \
 9  antd \
10  @ant-design/icons \
11  @ant-design/nextjs-registry \
12  dotenv
13
14# 3. 安装开发依赖
15pnpm install -D \
16  prettier

智能合约部分初始化

 1# 在项目根目录初始化 Foundry(智能合约)
 2forge init contracts
 3cd contracts
 4
 5# 删除 foundry 的 git 仓库,统一使用上层的 git 仓库
 6rm -rf .git
 7
 8# 安装 OpenZeppelin,不能使用 git 安装,否则会使仓库管理混乱
 9forge install --no-git OpenZeppelin/openzeppelin-contracts
10
11# 生成 ABI 文件
12mkdir ../app/abis
13forge inspect Counter abi --json > ../app/abis/Counter.json

核心依赖分析

该项目的核心依赖包括:

  • Next.js:React 框架,提供 SSR 和现代化的开发体验
  • Viem:用于与以太坊区块链交互的 TypeScript 库,是项目的核心
  • Ant Design:UI 组件库
  • Foundry:以太坊开发工具链(智能合约部分)

配置文件

项目的 tsconfig.json、package.json 等配置文件均遵循 next.js 的最佳实践配置,同时 TypeScript 确保了代码的类型安全。

项目文件结构

simple-viem/
├── app/
│   ├── abis/                    # 智能合约ABI文件
│   │   └── Counter.json        # Counter合约的ABI
│   ├── favicon.ico             # 网站图标
│   ├── globals.css             # 全局样式
│   ├── layout.tsx              # Next.js布局组件
│   ├── page.tsx                # 主页面,包含所有交互逻辑
│   ├── providers.tsx           # 提供者组件(空实现)
│   └── types/
│       └── ethereum.d.ts       # TypeScript类型定义
├── contracts/                  # 智能合约目录
│   ├── .env                    # 环境变量配置
│   ├── foundry.toml            # Foundry配置文件
│   ├── lib/                    # 依赖库(OpenZeppelin)
│   ├── out/                    # 编译输出目录
│   ├── script/                 # 部署脚本
│   ├── src/                    # 合约源码
│   │   └── Counter.sol         # Counter智能合约
│   └── test/                   # 测试文件
├── .gitignore                  # Git忽略文件
├── .prettierrc.cjs             # Prettier配置
├── eslint.config.mjs           # ESLint配置
├── next-env.d.ts               # Next.js类型声明
├── next.config.ts              # Next.js配置
├── package.json                # 项目依赖配置
├── postcss.config.mjs          # PostCSS配置
├── public/                     # 静态资源目录
│   ├── favicon.ico             # 网站图标
│   └── vercel.svg              # Vercel图标
└── tsconfig.json               # TypeScript配置

核心代码实现

Page.tsx 页面的 Viem 操作分析

项目的核心交互逻辑集中在 app/page.tsx 文件中,下面详细分析其中的关键操作:

支持多链配置

1// 支持的链
2const SUPPORTED_CHAINS = [foundry, sepolia] as const;
3
4const COUNTER_ADDRESS_FOUNDRY = '0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9';
5const COUNTER_ADDRESS_SEPOLIA = '0x7B6781B15b4f3eF8476af20Ed45Cf6d09e0Ef55F';
6
7function getCounterAddress(chainId: number) {
8  return chainId === foundry.id ? COUNTER_ADDRESS_FOUNDRY : COUNTER_ADDRESS_SEPOLIA;
9}

该项目支持多条链(Foundry 和 Sepolia 测试网),通过地址映射实现在不同网络上访问对应的合约实例。这种设计使得 DApp 能适应不同的开发和测试环境。

连接钱包

 1const connectWallet = async () => {
 2  if (typeof window.ethereum === 'undefined') {
 3    alert('请安装 MetaMask');
 4    return;
 5  }
 6  try {
 7    setIsLoading(true);
 8    const [address] = await window.ethereum.request({ method: 'eth_requestAccounts' });
 9    const chainId = await window.ethereum.request({ method: 'eth_chainId' });
10    setAddress(address as `0x${string}`);
11    setChainId(Number(chainId));
12    setIsConnected(true);
13  } catch (error) {
14    console.error('连接钱包失败:', error);
15  } finally {
16    setIsLoading(false);
17  }
18};

连接钱包功能通过直接调用 window.ethereum.request 方法实现,请求 eth_requestAccounts 方法获取用户授权的账户地址,同时获取当前链 ID。这种方式绕过了 wagmi 等高级抽象,直接使用 EIP-1193 标准与钱包通信。

获取钱包信息

 1// 读取余额
 2const fetchBalance = useCallback(async () => {
 3  if (!address || !chainId) return;
 4  try {
 5    const client = getPublicClient(chainId);
 6    const bal = await client.getBalance({ address });
 7    setBalance(formatEther(bal));
 8  } catch (err) {
 9    console.error('读取余额失败', err);
10  }
11}, [address, chainId]);
12
13function getPublicClient(chainId: number) {
14  const chain = SUPPORTED_CHAINS.find(c => c.id === chainId) ?? foundry;
15  return createPublicClient({
16    chain,
17    transport: http(),
18  }).extend(publicActions);
19}

获取钱包信息分为几个步骤:

  1. 创建 publicClient,用于与区块链进行只读交互
  2. 使用 client.getBalance 方法获取指定地址的余额
  3. 使用 formatEther 将 bigint 格式的余额转换为易读的 ETH 格式

读写智能合约

读取合约数据
 1const fetchCounterNumber = useCallback(async () => {
 2  if (!chainId) return;
 3  try {
 4    const client = getPublicClient(chainId);
 5    const contract = getContract({
 6      address: getCounterAddress(chainId),
 7      abi: Counter_ABI,
 8      client,
 9    });
10    const num = (await contract.read.number()) as bigint;
11    setCounterNumber(num.toString());
12  } catch (err) {
13    console.error('读取 Counter 失败', err);
14  }
15}, [chainId]);

读取合约数据的步骤:

  1. 创建 publicClient
  2. 使用 getContract 创建合约实例
  3. 调用 contract.read[functionName] 方法读取合约状态
写入合约数据
 1const handleIncrement = async () => {
 2  if (!address || !window.ethereum || !chainId) return;
 3  const walletClient = getWalletClient();
 4  if (!walletClient) return alert('钱包未连接');
 5  try {
 6    setIsLoading(true);
 7    const hash = await walletClient.writeContract({
 8      address: getCounterAddress(chainId),
 9      abi: Counter_ABI,
10      functionName: 'increment',
11      account: address,
12    });
13    console.log('Transaction hash:', hash);
14
15    const receipt = await walletClient.waitForTransactionReceipt({ hash: hash });
16    console.log(`交易状态: ${receipt.status === 'success' ? '成功' : '失败'}`);
17
18    // 更新数值显示
19    await fetchCounterNumber();
20  } catch (error) {
21    console.error('调用 increment 失败:', error);
22  } finally {
23    setIsLoading(false);
24  }
25};
26
27// 创建 walletClient(只在需要签名时创建)
28const getWalletClient = useCallback(() => {
29  if (!window.ethereum || !chainId || !address) return null;
30  const chain = SUPPORTED_CHAINS.find(c => c.id === chainId) ?? foundry;
31  return createWalletClient({
32    account: address,
33    chain,
34    transport: custom(window.ethereum),
35  }).extend(publicActions);
36}, [address, chainId]);

写入合约数据的步骤:

  1. 创建 walletClient,用于需要签名的交易
  2. 调用 writeContract 方法发送交易到合约
  3. 使用 waitForTransactionReceipt 等待交易确认
  4. 更新相关状态

时序图:

sequenceDiagram
    participant User as 用户(浏览器)
    participant Page as React 页面 (page.tsx)
    participant WalletClient as Viem WalletClient
    participant MetaMask as MetaMask 钱包
    participant Node as 节点 (Infura/Alchemy/Anvil)
    participant Chain as 区块链
    participant publicClient as publicClient 

    User->>Page: 点击 “+1” 按钮
    Page->>Page: 调用 handleIncrement()
    Page->>Page: getWalletClient() 创建 walletClient
    Page->>WalletClient: writeContract({ ..., functionName: 'increment' })
    WalletClient->>MetaMask: eth_sendTransaction (签名请求弹窗)
    MetaMask-->>User: 请确认交易…
    User->>MetaMask: 点击【确认】
    MetaMask->>WalletClient: 返回已签名的交易
    WalletClient->>Node: 广播交易 (hash)
    Node-->>Chain: 提交交易
    WalletClient-->>Page: 返回 transaction hash
    Page->>Page: console.log('Transaction hash:', hash)

    Note over Page,Chain: 等待上链确认
    Page->>WalletClient: waitForTransactionReceipt({ hash })
    WalletClient->>Node: 轮询 receipt
    Node-->>Chain: 区块已打包
    Node-->>WalletClient: 返回 receipt (status: success)
    WalletClient-->>Page: receipt
    Page->>Page: console.log('交易成功')

    Page->>Page: 调用 fetchCounterNumber()
    Page->>publicClient: readContract({ functionName: 'number' })
    publicClient->>Node: call (只读)
    Node-->>publicClient: 返回最新 number
    publicClient-->>Page: 返回新值
    Page->>Page: setCount(新值) → 页面更新

    Note over User,Chain: 整个过程用户只点了一次确认, 所有状态自动同步

断开连接

 1const disconnectWallet = useCallback(async () => {
 2  if (!address || !window.ethereum || !chainId) return;
 3  setIsConnected(false);
 4  setAddress(undefined);
 5  setChainId(undefined);
 6  setBalance('0');
 7  setCounterNumber('0');
 8  try {
 9    // 对于 MetaMask 10.28+
10    await window.ethereum.request({
11      method: 'wallet_revokePermissions',
12      params: [{ eth_accounts: {} }],
13    });
14    // 老版本 MM 会抛 4200 错误,捕获即可
15  } catch (e: unknown) {
16    if (typeof e === 'object' && e !== null && 'code' in e && (e as { code: unknown }).code === 4200) {
17      alert('请手动在钱包里断开本次连接');
18    }
19  }
20}, [address, chainId]);

断开连接功能不仅清空了本地状态,还通过 wallet_revokePermissions 方法撤销了对钱包的访问权限。

监听钱包操作

 1useEffect(() => {
 2  if (!window.ethereum) return;
 3
 4  const handleAccountsChanged = (accounts: string[]) => {
 5    console.log('账户变化', accounts);
 6    if (accounts.length === 0) {
 7      disconnectWallet().catch(console.error);
 8    } else {
 9      setAddress(accounts[0] as `0x${string}`);
10    }
11  };
12
13  const handleChainChanged = (chainIdHex: string) => {
14    console.log('网络变化', chainIdHex);
15    setChainId(Number(chainIdHex));
16  };
17
18  window.ethereum.on('accountsChanged', handleAccountsChanged);
19  window.ethereum.on('chainChanged', handleChainChanged);
20
21  return () => {
22    window.ethereum?.removeListener('accountsChanged', handleAccountsChanged);
23    window.ethereum?.removeListener('chainChanged', handleChainChanged);
24  };
25}, [address, disconnectWallet, fetchBalance, fetchCounterNumber]);

通过监听 accountsChangedchainChanged 事件,DApp 能够实时响应用户的账户切换和网络切换操作,保持应用状态与钱包状态的一致性。

Counter 智能合约分析

Counter 合约是一个简单的计数器合约,包含以下功能:

 1// SPDX-License-Identifier: UNLICENSED
 2pragma solidity ^0.8.13;
 3
 4contract Counter {
 5    uint256 public number;
 6
 7    function setNumber(uint256 newNumber) public {
 8        number = newNumber;
 9    }
10
11    function increment() public {
12        number++;
13    }
14}

该合约提供了两个主要功能:

  1. number() - 读取当前计数值(getter function)
  2. increment() - 将计数值加 1
  3. setNumber() - 设置新的计数值

纯 Viem 脚手架项目的架构优势

更细粒度的控制

使用纯 Viem 相比于 wagmi 等抽象库,开发者能够更精确地控制每个操作,了解底层通信逻辑,便于调试和优化。

轻量级实现

不引入额外的状态管理库,减少了项目体积,提高了加载速度。

灵活性更高

可以根据项目需求定制特定的交互逻辑,而不受高级抽象库的限制。

更好的 TypeScript 支持

Viem 提供了优秀的 TypeScript 类型推断,确保合约交互的类型安全。

更直观的 API 设计

Viem 的 API 设计更接近以太坊的原生操作,便于理解区块链交互的本质。

对比 wagmi 的核心差异

功能wagmi 写法(抽象)纯 Viem 写法
连接钱包useConnect()window.ethereum.request('eth_requestAccounts')
切换链/账号自动更新wagmi 自动手动监听 accountsChanged / chainChanged
读余额useBalance()publicClient.getBalance()
读合约useReadContract()publicClient.readContract()
发交易useWriteContract()walletClient.writeContract()
等待确认自动walletClient.waitForTransactionReceipt()

完整代码示例

下面是一个完整的示例,展示了如何使用纯 Viem 构建一个功能完整的 DApp:

  1'use client';
  2
  3import { useState, useEffect, useCallback } from 'react';
  4import { createPublicClient, createWalletClient, http, formatEther, getContract, custom, publicActions } from 'viem';
  5import { foundry, sepolia } from 'viem/chains';
  6import Counter_ABI from './abis/Counter.json';
  7
  8// 支持的链
  9const SUPPORTED_CHAINS = [foundry, sepolia] as const;
 10
 11// Counter 合约地址
 12const COUNTER_ADDRESS_FOUNDRY = '0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9';
 13const COUNTER_ADDRESS_SEPOLIA = '0x7B6781B15b4f3eF8476af20Ed45Cf6d09e0Ef55F';
 14
 15function getCounterAddress(chainId: number) {
 16  return chainId === foundry.id ? COUNTER_ADDRESS_FOUNDRY : COUNTER_ADDRESS_SEPOLIA;
 17}
 18
 19function getPublicClient(chainId: number) {
 20  const chain = SUPPORTED_CHAINS.find(c => c.id === chainId) ?? foundry;
 21  return createPublicClient({
 22    chain,
 23    transport: http(),
 24  }).extend(publicActions);
 25}
 26
 27export default function Home() {
 28  const [balance, setBalance] = useState<string>('0');
 29  const [counterNumber, setCounterNumber] = useState<string>('0');
 30  const [address, setAddress] = useState<`0x${string}` | undefined>();
 31  const [isConnected, setIsConnected] = useState(false);
 32  const [chainId, setChainId] = useState<number | undefined>();
 33  const [isLoading, setIsLoading] = useState(false);
 34
 35  const currentChain = SUPPORTED_CHAINS.find(c => c.id === chainId);
 36
 37  // 创建 walletClient(只在需要签名时创建)
 38  const getWalletClient = useCallback(() => {
 39    if (!window.ethereum || !chainId || !address) return null;
 40    const chain = SUPPORTED_CHAINS.find(c => c.id === chainId) ?? foundry;
 41    return createWalletClient({
 42      account: address,
 43      chain,
 44      transport: custom(window.ethereum),
 45    }).extend(publicActions);
 46  }, [address, chainId]);
 47
 48  // 获取 Counter 合约的数值
 49  const fetchCounterNumber = useCallback(async () => {
 50    if (!chainId) return;
 51    try {
 52      const client = getPublicClient(chainId);
 53      const contract = getContract({
 54        address: getCounterAddress(chainId),
 55        abi: Counter_ABI,
 56        client,
 57      });
 58      const num = (await contract.read.number()) as bigint;
 59      setCounterNumber(num.toString());
 60    } catch (err) {
 61      console.error('读取 Counter 失败', err);
 62    }
 63  }, [chainId]);
 64
 65  // 读取余额
 66  const fetchBalance = useCallback(async () => {
 67    if (!address || !chainId) return;
 68    try {
 69      const client = getPublicClient(chainId);
 70      const bal = await client.getBalance({ address });
 71      setBalance(formatEther(bal));
 72    } catch (err) {
 73      console.error('读取余额失败', err);
 74    }
 75  }, [address, chainId]);
 76
 77  // 连接钱包
 78  const connectWallet = async () => {
 79    if (typeof window.ethereum === 'undefined') {
 80      alert('请安装 MetaMask');
 81      return;
 82    }
 83    try {
 84      setIsLoading(true);
 85      const [address] = await window.ethereum.request({ method: 'eth_requestAccounts' });
 86      const chainId = await window.ethereum.request({ method: 'eth_chainId' });
 87      setAddress(address as `0x${string}`);
 88      setChainId(Number(chainId));
 89      setIsConnected(true);
 90    } catch (error) {
 91      console.error('连接钱包失败:', error);
 92    } finally {
 93      setIsLoading(false);
 94    }
 95  };
 96
 97  // 断开连接
 98  const disconnectWallet = useCallback(async () => {
 99    if (!address || !window.ethereum || !chainId) return;
100    setIsConnected(false);
101    setAddress(undefined);
102    setChainId(undefined);
103    setBalance('0');
104    setCounterNumber('0');
105    try {
106      // 对于 MetaMask 10.28+
107      await window.ethereum.request({
108        method: 'wallet_revokePermissions',
109        params: [{ eth_accounts: {} }],
110      });
111      // 老版本 MM 会抛 4200 错误,捕获即可
112    } catch (e: unknown) {
113      if (typeof e === 'object' && e !== null && 'code' in e && (e as { code: unknown }).code === 4200) {
114        alert('请手动在钱包里断开本次连接');
115      }
116    }
117  }, [address, chainId]);
118
119  // 调用 increment 函数
120  const handleIncrement = async () => {
121    if (!address || !window.ethereum || !chainId) return;
122    const walletClient = getWalletClient();
123    if (!walletClient) return alert('钱包未连接');
124    try {
125      setIsLoading(true);
126      const hash = await walletClient.writeContract({
127        address: getCounterAddress(chainId),
128        abi: Counter_ABI,
129        functionName: 'increment',
130        account: address,
131      });
132      console.log('Transaction hash:', hash);
133
134      const receipt = await walletClient.waitForTransactionReceipt({ hash: hash });
135      console.log(`交易状态: ${receipt.status === 'success' ? '成功' : '失败'}`);
136
137      // 更新数值显示
138      await fetchCounterNumber();
139    } catch (error) {
140      console.error('调用 increment 失败:', error);
141    } finally {
142      setIsLoading(false);
143    }
144  };
145
146  // 全局监听(只添加一次)
147  useEffect(() => {
148    if (!window.ethereum) return;
149
150    const handleAccountsChanged = (accounts: string[]) => {
151      console.log('账户变化', accounts);
152      if (accounts.length === 0) {
153        disconnectWallet().catch(console.error);
154      } else {
155        setAddress(accounts[0] as `0x${string}`);
156      }
157    };
158
159    const handleChainChanged = (chainIdHex: string) => {
160      console.log('网络变化', chainIdHex);
161      setChainId(Number(chainIdHex));
162    };
163
164    window.ethereum.on('accountsChanged', handleAccountsChanged);
165    window.ethereum.on('chainChanged', handleChainChanged);
166
167    return () => {
168      window.ethereum?.removeListener('accountsChanged', handleAccountsChanged);
169      window.ethereum?.removeListener('chainChanged', handleChainChanged);
170    };
171  }, [address, disconnectWallet, fetchBalance, fetchCounterNumber]);
172
173  // 连接后自动读取数据
174  useEffect(() => {
175    if (address && chainId) {
176      console.log('连接后自动读取数据:', address);
177      fetchBalance().catch(console.error);
178      fetchCounterNumber().catch(console.error);
179    }
180  }, [address, chainId, fetchBalance, fetchCounterNumber]);
181
182  return (
183    <div className='min-h-screen flex flex-col items-center justify-center p-8'>
184      <h1 className='text-3xl font-bold mb-8'>Simple Viem Demo</h1>
185
186      <div className='bg-white p-6 rounded-lg shadow-lg w-full max-w-2xl'>
187        {!isConnected ? (
188          <button
189            onClick={connectWallet}
190            disabled={isLoading}
191            className='w-full bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600 transition-colors'
192          >
193            {isLoading ? '连接中...' : '连接 MetaMask'}
194          </button>
195        ) : (
196          <div className='space-y-4'>
197            <div className='text-center'>
198              <p className='text-gray-600'>钱包地址:</p>
199              <p className='font-mono break-all'>{address}</p>
200            </div>
201            <div className='text-center'>
202              <p className='text-gray-600'>当前网络:</p>
203              <p className='font-mono'>
204                {currentChain?.name || '未知网络'} (Chain ID: {chainId})
205              </p>
206            </div>
207            <div className='text-center'>
208              <p className='text-gray-600'>余额:</p>
209              <p className='font-mono'>{balance} ETH</p>
210            </div>
211            <div className='text-center'>
212              <p className='text-gray-600'>Counter 数值:</p>
213              <p className='font-mono'>{counterNumber}</p>
214              <button
215                onClick={handleIncrement}
216                disabled={isLoading}
217                className='mt-2 w-full bg-green-500 text-white py-2 px-4 rounded hover:bg-green-600 transition-colors'
218              >
219                {isLoading ? '交易进行中...' : '增加计数'}
220              </button>
221            </div>
222            <button
223              onClick={disconnectWallet}
224              className='w-full bg-red-500 text-white py-2 px-4 rounded hover:bg-red-600 transition-colors'
225            >
226              断开连接
227            </button>
228          </div>
229        )}
230      </div>
231    </div>
232  );
233}

总结

本文详细分析了一个纯 Viem 脚手架项目,展示了如何使用 Viem 库直接与 MetaMask 钱包和智能合约进行交互,包括:

  1. 项目创建步骤和核心依赖
  2. 如何使用 Viem 实现多链支持
  3. 如何连接和断开 MetaMask 钱包
  4. 如何获取钱包信息和余额
  5. 如何读写智能合约
  6. 如何监听钱包状态变化
  7. 纯 Viem 实现相对于 wagmi 等库的优势

纯 Viem 方案为开发者提供了更底层的控制和更灵活的实现方式,适合需要深入了解区块链交互逻辑的开发者使用。这种架构不仅保持了代码的简洁性,还提供了更大的扩展空间。

参考资料

附录 - C4 架构模型

1. 系统上下文图 (System Context Diagram)

C4Context
    Boundary(b0, "Simple Viem 应用", "Web3 DApp 应用") {
        System(s1, "Web3 DApp 前端", "基于 Next.js 的 Web3 应用", "React + Next.js + Viem")
    }
    System_Ext(eth, "以太坊网络", "Ethereum Blockchain", "Ethereum Mainnet/Sepolia")
    System_Ext(user, "用户", "MetaMask 钱包用户", "DApp 用户")
    System_Ext(mm, "MetaMask", "浏览器钱包扩展", "钱包和交易签名")

    Rel(user, mm, "使用", "浏览器扩展")
    Rel(mm, s1, "连接", "JSON-RPC")
    Rel(s1, eth, "读写合约 | 查询账户 | 发送交易", "JSON-RPC")

2. 容器图 (Container Diagram)

C4Container
    Boundary(b0, "Simple Viem 系统", "Web3 DApp 系统") {
        Container(web, "Next.js Web 应用", "React/Next.js", "提供 Web3 DApp 用户界面", "Web 应用")
        Container(ant, "Ant Design UI", "UI 组件库", "提供现代化 UI 组件", "UI 框架")
        Container(viem, "Viem 客户端", "Viem", "Ethereum 客户端库", "区块链交互")
    }
    Boundary(b1, "Ethereum 网络", "区块链基础设施") {
        Container(contract, "Counter 智能合约", "Solidity", "简单的计数器合约", "EVM 合约")
        Container(eth_node, "Ethereum 节点", "JSON-RPC 节点", "提供区块链数据访问", "区块链节点")
    }
    System_Ext(user, "用户", "MetaMask 钱包用户", "DApp 用户")
    System_Ext(mm, "MetaMask", "浏览器钱包扩展", "钱包和交易签名")

    Rel(user, web, "使用", "HTTPS")
    Rel(web, ant, "使用", "React 组件")
    Rel(web, viem, "使用", "Viem API")
    Rel(viem, mm, "连接", "JSON-RPC")
    Rel(viem, eth_node, "查询/交易", "JSON-RPC")
    Rel(eth_node, contract, "执行", "EVM")
    Rel(contract, eth_node, "依赖", "区块链状态")

3. 组件图 (Component Diagram)

C4Component
    Boundary(b0, "Next.js Web 应用", "前端应用") {
        Component(layout, "Root Layout", "React Component", "应用根布局", "Next.js Layout")
        Component(home, "Home Component", "React Component", "主页面组件", "Next.js Page")
        Component(providers, "Providers", "React Component", "应用上下文提供者", "React Context")
        Component(viem_api, "Viem API 集成", "React Hooks", "区块链交互逻辑", "Viem 库")
        Component(contract_api, "合约接口", "ABI", "合约接口定义", "JSON ABI")
    }
    Boundary(b1, "Viem 客户端", "区块链交互层") {
        Component(pub_client, "Public Client", "Viem Client", "用于读取操作", "Viem")
        Component(wal_client, "Wallet Client", "Viem Client", "用于写入操作", "Viem")
        Component(contract_inst, "合约实例", "Viem Contract", "合约交互封装", "Viem")
    }
    Boundary(b2, "智能合约", "EVM 合约") {
        Component(counter, "Counter 合约", "Solidity Contract", "计数器功能", "Solidity")
    }

    Rel(home, layout, "使用", "React 组合")
    Rel(home, providers, "使用", "React Context")
    Rel(home, viem_api, "调用", "React Hooks")
    Rel(viem_api, pub_client, "创建", "Viem API")
    Rel(viem_api, wal_client, "创建", "Viem API")
    Rel(viem_api, contract_inst, "使用", "合约 ABI")
    Rel(contract_api, contract_inst, "定义", "ABI")
    Rel(contract_inst, counter, "交互", "Ethereum 调用")
    Rel(pub_client, counter, "读取", "Ethereum 调用")
    Rel(wal_client, counter, "写入", "Ethereum 调用")

4. 代码图 (Code Diagram)

C4Component
    Container_Boundary(NextjsApp, "Next.js 应用") {
        Container(app, "app 目录", "Next.js App Router")
        Container_Boundary(appAbis, "abis") {
            Component(counterAbi, "Counter.json", "ABI 文件")
        }
        Container_Boundary(appTypes, "types") {
            Component(ethD, "ethereum.d.ts", "类型定义")
        }
        Component(layout, "layout.tsx", "根布局")
        Component(page, "page.tsx", "主页组件")
        Component(providers, "providers.tsx", "提供者")
        Component(pkg, "package.json", "项目依赖")
    }

    Container_Boundary(Contracts, "智能合约") {
        Container(contracts, "contracts 目录", "Foundry")
        Container_Boundary(src, "src") {
            Component(counterSol, "Counter.sol", "计数器合约")
        }
        Component(foundryToml, "foundry.toml", "Foundry 配置")
    }

    Rel(page, counterAbi, "导入", "ABI")
    Rel(page, counterSol, "对应", "合约接口")
    Rel(pkg, page, "包含", "React 组件")
    Rel(foundryToml, counterSol, "编译", "Foundry")

5. 部署图 (Deployment Diagram)

C4Deployment
    Deployment_Node(DevEnv, "开发环境") {
        Deployment_Node(Browser, "浏览器") {
            Deployment_Node(NextApp, "Next.js 应用") {
                Container(NextjsApp, "Next.js Web 应用", "React & Next.js", "提供 Web3 DApp 用户界面")
                Container(AntDesign, "Ant Design UI", "UI 组件库", "提供现代化 UI 组件")
                Container(ViemClient, "Viem 客户端", "Viem", "Ethereum 客户端库")
            }
            Deployment_Node(Wallet, "MetaMask 钱包") {
                Container(MetaMask, "MetaMask", "钱包扩展", "提供账户管理")
            }
        }

        Deployment_Node(Blockchain, "区块链网络") {
            Deployment_Node(LocalNode, "本地节点 [Foundry]", "Anvil", "本地开发链") {
                Container(LocalContract, "Counter 合约", "Solidity", "Foundry 本地合约")
            }
            Deployment_Node(TestNode, "测试节点 [Sepolia]", "RPC", "Sepolia 测试网") {
                Container(TestContract, "Counter 合约", "Solidity", "Sepolia 合约")
            }
        }
    }

    Rel(NextjsApp, MetaMask, "JSON-RPC 调用", "EIP-1193")
    Rel(ViemClient, LocalContract, "读写调用", "HTTP")
    Rel(ViemClient, TestContract, "读写调用", "HTTP")

6. 交互时序图 (Sequence Diagram)

sequenceDiagram
    participant U as 用户
    participant W as MetaMask
    participant A as Next.js 应用
    participant V as Viem 客户端
    participant C as Counter 合约

    U->>+A: 访问 DApp
    A->>+W: 请求连接钱包
    W->>-A: 返回账户地址
    A->>-U: 显示连接状态

    U->>+A: 点击"增加计数"
    A->>+V: 创建 walletClient
    V->>+W: 请求交易签名
    W->>-V: 返回签名
    V->>+C: 发送 increment 交易
    C-->>-V: 交易确认
    V-->>-A: 交易完成
    A->>-U: 更新计数值显示