目标:
掌握Provider连接区块链的各种方法,熟悉读取区块链数据和合约状态,学会监听合约事件,理解MetaMask Provider的工作原理,掌握请求账户连接和网络切换,学会发送交易和签名消息
Ethers.js
- 采用纯Promise API,可以直接使用async和await语法
- 提供了完整的类型定义,支持原生TypeScript
- 采用模块化设计,支持按需导入,减少了打包体积,提升了页面加载速度
- 提供了完善的Provider抽象,支持多种连接方式,包括HTTP、WebSocket、Infura、Alchemy
- 提供了大量的工具函数,包括地址格式化、单位转换、签名验证
Provider的主要作用是提供只读访问区块链的能力。它不能发送交易或签名消息,这些操作需要Signer。
typescript
Provider类型:
1. JsonRpcProvider:
import { ethers } from 'ethers';
const provider = new ethers.JsonRpcProvider('https://mainnet.infura.io/v3/YOUR_API_KEY');
2. WebSocketProvider:
支持实时事件订阅,不需要轮询。这对于监听新区块、待处理交易等场景非常有用。但需要注意的是,WebSocket连接可能不稳定,需要处理重连逻辑。
const provider = new ethers.WebSocketProvider('wss://mainnet.infura.io/ws/v3/YOUR_API_KEY');
3. InfuraProvider和AlchemyProvider:
// InfuraProvider
const provider = new ethers.InfuraProvider('mainnet', 'YOUR_INFURA_API_KEY');
// AlchemyProvider
const provider = new ethers.AlchemyProvider('mainnet', 'YOUR_ALCHEMY_API_KEY');
4. BrowserProvider(MetaMask Provider):
与用户钱包进行交互,包括请求账户连接、发送交易、签名消息。创建BrowserProvider需要传入window.ethereum对象:
if (typeof window.ethereum !== 'undefined') {
const provider = new ethers.BrowserProvider(window.ethereum);
}
连接测试网和主网:
typescript
// 使用RPC URL
const provider = new ethers.JsonRpcProvider('https://sepolia.infura.io/v3/YOUR_API_KEY');
const provider = new ethers.JsonRpcProvider('https://mainnet.infura.io/v3/YOUR_API_KEY');
// 使用InfuraProvider
const provider = new ethers.InfuraProvider('mainnet', 'YOUR_INFURA_API_KEY');
const provider = new ethers.InfuraProvider('sepolia', 'YOUR_INFURA_API_KEY');
验证连接:
typescript
const blockNumber = await provider.getBlockNumber();
console.log('当前区块号:', blockNumber);
const network = await provider.getNetwork();
console.log('网络名称:', network.name);
console.log('链ID:', network.chainId.toString());
--------------------------------------------------------
try {
const blockNumber = await provider.getBlockNumber();
console.log('连接成功,当前区块号:', blockNumber);
} catch (error) {
console.error('连接失败:', error.message);
}
读取区块链数据之区块信息查询:
typescript
// 获取最新区块号
const blockNumber = await provider.getBlockNumber();
console.log('最新区块号:', blockNumber);
// 获取最新区块
const block = await provider.getBlock('latest');
console.log('区块哈希:', block.hash);
console.log('区块号:', block.number);
console.log('时间戳:', new Date(block.timestamp * 1000));
console.log('交易数量:', block.transactions.length);
// 获取特定区块
const specificBlock = await provider.getBlock(1000000);
//获取包含交易的区块
const blockWithTxs = await provider.getBlockWithTransactions('latest');
读取区块链数据之账户信息查询:
typescript
//查询账户余额
const address = '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb';
const balance = await provider.getBalance(address);
console.log('余额(Wei):', balance.toString());
console.log('余额(ETH):', ethers.formatEther(balance));
//查询账户nonce
const nonce = await provider.getTransactionCount(address);
//查询合约代码
const code = await provider.getCode(address);
if (code === '0x') {
console.log('这是一个普通账户');
} else {
console.log('这是一个合约账户');
console.log('合约代码长度:', code.length);
}
读取区块链数据之交易信息查询:
typescript
//获取交易详情
const txHash = '0x...';
const tx = await provider.getTransaction(txHash);
console.log('发送者:', tx.from);
console.log('接收者:', tx.to);
console.log('金额:', ethers.formatEther(tx.value));
console.log('Gas限制:', tx.gasLimit.toString());
console.log('Gas价格:', tx.gasPrice?.toString());
//获取交易回执
const receipt = await provider.getTransactionReceipt(txHash);
console.log('交易状态:', receipt.status === 1 ? '成功' : '失败');
console.log('区块号:', receipt.blockNumber);
console.log('Gas使用:', receipt.gasUsed.toString());
console.log('日志数量:', receipt.logs.length);
//获取交易结果
const result = await provider.getTransactionResult(txHash);
读取区块链数据之存储信息查询:
typescript
//读取合约存储
const slot = 0; // 存储槽索引
const value = await provider.getStorage(address, slot);
console.log('存储值:', value);
单位转换:
typescript
// ETH转Wei
const wei = ethers.parseEther('1.0');
console.log('1 ETH =', wei.toString(), 'Wei');
// Wei转ETH
const eth = ethers.formatEther(wei);
console.log(wei.toString(), 'Wei =', eth, 'ETH');
// 格式化其他单位
const gwei = ethers.formatUnits(wei, 'gwei');
console.log(wei.toString(), 'Wei =', gwei, 'Gwei');
地址格式化:
typescript
//确保地址使用正确的校验和格式,这对于避免地址错误很重要
const address = '0x742d35cc6634c0532925a3b844bc9e7595f0beb';
const checksumAddress = ethers.getAddress(address);
其他工具函数:
typescript
// 计算地址
const address = ethers.computeAddress(privateKey);
// 验证地址格式
const isValid = ethers.isAddress(address);
// 格式化单位
const formatted = ethers.formatUnits(value, decimals);
读取合约状态:
typescript
--------------------创建合约实例-----------------------------------------
import { ethers } from 'ethers';
const contractAddress = '0x...';
const contractABI = [
"function getNumber() view returns (uint256)",
"function increment()",
"function setNumber(uint256)",
"event Incremented(address indexed user, uint256 newValue)"
];
// 使用Provider创建只读合约实例
const contract = new ethers.Contract(contractAddress, contractABI, provider);
// 使用Signer创建可签名合约实例
const contractWithSigner = new ethers.Contract(contractAddress, contractABI, signer);
//也可以利用下面的方法,将可读合约转换为可写合约:
const contract2 = contract.connect(signer)
-------------------------只读调用----------------------------------
// 调用无参数函数
const number = await contract.getNumber();
console.log('当前值:', number.toString());
// 调用带参数函数
const balance = await contract.balanceOf(address);
console.log('余额:', balance.toString());
//批量读取数据
const [name, symbol, decimals, totalSupply] = await Promise.all([
contract.name(),
contract.symbol(),
contract.decimals(),
contract.totalSupply()
]);
------------------------捕获调用异常-------------------------------
try {
const number = await contract.getNumber();
console.log('当前值:', number.toString());
} catch (error) {
if (error.code === 'CALL_EXCEPTION') {
console.error('合约调用失败:', error.message);
} else {
console.error('其他错误:', error.message);
}
}
//常见错误类型:
//CALL_EXCEPTION:合约调用失败
//INVALID_ARGUMENT:参数错误
//NETWORK_ERROR:网络错误
//UNPREDICTABLE_GAS_LIMIT:Gas估算失败
监听合约事件:
typescript
//持续监听事件
contract.on('Incremented', (user, newValue, event) => {
console.log('Incremented事件触发:');
console.log(' 用户:', user);
console.log(' 新值:', newValue.toString());
console.log(' 区块号:', event.log.blockNumber);
console.log(' 交易哈希:', event.log.transactionHash);
});
//监听一次事件
contract.once('Incremented', (user, newValue, event) => {
console.log('Incremented事件触发一次');
});
//查询最近N个区块的事件
const filter = contract.filters.Incremented();
const events = await contract.queryFilter(filter, -5); // 最近5个区块
console.log('找到', events.length, '个Incremented事件');
events.forEach((event, index) => {
console.log(`事件 ${index + 1}:`, {
user: event.args.user,
newValue: event.args.newValue.toString(),
blockNumber: event.blockNumber
});
});
//查询指定区块范围内的事件
const events = await contract.queryFilter(filter, 1000000, 1000100);
//使用过滤器查询...查询特定地址相关的事件
const filter = contract.filters.Transfer(null, targetAddress);
const events = await contract.queryFilter(filter);
//过滤来自 myAddress 地址的 Transfer 事件
contract.filters.Transfer(myAddress)
//过滤所有发给 myAddress 地址的 Transfer 事件
contract.filters.Transfer(null, myAddress)
//过滤所有从 myAddress 发给 otherAddress 的 Transfer 事件
contract.filters.Transfer(myAddress, otherAddress)
//过滤所有发给 myAddress 或 otherAddress 的 Transfer 事件
contract.filters.Transfer(null, [ myAddress, otherAddress ])
//添加错误处理
contract.on('error', (error) => {
console.error('事件监听错误:', error);
});
//清理监听器
// 在组件卸载时清理监听器
contract.removeAllListeners('Incremented');
// 或清理所有监听器
contract.removeAllListeners();
监听区块事件:
typescript
//监听新区块
provider.on('block', (blockNumber) => {
console.log('新区块:', blockNumber);
});
//监听待处理交易
provider.on('pending', (txHash) => {
console.log('待处理交易:', txHash);
});
监听网络切换:
typescript
provider.on('network', (newNetwork, oldNetwork) => {
console.log('网络切换:', oldNetwork?.name, '->', newNetwork.name);
// 重新初始化应用状态
});
创建Wallet实例并发送ETH:
typescript
//import { ethers } from 'ethers';
const ethers = require('ethers');
// 利用Alchemy的rpc节点连接以太坊测试网络
const ALCHEMY_SEPOLIA_URL = 'https://eth-sepolia.g.alchemy.com/v2/...';
const provider = new ethers.JsonRpcProvider(ALCHEMY_SEPOLIA_URL);
// 利用createRandom创建随机的wallet对象(方法一)
const wallet1 = ethers.Wallet.createRandom();
const wallet1WithProvider = wallet1.connect(provider);
const mnemonic = wallet1.mnemonic; // 获取助记词
// 利用私钥和provider创建wallet对象(方法二)
//真实开发时请将privateKey和ALCHEMY_SEPOLIA_URL配置在环境变量中,并确保引入dotenv库,用于加载环境变量
//import "dotenv/config";
const privateKey = '0f03a73988c990c2333bbbcd99d442377fedbe48083a8a9c4426ace223c33e5d';
const wallet2 = new ethers.Wallet(privateKey, provider);
// 利用助记词创建wallet对象(方法三)
const wallet3 = ethers.Wallet.fromPhrase(mnemonic.phrase);
async function getWalletAddresses() {
const address2 = await wallet2.getAddress();
console.log(`钱包2地址: ${address2}`);
const txCount2 = await provider.getTransactionCount(wallet2)
console.log(`钱包2发送交易次数: ${txCount2}`)
//构造交易请求,参数:to为接收地址,value为ETH数额
const tx = {
to: address1,
value: ethers.parseEther("0.001")
}
//发送交易,获得收据
console.log(`\nii. 等待交易在区块链确认(需要几分钟)`)
const receipt = await wallet2.sendTransaction(tx)
await receipt.wait() // 等待链上确认交易
console.log(receipt) // 打印交易详情
}
getWalletAddresses();
创建可写的Contract变量, 存入并发送ETH:
typescript
// 利用私钥和provider创建wallet对象,上文已有
// 创建可写 WETH 合约变量
const addressWETH = '0x7b79995e5f793a07bc00c21412e50ecae098e7f9'// WETH合约地址(sepolia测试网)
const abiWETH = [
"function balanceOf(address) public view returns(uint)",
"function deposit() public payable",
"function transfer(address, uint) public returns (bool)",
"function withdraw(uint) public",
];
const contractWETH = new ethers.Contract(addressWETH, abiWETH, wallet)
//调用 WETH 合约的 deposit() 函数, 存入0.001 ETH
const tx = await contractWETH.deposit({value: ethers.parseEther("0.001")})
await tx.wait()
//调用 WETH 合约的 transfer() 函数, 给某一个地址转账 0.001 WETH
const tx2 = await contractWETH.transfer("地址", ethers.parseEther("0.001"))
await tx2.wait()
部署智能合约:
typescript
//创建 provider 和 wallet 变量,上文已有
//创建合约工厂实例
//const contractFactory = new ethers.ContractFactory(abi, bytecode, signer);
const abiERC20 = [
"constructor(string memory name_, string memory symbol_)",
"function name() view returns (string)",
"function symbol() view returns (string)",
"function totalSupply() view returns (uint256)",
"function balanceOf(address) view returns (uint)",
"function transfer(address to, uint256 amount) external returns (bool)",
"function mint(uint amount) external",
];
// 填入合约字节码,在remix中,你可以在两个地方找到Bytecode
// 1. 编译面板的Bytecode按钮
// 2. 文件面板artifact文件夹下与合约同名的json文件中
// 里面"bytecode"属性下的"object"字段对应的数据就是Bytecode,挺长的,608060起始
// "object": "608060405260646000553480156100...
const bytecodeERC20 = "6080604052601260056..."
const factoryERC20 = new ethers.ContractFactory(abiERC20, bytecodeERC20, wallet);
const contractERC20 = await factoryERC20.deploy("RCC Token", "RCC")
console.log(`合约地址: ${contractERC20.target}`);
console.log(contractERC20.deploymentTransaction())//部署合约的交易详情
await contractERC20.waitForDeployment()//等待合约部署上链
//或者contractERC20.deployTransaction.wait()
检索事件:
typescript
//const transferEvents = await contract.queryFilter('事件名', 起始区块, 结束区块)
//queryFilter() 包含 3 个参数,分别是事件名(必填),起始区块(选填),和结束区块(选填)
//检索结果会以数组的方式返回
//要检索的事件必须包含在合约的 abi 中
const ethers = require('ethers');
const ALCHEMY_SEPOLIA_URL = 'https://eth-sepolia.g.alchemy.com/v2/....';
const provider = new ethers.JsonRpcProvider(ALCHEMY_SEPOLIA_URL);
// WETH ABI,只包含我们关心的Transfer事件
const abiWETH = [
...
"event Transfer(address indexed from, address indexed to, uint amount)"
...
];
const addressWETH = '0x7b79995e5f793a07bc00c21412e50ecae098e7f9'// 测试网WETH地址
const contract = new ethers.Contract(addressWETH, abiWETH, provider)// 声明合约实例
const main = async () => {
// 获取过去10个区块内的Transfer事件
const block = await provider.getBlockNumber()
console.log(`打印事件详情:`);
const transferEvents = await contract.queryFilter('Transfer', block - 10, block)
// 打印第1个Transfer事件
console.log(transferEvents[0])
// 解析Transfer事件的数据(变量在args中)
const amount = ethers.formatUnits(ethers.getBigInt(transferEvents[0].args["amount"]), "ether");
console.log(`地址 ${transferEvents[0].args["from"]} 转账${amount} WETH 到地址 ${transferEvents[0].args["to"]}`)
}
main()