Перейти к основному содержанию

The Graph: Исправление запросов данных в Web3

Solidity
Умные контракты
запросы
the graph
react
Intermediate
Markus Waas
6 сентября 2020 г.
7 минута прочтения

На этот раз мы подробнее рассмотрим The Graph, который за последний год по сути стал частью стандартного стека для разработки децентрализованных приложений. Давайте сначала посмотрим, как бы мы делали это традиционным способом...

Без 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, "Перевод не удался");
14 totalGamesPlayerWon++;
15 } else {
16 totalGamesPlayerLost++;
17 }
18
19 emit BetPlaced(msg.sender, msg.value, hasWon);
20 }
21}
Показать все

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

  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});
Показать все

Для нашего простого примера это все еще приемлемо. Но предположим, что мы хотим отображать суммы проигранных/выигранных ставок только для текущего игрока. Что ж, нам не повезло, вам лучше развернуть новый контракт, который хранит эти значения, и получать их. А теперь представьте себе гораздо более сложный смарт-контракт и децентрализованное приложение, все может быстро запутаться.

Нельзя просто так взять и сделать запрос

Вы можете видеть, что это не оптимально:

  • Не работает для уже развернутых контрактов.
  • Дополнительные расходы на газ для хранения этих значений.
  • Требуется еще один вызов для получения данных для узла 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.

Как создать подграф

Определение того, как индексировать данные, называется подграфом. Для этого требуются три компонента:

  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: Размещение ставок в Ethereum
3repository: - Ссылка на GitHub -
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 (или 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 также поддерживает потоки для новых входящих событий. Они поддерживаются на графе через Substreamsopens in a new tab, которые в настоящее время находятся в стадии открытого бета-тестирования.

В 2021 годуopens in a new tab The Graph начал свой переход к децентрализованной сети индексации. Подробнее об архитектуре этой децентрализованной сети индексации можно прочитать здесьopens in a new tab.

Два ключевых аспекта:

  1. Пользователи платят индексаторам за запросы.
  2. Индексаторы размещают в стейкинге токены Graph (GRT).

Последнее обновление страницы: 24 июня 2025 г.

Было ли это руководство полезным?