Индексатор Ethereum: журнал CourseCatalog и события
Оглавление курса «Web3: вход, смарт-аккаунты и индексатор» · Урок 11 из 30.
Текущее состояние реестра не содержит всю историю: publish заменяет запись, а deactivate меняет активность. События позволяют внешнему сервису восстановить последовательность этих действий. В этом уроке определим нормализованную запись события для будущего индексатора. Результат — история курса17 связана с конкретной сетью, экземпляром и блоком, а не только временем получения ответа.
Предполагается базовый CourseCatalog и его ABI. Предыдущая часть курса создала модель кабинета, теперь добавляем независимый источник публичной истории. Узел, программы, база и проверки не запускались. Все примеры событий заданы как учебные входы; реальные хеши не выдумываются.
История отличается от текущего getter
courses(17) возвращает последний отпечаток и активность. Если курс публиковался три раза, getter не перечисляет старые значения. Published сохраняет факт каждой успешной публикации и новый отпечаток. Deactivated сохраняет факт деактивации. Обработка журнала является внешней задачей, а не скрытой возможностью Solidity-mapping.
Оба события индексируют courseId, поэтому запрос можно ограничить курсом17. Адрес экземпляра также ограничивается. Событие другого контракта с тем же названием не относится к нашей истории автоматически. Выбор сети выполняет публичный клиент, а не кошелёк читателя в кабинете.
Внутри блока события имеют порядок. Несколько публикаций одного курса могут произойти в одной транзакции или в разных транзакциях одного блока. Время блока не различит их. Для нормализованной записи сохраняем номер блока, хеш блока, хеш транзакции и позицию журнала. Этот порядок станет основой проекции текущего состояния.
Собственный формат индексатора
Данные RPC преобразуются на границе чтения. Приложение не сохраняет непрозрачный объект библиотеки как единственный формат базы. Схема содержит нужные поля и собственную версию.
export function normalizeCatalogLog(log, deployment) {
if (log.blockNumber === null || log.blockHash === null
|| log.transactionHash === null || log.logIndex === null) {
throw new Error('Нужен журнал из известного блока');
}
if (log.eventName !== 'Published' && log.eventName !== 'Deactivated') {
throw new Error('Неизвестное событие реестра');
}
return {
schemaVersion: 1,
environmentId: deployment.environmentId,
chainId: deployment.chainId.toString(),
address: deployment.catalogAddress.toLowerCase(),
blockNumber: log.blockNumber.toString(),
blockHash: log.blockHash,
transactionHash: log.transactionHash,
logIndex: log.logIndex,
eventName: log.eventName,
courseId: log.args.courseId.toString(),
contentHash: log.eventName === 'Published' ? log.args.contentHash : null,
};
}
Функция предполагает предварительно декодированный журнал соответствующего ABI. На общей границе также проверяются адрес и необходимые аргументы. Тип courseId нормализуется в десятичную строку для хранения, но вычисления и сортировка номеров блока позднее используют bigint или подходящий числовой тип базы, а не лексикографический порядок строки.
contentHash = null у деактивации означает, что событие не содержит такого поля. Это не команда обнулить отпечаток курса. Проекция сохранит предыдущий хеш и изменит активность. Непонимание этого различия часто создаёт внешнюю историю, которая расходится с самим контрактом.
Назначение каноничности
Журнал получен из наблюдаемого блока. Этот блок может позже перестать быть частью текущей цепочки. Поэтому запись факта получения и решение о каноничности должны быть разделены. Индексатор может сохранять сырой журнал для расследования, а видимая история использует текущую выбранную ветвь по checkpoint.
Хеш транзакции сам по себе не определяет положение в канонической истории. Та же транзакция может наблюдаться в другом блоке после смены ветви. Пара blockHash и позиция помогает различать наблюдения. Позднее обработка реорганизации удалит или отметит неканонические события и пересоберёт производные данные.
Слово «подтверждено» в API истории определяется отдельной политикой сервиса. Локальная лаборатория может использовать текущий head для обучения, а публичная сеть — более устойчивый уровень. Этот выбор записывается в конфигурации и показывается клиенту. Не называем все полученные журналы безусловно финальными.
Событие и личный кабинет
История реестра публична. Она не хранит, кто прочитал статью или какой прогресс сохранил читатель. Эти данные остаются в персональной базе кабинета. Нельзя смешать публичные события с таблицей пользователей и затем выдать общий каталог как персональный ответ. У каждой области своя политика доступа и кеширования.
Publisher базового контракта является единственным разрешённым отправителем, но сами события не обязательно содержат адрес отправителя отдельным параметром. При необходимости расследования индексатор читает транзакцию из соответствующего источника. Он не вставляет выдуманный author только потому, что в интерфейсе есть название профессора.
Редакционная причина деактивации тоже отсутствует в событии. Внешний комментарий может быть связан с операцией в отдельном журнале. Его происхождение показывается как данные сайта, а не как поле цепочки. Такое разделение помогает не завышать проверяемость текста API.
Начальная точка
Первое чтение начинается с известного блока развёртывания. Он берётся из контролируемой записи экземпляра. Если начать позже, история окажется неполной; если заявить полноту без исходной точки, клиент не сможет понять пропуск. Для большой сети чтение всего от нулевого блока обычно лишнее и дорогое.
Смена environmentId после сброса лаборатории создаёт самостоятельную историю. Один chainId31337 не отличает все учебные запуски. Поэтому ключи таблиц и checkpoint учитывают поколение, сохраняя прежние записи архивными. Автоматически приклеивать новые события к старому проекту нельзя.
Для упражнения задайте последовательность PublishedA → Deactivated → PublishedB и выпишите каждую нормализованную запись. У деактивации отсутствует contentHash, а итоговая проекция имеет B и активностьtrue. Эти ожидаемые результаты не проверялись программой. Следующий урок ограничит диапазоны RPC. Формат журналов и запросов описан в документации Ethereum JSON-RPC.
Публичная история и пользовательское объяснение
Для преподавателя полезно представить одну последовательность: опубликован хеш A, запись деактивирована, затем опубликован хеш B. Эти три события показывают изменение реестра, но не рассказывают причины решений. Административное описание может существовать отдельно, однако его происхождение должно быть явным. Нельзя дописать объяснение «обнаружена ошибка в статье» к Deactivated так, будто эта строка была записана в блокчейне.
Если метаданные материала недоступны, история всё равно может показывать номер курса, тип события и хеш. Отсутствующее название не отменяет сетевой факт. И наоборот, успешно загруженный красивый JSON не доказывает, что он относится к данному contentHash: проверка материала находится в собственной линии приложения.
Для поиска предоставляем производные поля, например краткое название, но исходное наблюдение остаётся неизменным. При обновлении алгоритма обогащения можно пересобрать поисковый документ, не переписывая событие. Такое разделение помогает сохранить историю и одновременно улучшать интерфейс без выдуманных полей базового контракта.