Разработка subgraph для The Graph
Проблема, с которой сталкиваются все dApp-разработчики: смарт-контракты не хранят историю состояния в удобном для запросов виде. eth_getLogs с фильтром по событиям — это грубый инструмент: нет сортировки, нет агрегации, нет связей между событиями разных контрактов. В итоге фронтенд либо тащит тонны данных и обрабатывает их на клиенте, либо команда поднимает собственный индексирующий бэкенд. The Graph решает эту задачу стандартным способом — вы описываете, что индексировать, а сеть делает это за вас.
Subgraph — это по сути декларация: какие контракты слушать, какие события обрабатывать, как трансформировать данные в entities. Написать его правильно с первого раза сложнее, чем кажется.
Архитектура subgraph и типичные ошибки
Структура проекта
Subgraph состоит из трёх частей: subgraph.yaml (манифест), schema.graphql (модель данных), AssemblyScript handlers (логика трансформации). Манифест — самое важное место:
dataSources:
- kind: ethereum
name: UniswapV3Pool
network: mainnet
source:
address: "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640"
abi: UniswapV3Pool
startBlock: 12369621 # блок деплоя контракта — обязательно
mapping:
kind: ethereum/events
apiVersion: 0.0.7
language: wasm/assemblyscript
entities:
- Pool
- Swap
abis:
- name: UniswapV3Pool
file: ./abis/UniswapV3Pool.json
eventHandlers:
- event: Swap(indexed address,indexed address,int256,int256,uint160,uint128,int24)
handler: handleSwap
startBlock — критичный параметр. Если указать 0, индексирование пройдёт с genesis блока, синхронизация займёт дни. Всегда указываем блок деплоя контракта. Найти его можно через Etherscan, поле "Contract Creation".
Schema design: думать GraphQL-запросами
Схема должна проектироваться исходя из того, какие запросы нужны фронтенду — не из структуры событий контракта. Типичная ошибка: делать entities один-к-одному с событиями. Это ведёт к тому, что фронтенд делает N+1 запросов.
Правильный подход — денормализованные entities с предагрегированными данными:
type Pool @entity {
id: ID! # address пула
token0: Token!
token1: Token!
feeTier: BigInt!
totalVolumeUSD: BigDecimal! # накопительный объём — обновляем в каждом Swap
totalValueLockedUSD: BigDecimal!
txCount: BigInt!
swaps: [Swap!]! @derivedFrom(field: "pool")
}
type Swap @entity {
id: ID! # txHash + logIndex
pool: Pool!
sender: Bytes!
recipient: Bytes!
amount0: BigDecimal!
amount1: BigDecimal!
amountUSD: BigDecimal!
timestamp: BigInt!
blockNumber: BigInt!
}
@derivedFrom — виртуальная связь, не хранит массив ID в записи Pool. Это важно для производительности: пул с тысячами свапов не будет расти в размере записи.
AssemblyScript handlers: где всё ломается
AssemblyScript — строго типизированный язык, компилируется в WebAssembly. Привычки из TypeScript здесь опасны:
// НЕПРАВИЛЬНО — null reference в AS вызывает панику
let pool = Pool.load(event.address.toHexString())
pool.txCount = pool.txCount.plus(BigInt.fromI32(1)) // pool может быть null
// ПРАВИЛЬНО
let poolId = event.address.toHexString()
let pool = Pool.load(poolId)
if (pool === null) {
pool = new Pool(poolId)
pool.txCount = BigInt.fromI32(0)
pool.totalVolumeUSD = BigDecimal.fromString("0")
}
pool.txCount = pool.txCount.plus(BigInt.fromI32(1))
pool.save()
BigDecimal для финансовых значений — обязательно. BigInt из контракта нужно конвертировать с учётом decimals токена:
function convertTokenToDecimal(tokenAmount: BigInt, exchangeDecimals: BigInt): BigDecimal {
if (exchangeDecimals == BigInt.fromI32(0)) {
return tokenAmount.toBigDecimal()
}
return tokenAmount.toBigDecimal().div(
BigInt.fromI32(10).pow(exchangeDecimals.toI32() as u8).toBigDecimal()
)
}
Call handlers и block handlers
Помимо event handlers есть два других типа:
callHandlers — реагируют на вызовы конкретных функций. Используются, когда контракт не эмитит нужные события (встречается в старых контрактах). Значительно медленнее индексирования по событиям — The Graph должен обрабатывать каждый call trace.
blockHandlers — вызываются на каждый блок. Крайне дорогие для hosted service и decentralized network. Использовать только если нет альтернативы, обязательно с filter: { kind: once } или условной логикой внутри.
Деплой и работа с сетью
Hosted Service vs Decentralized Network
| Hosted Service | Decentralized Network | |
|---|---|---|
| Стоимость | Бесплатно (устаревает) | GRT токены (Indexer fees) |
| Latency | Низкая | Выше (~100–500ms) |
| Censorship resistance | Нет (centralized) | Да |
| SLA | Без гарантий | Зависит от Indexers |
| Подходит для | Разработка, тестирование | Production с требованием decentralization |
Для production протоколов — decentralized network. Graph Explorer позволяет мониторить статус синхронизации и выбирать Indexers.
# Деплой в Subgraph Studio
graph auth --studio <deploy-key>
graph codegen && graph build
graph deploy --studio <subgraph-name>
Отладка медленной синхронизации
Если subgraph синхронизируется медленнее ожидаемого:
- Проверить количество
callHandlers— заменить наeventHandlersгде возможно - Убедиться что
startBlockне слишком ранний - Проверить количество
eth_callв handlers — каждый вызов контракта из mapping это дополнительный RPC запрос - Использовать
ipfs.catминимально — медленная операция
Типичная скорость: ~2000–5000 блоков/минуту для event-only subgraph на hosted service. С callHandlers — в 5–10 раз медленнее.
Что входит в работу
- Анализ ABI контрактов и определение нужных events/calls
- Проектирование schema под конкретные запросы фронтенда
- Написание и тестирование AssemblyScript handlers
- Деплой и мониторинг синхронизации
- Документация GraphQL endpoints и примеры запросов







