китаев.tech

Работа с ABI и Interface

Как использовать ethers Interface: кодировать calldata, декодировать ответы функций, разбирать транзакции и события по ABI.

Что делает Interface

Interface — это объект ethers, который превращает ABI в инструмент для работы с байтами.

Через него можно вручную собрать data для вызова функции, декодировать ответ eth_call, разобрать calldata транзакции и прочитать event log.


Кодируем и декодируем ABI

Создайте проект:

bash
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:

typescript
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");

Запустите скрипт:

bash
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(...), а с сырыми данными:

text
tx.data
receipt.logs
eth_call result
revert data

Типичные задачи:

  • показать пользователю, какую функцию он собирается подписать
  • декодировать логи из receipt
  • собрать calldata для multisig, account abstraction или batch-вызова
  • разобрать неизвестную транзакцию по известному ABI

Function selector и event topic

Функция в calldata начинается с 4 байт:

text
balanceOf(address) -> 0x70a08231
transfer(address,uint256) -> 0xa9059cbb

Это selector функции. EVM использует его, чтобы понять, какую функцию вызвать.

Событие начинается с topic:

text
Transfer(address,address,uint256) -> 0xddf252ad...

Это хеш сигнатуры события. По нему Interface понимает, как декодировать topics и data.


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

Передавать имя перегруженной функции без сигнатуры → если в ABI есть несколько foo(...), используйте полную сигнатуру: foo(address,uint256).

Путать calldata и результат функцииencodeFunctionData() собирает входные данные, а decodeFunctionResult() декодирует ответ.

Декодировать лог без event ABI → для parseLog() в ABI должно быть описание события.

Форматировать uint256 как обычное число → большие значения храните как bigint, а для токенов используйте ethers.formatUnits().


Что дальше

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