'# 【Web3项目案例】Ethers.js极简入门+实战案例:实现ERC20协议代币查询、交易
一、背景与问题
在Web3生态中,智能合约的交互是核心操作之一。Ethers.js作为主流的以太坊开发库,提供了与以太坊网络交互的完整工具链。本文将通过一个完整的Web3项目案例,深入解析Ethers.js如何实现ERC20代币的查询与交易操作。
当前开发者在使用Ethers.js时,往往面临以下问题:
- 如何正确解析合约ABI并构建Contract对象
- 如何处理异步操作和交易确认
- 如何安全地进行代币转账
- 如何处理Gas费用和交易失败场景
这些问题在实际项目中可能导致严重的资金损失或功能缺陷,需要深入理解其底层原理。
二、基本原理
Ethers.js通过以下核心机制实现与以太坊网络的交互:
- Provider机制:连接以太坊节点(如Infura、Alchemy等),处理网络请求
- Contract对象:通过ABI和合约地址创建智能合约实例
- Transaction机制:处理交易发送、签名和确认
- Event监听:处理合约事件(如Transfer事件)
ERC20协议定义了标准代币接口,其核心方法包括:
balanceOf(address):获取账户余额transfer(address,uint256):转账transferFrom(address,address,uint256):授权转账allowance(address,address):获取授权额度totalSupply():获取总供应量
三、环境准备
1. 前提条件
- Node.js 18+
安装Ethers.js库:
npm install ethers
2. 网络配置
// 配置Infura节点
const provider = new ethers.providers.JsonRpcProvider(
'https://mainnet.infura.io/v3/YOUR_INFURA_PROJECT_ID'
);3. 合约ABI
需要获取目标ERC20合约的ABI,通常通过以下方式获取:
- 查看Etherscan合约页面的"Contract ABI"部分
- 使用
ethers.ContractFactory创建合约实例
四、核心实现
1. 查询账户余额
async function getBalance(address) {
const provider = new ethers.providers.JsonRpcProvider(
'https://mainnet.infura.io/v3/YOUR_INFURA_PROJECT_ID'
);
const contract = new ethers.Contract(
'0xYourTokenContractAddress',
[
"function balanceOf(address account) view returns (uint256)",
"function decimals() view returns (uint8)"
],
provider
);
const balance = await contract.balanceOf(address);
const decimals = await contract.decimals();
return balance.div(10 ** decimals);
}关键点解析:
- 使用
div处理代币小数位 - 调用
balanceOf时需确保合约地址正确 - 需要处理网络延迟和Gas费用
2. 获取交易历史
async function getTransactionHistory(address) {
const provider = new ethers.providers.JsonRpcProvider(
'https://mainnet.infura.io/v3/YOUR_INFURA_PROJECT_ID'
);
const filter = {
fromBlock: '0x0',
toBlock: 'latest',
address: '0xYourTokenContractAddress',
topics: [ethers.utils.id('Transfer(address,address,uint256)')]
};
const logs = await provider.getLogs(filter);
return logs.map(log => {
const decoded = ethers.utils.decodeLog(
[
{ name: 'from', type: 'address' },
{ name: 'to', type: 'address' },
{ name: 'value', type: 'uint256' }
],
log.data,
log.topics
);
return {
from: decoded.from,
to: decoded.to,
value: decoded.value
};
});
}关键点解析:
- 使用
getLogs获取特定事件 ethers.utils.id生成事件哈希- 需要处理事件数据解码
3. 发送代币交易
async function sendTransfer(from, to, amount, privateKey) {
const provider = new ethers.providers.JsonRpcProvider(
'https://mainnet.infura.io/v3/YOUR_INFURA_PROJECT_ID'
);
const wallet = new ethers.Wallet(privateKey, provider);
const contract = new ethers.Contract(
'0xYourTokenContractAddress',
[
"function transfer(address to, uint256 amount) returns (bool)",
"function decimals() view returns (uint8)"
],
wallet
);
const decimals = await contract.decimals();
const value = ethers.utils.parseUnits(amount, decimals);
const tx = await contract.transfer(to, value);
await tx.wait();
return tx.hash;
}关键点解析:
- 使用
Wallet对象进行签名 parseUnits处理代币小数位- 需要处理交易等待和确认
五、完整案例:代币查询与转账Web应用
1. 项目结构
erc20-demo/
├── server/
│ ├── index.js
│ └── routes/
│ └── tokens.js
├── client/
│ ├── App.js
│ └── index.js
└── .env2. 后端实现(Node.js)
// server/routes/tokens.js
const express = require('express');
const { getBalance, sendTransfer } = require('../utils/erc20');
const router = express.Router();
router.get('/balance/:address', async (req, res) => {
try {
const balance = await getBalance(req.params.address);
res.json({ balance });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
router.post('/transfer', async (req, res) => {
try {
const { from, to, amount, privateKey } = req.body;
const txHash = await sendTransfer(from, to, amount, privateKey);
res.json({ txHash });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
module.exports = router;3. 前端实现(React)
// client/App.js
import React, { useState } from 'react';
import axios from 'axios';
function App() {
const [address, setAddress] = useState('');
const [balance, setBalance] = useState(null);
const [txHash, setTxHash] = useState('');
const getBalance = async () => {
try {
const response = await axios.get(`http://localhost:3000/balance/${address}`);
setBalance(response.data.balance);
} catch (err) {
alert(err.message);
}
};
const sendTransfer = async () => {
try {
const response = await axios.post('http://localhost:3000/transfer', {
from: address,
to: '0xRecipientAddress',
amount: 100,
privateKey: 'YOUR_PRIVATE_KEY'
});
setTxHash(response.data.txHash);
} catch (err) {
alert(err.message);
}
};
return (
<div>
<h1>ERC20代币操作</h1>
<input
type="text"
placeholder="输入钱包地址"
value={address}
onChange={(e) => setAddress(e.target.value)}
/>
<button onClick={getBalance}>查询余额</button>
<p>当前余额: {balance}</p>
<button onClick={sendTransfer}>发送代币</button>
<p>交易哈希: {txHash}</p>
</div>
);
}
export default App;4. 安全注意事项
- 私钥不应直接暴露在前端
- 生产环境应使用HTTPS
- 需要验证用户身份(如通过MetaMask签名)
- 对金额进行校验防止溢出
六、源码解析
1. Contract对象创建
const contract = new ethers.Contract(
'0xYourTokenContractAddress',
[
"function balanceOf(address account) view returns (uint256)",
"function decimals() view returns (uint8)"
],
provider
);解析:
- 第一个参数是合约地址
- 第二个参数是合约方法列表
- 第三个参数是Provider对象
- 实际使用时应使用完整ABI
2. 交易发送机制
const tx = await contract.transfer(to, value);
await tx.wait();解析:
transfer是合约方法value是经过小数处理后的值tx.wait()等待交易确认- 返回的
tx.hash可用于查询交易状态
七、进阶使用
1. 支持多种网络
const provider = new ethers.providers.JsonRpcProvider(
'https://rinkeby.infura.io/v3/YOUR_INFURA_PROJECT_ID'
);2. 使用钱包管理器
const wallet = ethers.Wallet.fromPhrase(
'your mnemonic phrase',
'0xYourTokenContractAddress'
);3. 事件监听
contract.on('Transfer', (from, to, value) => {
console.log(`Transfer from ${from} to ${to} of ${value}`);
});4. 支持多签名合约
const multiSig = new ethers.Contract(
'0xMultiSigContractAddress',
[
"function submitTransaction(address[] calldata _targets, uint256[] calldata _values, bytes[] calldata _datas)",
"function executeTransaction(bytes memory _encodedFunctionCall)"
],
provider
);八、性能与工程实践
1. 性能优化
- 使用缓存机制存储常见合约数据
- 对频繁查询的接口进行限流
- 使用更高效的网络提供商(如Alchemy)
- 使用
ethers.providers.FallbackProvider实现网络故障转移
2. 安全建议
- 对金额进行严格校验
- 使用
ethers.utils.isAddress验证地址格式 - 对交易进行Gas价格优化
- 使用
ethers.utils.entropyToHex生成随机数 - 对签名进行验证(如使用
ethers.utils.verifyMessage)
3. 异常处理
try {
const tx = await contract.transfer(to, value);
await tx.wait();
} catch (err) {
if (err.reason.includes('insufficient funds')) {
alert('余额不足');
} else if (err.reason.includes('revert')) {
alert('交易失败');
} else {
alert('未知错误');
}
}4. Gas费用处理
const tx = await contract.transfer(to, value, {
gasLimit: 21000,
gasPrice: ethers.utils.parseUnits('20', 'gwei')
});九、常见问题与踩坑
1. 网络连接问题
错误示例:
const provider = new ethers.providers.JsonRpcProvider('https://mainnet.infura.io/v3/...');解决方案:
- 确认网络是否可达
- 使用
ethers.providers.FallbackProvider处理网络故障 - 添加超时机制
2. 交易失败
错误示例:
await contract.transfer(to, value);解决方案:
- 确认合约地址正确
- 确认代币小数位处理正确
- 确认Gas价格足够
- 检查网络拥堵情况
3. 签名错误
错误示例:
const wallet = new ethers.Wallet('privateKey', provider);解决方案:
- 使用
ethers.Wallet.fromPhrase生成钱包 - 使用
ethers.Wallet.fromPrivateKey加载私钥 - 确保使用正确的网络提供商
4. 事件监听失败
错误示例:
contract.on('Transfer', (from, to, value) => { ... });解决方案:
- 确认合约事件签名正确
- 确认监听的事件类型
- 确认合约部署正确
十、最佳实践
- 使用正式ABI:始终使用合约的正式ABI,避免手动定义接口
- 处理异常:对所有可能的错误进行分类处理
- Gas优化:根据网络拥堵情况动态调整Gas价格
- 安全存储:避免在前端存储私钥,使用钱包管理器
- 网络切换:支持不同网络(mainnet, testnet, rinkeby等)
- 日志记录:记录关键操作日志便于排查问题
- 限流机制:防止对网络的过度请求
十一、总结
Ethers.js作为以太坊开发的核心工具,提供了完整的合约交互解决方案。本文通过一个完整的Web3项目案例,深入解析了ERC20代币的查询与交易实现,涵盖了:
- 合约接口的创建与使用
- 交易发送的完整流程
- 常见错误的处理方法
- 性能优化和安全注意事项
在实际开发中,建议根据项目需求选择合适的实现方式。对于需要频繁交互的场景,建议采用更高级的封装方案;对于简单的查询需求,可以使用更轻量级的实现。同时,需要注意处理网络异常、交易确认和安全风险,确保系统的稳定性和可靠性。
