Перейти до основного контенту

The Graph: вирішення проблеми запитів даних Web3

мова програмування
Смарт-контракти
запити
the graph
реагування
Середнячок
Markus Waas
6 вересня 2020 р.
7 читається за хвилину

Цього разу ми детальніше розглянемо The Graph, який по суті став частиною стандартного стеку для розробки dapp за останній рік. Спочатку давайте подивимося, як це робиться традиційним способом...

Без The Graph...

Отже, давайте розглянемо простий приклад для ілюстрації. Ми всі любимо ігри, тому уявімо просту гру, в якій користувачі роблять ставки:

1pragma solidity 0.7.1;
2
3contract Game {
4 uint256 totalGamesPlayerWon = 0;
5 uint256 totalGamesPlayerLost = 0;
6 event BetPlaced(address player, uint256 value, bool hasWon);
7
8 function placeBet() external payable {
9 bool hasWon = evaluateBetForPlayer(msg.sender);
10
11 if (hasWon) {
12 (bool success, ) = msg.sender.call{ value: msg.value * 2 }('');
13 require(success, "Transfer failed");
14 totalGamesPlayerWon++;
15 } else {
16 totalGamesPlayerLost++;
17 }
18
19 emit BetPlaced(msg.sender, msg.value, hasWon);
20 }
21}
Показати все

Тепер припустимо, що в нашому dapp ми хочемо відображати загальну кількість ставок, загальну кількість програних/виграних ігор, а також оновлювати ці дані, щоразу, коли хтось грає знову. Підхід буде таким:

  1. Отримати totalGamesPlayerWon.
  2. Отримати totalGamesPlayerLost.
  3. Підписатися на події BetPlaced.

Ми можемо прослуховувати подію у Web3opens in a new tab, як показано праворуч, але це вимагає обробки чималої кількості випадків.

1GameContract.events.BetPlaced({
2 fromBlock: 0
3}, 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 API проти REST API

Ці два зображення практично передають суть GraphQL. За допомогою запиту праворуч ми можемо точно визначити, які дані нам потрібні, тож ми отримуємо все за один запит і нічого більше, крім того, що нам потрібно. Сервер GraphQL обробляє отримання всіх необхідних даних, тому його неймовірно легко використовувати на стороні клієнта (фронтенду). Ось гарне поясненняopens in a new tab того, як саме сервер обробляє запит, якщо вам цікаво.

Тепер, маючи ці знання, давайте нарешті зануримося у світ блокчейну та The Graph.

Що таке The Graph?

Блокчейн — це децентралізована база даних, але, на відміну від звичайних баз даних, у нас немає мови запитів для неї. Рішення для отримання даних є складними або взагалі неможливими. The Graph — це децентралізований протокол для індексування та запитів до даних блокчейну. І, як ви могли здогадатися, він використовує GraphQL як мову запитів.

The Graph

Приклади — це завжди найкращий спосіб щось зрозуміти, тож давайте використаємо The Graph для нашого прикладу з GameContract.

Як створити Subgraph

Визначення способу індексування даних називається субграфом (subgraph). Він потребує трьох компонентів:

  1. Маніфест (subgraph.yaml)
  2. Схема (schema.graphql)
  3. Відображення (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.1
2description: Placing Bets on Ethereum
3repository: - GitHub link -
4schema:
5 file: ./schema.graphql
6dataSources:
7 - kind: ethereum/contract
8 name: GameContract
9 network: mainnet
10 source:
11 address: '0x2E6454...cf77eC'
12 abi: GameContract
13 startBlock: 6175244
14 mapping:
15 kind: ethereum/events
16 apiVersion: 0.0.1
17 language: wasm/assemblyscript
18 entities:
19 - GameContract
20 abis:
21 - name: GameContract
22 file: ../build/contracts/GameContract.json
23 eventHandlers:
24 - event: PlacedBet(address,uint256,bool)
25 handler: handleNewBet
26 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}
7
8type Player @entity {
9 id: ID!
10 totalPlayedCount: Int
11 hasWonCount: Int
12 hasLostCount: Int
13 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"
3
4export function handleNewBet(event: PlacedBet): void {
5 let player = Player.load(event.transaction.from.toHex())
6
7 if (player == null) {
8 // створити, якщо ще не існує
9 player = new Player(event.transaction.from.toHex())
10 player.bets = new Array<string>(0)
11 player.totalPlayedCount = 0
12 player.hasWonCount = 0
13 player.hasLostCount = 0
14 }
15
16 let bet = new Bet(
17 event.transaction.hash.toHex() + "-" + event.logIndex.toString()
18 )
19 bet.player = player.id
20 bet.playerHasWon = event.params.hasWon
21 bet.time = event.block.timestamp
22 bet.save()
23
24 player.totalPlayedCount++
25 if (event.params.hasWon) {
26 player.hasWonCount++
27 } else {
28 player.hasLostCount++
29 }
30
31 // оновити масив таким чином
32 let bets = player.bets
33 bets.push(bet.id)
34 player.bets = bets
35
36 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})
5
6ReactDOM.render(
7 <ApolloProvider client={client}>
8 <App />
9 </ApolloProvider>,
10 document.getElementById("root")
11)
Показати все

А тепер ми можемо написати, наприклад, такий запит. Це дозволить нам отримати

  • скільки разів виграв поточний користувач
  • скільки разів програв поточний користувач
  • список часових міток з усіма його попередніми ставками

Усе в одному запиті до сервера GraphQL.

1const myGraphQlQuery = gql`
2 players(where: { id: $currentUser }) {
3 totalPlayedCount
4 hasWonCount
5 hasLostCount
6 bets {
7 time
8 }
9 }
10`
11
12const { loading, error, data } = useQuery(myGraphQlQuery)
13
14React.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.

The Graph Explorer

Запуск власного вузла

Крім того, ви можете запустити власний вузол. Документація тут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.

Два ключові аспекти:

  1. Користувачі платять індексаторам за запити.
  2. Індексатори роблять ставки в токенах Graph (GRT).

Останні оновлення сторінки: 24 червня 2025 р.

Чи була ця інструкція корисною?