Работа с ABI и Interface
Как использовать ethers Interface: кодировать calldata, декодировать ответы функций, разбирать транзакции и события по ABI.
Что делает Interface
Interface — это объект ethers, который превращает ABI в инструмент для работы с байтами.
Через него можно вручную собрать data для вызова функции, декодировать ответ eth_call, разобрать calldata транзакции и прочитать event log.
Кодируем и декодируем ABI
Создайте проект:
mkdir abi-and-interface-example
cd abi-and-interface-example
npm init -y
npm pkg set type="module"
npm install ethers tsx typescript @types/node
mkdir scriptsСоздайте файл scripts/interface-usdc.ts:
import { ethers } from "ethers";
const provider = new ethers.JsonRpcProvider("https://ethereum.publicnode.com");
const usdcAddress = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
const vitalikAddress = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045";
const binanceAddress = "0x28C6c06298d514Db089934071355E5743bf21d60";
const usdcAbi = [
"function balanceOf(address account) view returns (uint256)",
"function transfer(address to, uint256 amount) returns (bool)",
"event Transfer(address indexed from, address indexed to, uint256 value)",
];
const iface = new ethers.Interface(usdcAbi);
const balanceCalldata = iface.encodeFunctionData("balanceOf", [vitalikAddress]);
const rawBalance = await provider.call({
to: usdcAddress,
data: balanceCalldata,
});
const decodedBalance = iface.decodeFunctionResult("balanceOf", rawBalance);
const balance = decodedBalance[0] as bigint;
console.log("balanceOf calldata:", balanceCalldata);
console.log("Raw balance result:", rawBalance);
console.log("Decoded balance:", ethers.formatUnits(balance, 6), "USDC");
const transferCalldata = iface.encodeFunctionData("transfer", [
vitalikAddress,
1_000_000n,
]);
const parsedTransaction = iface.parseTransaction({
data: transferCalldata,
value: 0,
});
if (!parsedTransaction) {
throw new Error("Не удалось разобрать calldata transfer()");
}
console.log("Parsed function:", parsedTransaction.name);
console.log("Transfer to:", parsedTransaction.args[0]);
console.log("Transfer amount:", ethers.formatUnits(parsedTransaction.args[1], 6));
const encodedLog = iface.encodeEventLog("Transfer", [
binanceAddress,
vitalikAddress,
1_000_000n,
]);
const parsedLog = iface.parseLog({
data: encodedLog.data,
topics: encodedLog.topics,
});
if (!parsedLog) {
throw new Error("Не удалось разобрать лог Transfer");
}
console.log("Parsed event:", parsedLog.name);
console.log("From:", parsedLog.args[0]);
console.log("To:", parsedLog.args[1]);
console.log("Value:", ethers.formatUnits(parsedLog.args[2], 6), "USDC");Запустите скрипт:
npx tsx scripts/interface-usdc.tsСкрипт читает реальный USDC-контракт через raw provider.call, но не отправляет транзакцию. transfer и Transfer используются только для кодирования и разбора байтов.
Как это работает
new ethers.Interface(usdcAbi) — создаёт парсер ABI. Он знает сигнатуры функций и событий.
encodeFunctionData("balanceOf", [address]) — собирает calldata для вызова balanceOf(address).
provider.call({ to, data }) — отправляет raw eth_call в RPC. Это read-вызов без транзакции.
decodeFunctionResult("balanceOf", rawBalance) — декодирует bytes-ответ в uint256.
encodeFunctionData("transfer", [...]) — показывает, какие байты попали бы в поле data write-транзакции.
parseTransaction({ data, value }) — находит функцию по selector и возвращает имя функции вместе с аргументами.
encodeEventLog("Transfer", [...]) — собирает topics и data так, как это делает EVM для события.
parseLog({ topics, data }) — находит событие по первому topic и декодирует аргументы.
Где нужен Interface
Interface полезен, когда вы работаете не с удобным contract.balanceOf(...), а с сырыми данными:
tx.data
receipt.logs
eth_call result
revert dataТипичные задачи:
- показать пользователю, какую функцию он собирается подписать
- декодировать логи из receipt
- собрать calldata для multisig, account abstraction или batch-вызова
- разобрать неизвестную транзакцию по известному ABI
Function selector и event topic
Функция в calldata начинается с 4 байт:
balanceOf(address) -> 0x70a08231
transfer(address,uint256) -> 0xa9059cbbЭто selector функции. EVM использует его, чтобы понять, какую функцию вызвать.
Событие начинается с topic:
Transfer(address,address,uint256) -> 0xddf252ad...Это хеш сигнатуры события. По нему Interface понимает, как декодировать topics и data.
Частые ошибки
Передавать имя перегруженной функции без сигнатуры → если в ABI есть несколько foo(...), используйте полную сигнатуру: foo(address,uint256).
Путать calldata и результат функции → encodeFunctionData() собирает входные данные, а decodeFunctionResult() декодирует ответ.
Декодировать лог без event ABI → для parseLog() в ABI должно быть описание события.
Форматировать uint256 как обычное число → большие значения храните как bigint, а для токенов используйте ethers.formatUnits().
Что дальше
- Что такое ABI — базово разобрать, зачем ABI нужен приложению
- Вызов view-функций — read — использовать ABI через
ethers.Contract - Вызов state-changing функций — write — отправлять транзакции к функциям контракта