@1inch/multicall 中文文档教程
Multicall
This is the package for high-weight optimized calls to blockchain nodes
Installation
Node
npm install @1inch/multicall
Yarn
yarn install @1inch/multicall
Onchain addresses
- Ethereum mainnet:
0x8d035edd8e09c3283463dade67cc0d49d6868063
- BSC mainnet:
0x804708de7af615085203fa2b18eae59c5738e2a9
- Polygon mainnet:
0x59a0A6d73e6a5224871f45E6d845ce1574063ADe
Motivation
MultiCall 合约旨在一次执行多个视图调用。
例如,您有一个代币列表,您需要获取该列表中所有项目的余额。
让我们尝试以最明显的方式来做到这一点:
const provider = new Web3ProviderConnector(new Web3('...'));
const walletAddress = '0x1111111111111111111111111111111111111111';
const tokens = [
'0x6b175474e89094c44da98b954eedeac495271d0f',
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
'0xdac17f958d2ee523a2206206994597c13d831ec7'
];
const contractCalls = tokens.map((tokenAddress) => {
const callData = provider.contractEncodeABI(
ERC20ABI,
tokenAddress,
'balanceOf',
[walletAddress]
);
return provider.ethCall(
tokenAddress,
callData
);
});
const balances = await Promise.all(contractCalls);
此解决方案的缺点是您对合同的请求与列表中的代币一样多。
如果列表足够大,您将给提供者带来很大的负担。
Simple MultiCall
multiCallService.callByChunks() 合同获取请求列表,将它们分成块并分批调用提供程序。
Default params
- maxChunkSize: 100
- retriesLimit: 3
- blockNumber: 'latest'
例子:
const provider = new Web3ProviderConnector(new Web3('...'));
const walletAddress = '0x1111111111111111111111111111111111111111';
const contractAddress = '0x8d035edd8e09c3283463dade67cc0d49d6868063';
const multiCallService = new MultiCallService(provider, contractAddress);
const tokens = [
'0x6b175474e89094c44da98b954eedeac495271d0f',
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
'0xdac17f958d2ee523a2206206994597c13d831ec7'
];
// The parameters are optional, if not specified, the default will be used
const params: MultiCallParams = {
chunkSize: 100,
retriesLimit: 3,
blockNumber: 'latest'
};
const callDatas = tokens.map((tokenAddress) => {
return {
to: tokenAddress,
data: provider.contractEncodeABI(
ERC20ABI,
tokenAddress,
'balanceOf',
[walletAddress]
)
};
});
const balances = await multiCallService.callByChunks(callDatas, params);
好多了! 我们没有为每个项目向供应商发出单独的请求,而是将它们分组到 butches 中并发出更少的请求。
笔记: 如果对该方法的调用超过气体限制,则整个请求将被还原。
MultiCall by gas limit
问题:
关键是节点每次调用合约的 gas 都有限制。
并且可能发生通过简单的 MultiCall 我们将不会满足此限制。
如果节点上的 gas limit 足够大,我们可能会面临执行合约方法的时间限制。
总的来说,对一个节点有2个限制:
- by gas
- by time
为了避免这些限制,有一个更高级的方法:
multiCallService.callByGasLimit()
Default params
- maxChunkSize: 500
- retriesLimit: 3
- blockNumber: 'latest'
- gasBuffer: 3000000
- maxGasLimit: 150000000
示例:
const contractAddress = '0x8d035edd8e09c3283463dade67cc0d49d6868063';
const provider = new Web3ProviderConnector(new Web3('...'));
const gasLimitService = new GasLimitService(provider, contractAddress);
const multiCallService = new MultiCallService(provider, contractAddress);
const balanceOfGasUsage = 30_000;
const tokens = [
'0x6b175474e89094c44da98b954eedeac495271d0f',
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
'0xdac17f958d2ee523a2206206994597c13d831ec7'
];
const requests: MultiCallRequest[] = tokens.map((tokenAddress) => {
return {
to: tokenAddress,
data: provider.contractEncodeABI(
ERC20ABI,
tokenAddress,
'balanceOf',
[walletAddress]
),
gas: balanceOfGasUsage
};
});
const gasLimit: number = await gasLimitService.calculateGasLimit();
// The parameters are optional, if not specified, the default will be used
const params: MultiCallParams = {
maxChunkSize: 500,
retriesLimit: 3,
blockNumber: 'latest',
gasBuffer: 100_000
};
const response = await multiCallService.callByGasLimit(
requests,
gasLimit,
params
);
这个想法是我们从节点请求 gas 限制,并根据此限制将请求分成块。
因此,我们必须为每个请求设置气体限制。
值得注意的是,如果请求突然不符合 gas 限制,整个请求将不会被恢复,请求将返回符合 gas 限制的那些调用的结果。
如果对合约的调用仍然不符合气体限制,那么 callByGasLimit() 将自动重新请求那些尚未完成的元素。
您可以在下图中看到有关库工作的更详细描述。
GasLimitService
此服务用于正确计算调用 MultiCall 的气体限制。
极限计算的基本公式如下:
const gasLimitForMultiCall = Math.min(gasLimitFromNode, maxGasLimit) - gasBuffer;
其中:gasLimitFromNode
- 是从节点获取的 gas 限制maxGasLimit
- 顶部的限制器,以防节点的 gas 限制太大(可能导致超时)gasBuffer
- 是一些安全缓冲区,可让您避免在不可预见的情况下超过限制
示例:
const gasLimitForMultiCall = (Math.min(12_000_000, 40_000_000)) - 100_000; // 11_990_000
我们认为 multicall 调用应适合 11990000 gas。
Default params:
- gasBuffer: 3000000
- maxGasLimit: 150000000
GasLimitService.calculateGasLimit()
的参数是可选的,如果未指定,则将从节点请求 gas limit 并使用默认参数。
Example:
const contractAddress = '0x8d035edd8e09c3283463dade67cc0d49d6868063';
const provider = new Web3ProviderConnector(new Web3('...'));
const gasLimitService = new GasLimitService(provider, contractAddress);
const gasLimit: number = await gasLimitService.calculateGasLimit();
Alternatively, you can specify your own parameters:
const contractAddress = '0x8d035edd8e09c3283463dade67cc0d49d6868063';
const provider = new Web3ProviderConnector(new Web3('...'));
const gasLimitService = new GasLimitService(provider, contractAddress);
// 190_000
const gasLimit: number = await gasLimitService.calculateGasLimit({
gasLimit: 200_000,
maxGasLimit: 200_000,
gasBuffer: 10_000,
});
Contract code
Algorithm activity diagram
Algorithm visualization
Multicall
This is the package for high-weight optimized calls to blockchain nodes
Installation
Node
npm install @1inch/multicall
Yarn
yarn install @1inch/multicall
Onchain addresses
- Ethereum mainnet:
0x8d035edd8e09c3283463dade67cc0d49d6868063
- BSC mainnet:
0x804708de7af615085203fa2b18eae59c5738e2a9
- Polygon mainnet:
0x59a0A6d73e6a5224871f45E6d845ce1574063ADe
Motivation
The MultiCall contract is designed to execute multiple view calls at one time.
For example, you have a list of tokens, and you need to get balances for all the items on that list.
Let's try to do it in the most obvious way:
const provider = new Web3ProviderConnector(new Web3('...'));
const walletAddress = '0x1111111111111111111111111111111111111111';
const tokens = [
'0x6b175474e89094c44da98b954eedeac495271d0f',
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
'0xdac17f958d2ee523a2206206994597c13d831ec7'
];
const contractCalls = tokens.map((tokenAddress) => {
const callData = provider.contractEncodeABI(
ERC20ABI,
tokenAddress,
'balanceOf',
[walletAddress]
);
return provider.ethCall(
tokenAddress,
callData
);
});
const balances = await Promise.all(contractCalls);
The downside to this solution is that you make as many requests for a contract as you have tokens on the list.
And if the list is large enough, you will create a significant load on the provider.
Simple MultiCall
A multiCallService.callByChunks() contract takes a list of requests, splits them into chunks and calls the provider in batches.
Default params
- maxChunkSize: 100
- retriesLimit: 3
- blockNumber: 'latest'
Example:
const provider = new Web3ProviderConnector(new Web3('...'));
const walletAddress = '0x1111111111111111111111111111111111111111';
const contractAddress = '0x8d035edd8e09c3283463dade67cc0d49d6868063';
const multiCallService = new MultiCallService(provider, contractAddress);
const tokens = [
'0x6b175474e89094c44da98b954eedeac495271d0f',
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
'0xdac17f958d2ee523a2206206994597c13d831ec7'
];
// The parameters are optional, if not specified, the default will be used
const params: MultiCallParams = {
chunkSize: 100,
retriesLimit: 3,
blockNumber: 'latest'
};
const callDatas = tokens.map((tokenAddress) => {
return {
to: tokenAddress,
data: provider.contractEncodeABI(
ERC20ABI,
tokenAddress,
'balanceOf',
[walletAddress]
)
};
});
const balances = await multiCallService.callByChunks(callDatas, params);
Got better! Instead of making a separate request to the provider for each item, we group them into butches and make much fewer requests.
Note: If the call to this method exceeds the gas limit, then the entire request will be reverted.
MultiCall by gas limit
Problem:
The point is that the node has a limit for gas per call of the contract.
And it may happen that by making a simple MultiCall we will not meet this limit.
If the gas limit on the node is large enough, we may face a time limit on the execution of the contract method.
In total, there are 2 restrictions on a node:
- by gas
- by time
To avoid these limitations, there is a more advanced method:
multiCallService.callByGasLimit()
Default params
- maxChunkSize: 500
- retriesLimit: 3
- blockNumber: 'latest'
- gasBuffer: 3000000
- maxGasLimit: 150000000
Example:
const contractAddress = '0x8d035edd8e09c3283463dade67cc0d49d6868063';
const provider = new Web3ProviderConnector(new Web3('...'));
const gasLimitService = new GasLimitService(provider, contractAddress);
const multiCallService = new MultiCallService(provider, contractAddress);
const balanceOfGasUsage = 30_000;
const tokens = [
'0x6b175474e89094c44da98b954eedeac495271d0f',
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
'0xdac17f958d2ee523a2206206994597c13d831ec7'
];
const requests: MultiCallRequest[] = tokens.map((tokenAddress) => {
return {
to: tokenAddress,
data: provider.contractEncodeABI(
ERC20ABI,
tokenAddress,
'balanceOf',
[walletAddress]
),
gas: balanceOfGasUsage
};
});
const gasLimit: number = await gasLimitService.calculateGasLimit();
// The parameters are optional, if not specified, the default will be used
const params: MultiCallParams = {
maxChunkSize: 500,
retriesLimit: 3,
blockNumber: 'latest',
gasBuffer: 100_000
};
const response = await multiCallService.callByGasLimit(
requests,
gasLimit,
params
);
The idea is that we request the gas limit from the node and split the requests into chunks regarding this limit.
Accordingly, we must set the gas limit for each request.
It is noteworthy that if suddenly the request does not fit into the gas limit, the entire request will not be reverted, and the request will return the results of those calls that fit into the gas limit.
If the call to the contract all the same does not fit into the gas limit, then the callByGasLimit() will automatically re-request those elements that have not been fulfilled.
You can see a more detailed description of the library's work in the diagrams below.
GasLimitService
This service is used to correctly calculate the gas limit for calling a MultiCall.
The basic formula for calculating the limit is as follows:
const gasLimitForMultiCall = Math.min(gasLimitFromNode, maxGasLimit) - gasBuffer;
Where:gasLimitFromNode
- is the gas limit taken from the nodemaxGasLimit
- limiter on top, in case the gas limit from the node is too large (may cause timeout)gasBuffer
- is some safe buffer that allows you to avoid crossing the limit in case of unforeseen situations
Example:
const gasLimitForMultiCall = (Math.min(12_000_000, 40_000_000)) - 100_000; // 11_990_000
We believe that the multicall call should fit into 11990000 gas.
Default params:
- gasBuffer: 3000000
- maxGasLimit: 150000000
Params for GasLimitService.calculateGasLimit()
are optional, if not specified, then gas limit will be requested from the node and the default params will be used.
Example:
const contractAddress = '0x8d035edd8e09c3283463dade67cc0d49d6868063';
const provider = new Web3ProviderConnector(new Web3('...'));
const gasLimitService = new GasLimitService(provider, contractAddress);
const gasLimit: number = await gasLimitService.calculateGasLimit();
Alternatively, you can specify your own parameters:
const contractAddress = '0x8d035edd8e09c3283463dade67cc0d49d6868063';
const provider = new Web3ProviderConnector(new Web3('...'));
const gasLimitService = new GasLimitService(provider, contractAddress);
// 190_000
const gasLimit: number = await gasLimitService.calculateGasLimit({
gasLimit: 200_000,
maxGasLimit: 200_000,
gasBuffer: 10_000,
});