Отслеживание смены аккаунта и сети
Как слушать accountsChanged и chainChanged в MetaMask, обновлять адрес и сеть в интерфейсе и правильно удалять обработчики.
Зачем слушать изменения кошелька
Пользователь может сменить аккаунт или сеть прямо в MetaMask, не нажимая кнопки в вашем dApp.
Если интерфейс не слушает эти события, он покажет старый адрес, старый баланс или отправит транзакцию в неправильную сеть. Поэтому dApp должен реагировать на accountsChanged и chainChanged.
Слушаем аккаунт и сеть MetaMask
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 может вернуть пустой массив:
[]Это значит, что у сайта больше нет доступа к аккаунтам MetaMask. Например, пользователь отключил dApp в настройках кошелька.
В таком состоянии нельзя показывать старый адрес. Очистите UI и снова покажите кнопку подключения.
Частые ошибки
Не обрабатывать пустой массив аккаунтов → если accounts.length === 0, считайте кошелёк отключённым.
Создавать listeners при каждом render → в React или другом UI-фреймворке добавляйте listeners один раз и удаляйте их при unmount.
Игнорировать chainChanged → после смены сети пересоздайте BrowserProvider или заново прочитайте network, balance и данные контрактов.
Что дальше
- Подключение MetaMask через BrowserProvider — подключить аккаунт перед подпиской на события
- Смена сети — addChain и switchChain — попросить пользователя перейти в нужную сеть
- JsonRpcProvider и WebSocketProvider — понять, какие события приходят от RPC, а какие от браузерного кошелька