Интеграция фронтенда с Ethers.js
Ethers.js v6 сломал обратную совместимость с v5 в нескольких критических местах: BigNumber заменён на нативный bigint, providers.Web3Provider переименован в BrowserProvider, изменился API для получения signer. Если подключаете библиотеку к существующему проекту — проверьте версию в package.json до начала работы.
Подключение кошелька
Современный подход — wagmi + viem для React проектов, или rainbowkit/web3modal для готового UI подключения. Но если задача именно «чистый ethers.js без абстракций»:
import { BrowserProvider, Contract, parseEther, formatEther } from 'ethers';
async function connectWallet() {
if (!window.ethereum) throw new Error('No wallet detected');
const provider = new BrowserProvider(window.ethereum);
await provider.send('eth_requestAccounts', []);
const signer = await provider.getSigner();
const address = await signer.getAddress();
const network = await provider.getNetwork();
return { provider, signer, address, chainId: network.chainId };
}
Обработка событий изменения аккаунта и сети:
window.ethereum.on('accountsChanged', (accounts: string[]) => {
if (accounts.length === 0) {
// Пользователь отключил кошелёк
setConnected(false);
} else {
setAddress(accounts[0]);
}
});
window.ethereum.on('chainChanged', (chainId: string) => {
// chainId приходит как hex string
window.location.reload(); // простейший способ пересоздать provider
});
Работа с контрактами
const ERC20_ABI = [
'function balanceOf(address owner) view returns (uint256)',
'function transfer(address to, uint256 amount) returns (bool)',
'function approve(address spender, uint256 amount) returns (bool)',
'function allowance(address owner, address spender) view returns (uint256)',
'event Transfer(address indexed from, address indexed to, uint256 value)',
];
const contract = new Contract(TOKEN_ADDRESS, ERC20_ABI, signer);
// Чтение (бесплатно)
const balance = await contract.balanceOf(userAddress);
console.log(formatEther(balance)); // для 18 decimals
// Запись (транзакция)
const tx = await contract.transfer(recipientAddress, parseEther('1.0'));
const receipt = await tx.wait(); // ждём подтверждения
console.log('Mined in block:', receipt.blockNumber);
Оценка газа и переопределение
// Оценить газ до отправки
const gasEstimate = await contract.transfer.estimateGas(recipient, amount);
const gasLimit = gasEstimate * 120n / 100n; // +20% буфер
// Отправить с кастомными параметрами газа
const tx = await contract.transfer(recipient, amount, {
gasLimit,
maxFeePerGas: parseUnits('30', 'gwei'),
maxPriorityFeePerGas: parseUnits('2', 'gwei'),
});
Подписание сообщений
EIP-712 (typed data) — для подписей с human-readable контекстом:
const domain = {
name: 'MyDapp',
version: '1',
chainId: 1,
verifyingContract: CONTRACT_ADDRESS,
};
const types = {
Order: [
{ name: 'seller', type: 'address' },
{ name: 'tokenId', type: 'uint256' },
{ name: 'price', type: 'uint256' },
{ name: 'deadline', type: 'uint256' },
],
};
const value = { seller: address, tokenId: 42n, price: parseEther('1'), deadline: BigInt(Math.floor(Date.now()/1000) + 3600) };
const signature = await signer.signTypedData(domain, types, value);
Верификация на бэкенде через ethers.verifyTypedData(domain, types, value, signature).
Типичные проблемы
bigint сериализация в JSON: нативный bigint не сериализуется через JSON.stringify. Конвертируйте в string до отправки на бэкенд или в state: balance.toString().
Multiple injected providers: если установлены MetaMask и Rabby одновременно, window.ethereum может быть любым из них. EIP-6963 решает это через window.addEventListener('eip6963:announceProvider', ...) — все кошельки анонсируют себя, пользователь выбирает явно.
Read-only provider: для чтения данных без кошелька используйте JsonRpcProvider с Alchemy/Infura ключом вместо BrowserProvider. Не стоит требовать подключения кошелька для просмотра данных.
Ориентиры по срокам
Базовая интеграция (connect/disconnect, read contract, send tx) — 1 день. С EIP-712 подписями, multi-chain поддержкой и обработкой всех edge cases — 2-3 дня.







