Ethers 是一个用于与以太坊区块链进行交互的 JavaScript 库。它提供了一套简洁且功能强大的工具,用于处理以太坊账户、交易、智能合约等诸多方面的操作。无论是开发去中心化应用(DApp),还是进行区块链相关的工具开发如钱包等,Ethers 都扮演着重要的角色。
这是对 Ethers 的一个非常简短的介绍,但涵盖了开发者需要的许多常见操作,并为以太坊新手提供了起点。
npm install ethers
Ethers 中的所有内容都从其根目录导出,也可以通过 ethers
对象访问。package.json
中还有导出配置,方便更细粒度的导入。
// 导入全部内容
import { ethers } from "ethers";
// 仅导入特定项
import { BrowserProvider, parseUnits } from "ethers";
// 从特定模块导入
import { HDNodeWallet } from "ethers/wallet";
<script type="module">
import { ethers } from "https://cdnjs.cloudflare.com/ajax/libs/ethers/6.7.0/ethers.min.js";
// 你的代码...
</script>
首先,从高层了解可用对象的类型及其职责是有用的。
call
评估只读代码。Wallet
),或通过 IPC 层保护(如 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();
}
如果运行自己的以太坊节点(如 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'
拥有提供商后,可只读访问区块链数据,用于查询账户状态、获取历史日志、查找合约代码等。
// 查询当前区块号
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
向区块链写入需访问控制账户的私钥。多数情况下,私钥不直接暴露给代码,而是通过签名者向服务(如 MetaMask)发送请求,该服务严格控制访问并需用户反馈以批准操作。
// 发送交易时,值为 wei,用 parseEther 转换
const tx = await signer.sendTransaction({
to: "ethers.eth",
value: parseEther("1.0")
});
// 等待交易被打包
const receipt = await tx.wait();
合约是元类,其定义在运行时基于传入的 ABI 推导,ABI 决定了可用的方法和属性。
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);
只读方法(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'
状态变更方法
修改 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
为命名事件添加监听器时,事件参数会被解构传递给监听器。监听器始终会收到一个额外参数 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
});
在大范围区块中查询时,某些后端可能极慢、返回错误或截断结果(由后端决定)。
查询历史 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
// 第一个事件包含地址、区块哈希、交易哈希等详细信息
私钥不仅能签名交易,还能签名其他数据,用于验证所有权等场景。
例如,签名消息可证明账户所有权,供网站用于用户认证和登录。
// 本地钱包(测试用,生产环境用 MetaMask 等)
const signer = new ethers.Wallet(privateKey);
const message = "sign into ethers.org?";
// 签名消息
const sig = await signer.signMessage(message);
// 验证签名,返回账户地址
ethers.verifyMessage(message, sig); // '0xC08B5542D177ac6686946920409741463a15dDdB'
view
/pure
)。ethers.getDefaultProvider()
(默认第三方节点)、BrowserProvider
(MetaMask 只读)、JsonRpcProvider
(自定义 RPC)。provider.getSigner()
(如 MetaMask 签名者)、new Wallet(privateKey)
(本地钱包,慎用)。sendTransaction()
(发送交易)、signMessage()
(消息签名)。为什么重要?
区块链内部使用最小单位(如 wei),而用户输入 / 显示需十进制(如 1 ETH = 1e18 wei)。直接操作大数易出错,ethers 提供工具函数:
parseEther("1.0")
:字符串转 wei(适用于 ETH)。parseUnits("1.0", 18)
:自定义小数位数(适用于 ERC-20 代币,如 DAI 小数位 18)。formatEther(weiValue)
/formatUnits(weiValue, "gwei")
:反向转换为可读格式。"function transfer(...)"
)。new Contract(address, abi, provider)
(用于查询)。new Contract(address, abi, signer)
(用于状态变更,需签名者权限)。contract.symbol()
)。tx.wait()
等待确认(交易上链)。to
、value
、gasLimit
等参数(value
需转 wei)。signer.sendTransaction(transaction)
,此时交易未确认。tx.wait([confirmations])
,默认等待 1 个区块确认,确保交易被打包。receipt
包含交易状态(receipt.status === 1
表示成功)、事件日志等。contract.on("事件名", 回调)
,参数自动解构(如 Transfer
事件的 from
、to
、amount
)。contract.filters.事件名(参数)
可缩小监听范围(如仅监听 to
为某地址的事件)。contract.queryFilter(filter, fromBlock, toBlock)
:按区块范围查询事件,返回 EventLog
数组,包含区块哈希、交易哈希、解码后的参数等。BrowserProvider
)。Wallet
,但生产环境必须通过安全的签名者(如硬件钱包、托管服务)。tx.wait()
等待交易确认,避免处理未打包的交易。balanceOf
时,ABI 中仅定义该方法)。通过掌握上述模块,开发者可快速上手以太坊开发,从简单的余额查询到复杂的合约交互和事件监听,逐步构建健壮的区块链应用。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!