китаев.tech

Отслеживание смены аккаунта и сети

Как слушать accountsChanged и chainChanged в MetaMask, обновлять адрес и сеть в интерфейсе и правильно удалять обработчики.

Зачем слушать изменения кошелька

Пользователь может сменить аккаунт или сеть прямо в MetaMask, не нажимая кнопки в вашем dApp.

Если интерфейс не слушает эти события, он покажет старый адрес, старый баланс или отправит транзакцию в неправильную сеть. Поэтому dApp должен реагировать на accountsChanged и chainChanged.


Слушаем аккаунт и сеть MetaMask

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

type MetaMaskProvider = Eip1193Provider & {
  on(event: "accountsChanged", handler: (accounts: string[]) => void): void;
  on(event: "chainChanged", handler: (chainId: string) => void): void;
  removeListener(
    event: "accountsChanged",
    handler: (accounts: string[]) => void,
  ): void;
  removeListener(event: "chainChanged", handler: (chainId: string) => void): void;
};

declare global {
  interface Window {
    ethereum?: MetaMaskProvider;
  }
}

const app = document.createElement("main");
app.innerHTML = `
  <button id="connect-wallet">Подключить кошелёк</button>
  <button id="stop-listening">Остановить отслеживание</button>
  <pre id="wallet-state">Кошелёк не подключён</pre>
`;
document.body.appendChild(app);

const connectButton = document.querySelector<HTMLButtonElement>("#connect-wallet");
const stopButton = document.querySelector<HTMLButtonElement>("#stop-listening");
const output = document.querySelector<HTMLPreElement>("#wallet-state");

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

async function renderWalletState(accounts: string[]) {
  if (!window.ethereum) {
    output.textContent = "MetaMask не найден. Установите расширение браузера.";
    return;
  }

  if (accounts.length === 0) {
    output.textContent = "Сайт не имеет доступа к аккаунтам MetaMask.";
    return;
  }

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

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

function handleAccountsChanged(accounts: string[]) {
  renderWalletState(accounts).catch((error: unknown) => {
    const message = error instanceof Error ? error.message : "Неизвестная ошибка";
    output.textContent = `Ошибка обновления аккаунта: ${message}`;
  });
}

function handleChainChanged(chainId: string) {
  output.textContent = `Сеть изменилась: ${chainId}. Обновляем данные...`;

  window.ethereum
    ?.request({ method: "eth_accounts" })
    .then((accounts) => renderWalletState(accounts as string[]))
    .catch((error: unknown) => {
      const message = error instanceof Error ? error.message : "Неизвестная ошибка";
      output.textContent = `Ошибка обновления сети: ${message}`;
    });
}

async function connectAndSubscribe() {
  if (!window.ethereum) {
    output.textContent = "MetaMask не найден. Установите расширение браузера.";
    return;
  }

  const accounts = (await window.ethereum.request({
    method: "eth_requestAccounts",
  })) as string[];

  await renderWalletState(accounts);

  window.ethereum.on("accountsChanged", handleAccountsChanged);
  window.ethereum.on("chainChanged", handleChainChanged);
}

function stopListening() {
  if (!window.ethereum) {
    return;
  }

  window.ethereum.removeListener("accountsChanged", handleAccountsChanged);
  window.ethereum.removeListener("chainChanged", handleChainChanged);
  output.textContent = "Отслеживание аккаунта и сети остановлено.";
}

connectButton.addEventListener("click", () => {
  connectAndSubscribe().catch((error: unknown) => {
    const message = error instanceof Error ? error.message : "Неизвестная ошибка";
    output.textContent = `Ошибка подключения: ${message}`;
  });
});

stopButton.addEventListener("click", stopListening);

Этот код обновляет адрес, сеть и баланс после смены аккаунта или сети в MetaMask.


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

eth_requestAccounts — запрашивает доступ к аккаунтам и возвращает массив адресов.

accountsChanged — срабатывает, когда меняется результат eth_accounts. Первый адрес в массиве — текущий выбранный аккаунт.

chainChanged — срабатывает, когда пользователь переключает сеть. MetaMask передаёт chainId строкой в hex-формате, например "0x1" или "0x2105".

eth_accounts — читает аккаунты без повторного popup, если пользователь уже дал доступ сайту.

removeListener() — удаляет конкретный обработчик. Важно передавать ту же функцию, которую передали в on().


Что делать при пустом accounts

accountsChanged может вернуть пустой массив:

text
[]

Это значит, что у сайта больше нет доступа к аккаунтам MetaMask. Например, пользователь отключил dApp в настройках кошелька.

В таком состоянии нельзя показывать старый адрес. Очистите UI и снова покажите кнопку подключения.


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

Не обрабатывать пустой массив аккаунтов → если accounts.length === 0, считайте кошелёк отключённым.

Создавать listeners при каждом render → в React или другом UI-фреймворке добавляйте listeners один раз и удаляйте их при unmount.

Игнорировать chainChanged → после смены сети пересоздайте BrowserProvider или заново прочитайте network, balance и данные контрактов.


Что дальше

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