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

Web3 в браузере: кошелёк, подписи и транзакции

У нас есть реестр учебных курсов. Теперь посетитель должен увидеть опубликованный выпуск, а редактор — отправить изменение из браузера. При этом страница обязана работать и у человека без кошелька. Начнём с чтения, затем добавим действия редактора и отделим подтверждение транзакции от входа на сайт.

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

Разделяем чтение и подпись

Для чтения реестра нужен RPC-клиент, адрес контракта и ABI. Пользователь не обязан разрешать доступ к своему аккаунту только ради просмотра курса. Публичный клиент получает данные от узла; клиент кошелька обращается к провайдеру пользователя, когда требуется согласованное с ним действие.

Действие Откуда приходит разрешение Что получает сайт
Чтение текущего курса Запрос к публичному RPC Ответ на выбранном состоянии
Подключение кошелька Пользователь разрешает доступ Доступные аккаунты и связь с провайдером
Подпись сообщения Пользователь подтверждает сообщение Подпись, которую ещё нужно проверить
Отправка транзакции Пользователь подтверждает параметры Хеш отправленной операции
Вход в личный кабинет Сервер проверяет протокол входа Серверную сессию

Видимый адрес не означает, что сайт получил приватный ключ. Также нельзя считать подключение доказательством полномочий редактора: кто-то может подключить любой свой аккаунт. Права определяются контрактом или серверным протоколом в зависимости от действия.

В локальной лаборатории зафиксируем отдельную сеть с chain ID 31337 и собственный адрес развернутого реестра. Адрес нельзя придумать заранее: его записывают после создания экземпляра. Конфигурация приложения содержит сведения о каждой поддерживаемой сети. Если контракт для сети отсутствует, кнопка публикации объясняет это состояние, а не вызывает случайный адрес.

Сеть и аккаунт входят в состояние интерфейса

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

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

В React удобно использовать библиотеку связей с кошельками и отдельный слой запросов. При выборе wagmi нужно фиксировать основную версию. В документации wagmi v3 названия useAccount, useAccountEffect и useSwitchAccount изменены на useConnection, useConnectionEffect и useSwitchConnection. Старый пример с v2 нельзя объединять с установкой произвольной свежей версии. Руководство миграции wagmi.

Сам выбор библиотеки не решает продуктовые вопросы. Нужно явно показать неподдерживаемую сеть, отсутствие кошелька, отказ пользователя и недоступность RPC. Кнопка «Подключить» должна быть намеренным действием; автоматически открывать окно кошелька при каждом просмотре учебной страницы неудобно.

Готовим запись через viem

Для публикации курса используем три этапа: предварительное моделирование вызова, отправку через кошелёк и наблюдение за результатом. Ниже — функция, в которую приложение передаёт уже настроенные клиенты, адрес реестра, ABI, аккаунт, номер курса и отпечаток. Она использует API viem 2.x, совместимый с учебным профилем существующей серии.

export async function publishCourse({
  publicClient,
  walletClient,
  address,
  abi,
  account,
  courseId,
  contentHash,
  onHash,
  onReplacement,
}) {
  const { request } = await publicClient.simulateContract({
    address,
    abi,
    functionName: 'publish',
    args: [courseId, contentHash],
    account,
  });

  const hash = await walletClient.writeContract(request);
  onHash(hash);

  const receipt = await publicClient.waitForTransactionReceipt({
    hash,
    confirmations: 2,
    onReplaced: onReplacement,
  });

  if (receipt.status !== 'success') {
    throw new Error('Транзакция включена, но исполнение откатилось');
  }

  return receipt;
}

Функция является частью интерфейса, а не готовым приложением. courseId передаётся как bigint, например 17n, а contentHash — корректная 32-байтовая шестнадцатеричная строка. Произвольная строка из текстового поля этим требованиям не соответствует. Проверка длины и формата нужна до открытия окна кошелька; контракт затем отдельно проверяет свои условия.

simulateContract помогает обнаружить ошибки по наблюдаемому состоянию и готовит запрос для записи. Между моделированием и включением транзакции состояние сети может измениться. Поэтому успешное моделирование не гарантирует будущего успешного исполнения. Предварительную оценку комиссии тоже нельзя показывать как неизменный окончательный расход. Описание моделирования viem.

