メインコンテンツへスキップ

The Graph: Web3データクエリ問題を解決

Solidityスマートコントラクトクエリthe graphcreate-eth-appreact
中級
Markus Waas
soliditydeveloper.com(opens in a new tab)
2020年9月6日
13 分の読書 minute read

今回は、The Graphについて詳しく見ていきます。The Grashは昨年、分散型アプリケーション(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イベントのサブスクライブ

右に示したようにWeb3イベント(opens in a new tab)をリッスンできますが、多くのケースを処理する必要があります。

1GameContract.events.BetPlaced({
2 fromBlock: 0
3}, function(error, event) { console.log(event); })
4.on('data', function(event) {
5 // event fired
6})
7.on('changed', function(event) {
8 // event was removed again
9})
10.on('error', function(error, receipt) {
11 // tx rejected
12});
すべて表示
コピー

ここでの簡単な例では、これはまだある程度は大丈夫のようです。 しかし今度は、現在のプレイヤーが賭けで失った金額と獲得した金額を表示したいとしましょう。 こうなると、運が悪いとしか言えません。これらの値の格納や取得を行う新しいコントラクトをデプロイした方が良いでしょう。 では、さらに複雑なスマートコントラクトと分散型アプリケーション(Dapp)を想像してみてください。あっという間に厄介な状況になります。

単純なクエリではない

これが最適でないことは次のことからわかります。

  • すでにデプロイ済みのコントラクトでは機能しないこと
  • これらの値を格納するのに追加のガス代がかかること
  • イーサリアムノードのデータを取得するのに別の呼び出しが必要なこと

不十分

では、より良い解決策を見ていきましょう。

GraphQLの紹介

最初にGraphQLについて説明します。GraphQLは、もともとフェイスブック社によって設計され、実装されました。 従来のRest APIモデルについては、ご存知かもしれません。 では、今度は次のように必要なデータを正確に取得できるクエリを作成できると想像してみてください。

GraphQL APIとREST APIの比較

(opens in a new tab)

2つの画像は、GraphQLの本質をほぼ捉えています。 右のクエリーでは、必要なデータを正確に定義できるので、1回のリクエストで必要なものだけを取得できます。 GraphQLサーバーは必要とされるすべてのデータの取得を処理できるので、フロントエンドのコンシューマ側にとっては極めて使いやすいツールとなっています。 ご興味があれば、サーバーが具体的にどのようにクエリを処理するかについてわかりやすい説明(opens in a new tab)をご覧ください。

この知識をもとに、ブロックチェーン空間とThe Graphの世界に入って行きましょう。

The Graphとは

ブロックチェーンは、分散型データベースですが、通常のデータベースとは対照的に、データベースに対するクエリ言語がありません。 データを取得することにおいては、苦痛を伴うか不可能かのどちらかです。 The Graphは、ブロックチェーンデータのインデックス作成とクエリを行うための分散型プロトコルです。 ご想像の通りThe Graphは、GraphQLをクエリ言語として使用しています。

The Graph

何かを理解するには例を見るのが最善なので、先ほどのGameContractでThe Graphを使ってみましょう。

サブグラフの作成方法

サブグラフは、データにインデックスを作成する方法を定義するものです。 定義には、次の3つのコンポーネントが必要です。

  1. マニフェスト(subgraph.yaml)
  2. スキーマ(schema.graphql)
  3. マッピング(mapping.ts)

マニフェスト(subgraph.yaml)

マニフェストは設定ファイルであり、次のことを定義します。

  • どのスマートコントラクトにインデックスを作成するか(アドレス、ネットワーク、アプリケーションバイナリインターフェース(ABI)等)
  • どのイベントをリッスンするか
  • 関数呼び出しやブロックなど、その他に何をリッスンするか
  • 呼び出されるマッピング関数 (後述のmapping.tsを参照)

マニフェストには複数のコントラクトとハンドラを定義できます。 典型的な設定では、TruffleまたはHardhatプロジェクト内にサブグラフフォルダと独自のリポジトリがあります。 それにより、簡単にアプリケーションバイナリインターフェース(ABI)を参照することができます。

便利さの観点から、Mustacheのようなテンプレートツールを使用することもできます。 subgraph.template.yamlを作成し、最新のデプロイメントに基づいたアドレスを挿入します。 より高度な設定例については、Aaveサブグラフリポジトリ(opens 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型
  • 文字列型
  • ブール型
  • 整数型
  • BigInt型
  • BigDecimal型

リレーションシップを定義するために、エンティティをタイプとして使用することもできます。 この例では、プレイヤーと賭け(Bet)で1対多のリレーションシップを定義します。 「!」 は、空の値を取れないこと意味します。 完全なドキュメントは、こちら(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のマッピングファイルは、受信したイベントをエンティティに変換する関数を定義します。 TypescriptのサブセットであるAssemblyScriptで書きます。 これは、より効率化され、よりポータブル化されたマッピングの実行を実現するため、WebAssembly(WASM)にコンパイルされます。

各関数をsubgraph.yamlファイルに定義する必要があります。この例では、handleNewBetの一つだけが必要です。 まず、idとして送信者アドレスからPlayerエンティティを読み込もうとします。 存在しない場合は、新しいエンティティを作成して開始値を入れます。

次に、Betエンティティを作成します。 idは、event.transaction.hash.toHex() + "-" + event.logIndex.toString()になり、常に一意の値になります。 誰かがスマートコントラクトを介して1つのトランザクションでplaceBet関数を複数回呼び出す可能性があるため、ハッシュのみの使用では十分ではありません。

最後に、すべてのデータでPlayerエンティティを更新します。 配列を直接プッシュすることはできませんが、ここに示すように更新する必要があります。 betを参照するためにidを使用します。 エンティティを保存するには、.save()が最後に必要です。

ドキュメント全文については、こちらをご覧ください。https://thegraph.com/docs/en/developing/creating-a-subgraph/#writing-mappings(opens 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 // create if doesn't exist yet
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 // update array like this
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)の分散型アプリケーション(Dapp)に簡単に統合できます。 特にReactフックとApolloを使用する場合は、コンポーネントに単一のGraphQLクエリを記述するのと同じくらいデータの取得が簡単です。 典型的な設定は次のようになります。

1// See all subgraphs: 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])
すべて表示

マジック

しかし、最後のパズルの1つが欠けています。それがサーバーについてです。 自分のノードでサーバーを実行することも、ホストサービスを使用することもできます。

The Graphサーバー

Graph エクスプローラー: ホストサービス

最も簡単な方法は、ホストサービスを利用することです。 こちらの手順(opens in a new tab)に従ってサブグラフをデプロイしてください。 エクスプローラー(opens in a new tab)では、さまざまなプロジェクト向けに既存のサブグラフを探すことができます。

The Graphエクスプローラー

自分のノードで実行

自分のノードでも実行できます。 実行方法については、こちら(opens in a new tab)のドキュメントをご覧ください。 これにより、ホストサービスでサポートされていないネットワークでも使用できます。 現在サポートしているネットワークについては、こちら(opens in a new tab)をご覧ください。

非中央集権型の未来

GraphQLは、新しく受信するイベントのストリームもサポートしています。 これらの機能は、現在オープンベータ版のSubstreams(opens in a new tab)を通して、グラフ上でサポートされています。

2021(opens in a new tab)年に、The Graphは分散型インデックスネットワークへの移行を開始しました。 分散型インデックスネットワークのアーキテクチャの詳細については、こちら(opens in a new tab)をご覧ください。

次の2つの重要な点があります。

  1. ユーザーは、クエリのインデックス作成者に料金を支払う。
  2. インデックス作成者は、グラフトークン(GRT)をステーキングする。

最終編集者: @HiroyukiNaito(opens in a new tab), 2024年1月18日

このチュートリアルは役に立ちましたか?