The Graph: Web3データクエリ問題を解決
今回は、The Graphについて詳しく見ていきます。The Grashは昨年、分散型アプリケーション(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
イベントのサブスクライブ
右に示したようにWeb3イベント(opens in a new tab)をリッスンできますが、多くのケースを処理する必要があります。
1GameContract.events.BetPlaced({2 fromBlock: 03}, function(error, event) { console.log(event); })4.on('data', function(event) {5 // event fired6})7.on('changed', function(event) {8 // event was removed again9})10.on('error', function(error, receipt) {11 // tx rejected12});すべて表示コピー
ここでの簡単な例では、これはまだある程度は大丈夫のようです。 しかし今度は、現在のプレイヤーが賭けで失った金額と獲得した金額を表示したいとしましょう。 こうなると、運が悪いとしか言えません。これらの値の格納や取得を行う新しいコントラクトをデプロイした方が良いでしょう。 では、さらに複雑なスマートコントラクトと分散型アプリケーション(Dapp)を想像してみてください。あっという間に厄介な状況になります。
これが最適でないことは次のことからわかります。
- すでにデプロイ済みのコントラクトでは機能しないこと
- これらの値を格納するのに追加のガス代がかかること
- イーサリアムノードのデータを取得するのに別の呼び出しが必要なこと
では、より良い解決策を見ていきましょう。
GraphQLの紹介
最初にGraphQLについて説明します。GraphQLは、もともとフェイスブック社によって設計され、実装されました。 従来のRest APIモデルについては、ご存知かもしれません。 では、今度は次のように必要なデータを正確に取得できるクエリを作成できると想像してみてください。
2つの画像は、GraphQLの本質をほぼ捉えています。 右のクエリーでは、必要なデータを正確に定義できるので、1回のリクエストで必要なものだけを取得できます。 GraphQLサーバーは必要とされるすべてのデータの取得を処理できるので、フロントエンドのコンシューマ側にとっては極めて使いやすいツールとなっています。 ご興味があれば、サーバーが具体的にどのようにクエリを処理するかについてわかりやすい説明(opens in a new tab)をご覧ください。
この知識をもとに、ブロックチェーン空間とThe Graphの世界に入って行きましょう。
The Graphとは
ブロックチェーンは、分散型データベースですが、通常のデータベースとは対照的に、データベースに対するクエリ言語がありません。 データを取得することにおいては、苦痛を伴うか不可能かのどちらかです。 The Graphは、ブロックチェーンデータのインデックス作成とクエリを行うための分散型プロトコルです。 ご想像の通りThe Graphは、GraphQLをクエリ言語として使用しています。
何かを理解するには例を見るのが最善なので、先ほどのGameContractでThe Graphを使ってみましょう。
サブグラフの作成方法
サブグラフは、データにインデックスを作成する方法を定義するものです。 定義には、次の3つのコンポーネントが必要です。
- マニフェスト(
subgraph.yaml
) - スキーマ(
schema.graphql
) - マッピング(
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.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型
- 文字列型
- ブール型
- 整数型
- 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}78type Player @entity {9 id: ID!10 totalPlayedCount: Int11 hasWonCount: Int12 hasLostCount: Int13 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"34export function handleNewBet(event: PlacedBet): void {5 let player = Player.load(event.transaction.from.toHex())67 if (player == null) {8 // create if doesn't exist yet9 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 // update array like this32 let bets = player.bets33 bets.push(bet.id)34 player.bets = bets3536 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})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])すべて表示
しかし、最後のパズルの1つが欠けています。それがサーバーについてです。 自分のノードでサーバーを実行することも、ホストサービスを使用することもできます。
The Graphサーバー
Graph エクスプローラー: ホストサービス
最も簡単な方法は、ホストサービスを利用することです。 こちらの手順(opens in a new tab)に従ってサブグラフをデプロイしてください。 エクスプローラー(opens in a new tab)では、さまざまなプロジェクト向けに既存のサブグラフを探すことができます。
自分のノードで実行
自分のノードでも実行できます。 実行方法については、こちら(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つの重要な点があります。
- ユーザーは、クエリのインデックス作成者に料金を支払う。
- インデックス作成者は、グラフトークン(GRT)をステーキングする。
最終編集者: @sumitvekariya(opens in a new tab), 2024年8月29日