走进web3的世界从Ethers开始(五)

687 阅读3分钟

最近在学习web3所以根据自己的学习内容每天写一篇笔记出来,算是联系也算是巩固! 希望自己早日可以找到web3的工作。 image.png

在我看来ethers就是传统开发中的axios,他使得我们可以非常快速简单的访问区块链上的信息。那么ethers究竟为我们提供了哪些功能呢?让我们来一起学习一下吧!

今天我们来学习一下ethers中的event事件,首先我们梳理一下contract中的事件都有哪些呢

返回与event匹配的事件。

contract.queryFilter( event [ , fromBlockOrBlockHash [ , toBlock ] ) ⇒ Promise< Array< Event > >

返回订阅该event的监听器数量。如果没有提供event,则返回所有事件的总数。

contract.listenerCount( [ event ] ) ⇒ number

返回订阅该event的监听器列表。

contract.listeners( event ) ⇒ Array< Listener >

监听器取消订阅event事件。

contract.off( event , listener ) ⇒ this

监听event事件,当事件发生时,会调用listener函数。

contract.onevent , listener ) ⇒ this

监听event事件,当事件发生时,仅调用一次listener函数。

contract.once( event , listener ) ⇒ this

取消所有订阅event事件的监听器。如果未提供event事件,则取消订阅所有事件的监听。

contract.removeAllListeners( [ event ] ) ⇒ this

事件过滤器,返回EVENT_NAME的过滤器,可以通过增加其他约束进行过滤。只有indexed索引的事件参数可以被过滤。如果参数为空(或未提供),则该字段中的任何值都匹配。

contract.filters.EVENT_NAME( ...args ) ⇒ Filter

那么我们接下来一点一点来实现这些事件:

My Web3文件夹中创建07_Event文件夹,并在其中创建文件Event.js,我们在这里实现一下如何检索区块内的Transfer事件

image.png

具体的代码和解释如下

// 检索事件的方法:
// const transferEvents = await contract.queryFilter("事件名", [起始区块高度,结束区块高度])
// 其中起始区块高度和结束区块高度为选填参数。

import { ethers } from "ethers";

// 利用Alchemy的rpc节点连接以太坊主网
const ALCHEMY_MAINNET_URL = '你申请的Connect to Alchemy URL(主网)';
const provider = new ethers.JsonRpcProvider(ALCHEMY_MAINNET_URL);

// WETH ABI,只包含我们关心的Transfer事件
const abiWETH = [
    "event Transfer(address indexed from, address indexed to, uint amount)"
];

// WETH合约地址(主网)
const addressWETH = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'
// 声明合约实例
const contract = new ethers.Contract(addressWETH, abiWETH, provider)

const main = async () => {

    // 获取过去10个区块内的Transfer事件
    console.log("\n1. 获取过去10个区块内的Transfer事件,并打印出1个");
    // 得到当前block
    const block = await provider.getBlockNumber()
    console.log(`当前区块高度: ${block}`);
    console.log(`打印事件详情:`);
    const transferEvents = await contract.queryFilter('Transfer', block - 10, block)
    // 打印第1个Transfer事件
    console.log(transferEvents[0])

    // 解析Transfer事件的数据(变量在args中)
    console.log("\n2. 解析事件:")
    const amount = ethers.formatUnits(ethers.getBigInt(transferEvents[0].args["amount"]), "ether");
    console.log(`地址 ${transferEvents[0].args["from"]} 转账${amount} WETH 到地址 ${transferEvents[0].args["to"]}`)
}

main()

执行这个看一下过去10个区块内的Transfer事件信息

image.png

接下来我们再来实现一下contract.oncontract.once,然后我们再在contract.on的监听回调中取消订阅。

My Web3文件夹中创建08_ContractListener文件夹,并在其中创建文件ContractListener.js

image.png

实现代码如下

// 监听合约方法:
// 1. 持续监听
// contractUSDT.on("事件名", Listener)
// 2. 只监听一次
// contractUSDT.once("事件名", Listener)

import { ethers } from "ethers";

// 利用Alchemy的rpc节点连接以太坊主网
const ALCHEMY_MAINNET_URL = '你申请的Connect to Alchemy URL(主网)';
const provider = new ethers.JsonRpcProvider(ALCHEMY_MAINNET_URL);

// USDT的合约地址
const contractAddress = '0xdac17f958d2ee523a2206206994597c13d831ec7'
// 构建USDT的Transfer的ABI
const abi = [
  "event Transfer(address indexed from, address indexed to, uint value)"
];
// 生成USDT合约对象
const contractUSDT = new ethers.Contract(contractAddress, abi, provider);


