Парсинг 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 historya.
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, // історичний блок
});
Multicall для батчинга
Для отримання балансів множини адрес:
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 тижні в залежності від глибини історичних даних та кількості цільових контрактів.







