0%

Web3 使用multicall优化rpc请求

如果需要一次调用很多合约请求的话,直接通过rpc请求会等很久,我们可以通过Multicall.js来batch rpc请求为一个rpc,这样可以节省时间。

安装

Multicall.js

1
npm i @makerdao/multicall

用法

在代码中引用:

1
const {createWatcher} = require('@makerdao/multicall');

首先需要设定config:

1
2
3
4
// Preset can be 'mainnet', 'kovan', 'rinkeby', 'goerli' or 'xdai'
const config = {
preset: 'rinkeby'
}

因为目前是在测试网测试的,所以采用rinkeby的预设。

但感觉这个预设好像有点问题,所以我们还是手动配置一下:

1
2
3
4
const multiCallConfig = {
multicallAddress: '0x5ba1e12693dc8f9c48aad8770482f4739beed696',
rpcUrl: 'https://rinkeby.infura.io/v3/9aa3d95b3bc440fa88ea12eaa4456161',
}

multicallAddress可以在github multicall找到。

rpcUrl可以在rpc.info中找到。

然后创建watcher:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const watcher = createWatcher(
[
{
target: ContractId,
call: ['balanceOf(address)(uint256)', userAddress],
return [['balanceOf']],
}
]
)

watcher.batch().subscribe(updates => {
for (const item of updates) {
console.log('item', item)
}
});
watcher.start();

这样就是一个简单的例子。

watcher.start()就表示正在调用rpc请求了,请求成功之后会在watcher.batch().subscribe()里面能收到。

mulitcall的官网文档没有写清楚,导致我试了好久才正直用上了它。

calls解析

我们给createWatcher传递的是一个calls数组:

  • target 代表的是请求的合约地址
  • call 里面配置具体要请求什么方法,以及传递的参数
  • returns 代表的是接收rpc请求返回的key。

看下call的配置

1
call: ['balanceOf(address)(uint256)', userAddress],

传给call的是一个数组,数组的第一项会被解析为合约的方法,之后的项会被解析为方法的参数。

第一项里面,前面是方法名称,第一个括号里面是方法的参数,注意这里是参数的类型,而不是参数的名字,第二个括号里面是返回值的类型。

然后还需要注意returns里面的key配置,如果key一样的话,multiCall会把结果合并为一个,所以需要特别注意retures里面的key都需要不同。

实例

假设我们现在需要获取某个用户在合约里面的所有nft列表。

思路:

  1. 调用balanceOf方法获取账户拥有的代币数量。
  2. 调用tokenOfOwnerByIndex方法获取账号在给定索引处拥有的tokenId。

balanceOftokenOfOwnerByIndex这两个方法是ERC721标准里面的标准方法,每个合约都会有该方法。可以查看ERC721 标准

调用balanceOf会得到用户的nft数量,然后枚举这个数量,可以通过tokenOfOwnerByIndex方法得到该索引的tokenId,这样就能获得该用户的所有nft。

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
const multiCallConfig = {
multicallAddress: '0x5ba1e12693dc8f9c48aad8770482f4739beed696',
rpcUrl: 'https://rinkeby.infura.io/v3/9aa3d95b3bc440fa88ea12eaa4456161',
}

getAllNFTs() {
return fromPromise(this.getAccount()).pipe(
switchMap(() => {
const contractInstance = this.getContractInstance();
return fromPromise(contractInstance.methods.balanceOf(this._account[0]).call())
}),
switchMap((spacePunksBalance) => {
return new Promise((resolve, reject) => {
try {

const start = new Date().getTime();
console.log('start rpc get token')
const tokenList = [];
const queryList = []
for (let i = 0; i < spacePunksBalance; i++) {
queryList.push(
{
target: ContractAddress,
call: ['tokenOfOwnerByIndex(address,uint256)(uint256)', this._account[0], i],
returns: [[`tokenId${i}`]]
},
)

}
const watcher = createWatcher(
queryList,
multiCallConfig
);
watcher.batch().subscribe(updates => {
for (const item of updates) {
tokenList.push(item.value.toNumber());
}
const end = new Date().getTime()
console.log('[get all token] rpc progress time: ', (end - start) / 1000)
resolve(tokenList);
});
watcher.start();
} catch (e) {
reject(e)
}

})

})
)
}

可以看到这个去请求100个nft的时候,时间开销猜不到1秒,快了很多了。

码字辛苦,打赏个咖啡☕️可好?💘