如何提升使用ethers批量查询多合约代币余额的速度?
现有代码性能瓶颈
- 串行执行:循环内每一次
await都会等待前一个RPC请求返回后才发起下一个请求,100个代币就要走100次RPC往返,耗时直接累加 - 冗余请求:你的
tokens对象已经存储了decimals、symbol字段,代码仍然重复发起链上调用查询decimals,平白多了一倍请求量 - 单代币单请求:每个代币的
balanceOf都单独发起RPC请求,请求量大会触发RPC节点限流,也会额外增加握手、传输的开销
方案1:最小成本优化(改几行代码即可提速3~10倍)
把串行逻辑改成并行批量执行,同时删除冗余的decimals链上查询,修正原代码大数比较的精度问题:
// 先过滤掉不需要查询的原生币地址 const erc20Tokens = Object.values(tokens).filter(t => t.address !== "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee") // 批量发起所有balanceOf查询,并行执行 const balancePromises = erc20Tokens.map(async token => { const contract = new ethers.Contract(token.address, Abi, provider) const balanceWei = await contract.balanceOf(address) return { ...token, balanceWei } }) const allTokenBalances = await Promise.all(balancePromises) // 过滤余额大于0的,直接用本地存的decimals计算即可 const validTokens = allTokenBalances.filter(t => t.balanceWei.gt(0)).map(t => { // 用ethers内置方法格式化,避免大数精度问题 const balance = ethers.utils.formatUnits(t.balanceWei, t.decimals) return { address: t.address, decimals: t.decimals, symbol: t.symbol, balance: balance } }) // 输出结果 validTokens.forEach(t => { console.log(t.address, t.decimals, t.symbol, t.balance) })
方案2:Multicall批量调用(适合代币数量超过20个的场景,提速10~100倍)
通过Multicall合约把所有balanceOf调用打包成1次RPC请求,避免大量并发请求触发RPC限流,网络开销降到最低:
// 以太坊主网Multicall合约地址,其他链可替换成对应链的官方Multicall地址 const MULTICALL_ADDRESS = "0xeefBa1e63905eF1D7ACbA5a8513c70307C1cE441" const MULTICALL_ABI = [{"constant":true,"inputs":[{"components":[{"name":"target","type":"address"},{"name":"callData","type":"bytes"}],"name":"calls","type":"tuple[]"}],"name":"aggregate","outputs":[{"name":"blockNumber","type":"uint256"},{"name":"returnData","type":"bytes[]"}],"payable":false,"stateMutability":"view","type":"function"}] const erc20Tokens = Object.values(tokens).filter(t => t.address !== "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee") // 构造批量调用参数 const iface = new ethers.utils.Interface(Abi) const calls = erc20Tokens.map(token => { const callData = iface.encodeFunctionData("balanceOf", [address]) return { target: token.address, callData: callData } }) // 一次请求获取所有余额 const multicall = new ethers.Contract(MULTICALL_ADDRESS, MULTICALL_ABI, provider) const { returnData } = await multicall.aggregate(calls) // 解析返回结果 const validTokens = [] erc20Tokens.forEach((token, index) => { const balanceWei = ethers.BigNumber.from(returnData[index]) if (balanceWei.gt(0)) { const balance = ethers.utils.formatUnits(balanceWei, token.decimals) validTokens.push({ address: token.address, decimals: token.decimals, symbol: token.symbol, balance }) } }) // 输出结果 validTokens.forEach(t => { console.log(t.address, t.decimals, t.symbol, t.balance) })
方案3:预索引方案(适合生产环境高频查询场景)
如果你的服务需要给大量用户频繁查询余额,可以自行部署索引服务,监听链上所有ERC20的Transfer事件,把用户余额实时同步到数据库,用户查询时直接读数据库即可,响应耗时可以降到毫秒级,完全不需要实时发起链上调用。
内容的提问来源于stack exchange,提问作者Mariusz Sidorowicz
相关产品推荐
相关产品推荐

