本文介绍了Multicall,一个用于优化以太坊应用数据查询效率的链上合约。它通过将多个静态调用打包成一次eth_call,显著减少了RPC请求次数、降低延迟并确保所有查询结果来自同一区块快照,解决了JSON-RPC批量调用无法保证状态一致性的问题。文章提供了一个Go语言的实现示例。
如果你的应用程序对余额、符号或储备进行几十次 eth_call 调用,你将浪费时间和速率限制预算。
Multicall 解决了这个问题。它是一个简单的链上合约,可以一次性执行多个 staticcall,并从单个区块快照中返回所有结果。
在这篇文章中,你将看到如何将多个读取打包到一个 eth_call 中,通过 Polygon Amoy 上的 Multicall3 运行它们,并在 Go 中解码结果。我们还将它与 JSON-RPC batching 进行比较,并解释为什么只有 Multicall 才能保证所有结果的状态一致性。
当你的应用程序需要大量视图调用时:余额、授权、符号、池储备,逐一执行它们意味着:
Multicall 解决了所有这些问题。它是一个微型合约,可以一次性执行许多 staticcall 并返回所有结果,这样你只需发出一个 eth_call,就能得到 N 个答案,所有答案都来自同一个区块。
你将每个读取(例如 balanceOf(user))进行 ABI 编码,将它们打包到一个数组中 → 通过一个 eth_call 调用 Multicall 的 aggregate/tryAggregate → 解码每个返回数据块。
注意: 你可以在 这里 找到 Multicall 的部署。
示例:
package main
import (
"context"
"fmt"
"log"
"math/big"
"strings"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
)
const multicallABI = `[ { "inputs":[ {"internalType":"bool","name":"requireSuccess","type":"bool"}, { "components":[ {"internalType":"address","name":"target","type":"address"}, {"internalType":"bytes","name":"callData","type":"bytes"} ],\
"internalType":"struct Call[]",\
"name":"calls",\
"type":"tuple[]"\
}\
],\
"name":"tryAggregate",\
"outputs":[\
{\
"components":[\
{"internalType":"bool","name":"success","type":"bool"},\
{"internalType":"bytes","name":"returnData","type":"bytes"}\
],\
"internalType":"struct Result[]",\
"name":"returnData",\
"type":"tuple[]"\
}\
],\
"stateMutability":"nonpayable",\
"type":"function"\
}\
]`
const erc20ABI = `[ {"name":"balanceOf","type":"function","stateMutability":"view","inputs":[{"name":"owner","type":"address"}],"outputs":[{"type":"uint256"}]},\
{"name":"symbol","type":"function","stateMutability":"view","inputs":[],"outputs":[{"type":"string"}]},\
{"name":"decimals","type":"function","stateMutability":"view","inputs":[],"outputs":[{"type":"uint8"}]}\
]`
var (
RPC = "https://polygon-amoy.drpc.org"
MULTICALL_ADDR = common.HexToAddress("0xcA11bde05977b3631167028862bE2a173976CA11") // Multicall3
TOKEN_ADDR = common.HexToAddress("0x0Fd9e8d3aF1aaee056EB9e802c3A762a667b1904") // Amoy 上的 LINK
USER = common.HexToAddress("0x7F8b1ca29F95274E06367b60fC4a539E4910FD0c")
)
func main() {
ctx := context.Background()
client, err := ethclient.Dial(RPC)
if err != nil {
log.Fatalf("dial rpc: %v", err)
}
mabi, err := abi.JSON(strings.NewReader(multicallABI))
if err != nil {
log.Fatalf("parse multicall abi: %v", err)
}
eabi, err := abi.JSON(strings.NewReader(erc20ABI))
if err != nil {
log.Fatalf("parse erc20 abi: %v", err)
}
// 为 ERC20 读取构建 calldata
balData, err := eabi.Pack("balanceOf", USER)
if err != nil {
log.Fatalf("pack balanceOf: %v", err)
}
symData, err := eabi.Pack("symbol")
if err != nil {
log.Fatalf("pack symbol: %v", err)
}
decData, err := eabi.Pack("decimals")
if err != nil {
log.Fatalf("pack decimals: %v", err)
}
type Call struct {
Target common.Address
CallData []byte
}
calls := []Call{
{Target: TOKEN_ADDR, CallData: balData},
{Target: TOKEN_ADDR, CallData: symData},
{Target: TOKEN_ADDR, CallData: decData},
}
input, err := mabi.Pack("tryAggregate", false, calls)
if err != nil {
log.Fatalf("pack tryAggregate: %v", err)
}
// 单次 eth_call 调用
msg := ethereum.CallMsg{To: &MULTICALL_ADDR, Data: input}
out, err := client.CallContract(ctx, msg, nil)
if err != nil {
log.Fatalf("CallContract (eth_call): %v", err)
}
// 解码结果
var results []struct {
Success bool
ReturnData []byte
}
if err := mabi.UnpackIntoInterface(&results, "tryAggregate", out); err != nil {
log.Fatalf("unpack tryAggregate: %v", err)
}
if len(results) != 3 {
log.Fatalf("unexpected results len: %d", len(results))
}
var (
balance *big.Int
symbol string
decimals uint8
)
// 0: balanceOf
if results[0].Success {
vals, err := eabi.Unpack("balanceOf", results[0].ReturnData)
if err != nil {
log.Fatalf("unpack balanceOf: %v", err)
}
balance = vals[0].(*big.Int)
} else {
log.Printf("balanceOf failed")
}
// 1: symbol
if results[1].Success {
vals, err := eabi.Unpack("symbol", results[1].ReturnData)
if err != nil {
log.Fatalf("unpack symbol: %v", err)
}
symbol = vals[0].(string)
} else {
log.Printf("symbol failed")
}
// 2: decimals
if results[2].Success {
vals, err := eabi.Unpack("decimals", results[2].ReturnData)
if err != nil {
log.Fatalf("unpack decimals: %v", err)
}
decimals = vals[0].(uint8)
} else {
log.Printf("decimals failed")
}
fmt.Printf("Symbol: %s, Decimals: %d\n", symbol, decimals)
if balance != nil {
fmt.Printf("Balance(%s): %s\n", TOKEN_ADDR.Hex(), balance.String())
}
}
结果将如下所示:
Symbol: LINK, Decimals: 18
Balance(0x0Fd9e8d3aF1aaee056EB9e802c3A762a667b1904): 38000000000000000000
一些提供商支持在一次 HTTP 调用中发送 eth_* 请求数组。这对于获取余额、区块头或交易收据等情况可能很方便。但对于 eth_call,通常不推荐这样做。节点仍然会单独执行每个调用,并且你将失去结果来自完全相同区块的保证。如果你关心一致的快照和速率限制节省,链上 Multicall 是更好的选择。
注意: JSON-RPC batching 有助于减少 HTTP 请求,但只有 Multicall 才能保证所有调用在同一个区块中具有一致的快照。
Multicall 是那些稀有的实用工具之一,它默默地为生态系统中的几乎每个仪表板、分析应用程序和链上数据服务提供支持。它通过单个 eth_call 为你带来一致性、更低的延迟和更少的 RPC 麻烦。
记住我以便更快登录
虽然 JSON-RPC batching 可以减少 HTTP 请求,但它不能保证所有结果都来自同一个区块。Multicall 可以做到。通过在链上以一个静态上下文执行读取,你可以获得原子级的、同步的状态快照——这是准确的数据管道或价格显示所必需的。
与追踪和事件流一起,Multicall 完善了任何构建实时、高效以太坊集成的开发者的工具包。
- 原文链接: medium.com/@andrey_obruc...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!