китаев.tech

Работа с несколькими кошельками

Как обнаружить несколько injected wallets через EIP-6963, дать пользователю выбрать кошелёк и подключить выбранный provider через ethers.js v6.

Почему window.ethereum недостаточно

В браузере может быть несколько кошельков: MetaMask, Rabby, Coinbase Wallet, OKX Wallet.

Старый подход через window.ethereum не говорит, какой именно кошелёк пользователь хочет подключить. EIP-6963 решает это: каждый кошелёк объявляет свой provider, а dApp показывает пользователю выбор.


Находим кошельки и подключаем выбранный

typescript
import { BrowserProvider, formatEther } from "ethers";
import type { Eip1193Provider } from "ethers";

type Eip6963ProviderInfo = {
  uuid: string;
  name: string;
  icon: string;
  rdns: string;
};

type Eip6963ProviderDetail = {
  info: Eip6963ProviderInfo;
  provider: Eip1193Provider;
};

type Eip6963AnnounceProviderEvent = CustomEvent<Eip6963ProviderDetail>;

const discoveredWallets = new Map<string, Eip6963ProviderDetail>();

const app = document.createElement("main");
app.innerHTML = `
  <button id="discover-wallets">Найти кошельки</button>
  <div id="wallet-list"></div>
  <pre id="wallet-info">Кошельки ещё не найдены</pre>
`;
document.body.appendChild(app);

const discoverButton = document.querySelector<HTMLButtonElement>("#discover-wallets");
const walletList = document.querySelector<HTMLDivElement>("#wallet-list");
const output = document.querySelector<HTMLPreElement>("#wallet-info");

if (!discoverButton || !walletList || !output) {
  throw new Error("Не удалось найти элементы интерфейса");
}

async function connectWallet(wallet: Eip6963ProviderDetail) {
  const accounts = (await wallet.provider.request({
    method: "eth_requestAccounts",
  })) as string[];

  if (accounts.length === 0) {
    output.textContent = "Кошелёк не вернул доступный аккаунт.";
    return;
  }

  const provider = new BrowserProvider(wallet.provider);
  const address = accounts[0];
  const network = await provider.getNetwork();
  const balance = await provider.getBalance(address);

  output.textContent = [
    `Кошелёк: ${wallet.info.name}`,
    `RDNS: ${wallet.info.rdns}`,
    `Адрес: ${address}`,
    `Сеть: ${network.name} (${network.chainId})`,
    `Баланс: ${formatEther(balance)} ETH`,
  ].join("\n");
}

function renderWallets() {
  walletList.replaceChildren();

  if (discoveredWallets.size === 0) {
    output.textContent = "Injected wallets не найдены.";
    return;
  }

  for (const wallet of discoveredWallets.values()) {
    const button = document.createElement("button");
    button.type = "button";
    button.textContent = `Подключить ${wallet.info.name}`;
    button.addEventListener("click", () => {
      connectWallet(wallet).catch((error: unknown) => {
        const message = error instanceof Error ? error.message : "Неизвестная ошибка";
        output.textContent = `Ошибка подключения: ${message}`;
      });
    });

    walletList.appendChild(button);
  }

  output.textContent = `Найдено кошельков: ${discoveredWallets.size}`;
}

function handleAnnounceProvider(event: Event) {
  const { detail } = event as Eip6963AnnounceProviderEvent;
  discoveredWallets.set(detail.info.uuid, detail);
  renderWallets();
}

window.addEventListener("eip6963:announceProvider", handleAnnounceProvider);

discoverButton.addEventListener("click", () => {
  discoveredWallets.clear();
  renderWallets();
  window.dispatchEvent(new Event("eip6963:requestProvider"));
});

window.dispatchEvent(new Event("eip6963:requestProvider"));

Если браузерные кошельки поддерживают EIP-6963, они ответят событием eip6963:announceProvider. Пользователь увидит отдельную кнопку для каждого найденного кошелька.


Как это работает

eip6963:requestProvider — событие, которым dApp просит кошельки объявить себя.

eip6963:announceProvider — событие, которым кошелёк возвращает info и provider.

info.uuid — уникальный идентификатор объявления. Его удобно использовать как ключ, чтобы не показывать один кошелёк дважды.

info.name — человекочитаемое имя кошелька: MetaMask, Rabby, Coinbase Wallet.

info.rdns — reverse DNS идентификатор кошелька. Например, MetaMask использует его, чтобы dApp мог отличить один wallet extension от другого.

new BrowserProvider(wallet.provider) — оборачивает выбранный EIP-1193 provider в ethers.js.


Как выбирать кошелёк

dApp не должен автоматически выбирать первый найденный provider.

Правильный UX:

text
1. Найти все injected wallets через EIP-6963
2. Показать пользователю список кошельков
3. Подключить только выбранный provider
4. Сохранить выбор на время сессии

Так пользователь сам решает, каким кошельком подписывать сообщения и транзакции.


Частые ошибки

Подключаться к window.ethereum без выбора → при нескольких расширениях это может быть не тот кошелёк, который ожидает пользователь.

Сравнивать кошельки только по имени → используйте uuid для списка и rdns для определения типа кошелька.

Добавлять один и тот же provider несколько раз → кошелёк может объявиться повторно. Храните найденные provider в Map.


Что дальше

Материалы китаev.tech публикуются в образовательных целях и не являются инвестиционной рекомендацией. Примеры кода и описания протоколов — для обучения; использование в продакшне на ваш собственный риск. Дисклеймер и политика конфиденциальности.