Реализация блокчейн-эксплорера на сайте под ключ
Представьте: вы запускаете новый блокчейн или sidechain. Пользователи хотят отслеживать свои транзакции, но без эксплорера это невозможно. Построить такой сервис с нуля — нетривиальная задача: нужно синхронизировать архивную ноду, индексировать каждый блок, реализовать быстрый поиск по хешам и адресам. Мы уже делали это для 8 проектов и знаем все подводные камни: от реоргов до деградации производительности при росте цепочки. Однажды к нам обратился стартап, чей эксплорер на основе The Graph падал при 50 запросах в секунду — пришлось переписывать полностью.
В этой статье я покажу, как устроен наш подход: индексатор на базе PostgreSQL, API на Next.js и декодирование смарт-контрактов. Вы узнаете, как избежать типичных ошибок и сократить время разработки. А также поймёте, почему инвестиции в кастомное решение окупаются уже через полгода активной эксплуатации. Мы реализовали уже 8 блокчейн-эксплореров для различных EVM-сетей, включая пару сайдчейнов с кастомными фичами.
Компоненты системы
Блокчейн-нода (archive) | ├── RPC/WebSocket (eth_getBlock, eth_getTransaction...) | Индексатор (собственный или The Graph) | Читает блоки → парсит транзакции → сохраняет | PostgreSQL + Redis (кеш) | REST API / GraphQL | Frontend (Next.js) ├── Поиск (tx hash, address, block) ├── Список блоков ├── Детали транзакции ├── Профиль адреса (баланс + история) └── Декодирование смарт-контракт вызовов Для корректной работы необходима архивная нода. Подробнее — в Ethereum development docs.
Как обеспечить минимальное время отклика при поиске по хешу?
Ключ — правильная индексация в базе и кэширование. Используем PostgreSQL с индексами на hash и block_number, а Redis — для горячих данных (последние блоки, популярные адреса). Типичная ошибка — дергать ноду при каждом поиске. Мы всегда прокладываем слой кэша, иначе TTFB может превышать 2 секунды.
При нагрузке 100+ запросов в секунду наше решение держит среднее время отклика ниже 200 мс. Это достигается за счет предварительного разогрева кэша и пагинации при выборке блоков.
Почему кастомный индексатор лучше готовых решений?
Готовые индексаторы (The Graph, Moralis) удобны для старта, но накладывают ограничения: зависимость от внешних серверов, невозможность тонкой настройки. Сравнение:
| Характеристика | Готовый индексатор | Кастомный индексатор |
|---|---|---|
| Контроль | Низкий — только subgraph | Полный — своя логика |
| Скорость | Зависит от сети индексатора | Оптимизирован под ваши данные |
| Стоимость | Бесплатно (self-host) или платно | Одноразовая разработка + хостинг |
| Гибкость | Ограниченная (GraphQL-схема) | Любые поля и связи |
| Обучение | Есть кривая обучения | Нужен разработчик |
Для продакшена с высокой нагрузкой (более 100 запросов/сек) мы всегда рекомендуем кастомный индексатор. Он даёт предсказуемую производительность и свободу в расширении. По нашим данным, переход с готового решения на кастомное снижает затраты на инфраструктуру на 30–40%.
Как индексировать блоки: три шага
- Настройка ноды и подключение. Разворачиваем архивную ноду (Geth/Nethermind) или используем WebSocket-провайдера. Убеждаемся, что нода готова отдавать полные блоки с транзакциями.
-
Реализация индексатора. С помощью библиотеки
viemподписываемся на новые блоки и парсим их в PostgreSQL. Важно обрабатывать реорганизации — храним хеш предыдущего блока и перепроверяем при форке. - Постобработка данных. После вставки транзакций обновляем статистику адресов (балансы, число транзакций) и помещаем горячие данные в Redis.
import { createPublicClient, webSocket } from 'viem'; import { mainnet } from 'viem/chains'; const client = createPublicClient({ chain: mainnet, transport: webSocket(process.env.ETH_WS_URL) }); class BlockIndexer { async indexBlock(blockNumber: bigint): Promise<void> { const block = await client.getBlock({ blockNumber, includeTransactions: true }); await db.transaction(async (trx) => { await trx('blocks').insert({ number: Number(block.number), hash: block.hash, parent_hash: block.parentHash, timestamp: new Date(Number(block.timestamp) * 1000), miner: block.miner, gas_used: block.gasUsed.toString(), gas_limit: block.gasLimit.toString(), transaction_count: block.transactions.length, base_fee_per_gas: block.baseFeePerGas?.toString() ?? null }); for (const tx of block.transactions) { await trx('transactions').insert({ hash: tx.hash, block_number: Number(tx.blockNumber), from_address: tx.from.toLowerCase(), to_address: tx.to?.toLowerCase() ?? null, value: tx.value.toString(), gas: tx.gas.toString(), gas_price: tx.gasPrice?.toString() ?? null, max_fee_per_gas: tx.maxFeePerGas?.toString() ?? null, input: tx.input, nonce: tx.nonce, transaction_index: tx.transactionIndex }); await this.updateAddressStats(trx, tx.from.toLowerCase()); if (tx.to) await this.updateAddressStats(trx, tx.to.toLowerCase()); } }); } async watchNewBlocks(): Promise<void> { const unwatch = client.watchBlocks({ onBlock: async (block) => { await this.indexBlock(block.number); }, onError: (error) => { logger.error('Block watch error', error); } }); const latestIndexed = await this.getLatestIndexedBlock(); const currentBlock = await client.getBlockNumber(); for (let i = latestIndexed + 1n; i <= currentBlock; i++) { await this.indexBlock(i); } } } API: поиск
app.get('/api/search', async (req, res) => { const query = req.query.q as string; if (!query) return res.status(400).json({ error: 'Query required' }); if (/^0x[0-9a-f]{64}$/i.test(query)) { const tx = await db('transactions').where('hash', query.toLowerCase()).first(); if (tx) return res.json({ type: 'transaction', data: tx }); const block = await db('blocks').where('hash', query.toLowerCase()).first(); if (block) return res.json({ type: 'block', data: block }); } else if (/^0x[0-9a-f]{40}$/i.test(query)) { return res.json({ type: 'address', address: query.toLowerCase() }); } else if (/^\d+$/.test(query)) { const block = await db('blocks').where('number', parseInt(query)).first(); if (block) return res.json({ type: 'block', data: block }); } res.json({ type: 'not_found' }); }); Frontend: декодирование input data
import { decodeFunctionData } from 'viem'; async function decodeTransactionInput( input: string, contractAddress: string ): Promise<DecodedInput | null> { if (input === '0x') return null; const abi = await getContractAbi(contractAddress); if (!abi) return { raw: input }; try { const decoded = decodeFunctionData({ abi, data: input as `0x${string}` }); return { functionName: decoded.functionName, args: decoded.args, raw: input }; } catch { return { raw: input }; } } Что входит в разработку эксплорера
| Компонент | Результат |
|---|---|
| Архитектурная документация | ER-диаграмма, спецификация API, описание потоков данных |
| Индексатор | Исходный код на TypeScript (или Python), Docker-контейнер для развёртывания |
| Бэкенд | REST/GraphQL API с кэшированием (Redis), документация в OpenAPI |
| Фронтенд | Next.js приложение с поиском, списком блоков, профилем адреса и декодированием |
| Тестирование | Нагрузочные тесты (k6) и отчёт о производительности |
| Документация для команды | Инструкции по запуску, настройке и мониторингу |
| Техническая поддержка | 2 недели после запуска: помощь с багами и адаптацией |
Процесс работы
Мы делим проект на пять этапов:
| Этап | Длительность | Результат |
|---|---|---|
| Аналитика | 2-3 дня | Документ с архитектурой |
| Проектирование | 3-5 дней | Схема БД, API, макеты |
| Реализация | 2-4 недели | Рабочий индексатор + API |
| Тестирование | 5-7 дней | Отчёт о нагрузочных тестах |
| Деплой и мониторинг | 2-3 дня | Запуск в продакшн |
На этапе аналитики мы уточняем, какие данные нужны: только блоки и транзакции или полная картина с внутренними вызовами и логами событий. Проектирование включает подготовку схемы базы данных (обычно около 15-20 таблиц) и спецификацию API в формате OpenAPI.
Сроки ориентировочно
- MVP-эксплорер (транзакции, блоки, адреса) без индексатора (через RPC) — 2-3 недели.
- Полный индексатор с PostgreSQL + API + Frontend — 6-10 недель.
- Эксплорер для кастомной EVM-сети — от 2 месяцев.
Затраты на инфраструктуру зависят от размера сети и обсуждаются индивидуально. Свяжитесь с нами, чтобы получить предварительную оценку вашего проекта.
Типичные ошибки при создании блокчейн-эксплорера
- Отсутствие backfill после сбоя индексатора — данные теряются. Решение: хранить последний обработанный блок в БД и при перезапуске продолжать с него.
- Игнорирование реорганизаций (reorg) — блоки могут меняться. Нужно подписываться на события
blockчерез WebSocket и проверять хеши. - Сканирование всех блоков по RPC без пагинации — нода падает от перегрузки. Используйте пакетную загрузку с задержками.
Если вы столкнулись с любой из этих проблем или хотите избежать их с самого начала, закажите консультацию — мы поможем спроектировать стабильный эксплорер.
Что дальше?
Кастомный индексатор — это инвестиция в производительность и надежность. Не нужно зависеть от внешних сервисов: ваши данные всегда под контролем. Закажите разработку — мы предложим архитектуру под вашу сеть за один рабочий день.