writeContract возвращает хеш. В этот момент интерфейс пишет «Отправлено, ожидаем включения», а не «Курс опубликован». Ответ waitForTransactionReceipt позволяет проверить статус исполнения и выбранное число подтверждений. Два подтверждения здесь — учебная настройка ожидания, а не универсальная политика финальности.

Пользователь может ускорить операцию, заменив транзакцию с тем же nonce, или отправить отменяющую замену. Обработчик onReplaced получает причину и сведения о замене. Интерфейс должен обновить наблюдаемый хеш и объяснить изменение. Успешный receipt отменяющей транзакции не означает успешную публикацию курса: проверяем назначение замены и новое состояние реестра. Наблюдение за receipt в viem.

Ошибка не всегда означает отказ операции

Отказ пользователя до отправки, откат включённой транзакции и прекращение ожидания RPC — разные ситуации. В последнем случае сеть могла принять операцию, а клиент потерял связь. Нужно сохранить хеш и дать возможность продолжить наблюдение, прежде чем повторно предлагать действие.

Состояние страницы Что известно Следующее действие
Ожидается подтверждение в кошельке Транзакция ещё не наблюдается Пользователь принимает или отклоняет
Хеш получен Отправка состоялась Наблюдаем исходную сеть
Ожидание прервалось Итог неизвестен Повторяем чтение по сохранённому хешу
Получен откат Исполнение не применило изменения Показываем причину и заново готовим вызов
Получен успешный receipt Вызов исполнен в блоке Читаем запись, следим за выбранной политикой подтверждений

Нельзя бесконечно отключать все кнопки на странице из-за одной зависшей операции. Отдельное состояние отправки позволяет пользователю читать материалы, открывать историю и понимать, что именно ещё ожидается. Для читателя, который не является издателем, само отсутствие кнопки редактирования удобно, но защита всё равно находится в контракте.

Подпись сообщения и вход на сайт

Предположим, редактору нужен личный кабинет с черновиками. Публичный адрес ещё не является серверной сессией. Для входа можно использовать Sign-In with Ethereum: сервер выдаёт одноразовый nonce, приложение формирует сообщение с доменом, URI, сетью и временными ограничениями, кошелёк подписывает его, а сервер проверяет результат.

Правильная проверка включает не только восстановление адреса из подписи. Сервер сверяет ожидаемый домен и URI, chain ID, время, nonce и назначение запроса. Использованный nonce нельзя принять повторно; его проверку и расходование выполняют согласованно, чтобы два параллельных запроса не открыли две сессии по одному доказательству. После этого сервер выдаёт обычную защищённую сессию с собственным сроком действия. Спецификация SIWE.

Подключение к сайту, подпись входа и разрешение токену расходовать средства — разные действия. Сообщение для входа должно быть читаемым и ограниченным по назначению. Нельзя просить безграничный approve под видом авторизации редактора. А состояние кошелька в браузере не отменяет серверных проверок роли для работы с черновиками.

Смарт-аккаунт может проверять подпись своей логикой. В этом случае предположение «каждый адрес соответствует одному ключу, который восстановится обычным алгоритмом» неверно. Для контрактных аккаунтов используется механизм проверки вроде ERC-1271 на соответствующей сети. Он требует корректной обработки ответа контракта; проверка одного формата строки подписи недостаточна. ERC-1271.

История событий и индексатор

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

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

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

RPC-провайдер также остаётся зависимостью приложения. Нужно ограничивать диапазоны запросов, сохранять прогресс, учитывать лимиты и разделять публичные настройки браузера с серверными секретами. Один сбой провайдера не должен стирать историю или заставлять сайт объявить все транзакции неуспешными.

Когда переходить к смарт-аккаунтам

Account abstraction расширяет модель кошелька: появляются операции пользователя, проверяющий контракт, инфраструктура доставки и иногда оплата комиссии отдельной стороной. В ERC-4337 это связано с UserOperation, EntryPoint, bundler и paymaster. Такая операция отличается от обычной транзакции, а её хеш нельзя безоговорочно передать в интерфейс наблюдения за стандартным transaction hash. ERC-4337.

Для библиотеки сначала достаточно прозрачного маршрута чтения и публикации. Смарт-аккаунты можно изучать после того, как известны границы подписи, повторения и исполнения. Иначе удобное окно кошелька скроет от ученика, кто проверяет разрешение и где операция способна завершиться ошибкой.

В следующем материале добавим хранение и внешние данные. Порядок самостоятельных упражнений и связь с остальными направлениями собраны в маршруте Web3.