Ethers库的基本操作
Ethers库的基本操作
这篇帖子用来记录WTF Academy ethers101课程的学习笔记。
第一个程序
const { ethers } = require("ethers");
const provider = new ethers.JsonRpcProvider(`https://bsc-rpc.publicnode.com`) //e.g ALCHEMY, INFURA
const address = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'
const main = async() => {
const balance = await provider.getBalance(address)
console.log(`\nETH Balance of ${address} --> ${ethers.formatEther(balance)} ETH\n`)}
main()
其中,provider
用来提供对区块链的连接,一般这里要写一个RPC
,它决定了你连接的是哪个网络。
main
函数要声明为async
,内部涉及到对链的操作或访问要用await
,因为需要等待网络反馈,这是与链交互的原则。与前端开发时等待后端反馈是一个原理。
这个例子中main
的功能是查询vitalik的余额,如果代码正确,可以看到终端输出:
ETH Balance of 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 --> 5.364667659529069834 ETH
读取合约信息
如果你看了课程网站,会发现我跳过了Provider,因为那个其实没什么可说的,只需要在infura申请一个就行了。
Ethers.js中有合约类,也就是说可以直接把一个合约作为对象来操作,这样能干很多事。
首先用自己的Key连接到以太坊主网:
import { ethers } from "ethers";
const INFURA_ID = '' // 这里就不给你看我的key了
const provider = new ethers.JsonRpcProvider(`https://mainnet.infura.io/v3/${INFURA_ID}`)
下面通过abi创建合约对象:
const abiERC20 = [
"function name() view returns (string)",
"function symbol() view returns (string)",
"function totalSupply() view returns (uint256)",
"function balanceOf(address) view returns (uint)",
];
const addressDAI = '0x6B175474E89094C44Da98b954EedeAC495271d0F' // DAI Contract
const contractDAI = new ethers.Contract(addressDAI, abiERC20, provider)
对这个合约对象的操作也很简单,直接调它的成员函数,别忘了await!!!
const nameDAI = await contractDAI.name()
const symbolDAI = await contractDAI.symbol()
const totalSupplDAI = await contractDAI.totalSupply()
发送ETH
既然要发送,就得有个钱包。在Ethers.js中,可以用Wallet
钱包类来创建钱包对象:
// 利用私钥和provider创建wallet对象
const privateKey = '0x227dbb8586117d55284e26620bc76534dfbd2394be34cf4a09cb775d593b6f2b'
const wallet = new ethers.Wallet(privateKey, provider)
Wallet
其实是Signer
的子类,Signer
是用来管理秘钥的,任何需要你签名的东西都是由它代劳和区块链交互。
有了钱包就可以创建一个交易请求并发送:
const tx = {
to: address1, // 接收方钱包地址
value: ethers.parseEther("0.001")
}
console.log(`等待交易在区块链确认(需要几分钟)`)
const receipt = await wallet2.sendTransaction(tx)
await receipt.wait() // 等待链上确认交易
console.log(receipt) // 打印交易详情
这里我把wallet2
换成了我自己的钱包的私钥后才试成功:
i. 发送前余额
钱包1: 0.0 ETH
钱包2: 0.05 ETH
iii. 发送后余额
钱包1: 0.001 ETH
钱包2: 0.048998449389946 ETH
主体113年,伟大领袖金正恩将军说:钱包里必须要有钱。
这句话他不一定说过,但用于转账的钱包除了转账金额外,必须额外有用来支付手续费的ETH,否则就会报inefficient funds
。在这个网站可以每天领到0.05个测试ETH。
合约交互
创建一个可写的Contract
:
const contract = new ethers.Contract(address, abi, signer)
其中address
为合约地址,abi
是合约的abi
接口,signer
是wallet
对象。注意,这里你需要提供signer
,而在声明可读合约时你只需要提供provider
。
只读合约可以通过传入Signer进化为可写合约:
const contract2 = contract.connect(signer)
交互的语法规则是:
// 发送交易
const tx = await contract.METHOD_NAME(args [, overrides])
// 等待链上确认
await tx.wait()
其中METHOD_NAME
为调用的函数名,args
为函数参数,[, overrides]
是可以选择传入的数据。
假设已经通过如下方式拿到了一个可写合约对象:
const abiWETH = [
"function balanceOf(address) public view returns(uint)",
"function deposit() public payable",
"function transfer(address, uint) public returns (bool)",
"function withdraw(uint) public",
];
// 这是Sepolia测试网的WETH合约地址,由于网站的教程用的goerli测试网已经寄了,所以在之后的笔记中我统一使用Sepolia来测试。
const addressWETH = '0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14'
const contractWETH = new ethers.Contract(addressWETH, abiWETH, wallet)
注意,其中的合约地址是Sepolia测试网的WETH合约地址,由于网站的教程用的goerli测试网已经寄了,所以在之后的笔记中我统一使用Sepolia来测试。
现在举一个最简单的例子,来调这个合约的存款逻辑,即把ETH换成WETH:
const address = await wallet.getAddress()
// 读取WETH合约的链上信息(WETH abi)
console.log("\n1. 读取WETH余额")
const balanceWETH = await contractWETH.balanceOf(address)
console.log(`存款前WETH持仓: ${ethers.formatEther(balanceWETH)}\n`)
console.log("\n2. 调用desposit()函数,存入0.001 ETH")
// 发起交易
const tx = await contractWETH.deposit({value: ethers.parseEther("0.001")})
// 等待交易上链
await tx.wait()
console.log(`交易详情:`)
console.log(tx)
const balanceWETH_deposit = await contractWETH.balanceOf(address)
console.log(`存款后WETH持仓: ${ethers.formatEther(balanceWETH_deposit)}\n`)
这样在交易成功后就能看到打印出的WETH余额有变化。
部署合约
在以太坊上,智能合约的部署也是一种特殊的交易:将编译智能合约得到的字节码发送到0地址。
要通过js部署一个合约,首先要创建合约工厂:
const contractFactory = new ethers.ContractFactory(abi, bytecode, signer);
需要有合约abi
,编译智能合约得到的字节码bytecode
(remix在compile之后可以在下方复制合约的字节码)和签名者变量signer
来创建合约工厂实例,注意:如果合约的构造函数有参数,那么在abi
中必须包含构造函数。
在创建好合约工厂实例之后,可以调用它的deploy
函数并传入合约构造函数的参数args
来部署并获得合约实例:
const contract = await contractFactory.deploy(args)
// 部署好后需要再等一下它真的部署成功:
await contractERC20.waitForDeployment();
部署完就可以按上边学过的合约交互方法来操作这个合约。
检索事件
所有被部署在链上的智能合约释放出的时间都会被记录在虚拟机日志里。日志分topics
和data
,topics
存所有事件的哈希和带有indexed
的变量。
比如在这个例子中:
event Transfer(address indexed from, address indexed to, uint256 amount);
Topics
包含3个数据,分别对应事件哈希,发出地址from
,和接收地址to
;而Data
中包含一个数据,对应转账数额amount
。
可以利用Ethers
中合约类型的queryFilter()
函数读取合约释放的事件:
const transferEvents = await contract.queryFilter('事件名', 起始区块, 结束区块)
要检索的事件必须包含在合约的abi
中。
比如:
const transferEvents = await contract.queryFilter('Transfer', block - 10, block)
// 打印第一个Transfer事件
console.log(transferEvents[0])
这段代码可以读取过去10个区块的Transfer
事件并存储在数组中。
监听合约事件
如果写过前端就知道,在js里监听器是一个超级常用的东西,幸运的是在Ethers中监听一个合约的事件只需要短短一行代码:
contract.on("eventName", function)
contract.on
有两个参数,一个是要监听的事件名称"eventName"
,需要包含在合约abi
中;另一个是我们在事件发生时调用的函数。
如果把on改成once,就能做到只监听一次合约释放事件:
contract.once("eventName", function)
比如,下面这段代码用来监听USDT合约,它能单次监听到USDT的Transfer事件并打印出三个事件参数:
console.log("\n1. 利用contract.once(),监听一次Transfer事件");
contractUSDT.once('Transfer', (from, to, value)=>{
// 打印结果
console.log(
`${from} -> ${to} ${ethers.formatUnits(ethers.getBigInt(value),6)}`
)
})
事件过滤
这部分网站上说的有点模棱两可,让人不明所以。其实过滤器就是用来帮你选出你感兴趣的事件并监听的。下面直接通过代码解释。创建过滤器的语法是:
const filter = contract.filters.EVENT_NAME( ...args )
其中EVENT_NAME
为要过滤的事件名,..args
为主题集/条件,下面是一些使用过滤器的例子:
// 过滤所有发给 myAddress地址的Transfer事件(可以看出,如果写了null,就可以匹配所有的东西)
contract.filters.Transfer(null, myAddress)
// 过滤所有从 myAddress发给otherAddress的Transfer事件
contract.filters.Transfer(myAddress, otherAddress)
// 过滤所有发给myAddress或otherAddress的Transfer事件(又可以看出,写成数组就是或运算)
contract.filters.Transfer(null, [ myAddress, otherAddress ])
BigInt类型
以太坊中,许多计算都对超出JavaScript
整数的安全值,容易溢出。因此,ethers.js
使用 JavaScript ES2020 版本原生的 BigInt
类安全地对任何数量级的数字进行数学运算。这里直接列出一些常用的运算和转换操作:
const oneGwei = ethers.getBigInt("1000000000"); // 从十进制字符串生成
console.log(oneGwei)
console.log(ethers.getBigInt("0x3b9aca00")) // 从hex字符串生成
console.log(ethers.getBigInt(1000000000)) // 从数字生成
// 不能从js最大的安全整数之外的数字生成BigNumber,下面代码会报错
// ethers.getBigInt(Number.MAX_SAFE_INTEGER);
// 运算
console.log("加法:", oneGwei + 1n)
console.log("减法:", oneGwei - 1n)
console.log("乘法:", oneGwei * 2n)
console.log("除法:", oneGwei / 2n)
// 比较
console.log("是否相等:", oneGwei == 1000000000n)
有一些奇怪的单位:

