Как реализовать поддержку EIP-6963 в вашем Web3 Dapp

Узнайте, как внедрить EIP-6963 в ваш web3 dapp для более удобного подключения кошелька

6 мин чтения
Как реализовать поддержку EIP-6963 в вашем Web3 Dapp

В мае 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-кошельков.

// EthereumProviderTypes.d.ts

// Interface for provider information following EIP-6963.
interface EIP6963ProviderInfo {
  walletId: string; // Unique identifier for the wallet e.g io.metamask, io.metamask.flask 
  uuid: string; // Globally unique ID to differentiate between provider sessions for the lifetime of the page
  name: string; // Human-readable name of the wallet
  icon: string; // URL to the wallet's icon
}

// Interface for Ethereum providers based on the EIP-1193 standard.
interface EIP1193Provider {
  isStatus?: boolean; // Optional: Indicates the status of the provider
  host?: string; // Optional: Host URL of the Ethereum node
  path?: string; // Optional: Path to a specific endpoint or service on the host
  sendAsync?: (request: { method: string, params?: Array<unknown> }, callback: (error: Error | null, response: unknown) => void) => void; // For sending asynchronous requests
  send?: (request: { method: string, params?: Array<unknown> }, callback: (error: Error | null, response: unknown) => void) => void; // For sending synchronous requests
  request: (request: { method: string, params?: Array<unknown> }) => Promise<unknown>; // Standard method for sending requests per EIP-1193
}

// Interface detailing the structure of provider information and its Ethereum provider.
interface EIP6963ProviderDetail {
  info: EIP6963ProviderInfo; // The provider's info
  provider: EIP1193Provider; // The EIP-1193 compatible provider
}

// Type representing the event structure for announcing a provider based on EIP-6963.
type EIP6963AnnounceProviderEvent = {
  detail: {
    info: EIP6963ProviderInfo; // The provider's info
    provider: EIP1193Provider; // The EIP-1193 compatible provider
  }
}
Копировать

Я добавил комментарии, которые помогут вам понять, что представляет собой каждый интерфейс и тип. Для более подробного объяснения обратитесь к предложению по улучшению.

Подписка на события и получение уведомлений о новых подключениях

Следующий шаг — настройка механизма оповещения вашего dapp-приложения при обнаружении новых подключений кошельков. Напомним, что цель EIP-6963 — обеспечить бесшовное подключение и переключение между несколькими кошельками. В React это можно реализовать с помощью приведённого ниже примера кода.

// store.tsx

declare global {
  interface WindowEventMap {
    "eip6963:announceProvider": CustomEvent<EIP6963AnnounceProviderEvent>;
  }
}

let providers: EIP6963ProviderDetail[] = [];

export const store = {
  value: () => providers,

  subscribe: (callback: () => void) => {
    function onAnnouncement(event: EIP6963AnnounceProviderEvent) {
      // Prevent adding a provider if it already exists in the list based on its uuid.
      if (providers.some(p => p.info.uuid === event.detail.info.uuid)) return;

      // Add the new provider to the list and call the provided callback function.
      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 позволяет получить текущее состояние внешнего хранилища, то есть список обнаруженных провайдеров кошельков.

  1. Функция subscribe позволяет компонентам или другим частям приложения подписываться на изменения в хранилище. Когда новый провайдер объявляется и добавляется в хранилище, подписанные сущности получают уведомление через механизм обратного вызова, что позволяет им реагировать на обновлённый список провайдеров.

Далее определим хук useSyncExternalStore для синхронизации локального состояния с внешним хранилищем, определённым в store.tsx выше:

// useSyncProviders.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.

Переведено ИИ. Может содержать ошибки. Пожалуйста, всегда проверяйте информацию.

Оцените перевод
  • Kingsley Okonkwo
    Kingsley Okonkwo

    Kingsley, a certified Ethereum Blockchain developer, serves as a technical writer at Consensys, with a special focus on developer tools. Before joining Consensys, Kingsley worked as a freelance backend developer on sevral platforms including Gigster and Braintrust. Boasting 5 years of software development experience, he's proficient with tools such as JavaScript (Node.js & React), GoLang, SQL, and Docker. Kingsley excels in producing step-by-step tutorials and how-to guides, utilizing the latest in web3 technology to help fellow developers build better dapps. Currently based in Dubai, Kingsley enjoys playing basketball and soccer in his free time

    Читать все статьи