Реалізація підключення крипто-гаманця до веб-програми
Підключення гаманця — перша точка входу в будь-яке Web3 застосування. Користувач клацає "Connect Wallet", браузер відкриває MetaMask або WalletConnect QR, застосування отримує адресу та підпис. Звучить просто, але під капотом — три різні протоколи, дюжина постачальників гаманців та ряд проблем із станом, які потрібно вирішити правильно з самого початку.
Що відбувається під час підключення
- Браузер перевіряє наявність
window.ethereum(вбудовані гаманці: MetaMask, Rabby, Brave Wallet) - Застосування викликає
eth_requestAccounts— з'являється спливаюче вікно підтвердження - Гаманець повертає масив адрес, перша — активна
- Застосування підписує SIWE (Sign-In with Ethereum) повідомлення для автентифікації на бекенді
- Сесія створюється на сервері за підписом
WalletConnect (мобільні гаманці) працює по-іншому: через сервер ретрансляції, WebSocket та QR-код. Coinbase Wallet підтримує обидва методи.
Мінімальна реалізація через ethers.js
// lib/wallet.ts
import { BrowserProvider, JsonRpcSigner } from 'ethers';
export interface WalletState {
address: string | null;
chainId: number | null;
provider: BrowserProvider | null;
signer: JsonRpcSigner | null;
}
export async function connectWallet(): Promise<WalletState> {
if (!window.ethereum) {
throw new Error('No injected wallet found. Install MetaMask.');
}
const provider = new BrowserProvider(window.ethereum);
const accounts = await provider.send('eth_requestAccounts', []);
const network = await provider.getNetwork();
const signer = await provider.getSigner();
return {
address: accounts[0],
chainId: Number(network.chainId),
provider,
signer,
};
}
export async function switchChain(chainId: number): Promise<void> {
await window.ethereum.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: `0x${chainId.toString(16)}` }],
});
}
Обробка подій гаманця
Гаманець змінює рахунок або мережу без сповіщення програми — потрібно підписатися на события:
// hooks/useWalletEvents.ts
import { useEffect } from 'react';
import { useWalletStore } from '@/store/wallet';
export function useWalletEvents() {
const { disconnect, setAddress, setChainId } = useWalletStore();
useEffect(() => {
if (!window.ethereum) return;
const handleAccountsChanged = (accounts: string[]) => {
if (accounts.length === 0) {
disconnect();
} else {
setAddress(accounts[0]);
}
};
const handleChainChanged = (chainIdHex: string) => {
setChainId(parseInt(chainIdHex, 16));
// Не перезавантажуємо сторінку — оновлюємо стан
};
window.ethereum.on('accountsChanged', handleAccountsChanged);
window.ethereum.on('chainChanged', handleChainChanged);
window.ethereum.on('disconnect', disconnect);
return () => {
window.ethereum.removeListener('accountsChanged', handleAccountsChanged);
window.ethereum.removeListener('chainChanged', handleChainChanged);
window.ethereum.removeListener('disconnect', disconnect);
};
}, [disconnect, setAddress, setChainId]);
}
SIWE-автентифікація
Адреса гаманця сама по собі не є ідентифікатором користувача. Його можна підробити в HTTP-запиті. Для сеансу бекенду потрібна підпис:
// lib/siwe.ts
import { SiweMessage } from 'siwe';
export async function signInWithEthereum(
address: string,
chainId: number,
signer: JsonRpcSigner,
): Promise<{ message: string; signature: string }> {
const nonce = await fetch('/api/auth/nonce').then(r => r.text());
const message = new SiweMessage({
domain: window.location.host,
address,
statement: 'Sign in to MyApp',
uri: window.location.origin,
version: '1',
chainId,
nonce,
});
const messageStr = message.prepareMessage();
const signature = await signer.signMessage(messageStr);
return { message: messageStr, signature };
}
Бекенд верифікує підпис через пакет siwe (Node.js) або будь-яку реалізацію ecrecover. Nonce у Redis з TTL 5 хвилин — захист від атак повторення.
Кілька гаманців через універсальний постачальник
Для підтримки MetaMask, WalletConnect, Coinbase Wallet без користувацької логіки для кожного — використовуйте @web3-onboard або стек wagmi + viem. Мінімальний приклад з @web3-onboard:
import Onboard from '@web3-onboard/core';
import injectedModule from '@web3-onboard/injected-wallets';
import walletConnectModule from '@web3-onboard/walletconnect';
const injected = injectedModule();
const walletConnect = walletConnectModule({
projectId: process.env.NEXT_PUBLIC_WC_PROJECT_ID!,
requiredChains: [1, 137],
});
export const onboard = Onboard({
wallets: [injected, walletConnect],
chains: [
{ id: '0x1', token: 'ETH', label: 'Ethereum Mainnet', rpcUrl: process.env.ETH_RPC_URL! },
{ id: '0x89', token: 'MATIC', label: 'Polygon', rpcUrl: process.env.POLYGON_RPC_URL! },
],
appMetadata: {
name: 'MyApp',
icon: '/logo.svg',
description: 'DeFi platform',
},
});
Стійкість підключення
Після перезавантаження сторінки стан гаманця потрібно відновити без повторного спливаючого вікна:
// Перевірка під час ініціалізації
async function restoreConnection(): Promise<void> {
if (!window.ethereum) return;
// eth_accounts (не eth_requestAccounts) — не викликає спливаюче вікно
const accounts: string[] = await window.ethereum.request({
method: 'eth_accounts',
});
if (accounts.length > 0) {
// Гаманець уже авторизований — відновімо стан
const provider = new BrowserProvider(window.ethereum);
const network = await provider.getNetwork();
walletStore.set({ address: accounts[0], chainId: Number(network.chainId) });
}
}
Часова шкала: базове підключення MetaMask + WalletConnect з SIWE-автентифікацією — 2–3 дні. Включаючи обробку подій, стійкість та підтримку 3–4 гаманців через @web3-onboard — 3–5 днів.







