以太坊前端交互库:Ethers.js 核心功能快速入门指南

Ethers 是一个用于与以太坊区块链进行交互的 JavaScript 库。它提供了一套简洁且功能强大的工具,用于处理以太坊账户、交易、智能合约等诸多方面的操作。无论是开发去中心化应用(DApp),还是进行区块链相关的工具开发如钱包等,Ethers 都扮演着重要的角色。

入门指南

这是对 Ethers 的一个非常简短的介绍,但涵盖了开发者需要的许多常见操作,并为以太坊新手提供了起点。

一、安装Ethers

1.NPM

npm install ethers

Ethers 中的所有内容都从其根目录导出,也可以通过 ethers 对象访问。package.json 中还有导出配置,方便更细粒度的导入。

// 导入全部内容
import { ethers } from "ethers";

// 仅导入特定项
import { BrowserProvider, parseUnits } from "ethers";

// 从特定模块导入
import { HDNodeWallet } from "ethers/wallet";

2.CDN

<script type="module">
  import { ethers } from "https://cdnjs.cloudflare.com/ajax/libs/ethers/6.7.0/ethers.min.js";
  // 你的代码...
</script>

二、常见术语

首先,从高层了解可用对象的类型及其职责是有用的。

  1. Provider(提供商) 提供商是与区块链的只读连接,允许查询区块链状态,如账户、区块或交易详情,查询事件日志,或使用 call 评估只读代码。
  2. Signer(签名者) 签名者封装了与账户交互的所有操作。账户通常在某处存储私钥,用于对各种类型的有效载荷进行签名。 私钥可能存储在内存中(使用 Wallet),或通过 IPC 层保护(如 MetaMask,它将网站的交互代理到浏览器插件,使私钥无法被网站获取,仅在用户授权后允许交互)。
  3. Transaction(交易) 要对区块链进行状态变更,必须发送交易,交易需要支付费用,该费用覆盖执行交易的相关成本(如读取磁盘、执行计算)和存储更新信息的成本。 如果交易回滚,仍需支付费用,因为验证者必须消耗资源尝试运行交易以确定回滚,且失败细节仍会被记录。 交易包括:向其他用户发送以太币、部署合约或对合约执行状态变更操作。
  4. Contract(合约) 合约是部署到区块链的程序,包含代码和分配的存储,可以读取和写入存储。 连接到提供商时可以读取合约,连接到签名者时可以调用状态变更操作。
  5. Receipt(收据) 交易提交到区块链后,会被放入内存池(mempool),直到验证者决定打包。 交易的变更仅在被打包到区块链后生效,此时会生成收据,包含交易详情,如所在区块、实际支付的费用、使用的 gas、发出的所有事件以及是否成功 / 回滚。

三、连接到以太坊

与区块链交互的第一步是使用提供商连接到它。

1.MetaMask(及其他注入式钱包)

在以太坊上进行实验和开发的最快最简单方法是使用 MetaMask,它是一个浏览器扩展,向窗口注入对象,提供:

  • 对以太坊网络的只读访问(提供商)
  • 由私钥支持的认证写访问(签名者)

    当请求访问认证方法(如发送交易或请求私钥地址)时,MetaMask 会向用户显示弹窗请求权限。

let signer = null;
let provider;

if (window.ethereum == null) {
  // 如果未安装 MetaMask,使用默认提供商(基于第三方服务如 INFURA),无读写权限
  console.log("MetaMask 未安装;使用只读默认提供商");

  provider = ethers.getDefaultProvider();
} else {
  // 连接到 MetaMask 的 EIP-1193 对象(标准协议,允许 Ethers 通过 MetaMask 进行只读请求)
  provider = new ethers.BrowserProvider(window.ethereum);

  // 请求写权限,使用 MetaMask 管理的私钥
  signer = await provider.getSigner();
}

2.自定义 RPC 后端

如果运行自己的以太坊节点(如 Geth)或使用自定义第三方节点服务(如 INFURA),可以直接使用 JsonRpcProvider,通过 JSON-RPC 协议通信。

