Реализация блокчейн-эксплорера на сайте под ключ
Представьте: вы запускаете новый блокчейн или 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 без пагинации — нода падает от перегрузки. Используйте пакетную загрузку с задержками.
Если вы столкнулись с любой из этих проблем или хотите избежать их с самого начала, закажите консультацию — мы поможем спроектировать стабильный эксплорер.
Что дальше?
Кастомный индексатор — это инвестиция в производительность и надежность. Не нужно зависеть от внешних сервисов: ваши данные всегда под контролем. Закажите разработку — мы предложим архитектуру под вашу сеть за один рабочий день.







