利用 Multicall 和 RPC 批量高效地批处理调用

本文介绍了Multicall,一个用于优化以太坊应用数据查询效率的链上合约。它通过将多个静态调用打包成一次eth_call,显著减少了RPC请求次数、降低延迟并确保所有查询结果来自同一区块快照,解决了JSON-RPC批量调用无法保证状态一致性的问题。文章提供了一个Go语言的实现示例。

如果你的应用程序对余额、符号或储备进行几十次 eth_call 调用,你将浪费时间和速率限制预算。

Multicall 解决了这个问题。它是一个简单的链上合约,可以一次性执行多个 staticcall,并从单个区块快照中返回所有结果。

在这篇文章中,你将看到如何将多个读取打包到一个 eth_call 中,通过 Polygon Amoy 上的 Multicall3 运行它们,并在 Go 中解码结果。我们还将它与 JSON-RPC batching 进行比较,并解释为什么只有 Multicall 才能保证所有结果的状态一致性。

使用 Multicall 批量调用

当你的应用程序需要大量视图调用时:余额、授权、符号、池储备,逐一执行它们意味着:

  • 额外的延迟(N 次往返),
  • 更高的提供商账单(N 个请求),
  • 更多的速率限制之痛(突发请求被限制),
  • 以及不一致的快照(来自略微不同的区块的结果)。

Multicall 解决了所有这些问题。它是一个微型合约,可以一次性执行许多 staticcall 并返回所有结果,这样你只需发出一个 eth_call,就能得到 N 个答案,所有答案都来自同一个区块

为什么 Multicall 是必备的

  • 更少的 RPCs → 更少达到速率限制 + 更低的成本。
  • 更低的延迟 → 一次往返而不是多次。
  • 一致的状态 → 所有读取都来自同一个区块(没有“半旧”数据)。

如何实现

你将每个读取(例如 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

JSON-RPC Batching

一些提供商支持在一次 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 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Andrey Obruchkov
Andrey Obruchkov
江湖只有他的大名,没有他的介绍。