Чтение данных из контракта: readContract
Вызовешь view-функцию через publicClient.readContract на примере balanceOf USDC с ABI as const.
Read без объекта Contract
readContract — вызов view/pure функции контракта через RPC (eth_call). State не меняется, gas не платишь, приватный ключ не нужен.
В отличие от ethers, где часто делают new Contract(...).balanceOf(addr), в viem можно обойтись без объекта контракта: передаёшь address, abi, functionName и args в один вызов.
Читаем balanceOf USDC
import { createPublicClient, http, formatUnits } from "viem";
import { mainnet } from "viem/chains";
const publicClient = createPublicClient({
chain: mainnet,
transport: http("https://ethereum.publicnode.com"),
});
const usdcAddress = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
const holder = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045";
const erc20Abi = [
{
type: "function",
name: "balanceOf",
stateMutability: "view",
inputs: [{ name: "account", type: "address" }],
outputs: [{ name: "", type: "uint256" }],
},
{
type: "function",
name: "decimals",
stateMutability: "view",
inputs: [],
outputs: [{ name: "", type: "uint8" }],
},
{
type: "function",
name: "symbol",
stateMutability: "view",
inputs: [],
outputs: [{ name: "", type: "string" }],
},
] as const;
const [rawBalance, decimals, symbol] = await Promise.all([
publicClient.readContract({
address: usdcAddress,
abi: erc20Abi,
functionName: "balanceOf",
args: [holder],
}),
publicClient.readContract({
address: usdcAddress,
abi: erc20Abi,
functionName: "decimals",
}),
publicClient.readContract({
address: usdcAddress,
abi: erc20Abi,
functionName: "symbol",
}),
]);
console.log(`${formatUnits(rawBalance, decimals)} ${symbol}`);Три независимых eth_call. Для пачки вызовов в одном RPC позже будет multicall.
Как это работает
readContract({ address, abi, functionName, args }) — кодирует calldata по ABI, шлёт eth_call, декодирует ответ. Результат типизирован: для balanceOf это bigint, для symbol — string.
args: [holder] — аргументы всегда массив. Даже один address нельзя передать «голым» значением.
as const на ABI — TypeScript знает список functionName и типы args/result. Без as const автокомплит и проверка типов почти пропадают.
formatUnits(rawBalance, decimals) — USDC использует 6 decimals, не 18. formatEther здесь даст неверный масштаб.
Если ты знаешь ethers.js
const usdc = new ethers.Contract(usdcAddress, erc20Abi, provider);
const balance = await usdc.balanceOf(holder);| ethers v6 | viem |
|---|---|
new Contract(addr, abi, provider) | не обязателен: достаточно readContract |
usdc.balanceOf(holder) | readContract({ functionName: "balanceOf", args: [holder] }) |
| human-readable ABI-строки | JSON-ABI + as const (или parseAbi([...])) |
| несколько вызовов = несколько RPC | то же через Promise.all, либо multicall |
getContract из статьи про миграцию — сахар над теми же read-вызовами: usdc.read.balanceOf([holder]).
Частые ошибки
Забыл args или передал не массив → balanceOf без args: [address] упадёт на кодировании. Один аргумент всё равно кладёшь в [...].
formatEther для ERC-20 → смотри decimals() контракта. Для USDC нужен formatUnits(value, 6).
ABI без as const → код может выполниться, но TypeScript не поймает опечатку в functionName и неверный тип аргумента.
Что дальше
- Запись в контракт: writeContract и simulateContract — симуляция и отправка state-changing вызова
- Автовывод типов из ABI — как
as constдаёт автокомплит сигнатур - Вызов view-функций — read — тот же сценарий на ethers.js v6