Розробка системи агрегації даних із блокчейнів
Завдання виглядає просто: "збирати дані з кількох блокчейнів". На практиці це одна з найскладніших технічних задач у Web3-інфраструктурі. Кожна мережа — це окрема модель даних, своя логіка фіналізації, свій RPC API, свої rate limits і свої специфічні помилки. Ethereum працює в UTC з ~12-секундними блоками, Solana виробляє ~400ms слоти і лічить підтвердження інакше, TON має шардовану архітектуру де "блок" — поняття умовне. Зібрати все це в єдиний API із послідовними даними — нетривіальне інженерне завдання.
Проблема гетерогенності: чому не можна просто "опитати всі RPC"
Різні моделі даних
EVM-мережі (Ethereum, Arbitrum, Polygon, BSC) мають спільну модель: блоки, транзакції, receipts з logs. Але навіть тут є відмінності:
-
Arbitrum додає
l1BlockNumberі специфічні системні транзакції (sequencer batch submissions) -
Optimism/Base мають тип
depositedTxдля L1→L2 транзакцій, які не мають стандартногоfrom - zkSync Era використовує Native AA — нема розділення на EOA і контракти, всі акаунти це контракти
Solana взагалі інша парадигма: нема "транзакція викликала метод контракту" — є "інструкції в транзакції передалися програмам". Для декодування потрібен аналог ABI — IDL (Interface Definition Language, формат Anchor).
UTXO-моделі (Bitcoin, Litecoin) принципово відрізняються: нема балансів акаунтів, є unspent outputs. "Баланс адреси" — це сума всіх UTXO де ця адреса є output.
Різна семантика фіналізації
| Мережа | Механізм | Фіналізація |
|---|---|---|
| Ethereum | PoS + Casper FFG | ~15 хв (finalized checkpoint) |
| Arbitrum One | Optimistic Rollup | ~7 днів (fraud proof window) для L1 finality |
| Polygon PoS | Heimdall checkpoints | ~30 хв для Ethereum finality |
| Solana | Tower BFT | ~12-32 слоти (~6–16 сек) |
| Bitcoin | PoW | 6 підтвердження (~60 хв) — конвенціональний стандарт |
Якщо система це не враховує, дані будуть некоректні: транзакція покажеться "фінальною" за кількістю підтвердження, але буде реорганізована.
Архітектура системи агрегації
Шар колекторів (Chain Collectors)
Кожен колектор — ізольований сервіс, відповідальний за одну мережу. Спільний інтерфейс:
interface ChainCollector {
getLatestBlock(): Promise<UnifiedBlock>;
getBlockRange(from: bigint, to: bigint): Promise<UnifiedBlock[]>;
getTransactionsByAddress(address: string, fromBlock: bigint): Promise<UnifiedTx[]>;
subscribeNewBlocks(callback: (block: UnifiedBlock) => void): Unsubscribe;
}
Уніфіковані типи нормалізують специфіку кожної мережі:
interface UnifiedTx {
chain: ChainId;
hash: string;
blockNumber: bigint;
timestamp: number; // unix
from: string; // нормалізований lowercase hex для EVM, base58 для Solana
to: string | null;
value: bigint; // у найменших одиницях нативного токена
status: 'success' | 'failed' | 'pending';
finality: 'unconfirmed' | 'safe' | 'finalized';
raw: unknown; // оригінальні дані мережі
}
Управління нодами і провайдерами
Проблема: публічні RPC ненадійні, rate limits непередбачувані, Alchemy/Infura дорожають зі масштабом.
Стратегія: tiered provider pool
Primary: Власні ноди (Geth+Lighthouse, Reth для архіву)
↓ failover
Secondary: Alchemy / QuickNode (premium tier)
↓ failover
Tertiary: Infura / публічні RPC (тільки для некритичних запитів)
Circuit breaker на кожному провайдері: якщо error rate > 5% за 60 сек або latency > 2x p99 baseline — видаляємо провайдера з rotation, health check кожні 30 сек.
Для архівних даних (історичні блоки > 128 блоків назад на Ethereum) потрібна archive node — це окрема історія. Erigon займає ~3TB для повного Ethereum архіву, Reth трохи менше. Для більшості проектів дешевше використовувати Alchemy Archive або QuickNode Archive ніж утримувати власну ноду.
Шар нормалізації і трансформації
Сирі дані блокчейну рідко потрібні як є. Типові трансформації:
Декодування ERC-20 Transfer подій
const ERC20_TRANSFER_TOPIC =
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
function decodeTransfer(log: Log): TokenTransfer | null {
if (log.topics[0] !== ERC20_TRANSFER_TOPIC) return null;
return {
token: log.address,
from: `0x${log.topics[1].slice(26)}`,
to: `0x${log.topics[2].slice(26)}`,
amount: BigInt(log.data),
};
}
Збагачення даних токена: для кожного log.address потрібно знати symbol, decimals, USD ціну. Кешуємо metadata токенів у Redis з TTL 24h, ціни оновлюємо кожні 30 сек з CoinGecko/CoinMarketCap.
Cross-chain агрегація: якщо потрібно показати "суммарний баланс адреси у всіх мережах у USD", потрібно нормалізувати різні decimals, конвертувати через price feeds, обробити wrapped-версії одного токена (USDC на Ethereum ≠ USDC.e на Arbitrum).
Шар зберігання
Для гарячих даних (останні 7–30 днів): PostgreSQL із партиціонуванням за chain_id + датою. Індекси на (chain_id, address, block_number) і (chain_id, tx_hash). TimescaleDB гіпертаблиці якщо даних багато — автоматична компресія старих партицій.
Для холодних даних (архів): ClickHouse — колончата БД, на порядок ефективніша за PostgreSQL для аналітичних запитів на великих періодах. Запит "всі USDC транзакції > $10k за 2023 рік по всіх EVM мережах" на 100M+ рядків — ClickHouse виведе результат за секунди, PostgreSQL — за хвилини.
Для пошуку за адресою/хешем: ElasticSearch або просто PostgreSQL із LIKE — hash-індекс достатній для точних совпадінь.
Обробка реорганізацій
Це найскладніша частина системи. Алгоритм:
- Кожен блок зберігаємо з прапором
is_canonical = trueіparent_hash - Новий блок з тим же
block_numberале іншимhash— потенціальна реорганізація - Йдемо по
parent_hashназад до знаходження спільного предка - Усі блоки на "старій" гілці помічаємо
is_canonical = false, додаємо блоки "нової" гілки - Дані в API завжди фільтруються по
is_canonical = true - Webhooks/downstream системи отримують події
tx.orphanedдля скасованих транзакцій
Для Ethereum глибина реорганізації крайне рідко > 2 блоків post-Merge. Для Polygon PoS — бачили реорги на 30+ блоків. Буфер спостереження: 128 блоків для EVM мереж.
API шар
REST + WebSocket для real-time:
GET /v1/address/{address}/transactions?chains=eth,arb,polygon&limit=50
GET /v1/tx/{chain}/{hash}
GET /v1/address/{address}/token-balances?chains=eth,bsc
WS /v1/subscribe?address={addr}&chains=eth,arb&events=transfer,swap
GraphQL зручний якщо клієнтам потрібна гнучкість у запитах: один запит отримує транзакції + балансу + metadata токенів. Але додає складність на бекенді — N+1 проблеми, потрібен DataLoader.
Rate limiting: per-API-key, sliding window, окремі лімити для REST і WebSocket (WebSocket з'єднання дорожчі). Redis + Lua script для атомарних інкрементів.
Моніторинг та операційна робота
Критичні метрики:
-
Collector lag — різниця між
latest block timestampу мережі та часом обробки в нашій системі. Alert при lag > 2 хв. - Reorg depth — максимальна глибина реорганізації за останні 24h. Alert при depth > 10.
- RPC error rate — по кожному провайдеру і методу. Alert при > 1%.
- Queue depth — якщо обробник не встигає за колектором, черга зростає. Alert при depth > 10k повідомлень.
Grafana дашборд з per-chain панелями: поточний блок, lag, TPS, error rate.
Стек
| Компонент | Технологія |
|---|---|
| Колектори | Node.js (viem/ethers) + Go для високонавантажених мереж |
| Черга | Apache Kafka (високий throughput) або RabbitMQ (помірний) |
| Гаряче сховище | PostgreSQL 15 + TimescaleDB |
| Холодне сховище | ClickHouse |
| Кеш | Redis Cluster |
| API | Node.js (Fastify) або Go (Fiber) |
| Моніторинг | Prometheus + Grafana + PagerDuty |
| Оркестрація | Kubernetes з HPA на колекторах |
Реалістичний срок MVP (3–4 EVM мережі, без архіву, REST API): 8–12 тижнів. Повна система з 10+ мережами, ClickHouse, WebSocket, моніторингом — 5–7 місяців.