注意:使用自己的节点或开发链(如 Hardhat、Ganache)时,可通过 JsonRpcProvider-getSigner 访问账户。(如果不传参数默认拿到第一个账户,如果想要指定账户,传递地址即可)

连接到 JSON-RPC URL

// 若未提供 url,默认连接到 http://localhost:8545(多数节点使用)
provider = new ethers.JsonRpcProvider(url);

// 通过签名者获取账户写权限
signer = await provider.getSigner();

// 通过签名者获取指定账户写权限(如 Hardhat、Ganache)时)
signer = await provider.getSigner('账户地址');

四、单位转换

以太坊中的所有单位倾向于使用整数,因为处理小数和浮点数在数学运算时可能导致不精确和意外结果。因此,内部使用的单位(如 wei)适合机器读取和数学运算,但通常非常大,不便于人类阅读。

例如,处理美元和美分:显示为 "$2.56",而区块链内部以美分为单位存储,即 256 美分。 因此,接受用户输入的数据时,需将十进制字符串(如 "2.56")转换为最小单位整数(如 256);向用户显示值时则反向操作。

在以太坊中,1 ether = 10^18 wei,1 gwei = 10^9 wei,值会迅速变大,因此提供了便利函数帮助转换。

// 将用户输入的以太字符串转为 wei
const eth = parseEther("1.0"); // 1000000000000000000n

// 将用户输入的 gwei 字符串转为 wei(用于最大基础费用)
const feePerGas = parseUnits("4.5", "gwei"); // 4500000000n

// 将 wei 值转为以太字符串显示
formatEther(eth); // '1.0'

// 将 wei 值转为 gwei 字符串显示
formatUnits(feePerGas, "gwei"); // '4.5'

五、与区块链交互

1.查询状态

拥有提供商后,可只读访问区块链数据,用于查询账户状态、获取历史日志、查找合约代码等。

// 查询当前区块号
await provider.getBlockNumber(); // 22350180

// 获取账户余额(通过地址或 ENS 名称)
const balance = await provider.getBalance("ethers.eth"); // 4085267032476673080n

// 将 wei 转为以太显示
formatEther(balance); // '4.08526703247667308'

// 获取发送交易所需的下一个 Nonce
await provider.getTransactionCount("ethers.eth"); // 2

2.发送交易

向区块链写入需访问控制账户的私钥。多数情况下,私钥不直接暴露给代码,而是通过签名者向服务(如 MetaMask)发送请求,该服务严格控制访问并需用户反馈以批准操作。

// 发送交易时,值为 wei,用 parseEther 转换
const tx = await signer.sendTransaction({
  to: "ethers.eth",
  value: parseEther("1.0")
});

// 等待交易被打包
const receipt = await tx.wait();

3.调用合约

