Розробка 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: думати GraphQL-запитами
Схема має проектуватися виходячи з того, які запити потрібні фронтенду — не з структури подій контракту. Типова помилка: робити entities один-до-одного з подіями. Це ведає до того, що фронтенд робить N+1 запитів.
Правильний підхід — денормалізовані entities з передагрегованими даними:
type Pool @entity {
id: ID! # адреса пулу
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 | Ні (централізована) | Так |
| 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 і приклади запитів







