Реализация взаимодействия с блокчейном через ethers.js/web3.js на сайте
ethers.js v6 и web3.js v4 — две зрелые библиотеки для работы с EVM-блокчейнами. ethers.js используется чаще: меньший размер бандла, лучшая типизация, более чистый API. web3.js встречается в legacy-проектах и там, где исторически использовался этот стек. Разбираем оба варианта.
ethers.js v6: ключевые концепции
В ethers v6 изменились импорты и появилась поддержка ESM без CJS-fallback:
import {
BrowserProvider, // для window.ethereum (кошелёк пользователя)
JsonRpcProvider, // для серверного RPC-доступа
Contract, // для взаимодействия с контрактами
formatEther, // BigInt → строка в ETH
parseEther, // строка в ETH → BigInt
formatUnits, // с учётом decimals
parseUnits,
isAddress, // валидация адреса
getAddress, // нормализация к checksum
} from 'ethers';
Провайдеры
// Серверный провайдер (для SSR, API routes, cron)
const serverProvider = new JsonRpcProvider(process.env.ETH_RPC_URL);
// Клиентский провайдер через кошелёк пользователя
async function getWalletProvider() {
if (!window.ethereum) throw new Error('Wallet not found');
const provider = new BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
return { provider, signer };
}
// Fallback: пробуем несколько RPC
import { FallbackProvider } from 'ethers';
const fallbackProvider = new FallbackProvider([
{ provider: new JsonRpcProvider('https://rpc1.example.com'), priority: 1, weight: 2 },
{ provider: new JsonRpcProvider('https://rpc2.example.com'), priority: 2, weight: 1 },
]);
Работа с контрактами через ethers.js
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)',
];
// Read-only контракт (серверный провайдер)
const tokenRead = new Contract(TOKEN_ADDRESS, ERC20_ABI, serverProvider);
// Контракт с подписью (кошелёк пользователя)
const { signer } = await getWalletProvider();
const tokenWrite = new Contract(TOKEN_ADDRESS, ERC20_ABI, signer);
// Чтение
const balance = await tokenRead.balanceOf(walletAddress);
console.log(formatUnits(balance, 18));
// Запись
const tx = await tokenWrite.transfer(recipientAddress, parseUnits('10', 18));
const receipt = await tx.wait(); // ждём подтверждения
console.log('Confirmed in block', receipt.blockNumber);
Подписка на события
// Слушаем Transfer-события токена в реальном времени
tokenRead.on('Transfer', (from, to, value, event) => {
console.log(`${from} → ${to}: ${formatUnits(value, 18)} токенов`);
console.log('Block:', event.log.blockNumber);
});
// Фильтрованные события — только входящие на адрес
const filter = tokenRead.filters.Transfer(null, walletAddress);
tokenRead.on(filter, (from, to, value) => {
console.log(`Получено ${formatUnits(value, 18)} от ${from}`);
});
// Отписка при unmount
return () => { tokenRead.removeAllListeners(); };
Исторические события
async function getTransferHistory(
contractAddress: string,
walletAddress: string,
fromBlock: number,
) {
const contract = new Contract(contractAddress, ERC20_ABI, serverProvider);
const filter = contract.filters.Transfer(null, walletAddress);
const events = await contract.queryFilter(filter, fromBlock, 'latest');
return events.map(event => ({
from: event.args[0],
to: event.args[1],
amount: formatUnits(event.args[2], 18),
blockNumber: event.blockNumber,
txHash: event.transactionHash,
}));
}
web3.js v4: основные паттерны
web3.js v4 — полный переписанный API с TypeScript-нативной поддержкой:
import { Web3 } from 'web3';
const web3 = new Web3(window.ethereum);
// Или с HTTP провайдером
const web3Server = new Web3(process.env.ETH_RPC_URL);
// Контракт через web3.js
const erc20Contract = new web3.eth.Contract(ERC20_ABI, TOKEN_ADDRESS);
// Чтение
const balance = await erc20Contract.methods.balanceOf(walletAddress).call();
const formatted = web3.utils.fromWei(balance, 'ether');
// Запись
const accounts = await web3.eth.getAccounts();
const receipt = await erc20Contract.methods
.transfer(recipient, web3.utils.toWei('10', 'ether'))
.send({ from: accounts[0] });
Обработка BigInt в UI
ethers v6 возвращает нативный BigInt, web3.js v4 тоже. Проблема — JSON.stringify не сериализует BigInt:
// Безопасная сериализация для state/API
const replacer = (_: string, value: unknown) =>
typeof value === 'bigint' ? value.toString() : value;
JSON.stringify(data, replacer);
// Или используйте SuperJSON для Prisma/tRPC стека
import superjson from 'superjson';
superjson.stringify(data); // автоматически обрабатывает BigInt
Утилиты, которые нужны в каждом проекте
// Сокращение адреса для отображения
export function shortenAddress(address: string): string {
return `${address.slice(0, 6)}…${address.slice(-4)}`;
}
// Проверка и нормализация адреса
export function safeGetAddress(input: string): string | null {
try {
return getAddress(input); // бросает если невалидный
} catch {
return null;
}
}
// Конвертация timestamp блока в дату
export async function blockToDate(blockNumber: number): Promise<Date> {
const block = await serverProvider.getBlock(blockNumber);
return new Date(Number(block!.timestamp) * 1000);
}
Сроки: интеграция чтения данных из контракта и подписки на события через ethers.js — 1–2 дня. Полноценный слой взаимодействия с несколькими контрактами, обработкой ошибок, fallback-провайдерами и историческими событиями — 3–4 дня.







