Исторические события через getLogs
Как читать исторические events через ethers v6: собрать topics-фильтр, вызвать provider.getLogs и декодировать logs через Interface.
Что делает getLogs
getLogs читает исторические logs из уже созданных блоков.
Это низкоуровневый способ получить events: вы сами задаёте адрес контракта, диапазон блоков и topics, а потом декодируете результат через ABI.
Читаем Transfer logs у WETH
Создайте проект:
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:
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("---");
}Запустите скрипт:
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:
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 три аргумента:
event Transfer(address indexed from, address indexed to, uint256 value);В log они раскладываются так:
topics[0] = hash("Transfer(address,address,uint256)")
topics[1] = from
topics[2] = to
data = valuefrom и to попали в topics, потому что они indexed. value не indexed, поэтому лежит в data.
getLogs vs queryFilter
queryFilter удобнее, когда у вас уже есть ethers.Contract:
const events = await weth.queryFilter(
weth.filters.Transfer(null, vitalikAddress),
fromBlock,
latestBlock,
);getLogs полезнее, когда вы строите индексатор, работаете с несколькими адресами или хотите контролировать raw-фильтр:
const logs = await provider.getLogs({
address: [wethAddress, usdcAddress],
topics: [transferTopic],
fromBlock,
toBlock,
});Оба способа используют один и тот же RPC-метод eth_getLogs. Разница в уровне абстракции.
Ограничения диапазона
Публичные RPC часто ограничивают getLogs по количеству блоков или найденных logs.
Для реального индексатора делите историю на чанки:
0 -> 999
1000 -> 1999
2000 -> 2999
...После каждого чанка сохраняйте последний обработанный блок. Так процесс можно безопасно продолжить после ошибки RPC или перезапуска сервера.
Частые ошибки
Запрашивать миллионы блоков за один раз → RPC может вернуть ошибку или timeout. Делите диапазон на небольшие чанки.
Передавать адрес в topics без padding → indexed address хранится как 32-байтное значение. Используйте ethers.zeroPadValue(address, 32).
Путать null и отсутствие topic → null внутри topics значит любой аргумент на этой позиции.
Декодировать log без ABI события → Interface.parseLog() сможет разобрать только events, которые описаны в ABI.
Что дальше
- Events и логи — как хранятся данные — понять структуру
topicsиdata - Events — подписка в реальном времени — перейти от истории к live-событиям
- Работа с ABI и Interface — вручную декодировать raw logs и calldata