Интеграция с The Graph (индексация данных)
Типичная проблема, с которой приходят: фронтенд делает десятки eth_call и getLogs при каждой загрузке страницы. На mainnet это медленно, на публичных RPC-нодах — ненадёжно, а когда нужна агрегация или исторические данные — просто невозможно через прямые вызовы. The Graph решает эту проблему правильно: один раз пишешь subgraph, который индексирует события контракта, и получаешь GraphQL API с произвольными запросами.
Архитектура subgraph
Subgraph состоит из трёх компонентов:
subgraph.yaml — манифест. Описывает источники данных: какие контракты слушать, с какого блока (startBlock), какие события и функции обрабатывать. Критически важно: startBlock должен быть блоком деплоя контракта, а не нулём — иначе индексация займёт дни.
schema.graphql — типы сущностей. Это то, что будет доступно через GraphQL. Проектируется исходя из потребностей фронтенда, а не из структуры событий контракта — это разные вещи.
mappings.ts — AssemblyScript обработчики. Трансформируют сырые события в сущности схемы.
Проектирование схемы
Самая частая ошибка — делать схему зеркалом событий контракта. Если событие Transfer(address from, address to, uint256 amount) — не нужна сущность TransferEvent. Нужно думать о запросах: "какой текущий баланс пользователя", "топ холдеров", "объём за 24 часа".
type Token @entity {
id: ID!
totalSupply: BigInt!
holderCount: Int!
}
type Account @entity {
id: Bytes!
balance: BigInt!
transfersIn: [Transfer!]! @derivedFrom(field: "to")
transfersOut: [Transfer!]! @derivedFrom(field: "from")
}
type Transfer @entity(immutable: true) {
id: Bytes!
from: Account!
to: Account!
amount: BigInt!
blockNumber: BigInt!
timestamp: BigInt!
}
@entity(immutable: true) для Transfer — важная оптимизация. Immutable сущности не хранятся в undo-буфере, индексация быстрее на 30–40%.
Обработчики: тонкости
В AssemblyScript нет полноценного TypeScript — нет null через ?., нет Array.from(), нет стандартных JS методов. Это частый источник ошибок для разработчиков, пришедших с фронтенда.
// Правильная загрузка или создание сущности
function getOrCreateAccount(address: Address): Account {
let account = Account.load(address)
if (account == null) {
account = new Account(address)
account.balance = BigInt.fromI32(0)
}
return account as Account
}
export function handleTransfer(event: TransferEvent): void {
let from = getOrCreateAccount(event.params.from)
let to = getOrCreateAccount(event.params.to)
from.balance = from.balance.minus(event.params.value)
to.balance = to.balance.plus(event.params.value)
from.save()
to.save()
// Immutable — создаём один раз, не загружаем
let transfer = new Transfer(
event.transaction.hash.concatI32(event.logIndex.toI32())
)
transfer.from = from.id
transfer.to = to.id
transfer.amount = event.params.value
transfer.blockNumber = event.block.number
transfer.timestamp = event.block.timestamp
transfer.save()
}
Call handlers и block handlers
Помимо событий, The Graph умеет обрабатывать вызовы функций (callHandlers) и каждый блок (blockHandlers). Call handlers нужны когда контракт не эмитит события для нужных операций — legacy контракты грешат этим. Block handlers — для периодических снапшотов (например, дневная статистика). Оба типа значительно замедляют индексацию, особенно block handlers — используйте только если без них не обойтись.
Деплой и эксплуатация
Hosted Service vs Decentralized Network
Hosted Service (The Graph Network hosted) — centralized, бесплатный, подходит для разработки и MVP. Не SLA-гарантированный, может быть недоступен.
Decentralized Network — индексеры получают GRT за запросы, сигнал курирования влияет на приоритет. Для production с требованиями к uptime — правильный выбор, но нужно понимать экономику: стоимость запросов зависит от объёма.
Self-hosted Graph Node — полный контроль, никаких затрат на GRT, но инфраструктурная нагрузка. Требует: PostgreSQL 14+, IPFS ноду, архивную ноду Ethereum (или Firehose). Минимальные требования для production: 16 CPU, 64GB RAM, NVMe SSD.
Типичные проблемы индексации
Subgraph fails с "store error" — чаще всего попытка сохранить сущность с null полем, который объявлен non-nullable в схеме. Проверяйте все поля перед save().
Индексация зависает на конкретном блоке — обычно это call handler на транзакцию, которая reverted. Нужно добавить обработку через receipt.status.
Расхождение данных с on-chain — subgraph не учитывает reorgs. Для критичных данных нужен minEthereumBlockConfirmations в манифесте или дополнительная логика в клиенте.
Интеграция на фронтенде
Стандартный стек: Apollo Client или urql для React приложений. The Graph поддерживает subscriptions через WebSocket — для realtime обновлений без поллинга.
const POSITIONS_QUERY = gql`
query UserPositions($account: Bytes!, $skip: Int!) {
positions(
where: { owner: $account, liquidity_gt: "0" }
orderBy: createdAt
orderDirection: desc
first: 100
skip: $skip
) {
id
pool { token0 { symbol } token1 { symbol } feeTier }
liquidity
depositedToken0
depositedToken1
}
}
`
Pagination через first/skip работает до skip: 5000 — дальше The Graph возвращает ошибку. Для больших датасетов нужна keyset pagination через id_gt.
Сроки и что входит в работу
За 2–3 дня: проектирование схемы под задачи клиента, написание mappings для всех событий и calls, тестирование на форке, деплой на Hosted Service или настройка self-hosted ноды, базовая интеграция в существующий фронтенд или предоставление GraphQL endpoint.







