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

ERC-4337: UserOperation и абстракция аккаунта

Оглавление курса «Web3: вход, смарт-аккаунты и индексатор» · Урок 21 из 30.

В предыдущих уроках мы отделили вход читателя от публичного реестра библиотеки и научились показывать состояние индексатора. Теперь рассмотрим другой путь отправки изменений: смарт-аккаунт принимает подписанное намерение пользователя, а специальная инфраструктура доставляет его в сеть. Результатом урока станет модель такого намерения для публикации курса 17. Она позволит понять, какие проверки необходимы до выбора конкретного кошелька и сервиса.

Это архитектурный пример, который здесь не запускался. Мы не создаём универсальный кошелёк и не объявляем собственный объект совместимым с RPC какого-либо провайдера. Профиль сети остаётся учебным: chainId 31337. Реальные адреса аккаунта, реестра и EntryPoint должны поступить из отдельного файла развёртывания. Вставлять случайный адрес вместо отсутствующего значения нельзя.

Сначала определим отправителя

В прямой транзакции из курса об интерфейсах реестр видит в msg.sender адрес публикующего аккаунта. При использовании отдельного смарт-аккаунта вызов реестра выполняет этот контракт, поэтому отправителем для CourseCatalog становится адрес контракта. Владелец ключа, который подписал намерение, остаётся участником внутренней проверки аккаунта. Эти две роли совпадать не обязаны.

У нашего CourseCatalog publisher неизменяемый. Если существующий экземпляр создан с обычным адресом преподавателя, новый смарт-аккаунт не получает его полномочия автоматически. Для лаборатории выбираем отдельный экземпляр, в котором publisher сразу равен адресу учебного смарт-аккаунта. Старый реестр продолжает существовать со своими записями. Перенос указателя сайта на новый экземпляр является явным изменением окружения, а не тихой заменой адреса в кеше.

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

Учебный объект намерения

Создадим собственную структуру для обсуждения. Это вход нашего приложения, а не сериализованная UserOperation. Название intent помогает не смешивать пользовательскую задачу с окончательным сетевым пакетом.

export function publicationIntent({ deployment, account, contentHash }) {
  if (!/^0x[0-9a-fA-F]{64}$/.test(contentHash)) {
    throw new Error('Нужен хеш из 32 байтов');
  }
  if (/^0x0{64}$/.test(contentHash)) {
    throw new Error('Нулевой хеш запрещён');
  }
  return Object.freeze({
    environmentId: deployment.environmentId,
    chainId: 31337,
    sender: account,
    target: deployment.catalogAddress,
    action: 'publish',
    courseId: 17n,
    contentHash,
    value: 0n,
  });
}

Значение value равно нулю: функция publish в базовом реестре не принимает оплату. Это не означает отсутствия затрат на выполнение в сети. Оплата вычислений относится к механизму доставки операции, который рассмотрим отдельно. Не добавляем в форму стоимость курса или перевод токенов, поскольку таких действий в контракте нет.

Где появляется UserOperation

ERC-4337 описывает дополнительную инфраструктуру смарт-аккаунтов: операция проходит через EntryPoint и доставляется bundler. Конкретная версия EntryPoint и SDK определяет представление полей, упаковку, вычисление хеша и доступные расширения. Современную спецификацию нельзя без проверки подменять объектом из старого видеопримера. Официальная спецификация ERC-4337.

Наш адаптер получает intent, проверяет профиль и готовит окончательный запрос. Его договор удобно записать до реализации:

// Интерфейс учебного адаптера, не вызов готовой библиотеки.
async function prepareWithAdapter(adapter, intent, profile) {
  if (intent.chainId !== profile.chainId) throw new Error('Другая сеть');
  if (intent.environmentId !== profile.environmentId) {
    throw new Error('Другое развёртывание');
  }
  return adapter.prepare({ intent, profile });
}

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

Почему подпись не равна выполнению

Преподаватель может подписать корректное намерение, а состояние реестра затем изменится. Может закончиться срок разрешения, исчезнуть доступность сервиса, вырасти стоимость доставки или быть использован nonce. Поэтому одна подпись не доказывает ни принятие операции, ни попадание в блок, ни успешную публикацию.

Для приложения вводим последовательность: подготовлено намерение, получена подпись, принята доставка, найдено включение, подтверждено выполнение, перечитано состояние курса. Каждая ступень имеет собственное доказательство. Например, receipt внешней транзакции относится к контейнеру операций; результат именно нашей операции ещё необходимо выделить. После выполнения проверяем courses(17) в соответствующем блоке или показываем отставание индексатора.

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

Разберём ожидаемые состояния

В первом сценарии publisher реестра равен адресу смарт-аккаунта. Подготовка проходит, пользователь видит курс 17 и новый хеш, адаптер получает подпись и возвращает идентификатор операции. В этот момент карточка говорит «доставляется», а текущий хеш курса остаётся прежним до подтверждения. После успешного выполнения приложение связывает результат с исходным намерением.

Во втором сценарии publisher равен обычному адресу владельца. Даже если этот владелец управляет смарт-аккаунтом, вызов от контракта должен быть отклонён реестром. Интерфейс объясняет несоответствие роли и не предлагает исправить его повторной подписью. Требуется другой экземпляр реестра или заранее спроектированная система изменяемых полномочий.

В третьем сценарии ответ сервиса потерян после отправки. Мы сохраняем состояние «результат неизвестен» и ищем исходную операцию. Новый запрос с изменённым nonce способен стать вторым действием, поэтому его нельзя выдавать за безопасный повтор просмотра статуса. Эта граница потребуется в следующих уроках о доставке и восстановлении.

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

Адрес будущего аккаунта

Некоторые системы вычисляют адрес аккаунта до его фактического создания. Само знание такого адреса не доказывает, что там уже существует код или что выбранная фабрика создаст ожидаемую реализацию в данной сети. Если проект использует такой режим, файл развёртывания должен фиксировать фабрику, способ вычисления и подготовку создания аккаунта.

В нашей лаборатории эта возможность не реализована. Для понимания изменения msg.sender достаточно уже существующего отдельного аккаунта. Ограничение позволяет не скрывать создание кошелька внутри кнопки публикации и не выдавать пустой адрес за готового publisher. Будущее расширение получает собственную программу подготовки и ожидаемых состояний.

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