китаев.tech

Events — подписка в реальном времени

Как слушать events смарт-контракта через ethers v6 и WebSocketProvider: подписаться, обработать Transfer и корректно закрыть соединение.

Что значит слушать events

Подписка на events получает новые logs сразу после появления блока.

Для live-обновлений нужен WebSocket RPC: он держит постоянное соединение и присылает событие без ручного опроса блоков.


Слушаем Transfer events WETH

Создайте проект:

bash
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:

typescript
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();
}

Запустите скрипт:

bash
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-процесс завершился.


Фильтр по адресу

Можно слушать не все переводы, а только входящие переводы на конкретный адрес:

typescript
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.


Реальное время не заменяет историю

Подписка получает новые события с момента запуска процесса.

Если приложение было выключено, события за это время нужно дочитать отдельно:

typescript
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 аргументам, если нужен конкретный адрес.


Что дальше

Материалы китаev.tech публикуются в образовательных целях и не являются инвестиционной рекомендацией. Примеры кода и описания протоколов — для обучения; использование в продакшне на ваш собственный риск. Дисклеймер и политика конфиденциальности.