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.