Миграция с 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 v6 | viem |
|---|---|
new JsonRpcProvider(url) | createPublicClient({ chain, transport: http(url) }) |
new BrowserProvider(window.ethereum) | createWalletClient({ chain, transport: custom(window.ethereum) }) |
Wallet / signer | walletClient + 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
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
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().
Что дальше
- Чтение данных из контракта: readContract — view-вызовы без
getContract, с типизацией ABI - Запись в контракт: writeContract и simulateContract — запись и симуляция вместо
contract.transfer(...) - Автовывод типов из ABI — зачем нужен
as constи как ловит ошибки на этапе компиляции