В мае 2023 года группа разработчиков Ethereum-кошельков объединилась для создания нового отраслевого стандарта подключения к dapp-приложениям. До этого распространённой практикой было предоставление кошельками EIP-1193 провайдера через глобальный объект window.ethereum в веб-браузерах. Этот подход хорошо работал на заре экосистемы, когда пользователи, как правило, использовали только один кошелёк.
По мере роста экосистемы и перехода пользователей к одновременному использованию нескольких кошельков разработчикам dapp-приложений стало сложнее корректно управлять процессом определения подключённого кошелька или предоставлять пользователям возможность выбора нужного кошелька при наличии нескольких установленных.
В октябре 2023 года этот стандарт, получивший название EIP-6963 или Multi-Wallet Injected Provider Discovery, был принят. С тех пор разработчики кошельков начали внедрять EIP-6963 в качестве стандартного способа обнаружения провайдеров кошельков. MetaMask не стал исключением: мы обеспечили поддержку этого стандарта в MetaMask Wallet API и MetaMask SDK.
Кроме того, dapp-приложения, использующие window.ethereum, по-прежнему будут поддерживаться. Тем не менее мы рекомендуем внедрять поддержку EIP-6963 во все dapp-приложения как предпочтительный механизм обнаружения провайдеров. Проще всего это сделать путём интеграции со сторонними библиотеками, поддерживающими EIP-6963. Актуальный список доступных библиотек можно найти в документации MetaMask.
В этом руководстве мы рассмотрим основные шаги по реализации поддержки EIP-6963 в вашем dapp-приложении, если вы решите не использовать доступные сторонние библиотеки. Примеры кода написаны на React и TypeScript, однако любой JavaScript-разработчик сможет адаптировать их под свой фреймворк.
Определение поддерживаемых интерфейсов и типов для Web3-кошельков
Первый шаг к поддержке обнаружения нескольких кошельков в вашем dapp-приложении — определение всех поддерживаемых интерфейсов и типов в соответствии с предложением по улучшению. Эти интерфейсы и типы обеспечивают структурированный и стандартизированный способ работы с провайдерами Ethereum-кошельков, упрощая интеграцию и обнаружение нескольких Ethereum-кошельков.
interface EIP6963ProviderInfo {
walletId: string;
uuid: string;
name: string;
icon: string;
}
interface EIP1193Provider {
isStatus?: boolean;
host?: string;
path?: string;
sendAsync?: (request: { method: string, params?: Array<unknown> }, callback: (error: Error | null, response: unknown) => void) => void;
send?: (request: { method: string, params?: Array<unknown> }, callback: (error: Error | null, response: unknown) => void) => void;
request: (request: { method: string, params?: Array<unknown> }) => Promise<unknown>;
}
interface EIP6963ProviderDetail {
info: EIP6963ProviderInfo;
provider: EIP1193Provider;
}
type EIP6963AnnounceProviderEvent = {
detail: {
info: EIP6963ProviderInfo;
provider: EIP1193Provider;
}
}
Я добавил комментарии, которые помогут вам понять, что представляет собой каждый интерфейс и тип. Для более подробного объяснения обратитесь к предложению по улучшению.
Подписка на события и получение уведомлений о новых подключениях
Следующий шаг — настройка механизма оповещения вашего dapp-приложения при обнаружении новых подключений кошельков. Напомним, что цель EIP-6963 — обеспечить бесшовное подключение и переключение между несколькими кошельками. В React это можно реализовать с помощью приведённого ниже примера кода.
declare global {
interface WindowEventMap {
"eip6963:announceProvider": CustomEvent<EIP6963AnnounceProviderEvent>;
}
}
let providers: EIP6963ProviderDetail[] = [];
export const store = {
value: () => providers,
subscribe: (callback: () => void) => {
function onAnnouncement(event: EIP6963AnnounceProviderEvent) {
if (providers.some(p => p.info.uuid === event.detail.info.uuid)) return;
providers = [...providers, event.detail];
callback();
}
window.addEventListener("eip6963:announceProvider", onAnnouncement as EventListener);
window.dispatchEvent(new Event("eip6963:requestProvider"));
return () => window.removeEventListener("eip6963:announceProvider", onAnnouncement as EventListener);
}
}
Разберём приведённый пример кода:
Мы расширяем WindowEventMap пользовательским событием «eip6963:announceProvider», тем самым устанавливая глобальный тип события для объявления о доступности новых провайдеров Ethereum-кошельков.
Мы инициализируем внешнее хранилище, представленное массивом именованных провайдеров, которое используется для отслеживания всех обнаруженных провайдеров кошельков. Этот массив хранит объекты, соответствующие интерфейсу EIP6963ProviderDetail, содержащему информацию о провайдере кошелька и соответствующем Ethereum-провайдере.
Мы вводим механизм подписки через функцию subscribe внутри объекта хранилища. Эта функция прослушивает событие «eip6963:announceProvider» и обновляет внешнее хранилище данными о любых вновь объявленных провайдерах. При этом провайдеры добавляются только в том случае, если их ещё нет в хранилище, что исключает дублирование записей.
Объект хранилища предоставляет две ключевые возможности: 1. Функция value позволяет получить текущее состояние внешнего хранилища, то есть список обнаруженных провайдеров кошельков.
Функция subscribe позволяет компонентам или другим частям приложения подписываться на изменения в хранилище. Когда новый провайдер объявляется и добавляется в хранилище, подписанные сущности получают уведомление через механизм обратного вызова, что позволяет им реагировать на обновлённый список провайдеров.
Далее определим хук useSyncExternalStore для синхронизации локального состояния с внешним хранилищем, определённым в store.tsx выше:
import { useSyncExternalStore } from "react";
import { store } from "./store";
export const useSyncProviders = ()=> useSyncExternalStore(store.subscribe, store.value, store.value)
Динамическое отображение кнопок для каждого обнаруженного провайдера Web3-кошелька
Определим React-компонент, который использует хук useSyncProviders для динамической отрисовки кнопки для каждого обнаруженного провайдера кошелька:
import { useState } from 'react';
import { useSyncProviders } from '../hooks/useSyncProviders';
import { formatAddress } from '~/utils';
export const DiscoverWalletProviders = () => {
const [selectedWallet, setSelectedWallet] = useState<EIP6963ProviderDetail | undefined>();
const [userAccount, setUserAccount] = useState<string>('');
const providers = useSyncProviders();
const handleConnect = async (providerWithInfo: EIP6963ProviderDetail) => {
const accounts = await providerWithInfo.provider.request({ method: 'eth_requestAccounts' }).catch(console.error);
if (accounts && accounts[0]) {
setSelectedWallet(providerWithInfo);
setUserAccount(accounts[0]);
}
};
return (
<>
<h2>Wallets Detected:</h2>
<div>
{providers.length > 0 ? (
providers.map((provider) => (
<button key={provider.info.uuid} onClick={() => handleConnect(provider)}>
<img src={provider.info.icon} alt={provider.info.name} />
<div>{provider.info.name}</div>
</button>
))
) : (
<div>There are no announced providers.</div>
)}
</div>
<hr />
<h2>{userAccount ? 'Wallet Selected' : 'No Wallet Selected'}</h2>
{userAccount && (
<div>
<img src={selectedWallet!.info.icon} alt={selectedWallet!.info.name} />
<div>{selectedWallet!.info.name}</div>
<div>({formatAddress(userAccount)})</div>
</div>
)}
</>
);
};
Разберём приведённый React-компонент:
Мы используем два хука состояния — selectedWallet и userAccount — для отслеживания текущего выбранного провайдера Ethereum-кошелька и адреса аккаунта пользователя соответственно.
Мы используем хук useSyncProviders для динамического обнаружения каждого доступного провайдера Ethereum-кошелька. Этот хук возвращает массив провайдеров, каждый из которых соответствует интерфейсу EIP6963ProviderDetail, включающему информацию о провайдере и объект Ethereum-провайдера.
Мы определяем функцию handleConnect, которая вызывается при нажатии кнопки провайдера кошелька. Эта функция использует метод request провайдера с методом eth_requestAccounts для запроса доступа к аккаунту пользователя. При предоставлении доступа адрес первого аккаунта сохраняется в userAccount, а данные провайдера — в selectedWallet.
Наконец, мы отрисовываем список кнопок для каждого обнаруженного провайдера кошелька, отображая имя и иконку провайдера.
После определения компонента последним шагом является его отрисовка.
import './App.css'
import { DiscoverWalletProviders } from './components/DiscoverWalletProviders'
function App() {
return (
<>
<DiscoverWalletProviders/>
</>
)
}
export default App
Выполнив эти несколько шагов, мы успешно реализовали базовую поддержку EIP-6963 в нашем dapp-приложении. Ниже представлена визуализация реализованной функциональности. Полный код dapp-приложения, показанного на визуализации, можно найти на GitHub.
EIP-6963 и перспективы развития Web3-кошельков
По мере того как этот стандарт получает всё более широкое распространение среди разработчиков кошельков и сторонних библиотек подключения, мы с воодушевлением смотрим на открывающиеся возможности для сообщества Ethereum — особенно в части улучшения пользовательского опыта при онбординге новых пользователей.
В MetaMask мы стремимся к сотрудничеству с другими разработчиками кошельков, dapp-приложений и сторонних библиотек подключения для обеспечения ещё более широкого распространения EIP-6963. Для получения дополнительной информации о нашей работе по поддержке этого стандарта обратитесь к документации MetaMask.