The Graph: вирішення проблеми запитів даних Web3
Цього разу ми детальніше розглянемо The Graph, який по суті став частиною стандартного стеку для розробки dapp за останній рік. Спочатку давайте подивимося, як це робиться традиційним способом...
Без The Graph...
Отже, давайте розглянемо простий приклад для ілюстрації. Ми всі любимо ігри, тому уявімо просту гру, в якій користувачі роблять ставки:
1pragma solidity 0.7.1;23contract Game {4 uint256 totalGamesPlayerWon = 0;5 uint256 totalGamesPlayerLost = 0;6 event BetPlaced(address player, uint256 value, bool hasWon);78 function placeBet() external payable {9 bool hasWon = evaluateBetForPlayer(msg.sender);1011 if (hasWon) {12 (bool success, ) = msg.sender.call{ value: msg.value * 2 }('');13 require(success, "Transfer failed");14 totalGamesPlayerWon++;15 } else {16 totalGamesPlayerLost++;17 }1819 emit BetPlaced(msg.sender, msg.value, hasWon);20 }21}Показати всеТепер припустимо, що в нашому dapp ми хочемо відображати загальну кількість ставок, загальну кількість програних/виграних ігор, а також оновлювати ці дані, щоразу, коли хтось грає знову. Підхід буде таким:
- Отримати
totalGamesPlayerWon. - Отримати
totalGamesPlayerLost. - Підписатися на події
BetPlaced.
Ми можемо прослуховувати подію у Web3opens in a new tab, як показано праворуч, але це вимагає обробки чималої кількості випадків.
1GameContract.events.BetPlaced({2 fromBlock: 03}, function(error, event) { console.log(event); })4.on('data', function(event) {5 // подія спрацювала6})7.on('changed', function(event) {8 // подію знову видалено9})10.on('error', function(error, receipt) {11 // транзакцію відхилено12});Показати всеДля нашого простого прикладу це все ще більш-менш підходить. Але припустимо, що тепер ми хочемо відображати суми програних/виграних ставок лише для поточного гравця. Що ж, нам не пощастило. Краще розгорнути новий контракт, який зберігатиме ці значення, та отримувати їх. А тепер уявіть набагато складніший смартконтракт і dapp, і все може швидко заплутатися.
Ви можете бачити, наскільки це неоптимально:
- Не працює для вже розгорнутих контрактів.
- Додаткові витрати на газ для зберігання цих значень.
- Потребує ще одного виклику для отримання даних з вузла Ethereum.
Тепер давайте розглянемо краще рішення.
Дозвольте представити вам GraphQL
По-перше, давайте поговоримо про GraphQL, який був розроблений та реалізований соціальною мережею Facebook. Можливо, ви знайомі з традиційною моделлю REST API. А тепер уявіть, що замість цього ви можете написати запит саме для тих даних, які вам потрібні:
Ці два зображення практично передають суть GraphQL. За допомогою запиту праворуч ми можемо точно визначити, які дані нам потрібні, тож ми отримуємо все за один запит і нічого більше, крім того, що нам потрібно. Сервер GraphQL обробляє отримання всіх необхідних даних, тому його неймовірно легко використовувати на стороні клієнта (фронтенду). Ось гарне поясненняopens in a new tab того, як саме сервер обробляє запит, якщо вам цікаво.
Тепер, маючи ці знання, давайте нарешті зануримося у світ блокчейну та The Graph.
Що таке The Graph?
Блокчейн — це децентралізована база даних, але, на відміну від звичайних баз даних, у нас немає мови запитів для неї. Рішення для отримання даних є складними або взагалі неможливими. The Graph — це децентралізований протокол для індексування та запитів до даних блокчейну. І, як ви могли здогадатися, він використовує GraphQL як мову запитів.
Приклади — це завжди найкращий спосіб щось зрозуміти, тож давайте використаємо The Graph для нашого прикладу з GameContract.
Як створити Subgraph
Визначення способу індексування даних називається субграфом (subgraph). Він потребує трьох компонентів:
- Маніфест (
subgraph.yaml) - Схема (
schema.graphql) - Відображення (
mapping.ts)
Маніфест (subgraph.yaml)
Маніфест — це наш файл конфігурації, і він визначає:
- які смартконтракти індексувати (адреса, мережа, ABI...)
- які події прослуховувати
- інші речі для прослуховування, наприклад виклики функцій або блоки
- функції відображення, що викликаються (див.
mapping.tsнижче)
Тут можна визначити кілька контрактів та обробників. Типове налаштування передбачає наявність папки субграфа в проєкті Hardhat із власним репозиторієм. Тоді ви зможете легко посилатися на ABI.
Для зручності ви також можете використовувати інструмент для шаблонів, наприклад mustache. Потім ви створюєте subgraph.template.yaml і вставляєте адреси на основі останніх розгортань. Для більш просунутого прикладу налаштування дивіться, наприклад, репозиторій субграфа Aaveopens in a new tab.
А повну документацію можна переглянути тутopens in a new tab.
1specVersion: 0.0.12description: Placing Bets on Ethereum3repository: - GitHub link -4schema:5 file: ./schema.graphql6dataSources:7 - kind: ethereum/contract8 name: GameContract9 network: mainnet10 source:11 address: '0x2E6454...cf77eC'12 abi: GameContract13 startBlock: 617524414 mapping:15 kind: ethereum/events16 apiVersion: 0.0.117 language: wasm/assemblyscript18 entities:19 - GameContract20 abis:21 - name: GameContract22 file: ../build/contracts/GameContract.json23 eventHandlers:24 - event: PlacedBet(address,uint256,bool)25 handler: handleNewBet26 file: ./src/mapping.tsПоказати всеСхема (schema.graphql)
Схема — це визначення даних GraphQL. Вона дозволить вам визначити, які сутності існують та їхні типи. Підтримувані типи від The Graph:
- Байти
- ID
- String
- Boolean
- Int
- BigInt
- BigDecimal
Ви також можете використовувати сутності як тип для визначення зв’язків. У нашому прикладі ми визначаємо зв’язок «один-до-багатьох» від гравця до ставок. Символ ! означає, що значення не може бути порожнім. Повну документацію можна переглянути тутopens in a new tab.
1type Bet @entity {2 id: ID!3 player: Player!4 playerHasWon: Boolean!5 time: Int!6}78type Player @entity {9 id: ID!10 totalPlayedCount: Int11 hasWonCount: Int12 hasLostCount: Int13 bets: [Bet]!14}Показати всеВідображення (mapping.ts)
Файл відображення в The Graph визначає наші функції, які перетворюють вхідні події на сутності. Він написаний на AssemblyScript, підмножині TypeScript. Це означає, що його можна скомпілювати у WASM (WebAssembly) для більш ефективного та портативного виконання відображення.
Вам потрібно буде визначити кожну функцію, названу у файлі subgraph.yaml, тож у нашому випадку нам потрібна лише одна: handleNewBet. Спочатку ми намагаємося завантажити сутність Player з адреси відправника як id. Якщо її не існує, ми створюємо нову сутність і заповнюємо її початковими значеннями.
Потім ми створюємо нову сутність Bet. Ідентифікатором для цього буде event.transaction.hash.toHex() + "-" + event.logIndex.toString(), що завжди забезпечує унікальне значення. Використання лише хешу недостатньо, оскільки хтось може викликати функцію placeBet кілька разів за одну транзакцію через смартконтракт.
Нарешті, ми можемо оновити сутність Player, додавши всі дані. Масиви не можна доповнювати напряму, їх потрібно оновлювати, як показано тут. Ми використовуємо id для посилання на ставку. І .save() потрібен наприкінці, щоб зберегти сутність.
Повну документацію можна переглянути тут: https://thegraph.com/docs/en/developing/creating-a-subgraph/#writing-mappingsopens in a new tab. Ви також можете додати вивід журналу у файл відображення, див. тутopens in a new tab.
1import { Bet, Player } from "../generated/schema"2import { PlacedBet } from "../generated/GameContract/GameContract"34export function handleNewBet(event: PlacedBet): void {5 let player = Player.load(event.transaction.from.toHex())67 if (player == null) {8 // створити, якщо ще не існує9 player = new Player(event.transaction.from.toHex())10 player.bets = new Array<string>(0)11 player.totalPlayedCount = 012 player.hasWonCount = 013 player.hasLostCount = 014 }1516 let bet = new Bet(17 event.transaction.hash.toHex() + "-" + event.logIndex.toString()18 )19 bet.player = player.id20 bet.playerHasWon = event.params.hasWon21 bet.time = event.block.timestamp22 bet.save()2324 player.totalPlayedCount++25 if (event.params.hasWon) {26 player.hasWonCount++27 } else {28 player.hasLostCount++29 }3031 // оновити масив таким чином32 let bets = player.bets33 bets.push(bet.id)34 player.bets = bets3536 player.save()37}Показати всеВикористання у фронтенді
Використовуючи щось на зразок Apollo Boost, ви можете легко інтегрувати The Graph у свій React dapp (або Apollo-Vue). Особливо при використанні хуків React і Apollo, отримання даних є таким же простим, як написання одного запиту GraphQL у вашому компоненті. Типове налаштування може виглядати так:
1// Переглянути всі субграфи: https://thegraph.com/explorer/2const client = new ApolloClient({3 uri: "{{ subgraphUrl }}",4})56ReactDOM.render(7 <ApolloProvider client={client}>8 <App />9 </ApolloProvider>,10 document.getElementById("root")11)Показати всеА тепер ми можемо написати, наприклад, такий запит. Це дозволить нам отримати
- скільки разів виграв поточний користувач
- скільки разів програв поточний користувач
- список часових міток з усіма його попередніми ставками
Усе в одному запиті до сервера GraphQL.
1const myGraphQlQuery = gql`2 players(where: { id: $currentUser }) {3 totalPlayedCount4 hasWonCount5 hasLostCount6 bets {7 time8 }9 }10`1112const { loading, error, data } = useQuery(myGraphQlQuery)1314React.useEffect(() => {15 if (!loading && !error && data) {16 console.log({ data })17 }18}, [loading, error, data])Показати всеАле нам не вистачає останнього шматочка пазла, і це сервер. Ви можете або запустити його самостійно, або використовувати розміщений сервіс.
Сервер The Graph
Graph Explorer: розміщений сервіс
Найпростіший спосіб — використовувати розміщений сервіс. Дотримуйтесь інструкцій тутopens in a new tab, щоб розгорнути субграф. Для багатьох проєктів ви можете знайти існуючі субграфи в провідникуopens in a new tab.
Запуск власного вузла
Крім того, ви можете запустити власний вузол. Документація тутopens in a new tab. Однією з причин для цього може бути використання мережі, яка не підтримується розміщеним сервісом. Поточні підтримувані мережі можна знайти тутopens in a new tab.
Децентралізоване майбутнє
GraphQL також підтримує потоки для нових вхідних подій. Вони підтримуються в The Graph через Substreamsopens in a new tab, які зараз перебувають у відкритому бета-тестуванні.
У 2021opens in a new tab році The Graph розпочав перехід до децентралізованої мережі індексації. Ви можете прочитати більше про архітектуру цієї децентралізованої мережі індексації тутopens in a new tab.
Два ключові аспекти:
- Користувачі платять індексаторам за запити.
- Індексатори роблять ставки в токенах Graph (GRT).
Останні оновлення сторінки: 24 червня 2025 р.






