События контракта: подписка и обновление UI

Как фронт узнаёт об изменениях в контракте без постоянного опроса: события (events) и логи.

Событие (event) — запись, которую контракт оставляет в логах транзакции. Фронт может подписаться на события и обновлять UI, как только что-то произошло on-chain.

Представьте чат на блокчейне. Опрашивать контракт «есть новые сообщения?» каждую секунду — расточительно. Контракт при каждом сообщении эмитит событие MessageSent, а фронт просто слушает. Это Web3-аналог WebSocket-уведомлений.

Подписка на новые события

В ethers контракт умеет слушать события через contract.on(). Нужен Provider, желательно WebSocket для realtime:

const abi = [
  "event Transfer(address indexed from, address indexed to, uint256 value)",
];
const token = new Contract(addr, abi, wsProvider);

token.on("Transfer", (from, to, value, event) => {
  console.log(`${from} -> ${to}: ${value.toString()}`);
  // обновить UI: новый перевод
});

Аргументы колбэка идут в том же порядке, что параметры события в ABI, плюс последний объект event с метаданными (номер блока, hash транзакции). Не забывайте отписываться (contract.off(...) или removeAllListeners) при размонтировании компонента, иначе утечёте подписками.

Чтение прошлых событий (логов)

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

const filter = token.filters.Transfer(null, userAddress); // все переводы НА userAddress
const past = await token.queryFilter(filter, -10000); // за последние ~10000 блоков
for (const e of past) {
  console.log(e.args.from, e.args.value.toString());
}

Фильтры по indexed-полям работают эффективно — нода умеет искать по ним. Поэтому в контрактах важные для поиска поля помечают indexed (видно в ABI).

Как работает под капотом

Событие — это запись в специальной области receipt'а транзакции. У неё есть «топики»: первый — хеш сигнатуры события, остальные — indexed-аргументы. Неиндексированные аргументы лежат в data-поле. Подписка через WebSocket использует eth_subscribe на логи с фильтром; нода присылает новые логи по мере появления блоков. Чтение истории — это eth_getLogs по диапазону блоков и топикам. ethers по ABI декодирует сырые топики/data обратно в удобные именованные args.

Ограничение: глубина истории

Запрос eth_getLogs на огромный диапазон блоков провайдеры ограничивают (часто несколько тысяч блоков за раз). Прочитать «все события за всё время» напрямую с ноды на больших контрактах нереально — это причина существования индексаторов вроде The Graph (отдельный урок).

Частые ошибки

  • Не отписываться от событий. В React при ремоунте копятся слушатели — утечка и дубли в UI.
  • Слушать события по HTTP-провайдеру. Работает через медленный polling; для realtime берите WebSocket.
  • Тянуть всю историю одним queryFilter. Провайдер ограничит диапазон; листайте по блокам или используйте индексатор.

Итоги

  • События — способ узнать об изменениях без опроса; подписка через contract.on().
  • История читается через queryFilter; ищите по indexed-полям.
  • Глубина истории с ноды ограничена — для аналитики нужен индексатор.
Проверьте себя
1. Зачем контракт эмитит события?
AЧтобы хранить состояние дешевле
BЧтобы фронт узнавал об изменениях без постоянного опроса
CЧтобы платить меньше газа за чтение
DЧтобы подписывать транзакции
2. Для realtime-подписки на события какой провайдер предпочтителен?
AHTTP
BWebSocket
CBrowserProvider обязательно
DЛюбой публичный
3. Почему нельзя одним queryFilter прочитать всю историю большого контракта?
Aethers это запрещает
BПровайдеры ограничивают диапазон блоков в eth_getLogs
CСобытия не хранятся
DНужен Signer