合约事件的监听

Posted by Thomas_Xu on 2022-11-14

合约事件的监听


author:Thomas_Xu

这两天在研究js中各种对事件的监听方式,并试图找出其中的不同。接下来会逐一对事件的监听方式进行解析。

事件和日志的区别

在介绍事件的监听前,我们先明确事件,日志这两个概念。事件发生后被记录到区块链上成为了日志。总的来说,事件强调功能,一种行为;日志强调存储,内容。

事件是以太坊EVM提供的一种日志基础设施。事件可以用来做操作记录,存储为日志。也可以用来实现一些交互功能,比如通知UI,返回函数调用结果等

事件的监听

我们在ether.js中可以直接使用contract.on函数对当前合约事件进行监听。但是contract.on函数只会监听当前区块的该合约事件,后面会讲解查询历史所有事件的方法。

此函数的应用场景:

在一个非view函数中如果想要获取合约中的某个变量的值,是需要发送交易的,很明显,这样的操作不划算。那么我们可以用一种更优雅的方式获取这样的数据。

通过事件的形式。(但这样的形式会带来新的问题,由于合约发送事件是需要在链上保存日志的,这也会带来花费,这就需要在两个需求之间进行自己的权衡了)

日志的gas花费成本:

根据黄皮书、日志的基础成本是375 gas 。另外每个的主题需要额外支付375 gas 的费用。最后,每个字节的数据需要8个 gas

这实际上很便宜!日志是一种以少量价格将少量数据存储在以太坊区块链上的优雅方法。

示例合约:

1
2
3
4
5
6
7
8
9
10
11
12
pragma solidity ^0.7.0;

contract Example {
event Return(uint256 num);
uint256 _accum = 0;

function increment() public returns (uint256 ) {
_accum++;
emit Return(_accum);
return _accum;
}
}

使用ether.js监听:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
const { expect } = require("chai");
const { Contract } = require("ethers");
const { ethers } = require('hardhat');

describe("Example test", function () {
let contractAddress = "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9"

const tokenAbi =[
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "num",
"type": "uint256"
}
],
"name": "Return",
"type": "event"
},
{
"inputs": [],
"name": "increment",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
}
]
let provider = new ethers.providers.JsonRpcProvider('http://localhost:8545')
let privKey = ''
let wallet = new ethers.Wallet(privKey,provider)


it("should fire the event", async function () {
//修改轮询时间?
// const provider = ethers.provider;
// provider.pollingInterval = 100;
const [deployer] = await ethers.getSigners();
const TokenContract = new ethers.Contract(contractAddress, tokenAbi, deployer);
contractSigner = TokenContract.connect(wallet)
//触发contract.on事件
contractSigner.on("Return", (num) => {
console.log('The event data is', num)
})

await contractSigner.increment()
//等待5秒,因为ether.js的轮询时间为4s
await new Promise(res => setTimeout(() => res(null), 5000));
});
});

在这里有一个轮询时间的概念,默认情况下,ethers.js使用轮询来获取事件,轮询间隔是4秒,所以您在测试的末尾添加以下内容:

1
2
//等待5秒,因为ether.js的轮询时间为4s
await new Promise(res => setTimeout(() => res(null), 5000));

然而!您还可以调整给定合约的轮询间隔,如下所示:

1
2
const provider = greeter.provider as EthersProviderWrapper;
provider.pollingInterval = 100;

历史事件的查询

不管是web3.js还是ether.js都给我们提供了很全的库来查询历史交易

web3.js

在web3.js中使用contract.getPastEvents函数来读取合约历史事件

调用

1
myContract.getPastEvents(event[, options][, callback])

参数:

  • event - String: 事件名,或者使用 “allEvents” 来读取所有的事件
  • options - Object: 用于部署的选项,包含以下字段:
    • filter - Object : 可选,按索引参数过滤事件,例如 {filter: {myNumber: [12,13]}} 表示所有“myNumber” 为12 或 13的事件
    • fromBlock - Number : 可选,仅读取从该编号开始的块中的历史事件。
    • toBlock - Number : 可选,仅读取截止到该编号的块中的历史事件,默认值为”latest”
    • topics - Array : 可选,用来手动设置事件过滤器的主题。如果设置了filter属性和事件签名,那么(topic[0])将不会自动设置
  • callback - Function : 可选的回调参数,触发时其第一个参数为错误对象,第二个参数为历史事件数组

返回值:

一个Promise对象,其解析值为历史事件对象数组

使用contract.getPastEvents监听:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
const Web3 = require('web3');
const web3 = new Web3('http://127.0.0.1:8545/');
const contractAddress = '0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9';
const contractAbi = [
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "num",
"type": "uint256"
}
],
"name": "Return",
"type": "event"
},
{
"inputs": [],
"name": "increment",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
}
]
const contract = new web3.eth.Contract(contractAbi, contractAddress);

