китаев.tech

Работа с событиями: watchContractEvent и getLogs

Прочитаешь исторические Transfer через getLogs и подпишешься на новые события WETH через watchContractEvent.

История и live — два разных API

События контракта (Transfer, Approval, …) попадают в logs блоков. getLogs читает уже записанную историю за диапазон блоков. watchContractEvent подписывается на новые логи по мере появления блоков — замена contract.on("Transfer", ...) из ethers.

Для live нужен WebSocket-транспорт (или HTTP с polling). История работает и через обычный http().


История: getLogs за последние блоки

typescript
import {
  createPublicClient,
  http,
  formatEther,
  parseAbiItem,
} from "viem";
import { mainnet } from "viem/chains";

const publicClient = createPublicClient({
  chain: mainnet,
  transport: http("https://ethereum.publicnode.com"),
});

const wethAddress = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";

const latestBlock = await publicClient.getBlockNumber();
const fromBlock = latestBlock - 50n;

const logs = await publicClient.getLogs({
  address: wethAddress,
  event: parseAbiItem(
    "event Transfer(address indexed from, address indexed to, uint256 value)",
  ),
  fromBlock,
  toBlock: latestBlock,
});

console.log(`Блоки ${fromBlock} → ${latestBlock}, логов: ${logs.length}`);

for (const log of logs.slice(0, 5)) {
  console.log({
    from: log.args.from,
    to: log.args.to,
    value: formatEther(log.args.value ?? 0n),
    tx: log.transactionHash,
  });
}

log.args уже декодирован — вручную через Interface.parseLog ходить не нужно.


Live: watchContractEvent

typescript
import {
  createPublicClient,
  webSocket,
  formatEther,
} from "viem";
import { mainnet } from "viem/chains";

const publicClient = createPublicClient({
  chain: mainnet,
  transport: webSocket("wss://ethereum.publicnode.com"),
});

const wethAddress = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";

const transferAbi = [
  {
    type: "event",
    name: "Transfer",
    inputs: [
      { name: "from", type: "address", indexed: true },
      { name: "to", type: "address", indexed: true },
      { name: "value", type: "uint256", indexed: false },
    ],
  },
] as const;

let received = 0;
const maxEvents = 3;

console.log("Ждём новые WETH Transfer...");

const unwatch = publicClient.watchContractEvent({
  address: wethAddress,
  abi: transferAbi,
  eventName: "Transfer",
  onLogs: (logs) => {
    for (const log of logs) {
      received += 1;
      console.log({
        from: log.args.from,
        to: log.args.to,
        value: formatEther(log.args.value ?? 0n),
        tx: log.transactionHash,
      });

      if (received >= maxEvents) {
        unwatch();
        console.log("Подписка закрыта");
      }
    }
  },
  onError: (error) => {
    console.error("Ошибка подписки:", error);
  },
});

unwatch() снимает подписку. Без него скрипт будет слушать бесконечно.

Фильтр по indexed-полям:

typescript
publicClient.watchContractEvent({
  address: wethAddress,
  abi: transferAbi,
  eventName: "Transfer",
  args: {
    from: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
  },
  onLogs: (logs) => console.log(logs),
});

Как это работает

getLogs({ address, event, fromBlock, toBlock }) — RPC eth_getLogs. parseAbiItem("event Transfer(...)") задаёт сигнатуру: viem сам считает topic0 и декодирует data/topics в args.

watchContractEvent({ abi, eventName, onLogs }) — при WebSocket это eth_subscribe на логи; при http() viem сам поллит новые блоки. Для dApp с постоянным UI бери webSocket.

fromBlock / toBlockbigint. Широкий диапазон на публичном RPC часто режется лимитом ноды — начинай с десятков/сотен блоков.

args.from / args.to в фильтре работают только для indexed параметров события. value у ERC-20 Transfer не indexed — отфильтровать по сумме на уровне RPC нельзя.


Если ты знаешь ethers.js

ethers v6viem
provider.getLogs({ topics }) + iface.parseLoggetLogs({ event }) → сразу log.args
contract.on("Transfer", callback)watchContractEvent({ eventName, onLogs })
contract.off / removeAllListenersфункция unwatch(), которую вернул watch
WebSocketProvidertransport: webSocket(url) у createPublicClient
callback (from, to, value, event)onLogs: (logs) => ... — массив логов за тик

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

Огромный fromBlocklatest на public RPC → нода отвечает ошибкой лимита. Сужай окно или используй архивный/платный endpoint, либо ходи чанками.

HTTP-клиент и ожидание «мгновенного» push → на http() подписка — polling, задержка в секунды нормальна. Для стрима нужен webSocket.

Забыл unwatch() → в скриптах процесс не завершится, в браузере утечёт подписка при размонтировании компонента.


Что дальше

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