Кошелёк в браузере: подключение и сеть
- Зачем это нужно
- Кошелёк — это объект с одним методом
- Два метода про счета, и путать их нельзя
- Отказ — это не ошибка
- Сеть: спросить и попросить переключить
- События: счёт и сеть могут смениться без вас
- Когда кошельков несколько
- Библиотека поверх кошелька
- Типичные ошибки
- Слова главы
- Тренажёр: набрать вслепую
- На чемпионате
- Куда углубиться
- Куда дальше: проверенные ресурсы
Зачем это нужно
До сих пор транзакции подписывал ключ, лежащий у нас в настройках. На чемпионате так нельзя: ключи пользователей у приложения быть не должно. Подписывает человек — в своём кошельке, своим ключом, который никогда не покидает расширение.
Задание требует именно этого: подключение кошелька, подтверждение операций в нём, работа в нужной сети.
Эта глава — про то, как страница вообще разговаривает с кошельком: какой у него интерфейс, как выглядит подключение, что приходит при отказе и как попросить переключить сеть.
Прогоны этой главы сделаны на подделке кошелька — объекте с тем же интерфейсом, что описан стандартом. Расширения в этом браузере нет: строка window.ethereum: нет в первом же прогоне — настоящая. Зато подделка позволяет показать то, чего вживую не поймать: отказ пользователя, чужую сеть, два кошелька сразу.
Кошелёк — это объект с одним методом
── есть ли кошелёк в этом браузере ──
window.ethereum: нет — расширение не установлено
Расширение кладёт на страницу объект. У него один по-настоящему важный метод и пара подписок:
await window.ethereum.request({ method: "eth_chainId" });
window.ethereum.on("accountsChanged", (accounts) => { … });
Приватный ключ страница не видит никогда. Она может только попросить; подписывает человек, нажимая кнопку в окне расширения. Отсюда все особенности: любая операция может быть отклонена, и любая занимает время.
Два метода про счета, и путать их нельзя
── до подключения ──
eth_accounts → [] (пусто: страница не знает, кто пользователь)
eth_chainId → 0x1 = 1
── пользователь разрешил ──
eth_requestAccounts → ["0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"]
eth_accounts теперь → ["0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"]
Спросить, кто пользователь. Отвечает пустым массивом, если доступа ещё нет. Ничего не спрашивает у человека — окно не откроется. Это то, что зовут при загрузке страницы: если доступ уже давали раньше, подключение восстановится само.
Попросить доступ. Открывает окно кошелька. Зовут только по нажатию кнопки — вызов при загрузке страницы кошельки блокируют, да и выглядит это навязчиво.
// при загрузке — тихо
useEffect(() => {
provider.request({ method: "eth_accounts" }).then(setAccounts);
}, []);
// по нажатию — с окном
const connect = () => provider.request({ method: "eth_requestAccounts" }).then(setAccounts);
Отказ — это не ошибка
── пользователь отказался ──
код 4001: User rejected the request. ← это не ошибка приложения, а решение человека
У ошибок кошелька есть номера, и знать стоит три:
| код | что значит | что делать |
|---|---|---|
| 4001 | пользователь отклонил | ничего: убрать «загружаем», оставить кнопку |
| 4100 | нет разрешения | попросить доступ заново |
| 4902 | такой сети у кошелька нет | предложить добавить сеть |
Код 4001 нельзя показывать как ошибку. Красная плашка «Ошибка: User rejected the request» после того, как человек сам нажал «Отклонить», выглядит как неисправность. Правильно — вернуть интерфейс в исходное состояние и промолчать.
Сеть: спросить и попросить переключить
── просим переключить сеть ──
чужая сеть → код 4902: Unrecognized chain ID. Try adding the chain first.
наша сеть → переключено, chainId = 0x539 = 1337
событие chainChanged: 0x539
await provider.request({
method: "wallet_switchEthereumChain",
params: [{ chainId: "0x539" }],
});
Для приватной сети порядок такой: попросить переключить, поймать код 4902, попросить добавить, повторить переключение. Три строки, которые на демонстрации выглядят как магия — кошелёк сам предлагает добавить вашу сеть.
Номер сети проверяют перед каждой операцией, меняющей состояние. Кошелёк мог переключиться в другую сеть в соседней вкладке. Транзакция, ушедшая не в ту сеть, в лучшем случае отклонится, в худшем — уйдёт куда-то по-настоящему.
События: счёт и сеть могут смениться без вас
provider.on("accountsChanged", (accounts) => {
if (accounts.length === 0) setConnected(false); // доступ отозвали
else setAddress(accounts[0]);
});
provider.on("chainChanged", () => window.location.reload());
Смена счёта — обычное дело: у людей их несколько. Все данные, привязанные к адресу (баланс, операции), надо перезапросить, а старые — убрать. Это ровно та ситуация с гонкой ответов из главы про данные из сети.
Смена сети — сам кошелёк советует просто перезагрузить страницу. Это не лень: в сети другие адреса контрактов, другие данные и, возможно, другой набор возможностей. Проще начать заново, чем чинить состояние по частям.
Пустой массив счетов означает, что пользователь отозвал доступ через интерфейс кошелька. Интерфейс должен вернуться к кнопке «подключить».
Когда кошельков несколько
── современный способ: расширения объявляют о себе сами ──
страница спросила один раз и получила 2 ответа:
MetaMask io.metamask 11111111…
Rabby Wallet io.rabby 22222222…
Старый способ — одно поле на странице, куда пишут все расширения сразу. Стоит поставить второй кошелёк, и какой из них увидит страница, решает случай.
Новый способ: страница один раз объявляет «кто здесь», а расширения отвечают — каждое своим именем, значком и обратным доменным именем. Дальше пользователю показывают список и дают выбрать.
const wallets = new Map();
window.addEventListener("eip6963:announceProvider", (e) =>
wallets.set(e.detail.info.uuid, e.detail));
window.dispatchEvent(new Event("eip6963:requestProvider"));
Старое поле при этом никуда не делось и работает — новый способ добавили рядом.
Библиотека поверх кошелька
── ethers работает поверх того же объекта ──
provider.getNetwork() → chainId 1337
provider.getSigner() → 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
библиотеке всё равно, кто перед ней: важен только интерфейс request
import { BrowserProvider } from "ethers";
const provider = new BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
const vault = new Contract(ADDRESS, ABI, signer);
Читать можно без подключения. Баланс контракта, список операций, параметры — всё это доступно любому. Подключение нужно только для того, чтобы писать.
Это стоит использовать: интерфейс, который показывает данные до нажатия «подключить», выглядит гораздо живее пустого экрана с одной кнопкой.
Типичные ошибки
Просят доступ при загрузке страницы. Кошельки это блокируют, а пользователи закрывают вкладку.
Показывают отказ пользователя как ошибку. Код 4001 — не ошибка.
Не проверяют сеть перед транзакцией. Кошелёк мог переключиться в соседней вкладке.
Номер сети передают десятичным числом. Ждут строку с приставкой.
Не обрабатывают смену счёта. На экране остаются данные прошлого адреса — та же гонка, что в главе про запросы.
Контракт создан с провайдером вместо подписывающего. Читает, но не пишет.
Считают, что кошелёк один. Два расширения — и страница видит то, которое записалось последним.
Ждут, что подпись будет мгновенной. Человек может отойти от компьютера; интерфейс должен оставаться в состоянии ожидания сколько угодно.
Слова главы
- Провайдер — объект, который кладёт на страницу расширение; у него один метод запроса и подписки.
- Спросить счета — тихо, без окна; отвечает пустым массивом, если доступа нет.
- Попросить доступ — открывает окно; зовут только по нажатию.
- Код 4001 — пользователь отклонил; не ошибка.
- Код 4902 — такой сети у кошелька нет; предложить добавить.
- Номер сети — строкой, шестнадцатеричным, с приставкой.
- Обнаружение кошельков — расширения объявляют о себе, а не дерутся за одно поле.
- Подписывающий — тот, через кого идут транзакции; без него контракт только читает.
Тренажёр: набрать вслепую
Цель: скорость ≥ 95 зн/мин, точность ≥ 88%
await window.ethereum.request({ method: "eth_requestAccounts" });
На чемпионате
Кнопка «подключить» и состояние рядом. Адрес сокращённо, номер сети, кнопка «отключить». Это первое, что смотрят.
Своя сеть добавляется одним нажатием. Поймать код 4902 и предложить добавить — десять строк, а выглядит как готовый продукт.
Проверка сети перед каждой транзакцией. Одна строка, которая спасает от «почему ничего не произошло».
Читайте данные до подключения. Пустой экран с одной кнопкой производит плохое впечатление.
И про подделку: свой кошелёк с известным ключом — отличный способ отлаживать интерфейс, пока настоящее расширение не настроено. Но показывать надо на настоящем: проверяющий захочет увидеть окно подтверждения.
Куда углубиться
- Добавление сети одним запросом — какие поля обязательны и что кошелёк покажет пользователю.
- Подключение мобильного кошелька — когда расширения нет, а телефон есть.
- Библиотеки подключения — берут на себя выбор кошелька, сеть и восстановление сеанса.
- Разрешения на счета — как отозвать доступ из интерфейса приложения.
- Наблюдение за блоками — обновлять баланс не по кнопке, а по приходу нового блока.
Сводная проверка по всей главе: 8 вопросов, 5:00 на всё, без подсказок по ходу. Вопросы показываются по одному, ответ изменить нельзя. Если время выйдет — экзамен завершится с тем, что успел ответить. Пересдавать можно сколько угодно раз.
Куда дальше: проверенные ресурсы
- MetaMask: документация для разработчиков — подключение, сети, методы.
- EIP-1193: интерфейс провайдера — метод запроса, события, коды ошибок.
- EIP-6963: обнаружение кошельков — как расширения объявляют о себе.
- ethers: BrowserProvider — обёртка вокруг кошелька.
- MetaMask: добавление сети — поля запроса и что увидит пользователь.
Комментарии
Обсуждение живёт в GitHub Discussions и грузится с чужого домена — поэтому только по нажатию.