китаев.tech

Мультивызовы: multicall в viem

Соберёшь несколько view-вызовов в один eth_call через publicClient.multicall на примере USDC и WETH.

Один eth_call вместо десятка

publicClient.multicall объединяет несколько readContract-подобных вызовов в один RPC-запрос. Под капотом viem ходит в Multicall3 на адресе сети — отдельно ABI Multicall3 и ручную сборку aggregate3 писать не нужно.

Это про чтение. Атомарный batch записи (несколько state-changing вызовов в одной tx) — другая задача; см. Batch через Multicall.


Читаем USDC и WETH одним multicall

typescript
import { createPublicClient, http, formatUnits, formatEther } from "viem";
import { mainnet } from "viem/chains";

const publicClient = createPublicClient({
  chain: mainnet,
  transport: http("https://ethereum.publicnode.com"),
});

const usdcAddress = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
const wethAddress = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";
const holder = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045";

const erc20Abi = [
  {
    type: "function",
    name: "balanceOf",
    stateMutability: "view",
    inputs: [{ name: "account", type: "address" }],
    outputs: [{ type: "uint256" }],
  },
  {
    type: "function",
    name: "decimals",
    stateMutability: "view",
    inputs: [],
    outputs: [{ type: "uint8" }],
  },
  {
    type: "function",
    name: "symbol",
    stateMutability: "view",
    inputs: [],
    outputs: [{ type: "string" }],
  },
] as const;

const results = await publicClient.multicall({
  contracts: [
    {
      address: usdcAddress,
      abi: erc20Abi,
      functionName: "symbol",
    },
    {
      address: usdcAddress,
      abi: erc20Abi,
      functionName: "decimals",
    },
    {
      address: usdcAddress,
      abi: erc20Abi,
      functionName: "balanceOf",
      args: [holder],
    },
    {
      address: wethAddress,
      abi: erc20Abi,
      functionName: "symbol",
    },
    {
      address: wethAddress,
      abi: erc20Abi,
      functionName: "balanceOf",
      args: [holder],
    },
  ],
});

const [usdcSymbol, usdcDecimals, usdcBalance, wethSymbol, wethBalance] =
  results;

if (
  usdcSymbol.status !== "success" ||
  usdcDecimals.status !== "success" ||
  usdcBalance.status !== "success" ||
  wethSymbol.status !== "success" ||
  wethBalance.status !== "success"
) {
  throw new Error("Один из вызовов multicall завершился с ошибкой");
}

console.log(
  `${usdcSymbol.result}: ${formatUnits(usdcBalance.result, usdcDecimals.result)}`,
);
console.log(`${wethSymbol.result}: ${formatEther(wethBalance.result)}`);

Пять view-функций — один round-trip к RPC.


Как это работает

multicall({ contracts: [...] }) — каждый элемент массива тот же набор полей, что у readContract: address, abi, functionName, args.

По умолчанию allowFailure: true: один revert не роняет весь batch. В ответе у каждого элемента status: "success" | "failure" и либо result, либо error.

typescript
const strict = await publicClient.multicall({
  contracts: [/* ... */],
  allowFailure: false, // любой revert → исключение, result — сразу значения
});

chain: mainnet (и другие сети в viem/chains) уже знает адрес Multicall3. Без поддерживаемой сети multicall некуда отправить.

Автобатчинг отдельных readContract без явного multicall:

typescript
const publicClient = createPublicClient({
  chain: mainnet,
  transport: http("https://ethereum.publicnode.com"),
  batch: {
    multicall: true,
  },
});

Тогда подряд идущие readContract в одном тике event loop могут схлопнуться в один eth_call. Явный multicall всё равно удобнее, когда нужна предсказуемая пачка и разбор status.


Если ты знаешь ethers.js

ethers + Multicall3 вручнуюviem
ABI Multicall3 + aggregate3 + кодирование calldatapublicClient.multicall({ contracts })
сам декодируешь returnDataresult уже типизирован по ABI
адрес Multicall3 копируешь самадрес из chain (viem/chains)
статья Multicall3этот API

По смыслу это тот же Multicall3 read-batching, что в DeFi-статье, только без бойлерплейта.


Частые ошибки

Не проверяешь status при allowFailure: true → обращение к .result у failed-элемента даст undefined / сюрприз в рантайме. Либо проверяй статус, либо ставь allowFailure: false.

Ждёшь атомарную записьmulticall у publicClient — только read (eth_call). State не меняет и gas с кошелька не берёт.

Кастомная / локальная сеть без Multicall3 → метод упадёт: в chain нет контракта. Для Hardhat задеплой Multicall3 или читай через отдельные readContract.


Что дальше

Материалы китаev.tech публикуются в образовательных целях и не являются инвестиционной рекомендацией. Примеры кода и описания протоколов — для обучения; использование в продакшне на ваш собственный риск. Дисклеймер и политика конфиденциальности.