китаев.tech

Чтение данных из контракта: 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

typescript
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, для symbolstring.

args: [holder] — аргументы всегда массив. Даже один address нельзя передать «голым» значением.

as const на ABI — TypeScript знает список functionName и типы args/result. Без as const автокомплит и проверка типов почти пропадают.

formatUnits(rawBalance, decimals) — USDC использует 6 decimals, не 18. formatEther здесь даст неверный масштаб.


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

typescript
const usdc = new ethers.Contract(usdcAddress, erc20Abi, provider);
const balance = await usdc.balanceOf(holder);
ethers v6viem
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 и неверный тип аргумента.


Что дальше

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