这是输出时的转换(注意,formatUnits(变量, 单位)
只改变输出时的格式,不会改变任何数值大小):
console.group('\n2. 格式化:小单位转大单位,formatUnits');
console.log(ethers.formatUnits(oneGwei, 0));
// '1000000000'
console.log(ethers.formatUnits(oneGwei, "gwei"));
// '1.0'
console.log(ethers.formatUnits(oneGwei, 9));
// '1.0'
console.log(ethers.formatUnits(oneGwei, "ether"));
// `0.000000001`
console.log(ethers.formatUnits(1000000000, "gwei"));
// '1.0'
console.log(ethers.formatEther(oneGwei));
// `0.000000001` 等同于formatUnits(value, "ether")
console.groupEnd();
console.group('\n3. 解析:大单位转小单位,parseUnits');
console.log(ethers.parseUnits("1.0").toString());
// { BigNumber: "1000000000000000000" }
console.log(ethers.parseUnits("1.0", "ether").toString());
// { BigNumber: "1000000000000000000" }
console.log(ethers.parseUnits("1.0", 18).toString());
// { BigNumber: "1000000000000000000" }
console.log(ethers.parseUnits("1.0", "gwei").toString());
// { BigNumber: "1000000000" }
console.log(ethers.parseUnits("1.0", 9).toString());
// { BigNumber: "1000000000" }
console.log(ethers.parseEther("1.0").toString());
// { BigNumber: "1000000000000000000" } 等同于parseUnits(value, "ether")
console.groupEnd();