Events — подписка в реальном времени
Как слушать events смарт-контракта через ethers v6 и WebSocketProvider: подписаться, обработать Transfer и корректно закрыть соединение.
Что значит слушать events
Подписка на events получает новые logs сразу после появления блока.
Для live-обновлений нужен WebSocket RPC: он держит постоянное соединение и присылает событие без ручного опроса блоков.
Слушаем Transfer events WETH
Создайте проект:
mkdir events-realtime-example
cd events-realtime-example
npm init -y
npm pkg set type="module"
npm install ethers tsx typescript @types/node
mkdir scriptsСоздайте файл scripts/watch-weth-transfers.ts:
import { ethers } from "ethers";
const websocketProvider = new ethers.WebSocketProvider("wss://ethereum.publicnode.com");
const wethAddress = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";
const wethAbi = [
"event Transfer(address indexed from, address indexed to, uint256 value)",
"function decimals() view returns (uint8)",
"function symbol() view returns (string)",
];
const weth = new ethers.Contract(wethAddress, wethAbi, websocketProvider);
const decimals = await weth.decimals();
const symbol = await weth.symbol();
let receivedEvents = 0;
const maxEvents = 3;
console.log("Ждём новые WETH Transfer events...");
try {
await new Promise<void>((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error("За 2 минуты пришло слишком мало Transfer events"));
}, 120_000);
weth.on("Transfer", async (from, to, value, event) => {
receivedEvents += 1;
console.log(`Event #${receivedEvents}`);
console.log("From:", from);
console.log("To:", to);
console.log("Value:", ethers.formatUnits(value, decimals), symbol);
console.log("---");
if (receivedEvents >= maxEvents) {
clearTimeout(timeout);
await event.removeListener();
resolve();
}
});
});
} finally {
websocketProvider.destroy();
}Запустите скрипт:
npx tsx scripts/watch-weth-transfers.tsСкрипт подпишется на новые Transfer events WETH, выведет три перевода и закроет WebSocket-соединение.
Как это работает
new ethers.WebSocketProvider() — создаёт постоянное соединение с RPC-нодой.
new ethers.Contract(wethAddress, wethAbi, websocketProvider) — подключает контракт к WebSocket provider.
weth.on("Transfer", listener) — подписывается на новые logs события Transfer.
from, to, value — декодированные аргументы event. ethers берёт их из ABI.
event.removeListener() — снимает текущий listener после нужного количества событий.
websocketProvider.destroy() — закрывает WebSocket, чтобы Node.js-процесс завершился.
Фильтр по адресу
Можно слушать не все переводы, а только входящие переводы на конкретный адрес:
const vitalikAddress = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045";
const filter = weth.filters.Transfer(null, vitalikAddress);
weth.on(filter, (from, to, value) => {
console.log("Incoming transfer:", from, to, value.toString());
});Первый аргумент null значит любой from. Второй аргумент фильтрует to, потому что to в событии Transfer помечен как indexed.
Реальное время не заменяет историю
Подписка получает новые события с момента запуска процесса.
Если приложение было выключено, события за это время нужно дочитать отдельно:
const latestBlock = await websocketProvider.getBlockNumber();
const events = await weth.queryFilter(
weth.filters.Transfer(),
latestBlock - 1000,
latestBlock,
);Обычно backend делает оба шага: сначала догоняет историю через queryFilter, потом включает live-подписку через on.
Что может пойти не так
WebSocket-соединение может оборваться. Production-сервис должен уметь переподключаться и продолжать чтение с последнего обработанного блока.
Блок может стать orphaned при реорганизации цепи. Для критичных действий ждите несколько подтверждений или сверяйте события по block hash.
Публичные RPC могут ограничивать подписки. Для стабильного backend-индексатора лучше использовать собственный RPC-провайдер или специализированный индексатор.
Частые ошибки
Слушать events через HTTP RPC → для live-подписок используйте WebSocketProvider. HTTP подходит для queryFilter и обычного чтения.
Не закрывать WebSocket → процесс продолжит работать. После одноразового скрипта вызывайте provider.destroy().
Думать, что подписка вернёт прошлые события → on слушает новые logs. Историю читайте через queryFilter.
Подписываться без фильтра на шумный event → популярные контракты генерируют много logs. Фильтруйте по indexed аргументам, если нужен конкретный адрес.
Что дальше
- Events и логи — как хранятся данные — понять, как logs устроены внутри receipt
- Работа с ABI и Interface — декодировать события и raw logs вручную
- JsonRpcProvider и WebSocketProvider — выбрать правильный provider для чтения и подписок