ERC-4337: статусы операции и receipt транзакции
Оглавление курса «Web3: вход, смарт-аккаунты и индексатор» · Урок 27 из 30.
У нас есть намерение, политика аккаунта, доставка и возможная оплата. Теперь соберём наблюдение результата в одну понятную карточку. Главное затруднение состоит в том, что операция смарт-аккаунта и внешняя транзакция доставки имеют разные идентификаторы и разные результаты. В уроке построим чистую модель статусов, которая не объявляет курс 17 опубликованным только по успеху общего контейнера.
Код описывает нашу модель интерфейса, а не готовый ответ определённого SDK. Адаптер должен нормализовать наблюдения выбранного EntryPoint и сервиса, проверить их контекст и сопоставить идентификаторы. Примеры здесь не запускались, действительных receipt нет. Все ожидаемые переходы объясняются по договору функций и состоянию реестра.
Два идентификатора
operationHash относится к подготовленной операции пользователя. transactionHash относится к внешней транзакции, в которой bundler доставил один или несколько запросов. Один контейнер способен включать много операций, поэтому совпадение transactionHash не делает их одним действием. При восстановлении карточки необходимо знать именно свою операцию и её отправителя.
Внешняя транзакция может успешно обработать пакет, а выполнение конкретного целевого действия завершиться неуспешно. Приложению требуется результат соответствующей операции, а не только верхний статус receipt. Механизм обработки пакетов и наблюдения операций описан в ERC-4337.
У нас курс 17 меняется только при выполнении publish в CourseCatalog. Полученная подпись, ответ bundler и найденный контейнер — промежуточные наблюдения. После отдельного успешного результата проверяем связь с намерением и состояние реестра. Если индексатор отстаёт, показываем успех операции и ожидание обновления списка одновременно, не стирая ни один из этих фактов.
Нормализованные наблюдения
Адаптер отдаёт ограниченные типы событий. Их названия выбраны для нашей модели, а не заимствованы из универсального стандарта статусов. В частности, timeout описывает предел ожидания приложения и не доказывает, что операция никогда не будет включена.
export function operationState(record, observation) {
if (observation.operationHash !== record.operationHash) {
return { ...record, state: 'inconsistent' };
}
if (observation.environmentId !== record.environmentId) {
return { ...record, state: 'inconsistent' };
}
switch (observation.kind) {
case 'accepted':
return { ...record, state: 'pending' };
case 'unavailable':
return { ...record, state: 'unknown' };
case 'included':
return {
...record,
state: observation.executionSuccess ? 'executed' : 'execution-failed',
transactionHash: observation.transactionHash,
blockNumber: observation.blockNumber,
blockHash: observation.blockHash,
};
case 'reorged':
return { ...record, state: 'pending', removedBlockHash: record.blockHash };
default:
throw new Error('Неизвестное наблюдение адаптера');
}
}
Функция не решает, достоверен ли источник наблюдения. Это обязанность адаптера: проверка сети, адреса EntryPoint, отправителя и идентификатора операции должна происходить до преобразования данных. Просто прислать observation.executionSuccess из пользовательского запроса недостаточно для фиксации результата на сервере.
Переход reorged не стирает историю найденного включения. Поле removedBlockHash помогает объяснить, почему ранее видимое наблюдение перестало быть каноническим. Для полноценного журнала сохраняем последовательность наблюдений отдельно от текущей карточки. Чистая функция показывает текущее представление и не заменяет долговечное хранилище.
Что показывать преподавателю
В pending пишем «доставка ожидается» и показываем исходную задачу: курс 17, целевой реестр и ожидаемый хеш. В unknown объясняем проблему источника или истечение времени наблюдения. В execution-failed сообщаем, что найдено включение, но действие реестра не выполнено. В executed показываем связь с блоком и начинаем сверку актуального состояния.
Нельзя объединить всё в одну красную надпись «ошибка транзакции». Для сетевого сбоя полезен повтор чтения. Для ошибочного publisher нужно исправить окружение. Для просроченной политики необходимо новое разрешение. Для успешного выполнения с отстающим индексатором вообще не нужен повтор действия: нужен следующий цикл чтения данных.
Кнопки тоже зависят от статуса. Просмотр статуса доступен для pending и unknown. Новая публикация создаёт новую задачу и требует актуальной подготовки. Когда результат исходной операции неясен, приложение предупреждает об этом в самом сценарии новой отправки, а не прячет обстоятельство в технической консоли.
Успех операции и последняя версия
Предположим, операция A успешно опубликовала хеш A. Через один блок другой разрешённый запрос публикует хеш B. Наблюдение A остаётся успешным, но текущая запись courses(17) уже содержит B. Карточка должна уметь показать оба факта: «эта операция выполнена» и «позже появилась другая редакция».
Сравнение выполняем в определённом блоке. Если проверяем именно результат включения, используем соответствующий срез и его hash. Если показываем актуальную версию, явно называем новый срез. Случайный запрос latest после каждого обновления не должен переписывать историю результата операции и делать вид, что она изначально публиковала другой хеш.
Персональный кабинет использует эту же границу. Запись пользовательского прогресса в базе не должна считаться подтверждением публикации. Она может пометить урок прочитанным, пока сетевой запрос остаётся pending. Один успешный HTTP-запрос не завершает другой процесс лишь потому, что оба происходят на одной странице.
Ожидаемые сценарии
В первом сценарии внешняя транзакция найдена, но нормализованный результат нашей операции содержит executionSuccess равный false. Состояние становится execution-failed, текущий хеш не изменяется автоматически. Пользователь видит причину от адаптера и может подготовить новое разрешённое действие после выяснения состояния.
Во втором сценарии успешное исполнение найдено в блоке 210, а индексатор ещё дошёл до 205. Карточка операции показывает выполнено, список библиотеки показывает источник на блоке 205. Это согласованные сведения о разных срезах. Когда индексатор догонит, список обновится без повторной публикации.
В третьем сценарии блок включения исключён из канонической ветви. Карточка возвращается к ожиданию с объяснением изменения наблюдения. Поиск продолжается для той же операции. Ни новый идентификатор, ни новый nonce не выдаются только из-за исчезновения старого блока.
После урока пользовательский интерфейс может назвать доказательство каждого состояния. Такая модель пригодится и для обычных транзакций, но здесь особенно важна связь операции с контейнером. Далее ограничим сетевую нагрузку, чтобы множество карточек не создало множество бесконтрольных наблюдателей.
Поздний ответ старого наблюдателя
Один сервис может позже вернуть pending после того, как другой уже дал более сильное подтверждение включения. Простая чистая функция в статье показывает отдельный переход, но реальный контроллер обязан учитывать порядок и происхождение наблюдений. Старое сообщение не должно затирать подтверждённое состояние без нового факта о ветви.
Для этого вместе с ответом храним контекст наблюдения и поколение задачи. В пределах одной канонической ветви более слабый поздний ответ обновляет диагностику источника, а не отменяет найденный receipt. Возврат к ожиданию требует самостоятельного основания, например подтверждённого исключения блока, которое мы представили отдельным kind reorged.