Web3-фронтенд: интерфейс и границы доверия
Оглавление курса «Web3-интерфейс: реестр в браузере» · Урок 1 из 30.
Реестр курсов становится полезным, когда читатель может открыть обычную веб-страницу и понять, какой выпуск сейчас опубликован. Начнём с архитектуры такого интерфейса. Он будет показывать курс 17, его отпечаток и признак активности, а издателю позволит подготовить публикацию или деактивацию. Разделение этих действий определит устройство всех следующих уроков.
Предполагается знакомство с JavaScript, асинхронными функциями и базовым контрактом CourseCatalog из обзора Solidity. Контракт содержит неизменяемого publisher, отображение courses, функции publish и deactivate. Пока не добавляем токены, цену или передачу владельца. Наш результат — схема приложения, в которой чтение, полномочия и подпись не перепутаны. Примеры этой серии подготовлены как исходники; их выполнение в данной работе не проводилось.
Три независимых участника
Публичный RPC отвечает на запрос состояния выбранной сети. Браузер получает ответ и рисует карточку. Для чтения номера курса не нужен секрет читателя, поэтому страница не должна открываться с настойчивым предложением подключить кошелёк. Посетитель может оценить материал, сравнить выпуск с загруженным файлом и уйти, вообще не предоставляя сайту адрес своего аккаунта.
Кошелёк имеет другую задачу: запросить согласие пользователя и подписать действие от имени выбранного аккаунта. Он не становится главным источником любого содержания страницы. Особенно заметна разница, если расширение сейчас подключено к чужой сети: наша публичная карточка по-прежнему относится к локальному реестру, а кнопка изменения должна сообщить о несовпадении окружений.
Контракт определяет допустимость изменения. Скрытая кнопка «Публиковать» полезна для удобства, однако пользователь может изменить JavaScript или обратиться к сети самостоятельно. Поэтому проверка msg.sender == publisher остаётся в контракте. Браузер помогает избежать ошибки, но не заменяет правило исполнения. Это также объясняет, почему успешное подключение кошелька ещё не даёт редакционных прав.
Данные и намерение
Зафиксируем минимальную модель страницы. Это самостоятельный файл model.js; адреса здесь пока не нужны, потому что схема описывает состояния интерфейса, а не выполняет запрос.
export const initialState = {
courseId: 17n,
read: { status: 'idle', value: null, blockNumber: null },
wallet: { status: 'absent', account: null, chainId: null },
intent: null,
operation: { status: 'idle', hash: null },
};
export function canPrepareWrite(state, publisher) {
return state.wallet.status === 'connected'
&& state.wallet.chainId === 31337
&& state.wallet.account?.toLowerCase() === publisher.toLowerCase();
}
В read находится наблюдение за сетью, в intent — ещё не исполненное желание пользователя, в operation — история отправки. Если издатель ввёл новый отпечаток, поле read.value не должно немедленно меняться. Иначе карточка выдаст локальный черновик за опубликованный выпуск. После получения успешного receipt выполняется новое чтение и только тогда обновляется наблюдаемое состояние.
Сравнение адресов в этой маленькой функции предполагает, что оба значения ранее проверены как адреса EVM. Оно не исправляет произвольную строку и не проверяет сеть. В дальнейшем конфигурация будет иметь отдельную проверку адреса, а кошелёк — собственную проверку chainId. Такое разделение облегчает объяснение ошибки: неправильный адрес экземпляра и отсутствие издательских полномочий требуют разных действий.
Первая последовательность работы
Рассмотрим посещение страницы обычным читателем. Сначала read.status становится loading; интерфейс показывает место будущей карточки. RPC возвращает отпечаток и active = true; карточка сообщает, что запись найдена. Поля кошелька могут оставаться пустыми. Здесь нет пропущенного обязательного шага: подключение действительно не требуется для просмотра.
Теперь приходит издатель. Он нажимает «Подключить», выбирает аккаунт и проверяет сеть 31337. Форма публикации становится доступной, но сохранённый выпуск остаётся прежним. Нажатие «Подготовить» создаёт намерение с номером курса и новым отпечатком. Предварительное моделирование может обнаружить откат; это полезная обратная связь до подписи. Если моделирование прошло, пользователь ещё раз видит параметры и отдельно подтверждает отправку в кошельке.
После получения хеша операция имеет состояние ожидания. Хеш доказывает наличие идентификатора отправленной транзакции, а не исполнение publish. Receipt может сообщить успешное выполнение или откат. После успеха повторное чтение показывает новый выпуск. На каждом шаге интерфейс называет наблюдаемое событие: «подготовлено», «ожидает подписи», «отправлено», «исполнено». Одно универсальное «готово» скрыло бы важные различия.
Изменение контекста
Допустим, между подготовкой и подписью пользователь переключил аккаунт. Намерение было рассчитано для издателя, а новый аккаунт принадлежит читателю. Форму можно сохранить как введённый текст, но её подготовленный запрос нужно считать устаревшим. Новое моделирование выполняется уже для нового отправителя. Автоматически подписывать прежний запрос нельзя, даже если сама форма визуально не изменилась.
Для каждой операции позже введём снимок контекста: сеть, адрес реестра, аккаунт и номер поколения состояния. Когда контекст меняется, поколение увеличивается. Асинхронный ответ старого поколения не перерисовывает карточку и не активирует кнопку подписи. Это средство согласования интерфейса; оно не отменяет транзакцию, если кошелёк уже отправил её в сеть.
Что остаётся за пределами страницы
Браузерный код доступен посетителю, поэтому в нём нет приватных ключей, серверных секретов RPC и административных паролей. Публичный endpoint может иметь ограниченный браузерный идентификатор, но это отдельная политика сервиса, а не скрытый секрет в сборке. Логи интерфейса также не должны записывать подписи входа или содержимое личного кабинета.
Статические материалы курса остаются обычными файлами сайта. Контракт хранит отпечаток выпуска, а не автоматически раздаёт Markdown и не гарантирует доступность внешнего файла. Проверку байтов и маршрут загрузки разберём отдельно. Так пользователь понимает, какое обещание действительно обеспечивает реестр, а какое зависит от веб-сервера.
В качестве упражнения нарисуйте две последовательности: читатель без кошелька и издатель, отказавшийся от подписи. В первой карточка должна загрузиться; во второй черновик формы должен сохраниться, опубликованный выпуск — остаться прежним. Ожидаемый итог урока — понятная карта ответственности, на основе которой в следующем шаге появится публичный клиент. Детали провайдера описаны в EIP-1193.
Чтение не является обещанием подлинности сайта
Даже корректно прочитанный реестр не объясняет посетителю, почему следует доверять именно этому издателю. Название «Учебная библиотека» находится в интерфейсе, а publisher является адресом. Связь имени с адресом устанавливается отдельно: опубликованной записью развёртывания, контролируемым доменом и процессом сопровождения проекта. Блокчейн помогает проверить сохранённое действие, но не выбирает за читателя надёжного автора.
Поэтому карточка показывает сеть и экземпляр по запросу пользователя, не скрывая происхождение состояния. Подпись издателя в цепочке не удостоверяет качество текста, его актуальность или отсутствие ошибок в примерах. В нашем приложении понятие «опубликован» означает конкретный переход CourseCatalog, а понятие «проверен редакцией» относится к другому процессу и не выводится автоматически из хеша.