Парсинг on-chain данных (транзакции, балансы, контракты)
Когда говорят «нужен парсинг блокчейна» — обычно имеют в виду одно из трёх: исторические данные для анализа, мониторинг конкретных адресов в реальном времени, или построение индексированной базы для собственного продукта. Технический подход к каждому случаю разный. Общее одно — нужен правильный источник данных и понимание того, что eth_getLogs не равно «все данные».
Что вообще можно получить из блокчейна
Транзакции уровня блока (eth_getBlockByNumber с fullTx: true):
- From/to/value/gas/gasPrice/nonce
- Input data (calldata в hex)
- Receipt: status (success/reverted), gasUsed, logs (события)
Internal transactions (вызовы между контрактами) — не видны в обычных транзакциях. Нужен debug_traceTransaction или trace_block (Erigon/OpenEthereum trace namespace). Это важно: перевод ETH внутри DeFi-протокола не создаёт обычную транзакцию — он виден только в traces.
События (logs) — эмитируются через emit Event(...) в Solidity. Доступны через eth_getLogs. Это самый производительный способ парсинга — фильтрация по address + topic на уровне ноды.
Storage state — значения storage variables контракта через eth_getStorageAt(address, slot, blockNumber). Для archive node — на любом историческом блоке. Нужно знать slot layout (из ABI + solc).
ERC-20 балансы — через balanceOf(address) view call или через Transfer event history.
ENS / identity — reverse resolution через ENS registry контракт.
Выбор источника данных
| Источник | Что даёт | Ограничения |
|---|---|---|
| Публичный RPC (Infura/Alchemy) | Стандартный JSON-RPC | Rate limits, нет traces |
| Self-hosted Geth | Полный JSON-RPC | Нет traces без --gcmode=archive |
| Self-hosted Erigon | JSON-RPC + trace namespace | ~2.5 TB, 3-5 дней синхронизации |
| Alchemy/QuickNode (платные планы) | Расширенный API + traces | Стоимость при высоком RPS |
| Firehose (StreamingFast) | Бинарный стриминг, все данные | Сложная настройка |
| Dune Analytics / Flipside | SQL интерфейс к indexed данным | Задержка, ограничения схемы |
Для большинства задач парсинга: Alchemy или QuickNode на платном плане — оптимальный старт без инфраструктурной нагрузки. Для высокого объёма или специфических данных (traces, storage) — self-hosted Erigon.
Парсинг транзакций
Базовый блок-парсер
import { createPublicClient, http, parseAbi } from 'viem';
const client = createPublicClient({
transport: http(RPC_URL),
});
async function processBlock(blockNumber: bigint) {
const block = await client.getBlock({
blockNumber,
includeTransactions: true,
});
for (const tx of block.transactions) {
if (typeof tx === 'string') continue; // hash-only mode
await db.insertTransaction({
hash: tx.hash,
blockNumber: Number(tx.blockNumber),
blockTimestamp: Number(block.timestamp),
from: tx.from,
to: tx.to,
value: tx.value.toString(),
gasPrice: tx.gasPrice?.toString(),
gasLimit: tx.gas.toString(),
input: tx.input,
nonce: tx.nonce,
});
}
}
Получение receipts
Receipt содержит: статус выполнения, фактический gasUsed, logs (события). Batch-запрос:
// Получаем receipts для всего блока одним запросом (доступно на Alchemy/QuickNode)
const receipts = await client.request({
method: 'eth_getBlockReceipts',
params: [blockNumber],
});
// Или отдельно для каждой TX (стандартный JSON-RPC)
const receipt = await client.getTransactionReceipt({ hash: tx.hash });
eth_getBlockReceipts — нестандартный метод, доступен на Alchemy, QuickNode, Erigon. На стандартном Geth нужно делать N запросов.
Парсинг событий (logs)
Самый эффективный метод для парсинга специфических данных:
// Парсинг всех ERC-20 Transfer событий на конкретном контракте
const logs = await client.getLogs({
address: TOKEN_ADDRESS,
event: parseAbiItem('event Transfer(address indexed from, address indexed to, uint256 value)'),
fromBlock: 19_000_000n,
toBlock: 19_100_000n,
});
for (const log of logs) {
await db.insertTransfer({
txHash: log.transactionHash,
blockNumber: Number(log.blockNumber),
from: log.args.from,
to: log.args.to,
value: log.args.value.toString(),
});
}
Ограничения eth_getLogs: диапазон не более 2000 блоков за запрос на большинстве публичных нод. Для исторических данных нужен chunked polling:
async function fetchLogsInChunks(
fromBlock: number,
toBlock: number,
chunkSize = 1000,
) {
for (let from = fromBlock; from <= toBlock; from += chunkSize) {
const to = Math.min(from + chunkSize - 1, toBlock);
const logs = await client.getLogs({
address: CONTRACT,
fromBlock: BigInt(from),
toBlock: BigInt(to),
});
await processLogs(logs);
// Rate limiting: пауза чтобы не исчерпать лимиты
await sleep(100);
}
}
Балансы
Текущий баланс
ERC-20 баланс через view call:
const balance = await client.readContract({
address: TOKEN_ADDRESS,
abi: erc20Abi,
functionName: 'balanceOf',
args: [walletAddress],
});
Исторический баланс
Два подхода:
Вычисление из Transfer событий — накапливаем все Transfer в/из адреса и вычисляем running balance. Точно, но требует полной истории событий.
eth_call на историческом блоке — вызов balanceOf с blockTag: blockNumber на archive node. Прямой способ, но нужна archive node.
const historicalBalance = await client.readContract({
address: TOKEN_ADDRESS,
abi: erc20Abi,
functionName: 'balanceOf',
args: [walletAddress],
blockNumber: 18_500_000n, // исторический блок
});
Мультиколл для батчинга
Для получения балансов множества адресов:
import { multicall } from 'viem';
const balances = await client.multicall({
contracts: addresses.map(addr => ({
address: TOKEN_ADDRESS,
abi: erc20Abi,
functionName: 'balanceOf',
args: [addr],
})),
allowFailure: true,
});
Один HTTP запрос вместо N — экономия rate limit квоты в 10-100x.
Парсинг контрактов
ABI получение через Etherscan API (или его форки для других сетей):
const abi = await fetch(
`https://api.etherscan.io/api?module=contract&action=getabi&address=${address}&apikey=${ETHERSCAN_KEY}`
).then(r => r.json()).then(d => JSON.parse(d.result));
Для unverified контрактов — 4byte.directory для декодирования сигнатур функций.
Bytecode анализ — eth_getCode возвращает deployed bytecode. Можно проверить: является ли адрес контрактом (код не пустой), является ли он прокси (EIP-1967 slot), сравнить bytecode хэши.
Storage layout — через solc --storage-layout получаем mapping переменных → storage slots. Затем eth_getStorageAt для чтения значений напрямую без ABI.
Многосетевой парсинг
Один и тот же код должен работать с разными сетями. Ключевые различия:
| Параметр | Ethereum | BNB Chain | Polygon | Arbitrum |
|---|---|---|---|---|
| Block time | ~12 сек | ~3 сек | ~2 сек | ~0.25 сек |
| Log chunk limit | 2000 блоков | 5000 | 3500 | 10000 |
| Native decimals | 18 | 18 | 18 | 18 |
| Trace API | Erigon/Besu | Нода с debug | Ограничено | Стандартный |
const CHAIN_CONFIGS = {
ethereum: { rpc: INFURA_ETH, chunkSize: 1000, blockTime: 12 },
bsc: { rpc: BSC_RPC, chunkSize: 2000, blockTime: 3 },
polygon: { rpc: POLYGON_RPC, chunkSize: 1500, blockTime: 2 },
arbitrum: { rpc: ARB_RPC, chunkSize: 5000, blockTime: 0.25 },
};
Производительность и хранение
Объём данных быстро растёт. Для Ethereum:
- ~6500 блоков/день × ~6000 TX/блок = ~40M транзакций/день
- Каждая транзакция с receipts: ~1-5 KB
- Итого: ~40-200 GB/день для полного парсинга
Для большинства задач не нужен full парсинг — только целевые контракты и события.
Хранение: PostgreSQL для нормализованных данных + TimescaleDB hypertables для time-series + S3 для raw JSON архива.
Индексы критичны:
CREATE INDEX CONCURRENTLY ON transactions (from_address, block_number DESC);
CREATE INDEX CONCURRENTLY ON transactions (to_address, block_number DESC);
CREATE INDEX CONCURRENTLY ON transfers (token_address, block_number DESC);
Стек и сроки
Парсер с поддержкой транзакций + событий + балансов для 2-3 сетей, PostgreSQL хранение, базовый REST API: 2-4 недели в зависимости от глубины исторических данных и количества целевых контрактов.







