События, диапазоны блоков и актуальный статус
Оглавление серии «Реестр» · Урок 9 из 10.
Веб-форма уже читает запись, выдаёт её и отзывает после подтверждения пользователя. Теперь добавим историю событий для известного хеша. Ожидаемый результат — страница покажет, в каких блоках произошли выдача и отзыв, но продолжит получать актуальный статус через get. Журнал отвечает на вопрос о случившихся действиях. Хранилище отвечает на вопрос о нынешнем значении записи. Эти результаты связаны, однако один не заменяет другой и не должен автоматически определять подпись «действителен».
События Issued и Revoked сохраняют хеш в индексированном поле. Поэтому RPC может выбирать журналы нужного сертификата без скачивания каждой записи контракта. Ограничим поиск адресом конкретного экземпляра и диапазоном от его начального блока до захваченного текущего блока. В архиве startBlock заполняется читателем после развёртывания. Неправильное позднее значение может скрыть первоначальную выдачу, поэтому граница является частью данных лаборатории, а не случайным способом уменьшить число результатов.
Фильтр относится к определённому контракту
Одинаковая сигнатура события может присутствовать в нескольких контрактах. Если искать только по названию и хешу, результаты не обязательно относятся к нашему реестру. В каждом запросе передаём address из проверенной конфигурации. Затем задаём конкретное ABI-описание события и его индексированный аргумент digest. Так фильтр выражает три условия: нужный экземпляр, нужный тип действия и нужный сертификат. Для настоящего сервиса к этому добавится выбранная доверенная сеть и её политика подтверждения данных.
parseAbiItem подготавливает описание одного события. Для Issued время хранится в данных, а хеш и издатель — в темах. Для Revoked индексированы хеш и действующее лицо. Фильтруем только известный хеш, оставляя адрес участника в полученном событии. Наличие события отзыва владельцем не меняет первоначального издателя в структуре, как определили четвёртым уроком. Это позволяет различать происхождение записи и причину текущего статуса по последовательности действий.
Полный модуль ограниченного запроса
Ниже history.js. Сначала превращаем стартовый блок в BigInt и получаем верхнюю границу один раз. Затем проверяем порядок и ограничение пяти тысяч блоков. Маленькая лаборатория не должна без предупреждения сканировать произвольную цепочку. Диапазон разбивается на куски до 500 блоков. Для каждого куска отдельно читаем два события. Такой вариант проще понять и ограничить, чем один огромный запрос, который может упереться в пределы RPC или вернуть неожиданно большой результат.
import {parseAbiItem} from 'viem';
import {publicClient} from './client.js';
const issued=parseAbiItem('event Issued(bytes32 indexed digest,address indexed issuer,uint64 issuedAt)');
const revoked=parseAbiItem('event Revoked(bytes32 indexed digest,address indexed actor)');
export async function history(config,digest) {
const from=BigInt(config.startBlock);const to=await publicClient.getBlockNumber();
if(from<0n || from>to || to-from>=5000n)throw Error('История ограничена 5000 блоками; уточните startBlock');
const rows=[];
for(let first=from;first<=to;first+=500n) {
const last=first+499n>to?to:first+499n;
for(const event of [issued,revoked]) {
const logs=await publicClient.getLogs({address:config.address,event,args:{digest},fromBlock:first,toBlock:last,strict:true});
for(const log of logs)rows.push({type:event.name,block:log.blockNumber.toString(),index:log.logIndex,transaction:log.transactionHash});
}
}
return rows.sort((a,b)=>BigInt(a.block)<BigInt(b.block)?-1:BigInt(a.block)>BigInt(b.block)?1:a.index-b.index);
}
Диапазоны включают обе границы. После куска от first до first + 499 следующий начинается с first + 500. Поэтому блоки не дублируются и не пропускаются. Последний кусок заканчивается захваченной верхней границей, даже если до полного размера не хватает блоков. Проверка разницы учитывает включительность: пять тысяч блоков означают разницу не больше 4999. Такие детали полезно проследить на маленьких числах, прежде чем переносить запрос на значительно больший набор данных.
Большие числа и порядок истории
Номер блока приходит как BigInt, а в JSON-строке показывается через toString. В итоговых строках также хранится индекс журнала и хеш транзакции. Сортировка сначала сравнивает номера блока как BigInt, затем индекс внутри одного блока. Если сортировать номера как обычные текстовые строки, блок 100 может оказаться раньше 20. Текстовое представление удобно для передачи, но порядок должен определяться числовым смыслом. Именно поэтому преобразование обратно выполняется в компараторе.
Хеш транзакции связывает событие с отдельной отправленной операцией. Он не заменяет хеш документа и не становится ключом mapping. В интерфейсе историю показываем отдельным блоком, чтобы не подменить поле сертификата случайным идентификатором операции. Для продуктовой страницы можно добавить ссылки на соответствующий просмотрщик выбранной сети. В нашей локальной лаборатории сторонний обозреватель не обязателен; полученные номера и хеши остаются данными собственной цепочки читателя.
Что означает strict
Запрос использует strict: true, чтобы библиотека оставила события, согласованные с выбранным ABI. Это относится к декодированию логов, а не к доверию к издателю. Строгая схема не подтверждает личность владельца адреса и не делает неизвестный контракт нужной организацией. Уже проверенные сеть и адрес определяют контекст запроса. Данные события затем интерпретируются по правилам реестра, а оценка доверия к конкретному издателю остаётся отдельным предметным шагом.
Если запрос к одному диапазону завершится ошибкой, модуль не выдаёт частично собранный массив за полную историю. Ошибка передаётся обработчику интерфейса. В большом приложении можно реализовать продолжение со сохранённой границей и явным статусом полноты. Но тогда пользователю нужно сообщать, какие блоки ещё не получены. Молчаливое отсутствие строки отзыва в незавершённой истории не является основанием объявить документ активным. Getter всё равно остаётся источником текущего поля revoked.
Снимок верхней границы
Верхний блок читается до цикла. Если во время нескольких запросов появляются новые блоки, они не добавляются незаметно в середину этой выдачи. Следующий запрос истории сможет захватить новую границу. Это помогает описать, какой интервал получен, и не превращает длинную последовательность RPC в бесконечное преследование постоянно растущей цепочки. Для интерфейса живой подписки понадобится другой механизм обновления и явное различение накопленной истории и новых уведомлений.
В публичной сети номер блока сам по себе не решает вопрос реорганизаций. Один и тот же диапазон может потребовать повторного чтения по политике подтверждений, а обработанные события — сопоставления с канонической цепочкой. Наша серия ограничена локальным Anvil и не скрывает эту разницу. Здесь изучаем фильтрацию и ограничение запросов. Переносить такой модуль на огромную настоящую сеть без индекса, политики финальности и оценки возможностей RPC было бы отдельным проектом.
История и решение о статусе
Представим две строки: сначала Issued, затем Revoked. По ним видно, что выдача действительно была, а позднее произошёл отзыв. Если показывать только наличие первой строки, интерфейс ошибётся. Поэтому рядом остаётся кнопка чтения, получающая текущую структуру. В финальном уроке проверка файла выполнит getter в выбранном блоке и сравнит издателя с доверенным адресом. История поможет человеку понять результат, но не станет единственным источником решения.
Отсутствие строк тоже имеет несколько причин: ключ не выдавался, стартовая граница неправильная, выбран другой экземпляр или произошла ошибка среды. Успешный пустой запрос с правильно заданными границами отличается от отказа RPC. Удобный интерфейс должен сохранять эту разницу. В лаборатории полученный массив и сообщение разделены: ошибка не оформляется как «история пуста». Такой порядок пригодится при любых журналируемых системах, где полнота данных имеет предметное значение.
Ожидаемые наблюдения
После самостоятельной выдачи и отзыва одного хеша ожидаются два упорядоченных события и актуальный revoked == true в getter. Для неизвестного хеша ожидается пустой массив при правильной среде. Если стартовую границу поставить после выдачи, результат станет неполным по смыслу, хотя запрос может быть успешным. При написании эти операции не выполнялись. Фильтры описаны в getLogs viem, структура журналов — в событиях Solidity. Следующий урок соединит расчёт файла, доверенный адрес и актуальное состояние, чтобы сформировать ограниченный и объяснимый результат проверки.
Исходники этого шага
Архив серии содержит полную папку lesson-09. Возьмите именно её; переносить файлы из соседних папок не требуется. Версии и самостоятельная подготовка описаны в README. Пример, установка зависимостей и его выполнение при написании не запускались; результаты в тексте ожидаются по разбору исходников. Материал сохранён локально как Markdown-черновик.