Кошелёк и выбранная сеть
Оглавление серии «Реестр» · Урок 7 из 10.
Чтение реестра уже работает независимо от кошелька в модели нашего приложения. Теперь подключим источник подписи для будущих изменений. Пользователь нажмёт отдельную кнопку, разрешит доступ к выбранному аккаунту и останется в локальной сети 31337. Ожидаемый результат — интерфейс знает текущий публичный адрес, но ещё не отправляет транзакцию. При смене аккаунта или сети сохранённое подключение сбрасывается. Страница не должна продолжать обещать прежние полномочия после изменения окружения кошелька.
Используем простой учебный профиль: один внедрённый провайдер EIP-1193 в window.ethereum. При нескольких расширениях реальный продукт может потребовать явного выбора через механизм обнаружения провайдеров, например EIP-6963. Наш курс не скрывает эту границу. Кошелёк не является обязательным для чтения, поэтому отсутствие расширения не блокирует просмотр сведений. Он понадобится только тому, кто хочет подписать изменение в своей локальной лаборатории.
Провайдер и RPC имеют разные роли
Public Client продолжает читать через заданный HTTP RPC. Wallet Client обращается к провайдеру кошелька через custom. Провайдер обслуживает запросы разрешения аккаунтов и подписи. Эти пути могут смотреть на разные сети, если не проверить конфигурацию. Поэтому перед подключением получаем eth_chainId у кошелька и требуем 31337. Успешная проверка HTTP RPC предыдущего урока не заменяет эту проверку: кошелёк имеет собственное состояние, выбранное пользователем.
Число идентификатора провайдер обычно возвращает шестнадцатеричной строкой. Number преобразует его в значение для сравнения с небольшим фиксированным chainId. Это не общая рекомендация превращать все числа блокчейна в Number. Денежные величины и номера могут превышать безопасный диапазон, как уже обсуждали. Здесь проверяется известный идентификатор локальной сети, и граница преобразования относится именно к нему. В интерфейсе нужно точно называть требуемую сеть, а не писать неопределённое «подключите блокчейн».
Полный модуль подключения
walletState хранит провайдер, выбранный аккаунт и созданный клиент в замыкании. Снаружи предоставляет connect, current и dispose. Остальные модули не получают неограниченную переменную, которую можно случайно заменить. Такой небольшой объект описывает жизненный цикл подключения и помогает перед каждой операцией снова проверять актуальность. Он не является хранилищем приватного ключа: подпись остаётся в расширении кошелька и запрашивается отдельным действием.
import {createWalletClient,custom} from 'viem';
import {local} from './client.js';
export function walletState(onChange) {
const provider=window.ethereum;let account=null,client=null;
const changed=()=>{account=null;client=null;onChange('Аккаунт или сеть изменились: подключитесь заново.');};
provider?.on('accountsChanged',changed);provider?.on('chainChanged',changed);
return {
async connect() {
if(!provider)throw Error('Нужен EIP-1193 кошелёк');
const chain=await provider.request({method:'eth_chainId'});if(Number(chain)!==31337)throw Error('Выберите локальную сеть 31337');
client=createWalletClient({chain:local,transport:custom(provider)});
[account]=await client.requestAddresses();if(!account)throw Error('Аккаунт не выбран');return account;
},
async current() {
if(!client || !account)throw Error('Подключите кошелёк');
if(Number(await provider.request({method:'eth_chainId'}))!==31337)throw Error('Сеть изменилась');
const addresses=await provider.request({method:'eth_accounts'});if(addresses[0]?.toLowerCase()!==account.toLowerCase())throw Error('Аккаунт изменился');
return {client,account};
},
dispose(){provider?.removeListener('accountsChanged',changed);provider?.removeListener('chainChanged',changed);}
};
}
requestAddresses инициирует запрос доступа к аккаунтам. Вызываем его после кнопки, а не сразу при загрузке страницы. Посетитель может хотеть только проверить документ; неожиданный запрос расширения мешал бы этому сценарию. Возвращённый публичный адрес не подтверждает, что он разрешён издателем. Это отдельное правило контракта. Подключение даёт возможность запросить подпись у выбранного аккаунта, но не превращает адрес автоматически во владельца реестра.
В нашей лаборатории сеть добавляется пользователем с RPC http://127.0.0.1:8545 и chainId 31337. Интерфейс не переключает сеть молча. Если сейчас выбрана другая, показывает понятную ошибку. В продукте можно предложить явную команду смены сети, но она должна быть понятна пользователю и проверяться после подтверждения. Нельзя продолжать операцию только потому, что был отправлен запрос переключения: действие могло быть отклонено или привести к другому состоянию, чем ожидалось.
События меняют наше знание
Слушаем accountsChanged и chainChanged. Обработчик очищает сохранённый аккаунт и клиент, затем просит подключиться снова. Он не пытается сохранить прежний адрес при пустом наборе аккаунтов или автоматически считать первый новый адрес подходящим издателем. Смена окружения означает, что прежняя договорённость о подписи больше не актуальна. Пользовательское действие повторного подключения помогает явно увидеть новый адрес до следующей операции.
Перед изменением контрактного состояния метод current дополнительно запрашивает текущую сеть и разрешённые аккаунты. Это полезно даже при подключённых событиях: состояние могло измениться между действиями, а приложение не должно полагаться только на давний текст статуса. Сравнение адресов выполняется без учёта регистра. В следующем уроке writeContract тоже проверит соответствие выбранной цепочке, но ранний guard даёт более предметное объяснение и не заставляет ждать ошибки после симуляции.
Отказ пользователя является обычным результатом
Кошелёк может отклонить запрос доступа. Такой исход не означает повреждение сертификата или отсутствие записи. Интерфейс показывает сообщение ошибки, а чтение продолжает быть доступным. Не нужно автоматически повторять запрос бесконечно или начинать транзакцию после любого открывшегося окна. Разрешение аккаунтов и разрешение подписи — две разные границы. Даже после успешного подключения пользователь сможет отказаться от конкретного изменения, которое приложение предложит позднее.
В тексте статуса покажем публичный адрес. Для лаборатории полный адрес удобен, потому что позволяет сравнить его с владельцем и полученными записями. В реальном интерфейсе можно сокращать отображение, сохраняя доступ к полному значению. Одних первых символов недостаточно, чтобы надёжно различать адреса. Имя профиля или картинка расширения тоже не должны заменять проверку точного значения, используемого в вызове контракта.
Завершение слушателей
При уходе страницы вызываем dispose, снимающий собственные обработчики провайдера. Это не «выход из кошелька» и не удаление аккаунта из расширения. Страница завершает только свои слушатели. В SPA при замене компонента такую операцию нужно выполнить на соответствующей границе, иначе несколько старых экземпляров продолжат отвечать на одну смену сети. Здесь веб-часть простая, но жизненный цикл подключения всё равно выражен явно.
Хранить приватный ключ в localStorage, deployment.json или исходниках не требуется. Для учебного аккаунта читатель настраивает отдельную локальную лабораторию кошелька самостоятельно. Такой способ не описывает производственное хранение ключей организации и не претендует на него. Смысл шага — понять, кто управляет подписью и какое состояние нужно проверить. Публичный адрес в переменной не даёт приложению право самостоятельно подписывать операции без решения пользователя.
Ожидаемые наблюдения
Самостоятельно откройте страницу без расширения: чтение должно оставаться отдельным действием, а подключение объяснит отсутствие провайдера. Затем используйте локальную сеть и разрешите доступ к аккаунту. Ожидается полный адрес в статусе. Смените сеть или аккаунт: приложение должно сбросить подключение и попросить повторное действие. Это ожидаемое поведение исходника, а не проведённый запуск. В статье не взаимодействовали с настоящим кошельком и не давали разрешений от имени читателя.
Договор провайдера описан в EIP-1193, клиент — в Wallet Client viem. Теперь у браузера есть два различимых пути: независимое чтение и подтверждаемая пользователем подпись. Следующий урок свяжет их через симуляцию операции, запрос подписи и ожидание квитанции. Это позволит не путать подключённый аккаунт с уже выполненной выдачей и показывать точную стадию изменения реестра.
Исходники этого шага
Архив серии содержит полную папку lesson-07. Возьмите именно её; переносить файлы из соседних папок не требуется. Версии и самостоятельная подготовка описаны в README. Пример, установка зависимостей и его выполнение при написании не запускались; результаты в тексте ожидаются по разбору исходников. Материал сохранён локально как Markdown-черновик.