合约是元类,其定义在运行时基于传入的 ABI 推导,ABI 决定了可用的方法和属性。

  1. Application Binary Interface (ABI)应用二进制接口(ABI)

    区块链上的所有操作必须编码为二进制数据,因此需要一种简洁的方式定义常见对象(如字符串、数字)与二进制表示的转换,以及合约调用和解释的编码方式。 对于需要使用的任何方法、事件或错误,必须包含一个片段(Fragment)告知 Ethers 如何编码请求和解码结果。无需的方法或事件可安全排除。 有几种常见格式描述 ABI

    Solidity 编译器通常输出 JSON 表示

    手动编写时使用人类可读的 ABI(即 Solidity 签名)更简单易读。

    简化的 ERC-20 ABI

    const abi = [
      "function decimals() view returns (string)",
      "function symbol() view returns (string)",
      "function balanceOf(address addr) view returns (uint)"
    ];
    
    // 创建合约实例(连接到提供商,只读)
    const contract = new ethers.Contract("dai.tokens.ethers.eth", abi, provider);
  2. 只读方法(View 和 Pure)

    只读方法不改变区块链状态,通常提供查询合约重要数据的简单接口。

    读取 DAI ERC-20 合约

    const abi = [
      "function decimals() view returns (uint8)",
      "function symbol() view returns (string)",
      "function balanceOf(address a) view returns (uint)"
    ];
    
    // 连接到提供商的合约(只读)
    const contract = new ethers.Contract("dai.tokens.ethers.eth", abi, provider);
    
    // 获取代币符号
    const sym = await contract.symbol(); // 'DAI'
    
    // 获取小数位数
    const decimals = await contract.decimals(); // 18n
    
    // 获取账户余额(wei)
    const balance = await contract.balanceOf("ethers.eth"); // 4000000000000000000000n
    
    // 转为人类可读格式(如 UI 显示)
    formatUnits(balance, decimals); // '4000.0'
  3. 状态变更方法

    修改 ERC-20 合约状态

    const abi = [
      "function transfer(address to, uint amount)"
    ];
    
    // 连接到签名者的合约(可发起状态变更,消耗账户以太币)
    const contract = new ethers.Contract("dai.tokens.ethers.eth", abi, signer);
    
    // 发送 1 DAI(18 位小数)
    const amount = parseUnits("1.0", 18);
    
    // 发送交易
    const tx = await contract.transfer("ethers.eth", amount);
    
    // 等待交易确认
    await tx.wait();

    强制调用(模拟)状态变更方法

    const abi = [
      "function transfer(address to, uint amount) returns (bool)"
    ];
    
    // 连接到提供商(仅需只读访问)
    const contract = new ethers.Contract("dai.tokens.ethers.eth", abi, provider);
    
    const amount = parseUnits("1.0", 18);
    
    // 静态调用模拟交易(不实际发送,用于预检查)
    await contract.transfer.staticCall("ethers.eth", amount); // true

4.监听事件

为命名事件添加监听器时,事件参数会被解构传递给监听器。监听器始终会收到一个额外参数 event(事件有效载荷),包含事件的更多信息(如过滤器、移除监听器的方法)。

监听 ERC-20 事件

const abi = [
  "event Transfer(address indexed from, address indexed to, uint amount)"
];

const contract = new ethers.Contract("dai.tokens.ethers.eth", abi, provider);

// 监听所有 Transfer 事件
contract.on("Transfer", (from, to, _amount, event) => {
  const amount = formatUnits(_amount, 18);
  console.log(`${from} => ${to}: ${amount}`);
  // event.log 包含完整的 EventLog 对象
  // 可选:移除监听器
  event.removeListener();
});

// 使用过滤器监听特定事件(如 to 为 ethers.eth)
const filter = contract.filters.Transfer(null, "ethers.eth");
contract.on(filter, (from, to, amount, event) => {
  // to 始终为 ethers.eth 的地址
});

// 监听所有事件(无论是否在 ABI 中,参数不解构)
contract.on("*", (event) => {
  // event.log 包含完整的 EventLog
});

5.查询历史事件

在大范围区块中查询时,某些后端可能极慢、返回错误或截断结果(由后端决定)。

查询历史 ERC-20 事件

const abi = [
  "event Transfer(address indexed from, address indexed to, uint amount)"
];

const contract = new ethers.Contract("dai.tokens.ethers.eth", abi, provider);

// 查询最近 100 个区块的 Transfer 事件
const filter = contract.filters.Transfer;
const events = await contract.queryFilter(filter, -100);

// 事件是普通数组
events.length; // 144
// 第一个事件包含地址、区块哈希、交易哈希等详细信息

6.消息签名

私钥不仅能签名交易,还能签名其他数据,用于验证所有权等场景。

例如,签名消息可证明账户所有权,供网站用于用户认证和登录。

// 本地钱包(测试用,生产环境用 MetaMask 等)
const signer = new ethers.Wallet(privateKey);

const message = "sign into ethers.org?";

// 签名消息
const sig = await signer.signMessage(message);

// 验证签名,返回账户地址
ethers.verifyMessage(message, sig); // '0xC08B5542D177ac6686946920409741463a15dDdB'

六、详细讲解

