китаев.tech

Исторические события через getLogs

Как читать исторические events через ethers v6: собрать topics-фильтр, вызвать provider.getLogs и декодировать logs через Interface.

Что делает getLogs

getLogs читает исторические logs из уже созданных блоков.

Это низкоуровневый способ получить events: вы сами задаёте адрес контракта, диапазон блоков и topics, а потом декодируете результат через ABI.


Читаем Transfer logs у WETH

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

bash
mkdir get-logs-example
cd get-logs-example

npm init -y
npm pkg set type="module"
npm install ethers tsx typescript @types/node
mkdir scripts

Создайте файл scripts/get-weth-logs.ts:

typescript
import { ethers } from "ethers";

const provider = new ethers.JsonRpcProvider("https://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 iface = new ethers.Interface(wethAbi);
const weth = new ethers.Contract(wethAddress, wethAbi, provider);

const latestBlock = await provider.getBlockNumber();
const fromBlock = latestBlock - 100;

const transferTopic = ethers.id("Transfer(address,address,uint256)");

const logs = await provider.getLogs({
  address: wethAddress,
  fromBlock,
  toBlock: latestBlock,
  topics: [transferTopic],
});

const decimals = await weth.decimals();
const symbol = await weth.symbol();

console.log(`Blocks: ${fromBlock} -> ${latestBlock}`);
console.log(`Found logs: ${logs.length}`);

for (const log of logs.slice(0, 5)) {
  const parsed = iface.parseLog({
    topics: log.topics,
    data: log.data,
  });

  if (!parsed) {
    continue;
  }

  const [from, to, value] = parsed.args;

  console.log("Tx:", log.transactionHash);
  console.log("Block:", log.blockNumber);
  console.log("From:", from);
  console.log("To:", to);
  console.log("Value:", ethers.formatUnits(value, decimals), symbol);
  console.log("---");
}

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

bash
npx tsx scripts/get-weth-logs.ts

Код найдёт WETH Transfer events за последние 100 блоков и выведет первые пять.


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

provider.getLogs(...) — отправляет RPC-запрос eth_getLogs.

address: wethAddress — ограничивает поиск logs одним контрактом.

fromBlock и toBlock — задают диапазон блоков. Оба края включаются в поиск.

ethers.id("Transfer(address,address,uint256)") — считает topic события Transfer.

topics: [transferTopic] — ищет только logs события Transfer.

iface.parseLog({ topics, data }) — декодирует raw log в имя события и аргументы from, to, value.


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

Чтобы найти входящие переводы на конкретный адрес, добавьте topic для to:

typescript
const vitalikAddress = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045";
const toTopic = ethers.zeroPadValue(vitalikAddress, 32);

const logs = await provider.getLogs({
  address: wethAddress,
  fromBlock,
  toBlock: latestBlock,
  topics: [
    transferTopic,
    null,
    toTopic,
  ],
});

null на позиции topics[1] значит любой from. toTopic на позиции topics[2] оставляет только logs, где to равен vitalikAddress.


Как устроены topics

У события ERC-20 Transfer три аргумента:

solidity
event Transfer(address indexed from, address indexed to, uint256 value);

В log они раскладываются так:

text
topics[0] = hash("Transfer(address,address,uint256)")
topics[1] = from
topics[2] = to
data      = value

from и to попали в topics, потому что они indexed. value не indexed, поэтому лежит в data.


getLogs vs queryFilter

queryFilter удобнее, когда у вас уже есть ethers.Contract:

typescript
const events = await weth.queryFilter(
  weth.filters.Transfer(null, vitalikAddress),
  fromBlock,
  latestBlock,
);

getLogs полезнее, когда вы строите индексатор, работаете с несколькими адресами или хотите контролировать raw-фильтр:

typescript
const logs = await provider.getLogs({
  address: [wethAddress, usdcAddress],
  topics: [transferTopic],
  fromBlock,
  toBlock,
});

Оба способа используют один и тот же RPC-метод eth_getLogs. Разница в уровне абстракции.


Ограничения диапазона

Публичные RPC часто ограничивают getLogs по количеству блоков или найденных logs.

Для реального индексатора делите историю на чанки:

text
0      -> 999
1000   -> 1999
2000   -> 2999
...

После каждого чанка сохраняйте последний обработанный блок. Так процесс можно безопасно продолжить после ошибки RPC или перезапуска сервера.


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

Запрашивать миллионы блоков за один раз → RPC может вернуть ошибку или timeout. Делите диапазон на небольшие чанки.

Передавать адрес в topics без padding → indexed address хранится как 32-байтное значение. Используйте ethers.zeroPadValue(address, 32).

Путать null и отсутствие topicnull внутри topics значит любой аргумент на этой позиции.

Декодировать log без ABI событияInterface.parseLog() сможет разобрать только events, которые описаны в ABI.


Что дальше

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