const PAST_EVENT = async () => {
await contract.methods.increment().send({from: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8"})
await contract.getPastEvents('Return',
{
// filter: {num: [0,100]},
fromBlock: 0,
toBlock: 'latest'
},
(err, events) => {
console.log(events);
});
};
PAST_EVENT();

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[
{
removed: false,
logIndex: 0,
transactionIndex: 0,
transactionHash: '0x18196a4c3350f1356269c8943b45bbb6685bcd67ff957f5f7beed2ab7c7be91e',
blockHash: '0x63a0cfea8f60061d569ec739bc909e7e2bed23d70620d4099b648672e0e1b96d',
blockNumber: 10437919,
address: '0x5FbDB2315678afecb367f032d93F642f64180aa3',
id: 'log_77c8ec37',
returnValues: Result { '0': '1', num: '1' },
event: 'Return',
signature: '0x336bea1cd71b6fced353b888a0beae82acd2a0b7a3c283407f4b79af29cfa7ab',
raw: {
data: '0x0000000000000000000000000000000000000000000000000000000000000001',
topics: [Array]
}
}
]

ether.js

ether.js中的类似函数我找了很久,翻遍了文档才找到。

provider.getLogs()

从名字中就能看出来这和函数是获取日志。

使用方法倒是简单很多:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
const { ethers } = require('hardhat');

let contractAddress = "0x5FbDB2315678afecb367f032d93F642f64180aa3"

const tokenAbi =[
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "num",
"type": "uint256"
}
],
"name": "Return",
"type": "event"
},
{
"inputs": [],
"name": "increment",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
}
]

async function getevent() {
const provider = new ethers.providers.JsonRpcProvider()
const [deployer] = await ethers.getSigners();
const TokenContract = new ethers.Contract(contractAddress, tokenAbi, provider);
TokenContract.connect(deployer).increment();

let filter = TokenContract.filters.Return();
let filterLog = {
fromBlock : 0,
toBlock : 'latest',
topics : filter.topics
}
provider.getLogs(filterLog).then((result) => {
console.log(result);
})


}
getevent()

事件查询结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[
{
blockNumber: 10437919,
blockHash: '0x63a0cfea8f60061d569ec739bc909e7e2bed23d70620d4099b648672e0e1b96d',
transactionIndex: 0,
removed: false,
address: '0x5FbDB2315678afecb367f032d93F642f64180aa3',
data: '0x0000000000000000000000000000000000000000000000000000000000000001',
topics: [
'0x336bea1cd71b6fced353b888a0beae82acd2a0b7a3c283407f4b79af29cfa7ab'
],
transactionHash: '0x18196a4c3350f1356269c8943b45bbb6685bcd67ff957f5f7beed2ab7c7be91e',
logIndex: 0
}
]
Contract.queryFilter

这也是从合约中获取历史事件的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
const { ethers } = require('hardhat');

let contractAddress = "0x5FbDB2315678afecb367f032d93F642f64180aa3"

const tokenAbi =[
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "num",
"type": "uint256"
}
],
"name": "Return",
"type": "event"
},
{
"inputs": [],
"name": "increment",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
}
]


// export const getListOfTransfers = async (pairAddress) => {
async function getevent() {
const provider = new ethers.providers.JsonRpcProvider()
const [deployer] = await ethers.getSigners();
const TokenContract = new ethers.Contract(contractAddress, tokenAbi, provider);
TokenContract.connect(deployer).increment();

let filter = TokenContract.filters.Return();
let filterLog = {
fromBlock : 0,
toBlock : 'latest',
topics : filter.topics
}
provider.getLogs(filterLog).then((result) => {
console.log(result);
})

// let eventsWith = await TokenContract.queryFilter(filterLog, 0, 'latest');
// console.log(eventsWith);
// return eventsWith;

}
getevent()

查询结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[
{
blockNumber: 10437919,
blockHash: '0x63a0cfea8f60061d569ec739bc909e7e2bed23d70620d4099b648672e0e1b96d',
transactionIndex: 0,
removed: false,
address: '0x5FbDB2315678afecb367f032d93F642f64180aa3',
data: '0x0000000000000000000000000000000000000000000000000000000000000001',
topics: [
'0x336bea1cd71b6fced353b888a0beae82acd2a0b7a3c283407f4b79af29cfa7ab'
],
transactionHash: '0x18196a4c3350f1356269c8943b45bbb6685bcd67ff957f5f7beed2ab7c7be91e',
logIndex: 0,
removeListener: [Function (anonymous)],
getBlock: [Function (anonymous)],
getTransaction: [Function (anonymous)],
getTransactionReceipt: [Function (anonymous)],
event: 'Return',
eventSignature: 'Return(uint256)',
decode: [Function (anonymous)],
args: [ BigNumber { value: "1" }, num: BigNumber { value: "1" } ]
},
]

比较三种查询历史事件的结果:

