viem / ethers.js 读写链
卷二我们手写过 JSON-RPC,理解了底层。日常开发当然不会那么原始——用 SDK。这一章把两个最主流的库(viem 与 ethers.js)拆成两个核心概念:Provider(只读连接)和 Signer(能签名的账户)。抓住这两个,读链写链的所有代码都顺理成章。
两个核心角色
回顾第 7 章的读写分水岭,SDK 把它具象成两个对象:
| Provider(公共客户端) | Signer(钱包客户端) | |
|---|---|---|
| 能干什么 | 只读:查余额、查状态、call、订阅事件 | 读 + 写:签名并发送交易 |
| 需要私钥吗 | 不需要 | 需要(由钱包或私钥提供) |
| 花钱吗 | 不花 | 写操作花 gas |
| 来自哪 | 一个 RPC 端点 URL | MetaMask 注入 / 本地私钥 |
Provider 像一个只读的 Repository:你能查询,但改不了后端数据。Signer 像带了认证 token、有写权限的那个客户端——它能提交会改变服务器状态的请求。在 dApp 里,读用户资产、渲染界面走 Provider;用户点"确认交易"时才用 Signer 去签名发送。
viem:现代、类型安全的首选
import { createPublicClient, http, formatEther } from 'viem'; import { mainnet } from 'viem/chains'; const client = createPublicClient({ chain: mainnet, transport: http('https://your-rpc-url'), }); // 查余额(只读,免费)—— 底层就是 eth_getBalance const wei = await client.getBalance({ address: '0xAlice…' }); console.log(formatEther(wei), 'ETH'); // formatEther 帮你除以 1e18
const balance = await client.readContract({ address: TOKEN_ADDRESS, abi: erc20Abi, // 第 8 章的 ABI,SDK 靠它编解码 functionName: 'balanceOf', args: ['0xAlice…'], });
写操作:需要 Signer
import { createWalletClient, custom } from 'viem'; const wallet = createWalletClient({ chain: mainnet, transport: custom(window.ethereum), // MetaMask 注入的对象(第 19 章) }); const [account] = await wallet.requestAddresses(); // 发交易:调 transfer(会弹钱包让用户确认、花 gas) const hash = await wallet.writeContract({ account, address: TOKEN_ADDRESS, abi: erc20Abi, functionName: 'transfer', args: ['0xBob…', 1000000n], }); // hash 是交易哈希;再用 client.waitForTransactionReceipt 等确认
ethers.js:老牌,同一套心智
ethers 概念完全对应,只是命名不同(Provider / Signer / Contract):
import { BrowserProvider, Contract, formatEther } from 'ethers'; const provider = new BrowserProvider(window.ethereum); const signer = await provider.getSigner(); // 只读用 provider,写用 signer const token = new Contract(TOKEN_ADDRESS, erc20Abi, signer); const bal = await token.balanceOf(addr); // 读 const tx = await token.transfer(to, 1000000n); // 写,返回交易对象 await tx.wait(); // 等确认
| 概念 | viem | ethers |
|---|---|---|
| 只读连接 | PublicClient | Provider |
| 能签名的账户 | WalletClient | Signer |
| 读合约 | readContract | contract.fn() |
| 写合约 | writeContract | contract.fn() (带 signer) |
| 风格 | 函数式、TS 优先、体积小 | 面向对象、生态老、文档多 |
新项目一般推荐 viem(类型安全、tree-shaking 友好、常配 wagmi 做 React hooks),但 ethers 依然到处都是。两者你都该能读。
永远记得处理"用户拒绝签名"和"交易失败"。用户在钱包弹窗点"取消"会抛错,别让 UI 卡死。交易发出后 wait() 前有段"pending"时间,要给明确反馈(第 4 章:可能几秒也可能很久)。还有单位:链上是 bigint(wei),显示要 format、输入要 parse,别用普通 number 处理金额,会精度丢失。
无论 viem 还是 ethers,心智模型只有两个:Provider/PublicClient 负责只读(不花钱、不用私钥),Signer/WalletClient 负责签名写入(花 gas、需钱包)。合约实例 = 地址 + ABI。读渲染走前者,用户确认的写操作走后者。SDK 只是把第 7 章的 JSON-RPC 包成类型安全的调用而已。下一章我们补上最关键的一环:怎么让用户的钱包连上你的 dApp,以及签名的几种姿势。
本章小结
- SDK 的两个核心对象:Provider/PublicClient(只读)与 Signer/WalletClient(签名写入)。
- 合约实例 = 地址 + ABI;读走 provider 免费,写走 signer 花 gas 并需确认。
- viem(现代、TS 优先、常配 wagmi)与 ethers(老牌、生态成熟)概念一一对应。
- 金额用 bigint/wei 处理,显示 format、输入 parse;务必处理用户拒签与交易失败。