Мы часто сталкиваемся с ситуацией, когда фронтенд делает десятки 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 и десятки успешных интеграций гарантируют результат. Закажите консультацию — мы подготовим индивидуальное предложение.







