Мультивызовы: 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
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.
const strict = await publicClient.multicall({
contracts: [/* ... */],
allowFailure: false, // любой revert → исключение, result — сразу значения
});chain: mainnet (и другие сети в viem/chains) уже знает адрес Multicall3. Без поддерживаемой сети multicall некуда отправить.
Автобатчинг отдельных readContract без явного multicall:
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 + кодирование calldata | publicClient.multicall({ contracts }) |
сам декодируешь returnData | result уже типизирован по 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.
Что дальше
- viem + wagmi — те же чтения через
useReadContract/useReadContracts - Чтение данных из контракта: readContract — одиночный view-вызов без batch
- Multicall3 — батчинг вызовов — тот же паттерн вручную на ethers.js v6