Web3-кошелёк: аккаунты и разрешения пользователя
Оглавление курса «Web3-интерфейс: реестр в браузере» · Урок 12 из 30.
Адрес кошелька может измениться, пока издатель редактирует форму публикации. Если интерфейс сохранит старые полномочия, он начнёт готовить действие для неправильного отправителя. В этом уроке обработаем accountsChanged и разделим сохранённый текст формы с действующим контекстом записи. Результат — смена аккаунта немедленно делает прежний подготовленный запрос устаревшим.
Предполагается подключение через выбранный EIP-1193-провайдер. Публичная карточка курса 17 не зависит от аккаунта и может остаться на экране. Учебный код не запускался; рассматриваем ожидаемые переходы двух адресов — издателя и читателя — без реальных ключей и транзакций.
Событие описывает доступные аккаунты
accountsChanged передаёт массив адресов, которые доступны приложению. Пустой массив означает отсутствие доступного аккаунта в текущем контексте; это может быть отзыв разрешения или другое действие кошелька. Не следует трактовать его как удаление аккаунта из Ethereum. Изменилось разрешение сайта или выбор источника подписи, а не глобальное существование адреса.
В простом интерфейсе выбираем первый адрес. Если приложение поддерживает выбор среди нескольких, событие обновляет список и проверяет, остался ли выбранный адрес доступным. Нельзя продолжать работу с адресом только потому, что его строка сохранена в памяти. Кошелёк может больше не предоставить подпись для этого адреса.
Сравнение с publisher помогает показать редакционную форму. Но сам факт совпадения не заменяет сетевого контекста. Адрес издателя в локальном реестре и тот же адрес в другой сети — разные окружения операции. Поэтому правило доступности записи использует и аккаунт, и текущую сеть, и выбранный экземпляр.
Управление поколением контекста
Ниже модуль wallet-context.js. Он сохраняет только текущий аккаунт и номер поколения; сетевой контекст добавим в следующем уроке. Чтение реестра работает в отдельном модуле.
import { getAddress, isAddress } from 'viem';
export function accountContext(provider, onChange) {
let account = null;
let revision = 0;
function changed(accounts) {
account = Array.isArray(accounts) && isAddress(accounts[0])
? getAddress(accounts[0]) : null;
revision++;
onChange({ account, revision });
}
provider.on('accountsChanged', changed);
return {
setInitial(accounts) { changed(accounts); },
snapshot() { return { account, revision }; },
isCurrent(before) {
return before.revision === revision && before.account === account;
},
dispose() { provider.removeListener('accountsChanged', changed); },
};
}
Поколение увеличивается даже при переходе от аккаунта A к B и обратно к A. Простое сравнение адреса обнаружило бы только текущее совпадение и могло бы признать старую симуляцию пригодной. Номер поколения сообщает, что между подготовкой и подписью происходили изменения. Для нового действия пользователь получает актуальные параметры заново.
Подписка использует одну функцию changed, чтобы затем снять именно её. Создание новой стрелочной функции в removeListener не удалило бы прежний обработчик: это другой объект функции. Такие ошибки незаметно накапливают обработчики и приводят к многократному сбросу формы после одного события.
Что сбрасывается
После смены аккаунта удаляем подготовленный запрос симуляции, оценку прав и состояние ожидания новой подписи. Введённый номер курса и отпечаток можно сохранить как черновик. Пользователь не теряет работу, но понимает, что параметры ещё не подготовлены для нового отправителя. Кнопка «Отправить» возвращается к шагу проверки.
Уже отправленная транзакция не исчезает. Она связана с фактическим отправителем и сетью, сохранёнными при отправке. После смены аккаунта история продолжает наблюдать прежний хеш, но не притворяется действием текущего адреса. Если интерфейс очистит хеш, пользователь потеряет возможность понять результат реальной операции.
Смена аккаунта во время открытого окна кошелька особенно важна. Страница не может гарантировать, что пользователь подтвердит прежний адрес, поэтому проверяет контекст непосредственно перед вызовом подписи и затем связывает возвращённую операцию с зафиксированными параметрами. Если источник изменился в процессе, интерфейс сообщает о необходимости сверки результата, а не автоматически повторяет действие.
Полномочия в карточке
Надпись «Издатель» появляется только после сопоставления нормализованного аккаунта с publisher выбранного экземпляра. Она означает право базового контракта публиковать и деактивировать, а не административный доступ ко всему сайту. Личный кабинет может иметь отдельную серверную сессию и другой набор ролей; подключение кошелька эту сессию не создаёт.
У обычного читателя форма может быть скрыта или заменена объяснением. Это удобство, а не защита. Если читатель самостоятельно вызовет publish, контракт должен отклонить действие. Разделение UI и проверки исполнения помогает не переоценивать эффект скрытых элементов и отключённых кнопок.
Базовый publisher неизменяем, поэтому потеря его ключа не решается сменой интерфейсного аккаунта. Для передачи полномочий нужен другой контракт или отдельный процесс миграции. Нельзя добавить в страницу кнопку «сменить издателя» и ожидать, что она изменит immutable-поле. Ограничение исходной модели должно оставаться явным.
Две последовательности
Первая: издатель ввёл отпечаток A, переключился на читателя, затем нажал отправку. Ожидается, что старый запрос недоступен и интерфейс объясняет отсутствие прав; введённый A остаётся черновиком. Вторая: издатель отправил публикацию, получил хеш, затем переключился на читателя. Ожидается, что наблюдение прежнего хеша продолжается независимо от нового аккаунта.
В обоих случаях нельзя автоматически отправлять транзакцию от другого адреса ради завершения пользовательского намерения. Согласие связано с конкретным контекстом. Приложение помогает человеку принять новое решение и сохраняет историю того, что уже произошло.
Для упражнения добавьте к последовательности возврат на исходный адрес. Объясните, почему номер поколения изменился дважды и почему прежнее моделирование не должно оживать только из-за совпадения строки аккаунта. Ожидаемый итог — понятное разделение черновика, полномочий и отправленной операции. Событие accountsChanged описано в EIP-1193.
Согласование начального чтения
Событие смены аккаунтов может прийти одновременно с ответом eth_accounts, начатым при загрузке. Если обработчик события уже установил новый адрес, поздний старый ответ не должен вернуть прежний. Перед начальным запросом фиксируем поколение и применяем ответ только при его сохранении. Это тот же способ согласования, который использовался для публичного чтения, но теперь источник управляется внешним кошельком.
Не каждый переход требует повторного eth_requestAccounts. Если разрешённый список уже известен, интерфейс обновляет локальное состояние по событию. Новое окно открывается только по действию человека, когда действительно нужен доступ. Такое правило сокращает случайные запросы и сохраняет понятное различие фонового наблюдения и согласия.
Если несколько экранов используют один кошелёк, общий контекст публикует нормализованное изменение один раз, а каждый экран самостоятельно инвалидирует только свою подготовку. Это не требует нового запроса разрешения.