const main = async () => {
  // 监听USDT合约的Transfer事件

  try {
    // 只监听一次
    console.log("\n1. 利用contract.once(),监听一次Transfer事件");
    contractUSDT.once('Transfer', (from, to, value) => {
      // 打印结果
      console.log('只监听一次')
      console.log(
        `${from} -> ${to} ${ethers.formatUnits(ethers.getBigInt(value), 6)}`
      )
    })

    // 持续监听USDT合约
    console.log("\n2. 利用contract.on(),持续监听Transfer事件");
    let count = 1
    contractUSDT.on('Transfer', (from, to, value) => {
      console.log(`持续监听Transfer事件第${count}次回调`)
      console.log(
        // 打印结果
        `${from} -> ${to} ${ethers.formatUnits(ethers.getBigInt(value), 6)}`
      )
      count++
      if(count > 5) {
        // 如果没有参数的话是默认异常所有的监听事件
        contractUSDT.removeAllListeners(['Transfer'])
      }
      
    })
  } catch (e) {
    console.log(e);

  }
}
main()

执行这个看一下监听合约获得的信息

image.png

一鼓作气我们接下来我们再过滤器,在我们学习过滤器之前我们应该先知道当合约创建日志(释放事件)时,它最多可以包含4条数据作为索引(indexed)。索引数据经过哈希处理并包含在布隆过滤器中,这是一种允许有效过滤的数据结构。因此,一个事件过滤器最多包含4个主题集,每个主题集是个条件,用于筛选目标事件。规则:

  • 如果一个主题集为null,则该位置的日志主题不会被过滤,任何值都匹配。
  • 如果主题集是单个值,则该位置的日志主题必须与该值匹配。
  • 如果主题集是数组,则该位置的日志主题至少与数组中其中一个匹配。

image.png

构建过滤器

ethers.js中的合约类提供了contract.filters来简化过滤器的创建:

const filter = contract.filters.EVENT_NAME( ...args ) 

其中EVENT_NAME为要过滤的事件名,..args为主题集/条件。前面的规则有一点抽象,下面举几个例子。

  1. 过滤来自myAddress地址的Transfer事件
contract.filters.Transfer(myAddress)
  1. 过滤所有发给 myAddress地址的Transfer事件
contract.filters.Transfer(null, myAddress)
  1. 过滤所有从 myAddress发给otherAddressTransfer事件
contract.filters.Transfer(myAddress, otherAddress)
  1. 过滤所有发给myAddressotherAddressTransfer事件
contract.filters.Transfer(null, [ myAddress, otherAddress ])

监听交易所的USDT转账

我们需要先看懂交易日志Logs,包括事件的topicsdata

  • address:USDT合约地址
  • topics[0]:事件哈希,keccak256("Transfer(address,address,uint256)")
  • topics[1]:转出地址(币安交易所热钱包)。
  • topics[2] 转入地址。
  • data:转账数量。

image.png

然后我们用代码来具体实现一下如果过滤监听交易所的USDT转账:

My Web3文件夹中创建09_EventFilter文件夹,并在其中创建文件EventFilter.js

image.png

具体代码实现如下

import { ethers } from "ethers";

// 利用Alchemy的rpc节点连接以太坊网络
const ALCHEMY_MAINNET_URL = 'https://eth-mainnet.g.alchemy.com/v2/GTnW6zSipqJb0Nj0Gfsd9L9Oy4BUb0Y7';
const provider = new ethers.JsonRpcProvider(ALCHEMY_MAINNET_URL);

// 合约地址
const addressUSDT = '0xdac17f958d2ee523a2206206994597c13d831ec7'
// 交易所地址
const accountBinance = '0x28C6c06298d514Db089934071355E5743bf21d60'
// 构建ABI
const abi = [
  "event Transfer(address indexed from, address indexed to, uint value)",
  "function balanceOf(address) public view returns(uint)",
];
// 构建合约对象
const contractUSDT = new ethers.Contract(addressUSDT, abi, provider);


(async () => {
  try {
    // 1. 读取币安热钱包USDT余额
    console.log("\n1. 读取币安热钱包USDT余额")
    const balanceUSDT = await contractUSDT.balanceOf(accountBinance)
    console.log(`USDT余额: ${ethers.formatUnits(balanceUSDT,6)}\n`)

    // 2. 创建过滤器,监听转移USDT进交易所
    console.log("\n2. 创建过滤器,监听USDT转进交易所")
    let filterBinanceIn = contractUSDT.filters.Transfer(null, accountBinance);
    console.log("过滤器详情:")
    console.log(filterBinanceIn);
    contractUSDT.on(filterBinanceIn, (res) => {
      console.log('---------监听USDT进入交易所--------');
      console.log(
        `${res.args[0]} -> ${res.args[1]} ${ethers.formatUnits(res.args[2],6)}`
      )
    })

    // 3. 创建过滤器,监听交易所转出USDT
    let filterToBinanceOut = contractUSDT.filters.Transfer(accountBinance);
    console.log("\n3. 创建过滤器,监听USDT转出交易所")
    console.log("过滤器详情:")
    console.log(filterToBinanceOut);
    contractUSDT.on(filterToBinanceOut, (res) => {
      console.log('---------监听USDT转出交易所--------');
      console.log(
        `${res.args[0]} -> ${res.args[1]} ${ethers.formatUnits(res.args[2],6)}`
      )
    }
    );
  } catch (e) {
    console.log(e);
  }
})()

好啦,让我们执行一下然后看一下运行结果。

image.png