The Graph: Web3のデータクエリの改善
今回は、昨年分散型アプリケーション (dapp) 開発の標準スタックの一部となったThe Graphについて詳しく見ていきます。まずは、従来の方法でどのように行うかを見てみましょう。
The Graphを使用しない場合...
説明のために簡単な例を見てみましょう。誰もがゲームを好きなので、ユーザーが賭けをする簡単なゲームを想像してみてください。
pragma solidity 0.7.1;
contract Game {
uint256 totalGamesPlayerWon = 0;
uint256 totalGamesPlayerLost = 0;
event BetPlaced(address player, uint256 value, bool hasWon);
function placeBet() external payable {
bool hasWon = evaluateBetForPlayer(msg.sender);
if (hasWon) {
(bool success, ) = msg.sender.call{ value: msg.value * 2 }('');
require(success, "Transfer failed");
totalGamesPlayerWon++;
} else {
totalGamesPlayerLost++;
}
emit BetPlaced(msg.sender, msg.value, hasWon);
}
}
さて、このdappで、合計の賭け金、負け/勝ちの合計ゲーム数を表示し、誰かが再びプレイするたびにそれを更新したいとします。そのアプローチは次のようになります。
totalGamesPlayerWonを取得する。totalGamesPlayerLostを取得する。BetPlacedイベントをサブスクライブする。
右に示されているように、Web3でイベント (opens in a new tab)をリッスンすることはできますが、かなり多くのケースを処理する必要があります。
GameContract.events.BetPlaced({
fromBlock: 0
}, function(error, event) { console.log(event); })
.on('data', function(event) {
// イベント発火
})
.on('changed', function(event) {
// イベントが再度削除された
})
.on('error', function(error, receipt) {
// トランザクションが拒否された
});
この簡単な例であれば、まだ何とかなります。しかし、現在のプレイヤーの負け/勝ちの賭け金のみを表示したいとします。残念ながら、それらの値を保存する新しいコントラクトをデプロイして取得するしかありません。さらに複雑なスマート・コントラクトとdappを想像してみてください。事態はすぐに収拾がつかなくなる可能性があります。
これが最適ではない理由は以下の通りです。
- すでにデプロイされたコントラクトでは機能しない。
- それらの値を保存するための追加のガスコストがかかる。
- イーサリアムのノードからデータを取得するために別の呼び出しが必要になる。
では、より良い解決策を見てみましょう。
GraphQLの紹介
まず、フェイスブックによって最初に設計および実装されたGraphQLについて説明します。従来のREST APIモデルには馴染みがあるかもしれません。代わりに、必要なデータだけを正確に取得するクエリを記述できると想像してみてください。
この2つの画像は、GraphQLの本質をよく表しています。右側のクエリを使用すると、必要なデータを正確に定義できるため、1回のリクエストですべてを取得でき、必要なもの以外は取得しません。GraphQLサーバーが必要なすべてのデータの取得を処理するため、フロントエンドのコンシューマー側にとって非常に使いやすくなっています。興味がある場合は、サーバーがクエリを正確に処理する方法についてこちらでわかりやすく説明されています (opens in a new tab)。
この知識を踏まえて、いよいよブロックチェーンの領域とThe Graphについて見ていきましょう。
The Graphとは?
ブロックチェーンは分散型データベースですが、通常とは異なり、このデータベース用のクエリ言語がありません。データを取得するための解決策は苦痛を伴うか、完全に不可能です。The Graphは、ブロックチェーンデータのインデックス作成とクエリを行うための分散型プロトコルです。ご想像の通り、クエリ言語としてGraphQLを使用しています。
何かを理解するには例を見るのが一番なので、GameContractの例でThe Graphを使ってみましょう。
サブグラフの作成方法
データのインデックス作成方法の定義はサブグラフと呼ばれます。これには3つのコンポーネントが必要です。
- マニフェスト (
subgraph.yaml) - スキーマ (
schema.graphql) - マッピング (
mapping.ts)
マニフェスト (subgraph.yaml)
マニフェストは設定ファイルであり、以下を定義します。
- インデックスを作成するスマート・コントラクト (アドレス、ネットワーク、ABIなど)
- リッスンするイベント
- 関数呼び出しやブロックなど、リッスンするその他の要素
- 呼び出されるマッピング関数 (以下の
mapping.tsを参照)
ここでは複数のコントラクトとハンドラーを定義できます。一般的なセットアップでは、Hardhatプロジェクト内に独自のリポジトリを持つサブグラフフォルダーを配置します。これにより、ABIを簡単に参照できます。
利便性のために、mustacheのようなテンプレートツールを使用することもできます。その場合、subgraph.template.yaml を作成し、最新のデプロイに基づいてアドレスを挿入します。より高度なセットアップ例については、アーベのサブグラフリポジトリ (opens in a new tab)などを参照してください。
完全なドキュメントはこちら (opens in a new tab)で確認できます。
specVersion: 0.0.1
description: Placing Bets on Ethereum
repository: - GitHub link -
schema:
file: ./schema.graphql
dataSources:
- kind: ethereum/contract
name: GameContract
network: mainnet
source:
address: '0x2E6454...cf77eC'
abi: GameContract
startBlock: 6175244
mapping:
kind: ethereum/events
apiVersion: 0.0.1
language: wasm/assemblyscript
entities:
- GameContract
abis:
- name: GameContract
file: ../build/contracts/GameContract.json
eventHandlers:
- event: PlacedBet(address,uint256,bool)
handler: handleNewBet
file: ./src/mapping.ts
スキーマ (schema.graphql)
スキーマはGraphQLのデータ定義です。これにより、存在するエンティティとその型を定義できます。The Graphでサポートされている型は以下の通りです。
- Bytes
- ID
- String
- Boolean
- Int
- BigInt
- BigDecimal
エンティティを型として使用して、関係を定義することもできます。この例では、プレイヤーから賭けへの1対多の関係を定義しています。! は値が空であってはならないことを意味します。完全なドキュメントはこちら (opens in a new tab)で確認できます。
type Bet @entity {
id: ID!
player: Player!
playerHasWon: Boolean!
time: Int!
}
type Player @entity {
id: ID!
totalPlayedCount: Int
hasWonCount: Int
hasLostCount: Int
bets: [Bet]!
}
マッピング (mapping.ts)
The Graphのマッピングファイルは、受信したイベントをエンティティに変換する関数を定義します。これはTypeScriptのサブセットであるAssemblyScriptで記述されています。つまり、マッピングをより効率的かつポータブルに実行するために、WASM (WebAssembly) にコンパイルできます。
subgraph.yaml ファイルで指定された各関数を定義する必要があります。この例では、handleNewBet の1つだけが必要です。まず、送信者のアドレスをIDとしてPlayerエンティティを読み込もうとします。存在しない場合は、新しいエンティティを作成し、初期値を入力します。
次に、新しいBetエンティティを作成します。このIDは event.transaction.hash.toHex() + "-" + event.logIndex.toString() となり、常に一意の値になるようにします。スマート・コントラクトを介して1つのトランザクション内でplaceBet関数を複数回呼び出す可能性があるため、ハッシュだけを使用するのでは不十分です。
最後に、すべてのデータでPlayerエンティティを更新できます。配列は直接プッシュすることはできず、ここに示すように更新する必要があります。IDを使用して賭けを参照します。そして、エンティティを保存するには、最後に .save() が必要です。
完全なドキュメントはこちらで確認できます: https://thegraph.com/docs/en/developing/creating-a-subgraph/#writing-mappings。マッピングファイルにログ出力を追加することもできます。[こちら (opens in a new tab)](https://thegraph.com/docs/en/subgraphs/developing/creating/graph-ts/api/#api-reference)を参照してください。 (opens in a new tab)
import { Bet, Player } from "../generated/schema"
import { PlacedBet } from "../generated/GameContract/GameContract"
export function handleNewBet(event: PlacedBet): void {
let player = Player.load(event.transaction.from.toHex())
if (player == null) {
// まだ存在しない場合は作成する
player = new Player(event.transaction.from.toHex())
player.bets = new Array<string>(0)
player.totalPlayedCount = 0
player.hasWonCount = 0
player.hasLostCount = 0
}
let bet = new Bet(
event.transaction.hash.toHex() + "-" + event.logIndex.toString()
)
bet.player = player.id
bet.playerHasWon = event.params.hasWon
bet.time = event.block.timestamp
bet.save()
player.totalPlayedCount++
if (event.params.hasWon) {
player.hasWonCount++
} else {
player.hasLostCount++
}
// このように配列を更新する
let bets = player.bets
bets.push(bet.id)
player.bets = bets
player.save()
}
フロントエンドでの使用
Apollo Boostなどを使用すると、Reactのdapp (またはApollo-Vue) にThe Graphを簡単に統合できます。特にReactフックとApolloを使用する場合、データの取得はコンポーネント内に単一のGraphQLクエリを記述するのと同じくらい簡単です。一般的なセットアップは次のようになります。
// すべてのサブグラフを参照: https://thegraph.com/explorer/
const client = new ApolloClient({
uri: "{{ subgraphUrl }}",
})
ReactDOM.render(
<ApolloProvider client={client}>
<App />
</ApolloProvider>,
document.getElementById("root")
)
これで、例えば次のようなクエリを記述できます。これにより、以下の情報が取得されます。
- 現在のユーザーが勝った回数
- 現在のユーザーが負けた回数
- 過去のすべての賭けのタイムスタンプのリスト
これらすべてをGraphQLサーバーへの1回のリクエストで取得できます。
const myGraphQlQuery = gql`
players(where: { id: $currentUser }) {
totalPlayedCount
hasWonCount
hasLostCount
bets {
time
}
}
`
const { loading, error, data } = useQuery(myGraphQlQuery)
React.useEffect(() => {
if (!loading && !error && data) {
console.log({ data })
}
}, [loading, error, data])
しかし、パズルの最後のピースが1つ欠けています。それはサーバーです。自分で実行するか、ホスト型サービスを使用することができます。
The Graphサーバー
Graph Explorer: ホスト型サービス
最も簡単な方法は、ホスト型サービスを使用することです。こちら (opens in a new tab)の手順に従ってサブグラフをデプロイします。多くのプロジェクトでは、実際にエクスプローラー (opens in a new tab)で既存のサブグラフを見つけることができます。
独自のノードの実行
あるいは、独自のノードを実行することもできます。ドキュメントはこちら (opens in a new tab)です。これを行う理由の1つは、ホスト型サービスでサポートされていないネットワークを使用することかもしれません。現在サポートされているネットワークはこちらで確認できます (opens in a new tab)。
分散型の未来
GraphQLは、新しく受信したイベントのストリームもサポートしています。これらは、現在オープンベータ版であるSubstreams (opens in a new tab)を通じてThe Graph上でサポートされています。
2021年 (opens in a new tab)に、The Graphは分散型インデックスネットワークへの移行を開始しました。この分散型インデックスネットワークのアーキテクチャについて詳しくは、こちら (opens in a new tab)をご覧ください。
2つの重要な側面は以下の通りです。
- ユーザーはクエリに対してインデクサーに支払う。
- インデクサーはGraph Token (GRT) をステークする。






