Мы часто сталкиваемся с ситуацией, когда фронтенд делает десятки eth_call и getLogs при каждой загрузке страницы. На mainnet это занимает 2–3 секунды, на публичных RPC — ненадёжно, а когда нужна агрегация или исторические данные — просто невозможно через прямые вызовы. The Graph решает эту проблему правильно: один раз пишется subgraph, который индексирует события контракта, и получается GraphQL API с произвольными запросами за миллисекунды. Экономия на инфраструктуре может достигать 90% (до $2000 в месяц для типового DeFi-проекта), а скорость запросов возрастает в десятки раз. Ниже разберём, как спроектировать и написать subgraph, чтобы избежать типичных ошибок и получить максимум производительности.
Как спроектировать схему 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 методов. Это частый источник ошибок для разработчиков, пришедших с фронтенда.
- Определите событие, которое обрабатываете (например,
Transfer). - Загрузите или создайте сущность с помощью
Account.load(address)илиnew Account(address). - Обновите поля (баланс, ссылки).
- Сохраните изменения через
save().
// Правильная загрузка или создание сущности 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 — используйте только если без них не обойтись.
| Тип обработчика | Назначение | Влияние на скорость индексации |
|---|---|---|
| Event handler | Обработка событий контракта | Минимальное (основной тип) |
| Call handler | Отслеживание вызовов функций | Среднее (нужен архивный узел) |
| Block handler | Периодическая обработка блоков | Высокое (выполняется на каждый блок) |
Почему The Graph быстрее прямых вызовов RPC?
Прямые вызовы RPC выполняются последовательно и загружают ноду. The Graph индексирует данные один раз и хранит их в оптимизированной базе, доступной через GraphQL. Запросы выполняются за миллисекунды (типичное время от 50 до 200 мс), а пагинация через first/skip работает до skip: 5000 — для больших датасетов используйте keyset pagination через id_gt. По данным документации The Graph, пропускная способность индексации может достигать 1000 событий в секунду на стандартном оборудовании. Экономия на инфраструктуре может достигать 90%, что подтверждается опытом крупных DeFi-проектов — например, Uniswap сократил затраты на RPC на $3000+ в месяц после перехода на subgraph.
Какой хостинг выбрать: Hosted Service, Decentralized или self-hosted?
Выбор варианта развёртывания subgraph зависит от требований к доступности, контролю и бюджету.
| Вариант | Доступность | Контроль | Стоимость |
|---|---|---|---|
| Hosted Service | Базовая (без SLA) | Ограниченный | Бесплатно для небольших проектов |
| Decentralized Network | Высокая (децентрализованные индексеры) | Средний | Требует GRT (токен) |
| Self-hosted Graph Node | Полная (собственное оборудование) | Полный | Затраты на инфраструктуру ($1000+ в месяц) |
Наша команда с десятилетним опытом в блокчейне реализовала более 20 интеграций The Graph. Мы помогаем выбрать оптимальный вариант под ваш проект и избежать типичных проблем.
Типичные проблемы индексации
- Subgraph fails с "store error" — проверьте non-nullable поля.
- Индексация зависает на блоке — добавьте обработку реверсированных транзакций через
receipt.status. - Расхождение данных из-за reorgs — установите
minEthereumBlockConfirmations. - Чрезмерное потребление газа при записи — используйте
@entity(immutable: true)для неизменяемых данных.
Что нужно знать о пагинации и фильтрации?
| Тип запроса | Пример | Ограничение |
|---|---|---|
| Pagination | first: 100, skip: 0 |
skip до 5000 |
| Keyset pagination | where: { id_gt: "..." } |
Без ограничений |
| Фильтрация | where: { balance_gt: "0" } |
Поддерживаются все операторы |
| Сортировка | orderBy: timestamp, orderDirection: desc |
По любому полю сущности |
Keyset pagination предпочтительна для больших наборов данных — она быстрее и не имеет ограничения по skip.
Интеграция на фронтенде
Стандартный стек: 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 } } ` Сроки и что входит в работу
За 2–5 дней: проектирование схемы под задачи клиента, написание mappings для всех событий и calls, тестирование на форке, деплой на Hosted Service или настройка self-hosted ноды, базовая интеграция в существующий фронтенд или предоставление GraphQL endpoint. Стоимость рассчитывается индивидуально, но экономия на RPC-запросах оправдывает инвестиции — типичная окупаемость наступает в течение 1-3 месяцев.
Свяжитесь с нами, чтобы обсудить интеграцию The Graph в ваш проект. Опыт работы с The Graph и десятки успешных интеграций гарантируют результат. Закажите консультацию — мы подготовим индивидуальное предложение.