web3.js—contract.getPastEvents()函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[
{
removed: false,
logIndex: 0,
transactionIndex: 0,
transactionHash: '0x18196a4c3350f1356269c8943b45bbb6685bcd67ff957f5f7beed2ab7c7be91e',
blockHash: '0x63a0cfea8f60061d569ec739bc909e7e2bed23d70620d4099b648672e0e1b96d',
blockNumber: 10437919,
address: '0x5FbDB2315678afecb367f032d93F642f64180aa3',
id: 'log_77c8ec37',
returnValues: Result { '0': '1', num: '1' },
event: 'Return',
signature: '0x336bea1cd71b6fced353b888a0beae82acd2a0b7a3c283407f4b79af29cfa7ab',
raw: {
data: '0x0000000000000000000000000000000000000000000000000000000000000001',
topics: [Array]
}
}
]

ether.js—provider.getLogs()函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[
{
blockNumber: 10437919,
blockHash: '0x63a0cfea8f60061d569ec739bc909e7e2bed23d70620d4099b648672e0e1b96d',
transactionIndex: 0,
removed: false,
address: '0x5FbDB2315678afecb367f032d93F642f64180aa3',
data: '0x0000000000000000000000000000000000000000000000000000000000000001',
topics: [
'0x336bea1cd71b6fced353b888a0beae82acd2a0b7a3c283407f4b79af29cfa7ab'
],
transactionHash: '0x18196a4c3350f1356269c8943b45bbb6685bcd67ff957f5f7beed2ab7c7be91e',
logIndex: 0
}
]

ether.js—Contract.queryFilter()函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[
{
blockNumber: 10437919,
blockHash: '0x63a0cfea8f60061d569ec739bc909e7e2bed23d70620d4099b648672e0e1b96d',
transactionIndex: 0,
removed: false,
address: '0x5FbDB2315678afecb367f032d93F642f64180aa3',
data: '0x0000000000000000000000000000000000000000000000000000000000000001',
topics: [
'0x336bea1cd71b6fced353b888a0beae82acd2a0b7a3c283407f4b79af29cfa7ab'
],
transactionHash: '0x18196a4c3350f1356269c8943b45bbb6685bcd67ff957f5f7beed2ab7c7be91e',
logIndex: 0,
removeListener: [Function (anonymous)],
getBlock: [Function (anonymous)],
getTransaction: [Function (anonymous)],
getTransactionReceipt: [Function (anonymous)],
event: 'Return',
eventSignature: 'Return(uint256)',
decode: [Function (anonymous)],
args: [ BigNumber { value: "1" }, num: BigNumber { value: "1" } ]
},
]

可以看到由于provider.getLogs()函数获取的数据是从日志里拿的,已经筛除掉了一部分的交易信息。

getLogs方法不会返回事件的返回值!

其他两个函数获取的数据差距不大,ether.js—Contract.queryFilter()函数的数据会全一点。并且会方便很多

获取待处理交易流:

什么是待处理交易

要在以太坊网络编写或者更新任何内容,需要有人创建,签署和发送交易。交易是外部世界与以太坊网络通信的方式。当发送到以太坊网络时,交易会停留在称为“mempool”的队列中,交易等待旷工被处理——- 处于这种等待交易称为待处理交易。发送交易所需要的少量费用称为gas;交易被旷工包含在一个区块中,并且根据它们包含的给旷工的gas 价格来确定优先级 。

查看这里, 将得到关于内存池和待处理交易的更多信息。

我为什么需要查看未处理的交易呢?

通过检查待处理的交易,可以执行以下操作:

  • 估计gas:理论上我们可以查看待处理的交易来预测下一个区块的最优gas价格。
  • 用于交易分析:我们可以分析去中心化交易所中的待处理交易,以便预测市场趋势。
  • 交易抢跑:在 DeFi 中,你可以预览即将到来的与价格(预言机)相关的交易,并可能对 MKR、COMP 和其他协议的保险库发出清算。

应用此方法我们可以完成一个简单的套利机器人

我们将使用WebSockets处理这些待处理的交易流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var ethers = require("ethers");
var url = "wss";

var init = function () {
var customWsProvider = new ethers.providers.WebSocketProvider(url);
console.log(customWsProvider.listeners.toString())
customWsProvider.on("pending", (tx) => {
customWsProvider.getTransaction(tx).then(function (transaction) {
console.log(transaction);
});
});

customWsProvider._websocket.on("error", async () => {
console.log(`Unable to connect to ${ep.subdomain} retrying in 3s...`);
setTimeout(init, 3000);
});
customWsProvider._websocket.on("close", async (code) => {
console.log(
`Connection lost with code ${code}! Attempting reconnect in 3s...`
);
customWsProvider._websocket.terminate();
setTimeout(init, 3000);
});
};

init();

查询结果:


notice

true

This is copyright.

...

...

00:00
00:00