китаев.tech

Миграция с ethers v6 на viem: таблица соответствий

Найдёшь соответствия API ethers.js v6 → viem и перепишешь типичный скрипт чтения баланса ERC-20 на клиенты viem.

Справочник, а не новый туториал

Если код на ethers.js v6 уже есть, миграция на viem — это в основном замена имён и модели объектов: Provider/Signer/Contract → PublicClient/WalletClient/getContract или readContract/writeContract.

Ниже — таблица соответствий и один полный пример «было → стало». Концепты publicClient / walletClient разобраны в предыдущей статье.


Таблица ethers v6 → viem

ethers.js v6viem
new JsonRpcProvider(url)createPublicClient({ chain, transport: http(url) })
new BrowserProvider(window.ethereum)createWalletClient({ chain, transport: custom(window.ethereum) })
Wallet / signerwalletClient + account (privateKeyToAccount или адрес из кошелька)
new Contract(address, abi, provider)getContract({ address, abi, client: publicClient }) или publicClient.readContract(...)
new Contract(address, abi, signer)getContract({ address, abi, client: { public, wallet } }) или walletClient.writeContract(...)
provider.getBalance(addr)publicClient.getBalance({ address: addr })
provider.getBlockNumber()publicClient.getBlockNumber()
provider.getNetwork()publicClient.getChainId() (+ chain при создании клиента)
signer.getAddress()account.address или walletClient.getAddresses()
signer.sendTransaction({...})walletClient.sendTransaction({...})
tx.wait()publicClient.waitForTransactionReceipt({ hash })
contract.balanceOf(addr)contract.read.balanceOf([addr]) или readContract({ functionName: "balanceOf", args: [addr] })
contract.transfer(to, amount)contract.write.transfer([to, amount]) или writeContract({ functionName: "transfer", args: [to, amount] })
parseEther / formatEtherте же имена: parseEther / formatEther из "viem"
parseUnits / formatUnitsте же имена из "viem"
BigNumberнативный bigint (1000n, не BigNumber.from)
abi как обычный массивABI с as const — иначе нет автовывода типов

Имена утилит (parseEther, formatEther, isAddress) часто совпадают. Ломается модель объектов и форма вызовов: аргументы контракта — массивом, опции — объектом.


Один скрипт: было / стало

Задача одна: прочитать balanceOf USDC на mainnet.

ethers.js v6

typescript
import { ethers } from "ethers";

const provider = new ethers.JsonRpcProvider("https://ethereum.publicnode.com");

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

const erc20Abi = [
  "function balanceOf(address owner) view returns (uint256)",
  "function decimals() view returns (uint8)",
];

const usdc = new ethers.Contract(usdcAddress, erc20Abi, provider);

const [rawBalance, decimals] = await Promise.all([
  usdc.balanceOf(holder),
  usdc.decimals(),
]);

console.log(ethers.formatUnits(rawBalance, decimals));

viem

typescript
import {
  createPublicClient,
  http,
  formatUnits,
  getContract,
} 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: "owner", type: "address" }],
    outputs: [{ name: "", type: "uint256" }],
  },
  {
    type: "function",
    name: "decimals",
    stateMutability: "view",
    inputs: [],
    outputs: [{ name: "", type: "uint8" }],
  },
] as const;

const usdc = getContract({
  address: usdcAddress,
  abi: erc20Abi,
  client: publicClient,
});

const [rawBalance, decimals] = await Promise.all([
  usdc.read.balanceOf([holder]),
  usdc.read.decimals(),
]);

console.log(formatUnits(rawBalance, decimals));

Human-readable ABI-строки ethers ("function balanceOf...") в viem не используются. Нужен JSON-ABI (или фрагмент) и желательно as const.


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

getContract(...) — ближайший аналог new Contract(...): получаешь объект с .read / .write. Альтернатива без объекта — publicClient.readContract({ address, abi, functionName, args }).

usdc.read.balanceOf([holder]) — аргументы всегда массивом, даже если параметр один. В ethers было balanceOf(holder).

as const на ABI — фиксирует литеральные имена функций и типы для TypeScript. Без него viem не выведет сигнатуры.

formatUnits(rawBalance, decimals)rawBalance уже bigint. Не вызывай .toString() «как у BigNumber», если дальше ждёшь bigint.


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

Оставляешь human-readable ABI → viem его не парсит как ethers. Конвертируй в JSON-фрагмент или бери ABI из артефакта компиляции.

contract.balanceOf(addr) без .read и без массива → в viem через getContract путь такой: contract.read.balanceOf([addr]).

tx.wait() на результате sendTransaction → viem возвращает hash (0x...), не объект транзакции. Жди через publicClient.waitForTransactionReceipt({ hash }).

Смешиваешь BigNumber и bigint → после миграции арифметика только через bigint (amount + 1n), не через .add().


Что дальше

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