1. 核心架构:Provider vs Signer

  • Provider(只读)
    • 职责:查询区块链状态(余额、区块、事件日志)、调用合约只读方法(view/pure)。
    • 示例:ethers.getDefaultProvider()(默认第三方节点)、BrowserProvider(MetaMask 只读)、JsonRpcProvider(自定义 RPC)。
    • 无状态:不涉及私钥,仅用于读取。
  • Signer(读写)
    • 职责:签名交易、发起状态变更(需私钥),封装账户操作(如 MetaMask 代理签名,避免私钥暴露)。
    • 获取方式:provider.getSigner()(如 MetaMask 签名者)、new Wallet(privateKey)(本地钱包,慎用)。
    • 核心方法:sendTransaction()(发送交易)、signMessage()(消息签名)。

2. 单位转换:避免精度问题

  • 为什么重要?

    区块链内部使用最小单位(如 wei),而用户输入 / 显示需十进制(如 1 ETH = 1e18 wei)。直接操作大数易出错,ethers 提供工具函数:

    • parseEther("1.0"):字符串转 wei(适用于 ETH)。
    • parseUnits("1.0", 18):自定义小数位数(适用于 ERC-20 代币,如 DAI 小数位 18)。
    • formatEther(weiValue)/formatUnits(weiValue, "gwei"):反向转换为可读格式。

3. 合约操作:ABI 是桥梁

  • ABI 作用: 定义合约方法、事件的编码 / 解码规则,是 Ethers 与合约交互的 “翻译器”。支持两种格式:
    1. Solidity 签名(人类可读,如 "function transfer(...)")。
    2. JSON 格式(Solidity 编译器输出,包含更多细节)。
  • 合约实例创建
    • 只读:new Contract(address, abi, provider)(用于查询)。
    • 写:new Contract(address, abi, signer)(用于状态变更,需签名者权限)。
  • 方法调用区别
    • 只读方法:直接调用,返回 Promise(如 contract.symbol())。
    • 状态变更方法:返回交易对象,需调用 tx.wait() 等待确认(交易上链)。

4. 交易生命周期

  1. 构造交易:指定 tovaluegasLimit 等参数(value 需转 wei)。
  2. 发送到 mempoolsigner.sendTransaction(transaction),此时交易未确认。
  3. 等待确认tx.wait([confirmations]),默认等待 1 个区块确认,确保交易被打包。
  4. 获取收据receipt 包含交易状态(receipt.status === 1 表示成功)、事件日志等。

5. 事件处理:实时监听与历史查询

  • 实时监听
    • 使用 contract.on("事件名", 回调),参数自动解构(如 Transfer 事件的 fromtoamount)。
    • 过滤器 contract.filters.事件名(参数) 可缩小监听范围(如仅监听 to 为某地址的事件)。
  • 历史查询
    • contract.queryFilter(filter, fromBlock, toBlock):按区块范围查询事件,返回 EventLog 数组,包含区块哈希、交易哈希、解码后的参数等。

6. 安全与最佳实践

  • 私钥管理
    • 避免在前端直接存储私钥,使用 MetaMask 等钱包代理签名(通过 BrowserProvider)。
    • 本地开发测试时使用 Wallet,但生产环境必须通过安全的签名者(如硬件钱包、托管服务)。
  • 交易确认
    • 永远调用 tx.wait() 等待交易确认,避免处理未打包的交易。
  • ABI 最小化
    • 仅包含所需的方法 / 事件片段,减少冗余(如只需 balanceOf 时,ABI 中仅定义该方法)。

总结:ethers.js 的设计哲学

  1. 分离关注点:读写分离(Provider 只读,Signer 负责签名),代码更清晰、安全。
  2. 开发者友好:内置工具函数(单位转换、ABI 解析)、灵活的导入方式、对 MetaMask 等钱包的原生支持。
  3. 轻量高效:相比 Web3.js,体积更小,专注底层功能,适合构建复杂工具(如钱包、DApp 前端)。

通过掌握上述模块,开发者可快速上手以太坊开发,从简单的余额查询到复杂的合约交互和事件监听,逐步构建健壮的区块链应用。

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Revel.eth
Revel.eth
0x4E7a...F3f3
生活如区块链,层层叠加,转瞬即逝