Перейти к содержанию

ABI в Web3-фронтенде: вызовы реестра CourseCatalog

Оглавление курса «Web3-интерфейс: реестр в браузере» · Урок 4 из 30.

ABI связывает понятные имена функций с байтами запроса и ответа. Он нужен не только для отправки транзакций: чтение courses(17) тоже должно правильно закодировать аргумент и разобрать результат. В этом уроке составим интерфейс базового CourseCatalog и научимся отличать описание метода от адреса его реализации.

Предполагаются публичный клиент и проверенная конфигурация экземпляра из предыдущих шагов. Результат — один модуль catalog-abi.js, который переиспользуют чтение, предварительное моделирование и разбор ошибок. Все листинги относятся к учебному профилю Solidity 0.8.30 и viem 2.37.9. Запуск клиента и вызовов не проводился; состояния ниже являются ожидаемыми.

Сигнатура и возвращаемые поля

Публичный getter отображения courses принимает uint256 и возвращает поля структуры: bytes32 contentHash, bool active. Его сигнатура для выбора функции определяется именем и типами входных аргументов. Имена аргументов удобны человеку, однако два ABI с разными именами параметра courseId описывают одинаковые входные байты, если тип остаётся uint256.

Возвращаемые значения не входят в селектор функции. Поэтому случайное описание getter как функции, возвращающей одну строку, не выбирает другой метод на контракте: клиент вызывает прежний метод и неправильно интерпретирует ответ. Иногда такая ошибка проявляется как невозможность декодирования, а иногда создаёт более опасное правдоподобное значение. ABI должен происходить из конкретного исходника и процесса его сборки, а не из догадки по названию кнопки.

В нашем учебном контракте нет метода getAllCourses. Отображение не перечисляет ключи автоматически. Нельзя получить полный каталог, просто придумав такую запись в ABI. Для списка номеров потребуется внешняя известная коллекция или история событий, которую позднее обработает индексатор. Отдельный интерфейс чтения курса 17 не решает задачу перечисления.

Полный ABI учебного реестра

Создадим модуль и заменим маленький ABI предыдущего урока этим экспортом. Значения ошибок и событий взяты из базового контракта без изменения семантики.

import { parseAbi } from 'viem';

export const catalogAbi = parseAbi([
  'function publisher() view returns (address)',
  'function courses(uint256 courseId) view returns (bytes32 contentHash, bool active)',
  'function publish(uint256 courseId, bytes32 contentHash)',
  'function deactivate(uint256 courseId)',
  'error Unauthorized()',
  'error InvalidCourse()',
  'error NotActive()',
  'event Published(uint256 indexed courseId, bytes32 contentHash)',
  'event Deactivated(uint256 indexed courseId)',
]);

parseAbi принимает читаемую форму ABI. Описание события помечает номер курса как indexed: этот параметр хранится в topics и может участвовать в фильтре журнала. Отпечаток в Published остаётся неиндексированным полем данных события. Менять это слово в клиентском модуле по желанию нельзя: реальная схема журнала определяется контрактом.

Ошибки полезны для разбора отката предварительного моделирования. Если ABI не содержит Unauthorized, клиент может показать неизвестные байты вместо понятного названия. Добавление ошибки помогает интерпретировать ответ; оно не создаёт проверку полномочий и не меняет поведение EVM. Саму проверку всё ещё выполняет модификатор onlyPublisher.

Кодирование отдельно от отправки

Чтобы понять роль ABI, подготовим данные вызова без кошелька и без сетевого запроса. Это отдельный фрагмент для будущего файла inspect-call.js.

import { encodeFunctionData, decodeFunctionData } from 'viem';
import { catalogAbi } from './catalog-abi.js';

const data = encodeFunctionData({
  abi: catalogAbi,
  functionName: 'courses',
  args: [17n],
});
const decoded = decodeFunctionData({ abi: catalogAbi, data });
// Ожидается: decoded.functionName === 'courses'; args содержат 17n.

Результат кодирования имеет вид шестнадцатеричных байтов. Первые четыре байта выбирают функцию, следующие кодируют её аргументы. Здесь не указан адрес и не выбрана сеть. Такие же данные можно направить к другому контракту, который обрабатывает селектор иначе. Именно поэтому подготовленные данные нельзя считать полным описанием операции.

В форме публикации пользователь видит номер курса и отпечаток; кошелёк получает адрес назначения, данные и сетевой контекст. Прозрачный интерфейс показывает обе стороны: понятное действие и фактический экземпляр. Если между подготовкой данных и подписью конфигурация поменялась, прежний запрос становится устаревшим, даже когда аргументы остались одинаковыми.

Получение результата

Для courses viem возвращает пару значений. В данном ABI обе позиции известны: отпечаток и булев флаг. Не надо проверять активность через истинность отпечатка как JavaScript-строки: строка из нулей тоже является непустой и считается истинной. Отсутствие записи определяется сравнением с нулевым bytes32, а активность — вторым возвращённым значением.

Не следует также сохранять результат как непрозрачный массив и затем обращаться к нему по разным позициям в нескольких компонентах. На границе чтения преобразуем пару в именованный объект. Тогда рендеринг использует course.active, а модуль сравнения файла — course.contentHash. Такая маленькая нормализация уменьшает зависимость интерфейса от расположения полей.

Несовместимое изменение

Представим, что новый контракт возвращает ещё и номер редакции. Обновление клиентского ABI без обновления экземпляра не делает новое поле доступным. Нужно сохранить обе версии интерфейса и выбрать ту, которая соответствует записи развёртывания. Для базового курса оставляем CourseCatalog.base.v1; новый контракт вводится отдельным шагом с собственным исходником.

Обратная ситуация тоже важна: правильный старый ABI может позволить читать часть нового контракта, но ничего не сообщать о новых ограничениях записи. Успешное чтение не является полной проверкой совместимости приложения. Для выпуска страницы требуется рассмотреть все используемые методы, события и ошибки, а также значения конфигурации.

Самостоятельно сопоставьте три варианта: неправильный тип аргумента, неправильное возвращаемое поле и правильный ABI у другого адреса. Запишите, на какой границе возникает проблема: кодирование, декодирование или выбор экземпляра. Ожидаемый итог — вы понимаете, почему ABI необходим, но недостаточен для надёжного чтения. Формат подробно описан в спецификации ABI Solidity, а клиентское кодирование — в encodeFunctionData.

Почему ABI не содержит исходник

Описание функции сообщает типы входа и выхода, но не раскрывает её внутренние проверки. Два контракта могут иметь одинаковую сигнатуру publish и совершенно разную логику полномочий. Поэтому изучение ABI не заменяет чтение onlyPublisher и тела метода. Для нашего примера смысл ошибок определён исходником, а клиентский модуль лишь помогает разобрать доступные байты ответа.

Байткод тоже не получается простым склеиванием ABI. На него влияют исходники, зависимости и параметры компиляции. Если приложение предлагает «проверить контракт», нужно назвать конкретное сопоставление этих данных с экземпляром. Добавление правильных названий методов на страницу само по себе не подтверждает происхождение кода.

При обновлении исходника храните ABI как артефакт того же выпуска приложения. Не редактируйте вручную только одну сигнатуру после чужого примера, оставляя старый address и журнал событий. В будущем процессе сборки исходника это правило обеспечит воспроизводимость, а сейчас оно помогает сохранить точную учебную модель.

Оглавление серии