找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4130

积分

0

好友

570

主题
发表于 3 小时前 | 查看: 1| 回复: 0

如果你的应用程序需要对余额、符号或储备信息进行几十次 eth_call 调用,你将浪费大量时间并很快耗尽RPC服务的速率限制预算。

Multicall 正是为解决这个问题而生。它是一个部署在链上的轻量级合约,能够一次性执行多个 staticcall(静态调用),并从一个统一的区块快照中返回所有结果,从而保证了数据的一致性。

本文将带你了解如何将多个读取操作打包进一次 eth_call,通过 Polygon Amoy 测试网上的 Multicall3 合约来执行,并使用 Go 语言解码返回结果。我们还会将其与 JSON-RPC 批处理(batching)进行对比,并解释为什么只有 Multicall 能确保所有结果的状态一致性。

使用 Multicall 进行批量调用

当你的应用需要进行大量视图函数调用时——例如查询余额(balanceOf)、授权额度(allowance)、代币符号(symbol)或流动性池储备金(reserves)——逐个执行它们意味着你需要面对:

  • 额外的网络延迟(N次网络往返)。
  • 更高的服务商费用(N个独立请求)。
  • 更多的速率限制困扰(突发请求易被限流)。
  • 以及可能出现的数据快照不一致问题(结果来自略有差异的区块)。

Multicall 一举解决了上述所有痛点。它是一个微型合约,能够一次性执行大量 staticcall 并返回全部结果。这意味着你只需发起一次 eth_call,就能获得 N 个答案,并且所有答案都来自同一个区块

为什么说 Multicall 必不可少

  • 更少的 RPC 请求 → 更不容易触发速率限制,同时降低成本。
  • 更低的延迟 → 一次网络往返替代多次。
  • 一致的状态视图 → 所有读取都基于同一个区块的状态,避免了“半新半旧”的数据混杂。

如何用 Go 实现 Multicall

其核心流程是:将每个读取调用(例如 balanceOf(user))进行 ABI 编码,打包到一个数组中,然后通过一次 eth_call 调用 Multicall 合约的 aggregatetryAggregate 函数,最后解码每一块返回数据。

注意: 你可以在 multicall3.com 找到 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)的对比

部分节点服务商支持 JSON-RPC 批处理,即允许在一个 HTTP 请求中发送一个 eth_* 方法数组。这对于批量获取余额、区块头或交易收据等场景可能比较方便。

但对于 eth_call,通常不推荐依赖这种方式。因为节点在内部仍然会单独执行每一个调用,你无法保证所有结果都来自完全相同的区块状态。如果你的应用关心数据快照的一致性以及节省速率限制,链上 Multicall 是更优的选择

注意: JSON-RPC 批处理有助于减少 HTTP 层面的请求数量,但只有 Multicall 才能保证所有调用在同一个区块中具有完全一致的状态快照。

总结

Multicall 是生态系统中那些默默无闻却至关重要的基础工具之一,它为几乎所有的数据仪表板、分析应用和链上数据服务提供了支持。通过单个 eth_call,它为你带来了数据一致性、更低延迟和更少的 RPC 管理负担。

虽然 JSON-RPC 批处理可以减少 HTTP 请求,但它不能保证原子性的状态视图。Multicall 可以。通过在链上以一个静态的、统一的上下文执行读取,你可以获得原子级的、同步的状态快照——这是构建精确数据管道或实时价格显示等功能所必需的。

将 Multicall 与事件监听、交易追踪等技术结合,它构成了每一位旨在构建高效、实时以太坊应用开发者的核心工具包。希望这篇指南能帮助你更好地理解和应用这一强大工具,提升你的区块链开发效率。如果你想了解更多此类实战技巧,欢迎关注云栈社区的更新。




上一篇:大模型如何重塑电商数仓:Claude在得物的集成、效能演进与风险管控
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-26 09:44 , Processed in 0.678602 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表