ethers.js 是以太坊生态中最受欢迎的JavaScript库之一,用于与以太坊区块链交互。2022年底发布的 v6 版本带来了重大架构重构,性能大幅提升,API更加简洁一致。掌握 ethers.js v6 是每个 Web3 开发者的必备技能。
本文将系统讲解 ethers.js v6 的核心概念、常用 API 和实战技巧,帮助你快速上手现代以太坊 DApp 开发。

v6 核心架构解析
Provider 与 Signer 的分离
v6 版本最重要的架构变化是明确分离了 Provider(只读)和 Signer(可写):
- Provider:只读接口,用于查询区块链状态、读取合约数据
- Signer:可写接口,用于签署交易、发送交易、签名消息
javascript
import { ethers } from "ethers";
// Provider:只读,连接公共节点
const provider = new ethers.JsonRpcProvider("https://eth.llamarpc.com");
// Signer:可签名交易
// 通过MetaMask等钱包连接时获得
const signer = await provider.getSigner();
// 或者通过私钥创建Signer
const wallet = new ethers.Wallet("0x...", provider);
const signerFromWallet = wallet.connect(provider);
地址与合约的抽象
v6 进一步抽象了地址概念,引入 AddressLike 和 ContractLike 类型注解,可以接受地址字符串或 ENS 名称:
javascript
// 使用ENS名称
const ensName = " vitalik.eth";
// 直接使用地址
const address = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045";
// 两者都可以作为地址参数
async function getBalance(address) {
return await provider.getBalance(address);
}
await getBalance(ensName); // works!
await getBalance(address); // works!
Provider 实战
连接不同网络
javascript
import { ethers } from "ethers";
// Ethereum Mainnet
const mainnetProvider = new ethers.JsonRpcProvider(
"https://eth.llamarpc.com"
);
// Sepolia Testnet
const sepoliaProvider = new ethers.JsonRpcProvider(
"https://rpc.sepolia.org"
);
// Polygon
const polygonProvider = new ethers.JsonRpcProvider(
"https://polygon-rpc.com"
);
// Base
const baseProvider = new ethers.JsonRpcProvider(
"https://mainnet.base.org"
);
// 使用Infura
const infuraProvider = new ethers.JsonRpcProvider(
`https://mainnet.infura.io/v3/${process.env.INFURA_API_KEY}`
);
// 使用Alchemy
const alchemyProvider = new ethers.JsonRpcProvider(
`https://eth-mainnet.g.alchemy.com/v2/${process.env.ALCHEMY_API_KEY}`
);
基础查询 API
javascript
// 获取当前区块号
const blockNumber = await provider.getBlockNumber();
console.log(`Current block: ${blockNumber}`);
// 获取余额
const balance = await provider.getBalance("0x...");
console.log(`Balance: ${ethers.formatEther(balance)} ETH`);
// 获取Gas价格
const feeData = await provider.getFeeData();
console.log(`Gas Price: ${ethers.formatUnits(feeData.gasPrice, "gwei")} gwei`);
console.log(`Max Fee: ${ethers.formatUnits(feeData.maxFeePerGas, "gwei")} gwei`);
console.log(`Max Priority Fee: ${ethers.formatUnits(feeData.maxPriorityFeePerGas, "gwei")} gwei`);
// 获取区块信息
const block = await provider.getBlock(blockNumber);
console.log(`Block timestamp: ${new Date(block.timestamp * 1000)}`);
console.log(`Transactions: ${block.transactions.length}`);
// 获取交易收据
const receipt = await provider.getTransactionReceipt("0x...");
console.log(`Status: ${receipt.status === 1 ? "Success" : "Failed"}`);
console.log(`Gas Used: ${receipt.gasUsed}`);
// 获取代码(验证合约是否存在)
const code = await provider.getCode("0x...");
console.log(`Has code: ${code !== "0x"}`);
监听事件
javascript
// 监听新区块
provider.on("block", (blockNumber) => {
console.log(`New block: ${blockNumber}`);
});
// 监听待处理交易
provider.on("pending", (txHash) => {
console.log(`Pending tx: ${txHash}`);
});
// 监听特定地址的转账
const filter = {
address: "0x...", // 代币合约地址
topics: [
ethers.id("Transfer(address,address,uint256)"),
ethers.ZeroHash, // from address (零地址表示铸币)
ethers.getAddress("0x...".padEnd(66, "0")) // to address
]
};
provider.on(filter, (log) => {
console.log("Transfer detected:", log);
});
// 移除监听器
function handleBlock(blockNumber) {
console.log(`Block: ${blockNumber}`);
}
provider.on("block", handleBlock);
provider.off("block", handleBlock);
Signer 实战
连接钱包
javascript
// 浏览器环境中从 window.ethereum 获取
async function connectWallet() {
if (typeof window.ethereum !== "undefined") {
try {
// 请求钱包连接
const accounts = await window.ethereum.request({
method: "eth_requestAccounts"
});
console.log("Connected:", accounts[0]);
// 创建Signer
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
return { provider, signer, account: accounts[0] };
} catch (error) {
console.error("Connection failed:", error);
}
} else {
console.log("Please install MetaMask!");
}
}
// 监听账户变化
window.ethereum.on("accountsChanged", (accounts) => {
if (accounts.length === 0) {
console.log("Please connect to MetaMask.");
} else {
console.log("Account changed to:", accounts[0]);
}
});
// 监听网络变化
window.ethereum.on("chainChanged", (chainId) => {
console.log("Network changed, chainId:", parseInt(chainId, 16));
window.location.reload();
});
发送交易
javascript
async function sendTransaction(signer) {
const address = await signer.getAddress();
// 获取当前Gas价格
const feeData = await signer.provider.getFeeData();
// 构建交易
const tx = {
to: "0x...", // 收款地址
value: ethers.parseEther("0.01"), // 发送0.01 ETH
maxFeePerGas: feeData.maxFeePerGas,
maxPriorityFeePerGas: feeData.maxPriorityFeePerGas,
};
// 发送交易
const txResponse = await signer.sendTransaction(tx);
console.log(`Transaction sent: ${txResponse.hash}`);
// 等待确认
const receipt = await txResponse.wait();
console.log(`Confirmed in block: ${receipt.blockNumber}`);
return receipt;
}
// 签名并发送自定义交易(更灵活的控制)
async function sendCustomTransaction(signer) {
const tx = {
type: 2, // EIP-1559 交易类型
to: "0x...",
value: 0,
data: "0x...", // 合约调用数据
maxFeePerGas: ethers.parseGwei("30"),
maxPriorityFeePerGas: ethers.parseGwei("2"),
chainId: 1
};
// 签名交易
const signedTx = await signer.signTransaction(tx);
console.log("Signed transaction:", signedTx);
// 发送已签名的交易
const txResponse = await signer.provider.broadcastTransaction(signedTx);
return await txResponse.wait();
}
签名消息
javascript
async function signMessage(signer) {
const message = "Hello, Web3!";
// 普通消息签名(EIP-191)
const signature = await signer.signMessage(message);
console.log("Signature:", signature);
// 验证签名
const recoveredAddress = ethers.verifyMessage(message, signature);
console.log("Recovered:", recoveredAddress);
// 签名Typed Data(EIP-712,更安全)
const domain = {
name: "MyDApp",
version: "1",
chainId: 1,
verifyingContract: "0x..."
};
const types = {
Person: [
{ name: "name", type: "string" },
{ name: "wallet", type: "address" }
],
Mail: [
{ name: "from", type: "Person" },
{ name: "to", type: "Person" },
{ name: "contents", type: "string" }
]
};
const value = {
from: { name: "Alice", wallet: await signer.getAddress() },
to: { name: "Bob", wallet: "0x..." },
contents: "Hello!"
};
const typedSignature = await signer.signTypedData(domain, types, value);
console.log("Typed signature:", typedSignature);
}
合约交互
创建合约实例
javascript
import { ethers } from "ethers";
// ERC20 代币合约 ABI(简化版)
const ERC20_ABI = [
"function name() view returns (string)",
"function symbol() view returns (string)",
"function decimals() view returns (uint8)",
"function totalSupply() view returns (uint256)",
"function balanceOf(address) view returns (uint256)",
"function transfer(address, uint256) returns (bool)",
"function allowance(address, address) view returns (uint256)",
"function approve(address, uint256) returns (bool)",
"event Transfer(address indexed from, address indexed to, uint256 value)"
];
// 使用地址和ABI创建合约
const USDC_ADDRESS = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
const usdcContract = new ethers.Contract(USDC_ADDRESS, ERC20_ABI, provider);
// 组合Provider和Signer
const usdcWithSigner = usdcContract.connect(signer);
// 读取合约数据(只读,用Provider)
async function getTokenInfo() {
const name = await usdcContract.name();
const symbol = await usdcContract.symbol();
const decimals = await usdcContract.decimals();
const totalSupply = await usdcContract.totalSupply();
console.log(`${name} (${symbol})`);
console.log(`Total Supply: ${ethers.formatUnits(totalSupply, decimals)}`);
}
// 读取用户余额
async function getBalance(userAddress) {
const balance = await usdcContract.balanceOf(userAddress);
const decimals = await usdcContract.decimals();
return ethers.formatUnits(balance, decimals);
}
发送合约交易
javascript
// 发送代币转账交易
async function transferTokens(signer, to, amount) {
const usdc = new ethers.Contract(USDC_ADDRESS, ERC20_ABI, signer);
// 单位转换:6位小数
const amountInWei = ethers.parseUnits(amount, 6);
// 估算Gas
const gasEstimate = await usdc.transfer.estimateGas(to, amountInWei);
console.log(`Estimated gas: ${gasEstimate}`);
// 发送交易
const tx = await usdc.transfer(to, amountInWei);
console.log(`Transaction hash: ${tx.hash}`);
// 等待确认
const receipt = await tx.wait();
console.log(`Confirmed in block ${receipt.blockNumber}`);
console.log(`Gas used: ${receipt.gasUsed}`);
return receipt;
}
// 批量调用(只读)
async function batchRead() {
const calls = [
usdcContract.name(),
usdcContract.symbol(),
usdcContract.totalSupply()
];
// 使用 Promise.all 并行执行
const [name, symbol, totalSupply] = await Promise.all(calls);
console.log(name, symbol, totalSupply);
}
处理合约事件
javascript
// 监听 Transfer 事件
function listenToTransfers(contractAddress, userAddress) {
const filter = {
address: contractAddress,
topics: [
ethers.id("Transfer(address,address,uint256)"),
null, // 任何 from
ethers.getAddress(userAddress) // to = user
]
};
provider.on(filter, (log) => {
const iface = new ethers.Interface(ERC20_ABI);
const parsed = iface.parseLog({
topics: log.topics,
data: log.data
});
console.log(`Received ${parsed.args.value} tokens from ${parsed.args.from}`);
});
// 监听所有事件(带过滤)
contract.on("Transfer", (from, to, value, event) => {
console.log(`${from} -> ${to}: ${value}`);
});
}
// 查询历史事件
async function queryPastEvents() {
const filter = {
address: USDC_ADDRESS,
topics: [
ethers.id("Transfer(address,address,uint256)")
],
fromBlock: 19000000,
toBlock: "latest"
};
const events = await provider.getLogs(filter);
console.log(`Found ${events.length} Transfer events`);
const iface = new ethers.Interface(ERC20_ABI);
const decoded = events.map(e => iface.parseLog(e));
return decoded;
}
实用工具函数
单位转换
javascript
import { ethers } from "ethers";
// ETH 单位转换
ethers.parseEther("1.0"); // string -> BigInt (wei)
ethers.formatEther(1000000000000000000n); // BigInt -> string (ETH)
ethers.parseGwei("30"); // Gwei -> wei
ethers.formatGwei(30000000000n); // wei -> Gwei
// 自定义小数位
ethers.formatUnits(1000000n, 6); // "1.0" (USDC等6位代币)
// 连接字符串到字节
ethers.toBeHex(255); // "0xff"
ethers.toBeHex(255, 32); // 指定32字节 padding
// 地址工具
ethers.isAddress("0x..."); // 验证地址格式
ethers.getAddress("0x..."); // 返回checksum地址
ethers.computeAddress("0x..."); // 从公钥计算地址
ethers.recoverAddress("0x...", sig); // 从签名恢复地址
签名验证
javascript
import { ethers } from "ethers";
// 验证消息签名
function verifyMessage(message, signature) {
const address = ethers.verifyMessage(message, signature);
return address;
}
// 验证 Typed Data 签名
function verifyTypedData(domain, types, value, signature) {
const address = ethers.verifyTypedData(domain, types, value, signature);
return address;
}
// 检查签名是否来自特定地址
function isValidSignature(expectedAddress, message, signature) {
const recovered = ethers.verifyMessage(message, signature);
return recovered.toLowerCase() === expectedAddress.toLowerCase();
}
编码与解码
javascript
import { ethers } from "ethers";
// ABI 编码
const iface = new ethers.Interface([
"function transfer(address to, uint256 amount)"
]);
// 编码函数调用
const data = iface.encodeFunctionData("transfer", [
"0x...",
ethers.parseEther("1")
]);
// 解码函数参数
const decoded = iface.decodeFunctionData("transfer", data);
// 编码事件日志
const parsed = iface.parseLog({
topics: [...],
data: "..."
});
console.log(parsed.args.to);
console.log(parsed.args.amount);
// 直接使用 Contract 编码
const contract = new ethers.Contract(address, [...], provider);
const populatedTx = await contract.transfer.populateTransaction(
"0x...",
ethers.parseEther("1")
);
完整 DApp 示例
简单的代币发送器
javascript
// index.html 中引入
// <script type="module" src="app.js"></script>
// app.js
import { ethers } from "ethers";
class TokenSender {
constructor() {
this.provider = null;
this.signer = null;
this.connected = false;
// 配置
this.tokenAddress = "0x..."; // 代币合约地址
this.minBalance = ethers.parseEther("0.001"); // 最低余额要求
}
async init() {
// 连接钱包
if (typeof window.ethereum !== "undefined") {
this.provider = new ethers.BrowserProvider(window.ethereum);
// 尝试自动连接
const accounts = await window.ethereum.request({
method: "eth_accounts"
});
if (accounts.length > 0) {
await this.connect();
}
}
}
async connect() {
try {
await this.provider.send("eth_requestAccounts", []);
this.signer = await this.provider.getSigner();
this.connected = true;
this.updateUI();
this.startEventListeners();
} catch (error) {
console.error("Connection failed:", error);
}
}
async sendTokens(recipient, amount) {
if (!this.connected) {
throw new Error("Wallet not connected");
}
const token = new ethers.Contract(
this.tokenAddress,
[
"function transfer(address to, uint256 amount) returns (bool)",
"function decimals() view returns (uint8)"
],
this.signer
);
// 获取代币精度
const decimals = await token.decimals();
const amountWei = ethers.parseUnits(amount, decimals);
// 估算Gas
const gasEstimate = await token.transfer.estimateGas(recipient, amountWei);
// 获取Fee Data
const feeData = await this.provider.getFeeData();
// 发送交易
const tx = await token.transfer(recipient, amountWei, {
gasLimit: gasEstimate * 120n / 100n, // 增加20% buffer
maxFeePerGas: feeData.maxFeePerGas,
maxPriorityFeePerGas: feeData.maxPriorityFeePerGas
});
console.log(`Transaction sent: ${tx.hash}`);
// 等待确认
const receipt = await tx.wait();
console.log(`Confirmed! Gas used: ${receipt.gasUsed}`);
return receipt;
}
async getBalance() {
if (!this.connected) return null;
const address = await this.signer.getAddress();
const balance = await this.provider.getBalance(address);
return ethers.formatEther(balance);
}
startEventListeners() {
// 账户变化
window.ethereum.on("accountsChanged", async (accounts) => {
if (accounts.length === 0) {
this.connected = false;
} else {
this.signer = await this.provider.getSigner();
}
this.updateUI();
});
// 网络变化
window.ethereum.on("chainChanged", () => {
window.location.reload();
});
// 区块更新
this.provider.on("block", (blockNumber) => {
document.getElementById("blockNumber").textContent = blockNumber;
});
}
updateUI() {
const status = document.getElementById("status");
const connectBtn = document.getElementById("connectBtn");
if (this.connected) {
status.textContent = "Connected";
connectBtn.textContent = "Connected";
connectBtn.disabled = true;
} else {
status.textContent = "Not Connected";
connectBtn.textContent = "Connect Wallet";
connectBtn.disabled = false;
}
}
}
// 初始化
const app = new TokenSender();
app.init();
// 导出供 HTML 调用
window.app = app;
常见问题与最佳实践
1. Provider 选择
javascript
// 不要在每次请求时创建新 Provider
// ❌ 错误
async function getBalance() {
const provider = new ethers.JsonRpcProvider("https://...");
return await provider.getBalance(address);
}
// ✅ 正确:复用 Provider 实例
const provider = new ethers.JsonRpcProvider("https://...");
async function getBalance() {
return await provider.getBalance(address);
}
2. BigInt 处理
v6 使用原生 BigInt,需要注意精度:
javascript
// ❌ 可能丢失精度
const amount = balance * 0.1;
// ✅ 使用 BigInt 运算
const amount = balance * 10n / 100n;
// ✅ 使用 ethers 辅助函数
const amount = ethers.parseEther("0.1");
3. 错误处理
javascript
async function safeCall() {
try {
const result = await contract.someFunction();
return result;
} catch (error) {
if (error.code === "CALL_EXCEPTION") {
console.log("Transaction would fail");
console.log("Args:", error.args);
} else if (error.code === "INSUFFICIENT_FUNDS") {
console.log("Not enough balance");
} else {
console.error("Unknown error:", error);
}
throw error;
}
}
4. Gas 估算
javascript
// 动态 Gas 策略
async function sendWithOptimalGas(tx) {
const feeData = await provider.getFeeData();
// 根据网络状况调整
const baseFee = feeData.maxFeePerGas;
const priorityFee = feeData.maxPriorityFeePerGas;
// 增加优先级费以加快确认
const expeditedPriorityFee = priorityFee * 2n;
return {
...tx,
maxFeePerGas: baseFee + expeditedPriorityFee,
maxPriorityFeePerGas: expeditedPriorityFee,
gasLimit: tx.gasLimit * 110n / 100n // 10% buffer
};
}
总结
ethers.js v6 为现代 Web3 开发提供了强大而优雅的工具集。掌握以下核心要点:
- Provider/Signer 分离:Provider 用于只读查询,Signer 用于交易签名
- 合约抽象:通过 Contract 类简化合约交互
- 事件监听:使用 on/off 方法监听链上事件
- 单位转换:熟练使用 parseEther/formatUnits 等辅助函数
- 错误处理:理解常见的错误类型和代码
V6 版本相比 V5 在性能、包体积和 API 一致性上都有显著提升,建议新项目直接使用 V6。
相关阅读:
- Web3.js与React构建DApp前端实战:从连接到交互
- ethers.js v5 与 ethers.js v6 主要区别对比
- [The Graph数据索引实战:构建高效的链上数据查询层](./2026-04-23/03-The Graph数据索引实战.md)

发表回复