跳至主要内容

The Graph:解決 Web3 資料查詢的問題

solidity
smart contracts
querying
the graph
react
中等
Markus Waas
2020年9月6日
12 分鐘閱讀

這次,我們將深入探討 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}
顯示全部

現在假設在我們的去中心化應用程式中,我們想要顯示總下注次數、輸贏的總場次,並且在有玩家再次遊玩時更新這些資訊。 做法會是:

  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 // 事件觸發
6})
7.on('changed', function(event) {
8 // 事件再次被移除
9})
10.on('error', function(error, receipt) {
11 // 交易被拒絕
12});
顯示全部

就我們這個簡單的範例而言,這樣做還算可以。 但假設我們現在只想顯示當前玩家輸/贏的下注總額。 這樣的話我們就沒輒了,你最好部署一份新合約來儲存並擷取那些數值。 現在想像一下更複雜的智能合約和去中心化應用程式,情況很快就會變得一團亂。

可不是簡簡單單就能查詢的

你可以看到為何這不是最佳做法:

  • 對已經部署的合約不管用。
  • 儲存這些數值會產生額外的 gas 費用。
  • 需要另一次呼叫才能從以太坊節點擷取資料。

那樣不夠好

現在讓我們來看看一個更好的解決方案。

為您介紹 GraphQL

首先我們來談談 GraphQL,它最初是由 Facebook 設計和實作的。 您可能熟悉傳統的 REST API 模型。 現在,想像一下,您可以編寫一個查詢,精準地取得您想要的資料:

GraphQL API 與 REST API 的比較

The Graph Playground 中 GraphQL 查詢的動畫演示

這兩張圖幾乎掌握了 GraphQL 的精髓。 透過右方的查詢,我們可以精確定義我們想要的資料,因此我們可以在一次請求中得到所有東西,而且不多不少,正好是我們需要的。 GraphQL 伺服器會處理所有必要資料的擷取,因此對於前端取用方來說,使用上非常簡單。 如果您有興趣,可以在這裡 (opens in a new tab)找到關於伺服器如何處理查詢的詳細說明。

了解了這些知識之後,讓我們終於可以進入區塊鏈領域和 The Graph 的世界了。

什麼是 The Graph?

區塊鏈是一種去中心化資料庫,但與通常情況不同的是,我們沒有用於此資料庫的查詢語言。 檢索資料的解決方案既痛苦又或者完全不可能。 The Graph 是一種用於索引和查詢區塊鏈資料的去中心化協定。 您可能已經猜到了,它使用 GraphQL 作為查詢語言。

The Graph

範例永遠是理解事物的最好方法,所以讓我們在 GameContract 範例中使用 The Graph。

如何建立子圖

關於如何索引資料的定義被稱為「子圖」。 它需要三個元件:

  1. 資訊清單 (subgraph.yaml)
  2. 結構 (schema.graphql)
  3. 映射 (mapping.ts)

資訊清單 (subgraph.yaml)

資訊清單是我們的設定檔,它定義了:

  • 要索引哪些智能合約(位址、網路、ABI......)
  • 要監聽哪些事件
  • 其他要監聽的東西,例如函式呼叫或區塊
  • 被呼叫的映射函式(請見下方的 mapping.ts

您可以在這裡定義多個合約和處理常式。 一個典型的設定,會在 Hardhat 專案中,有一個 subgraph 資料夾,並有自己的儲存庫。 然後您就可以輕易地引用 ABI。

為了方便起見,您可能也會想使用像是 mustache 這樣的樣板工具。 然後您可以建立一個 subgraph.template.yaml,並根據最新的部署插入位址。 如需更進階的設定範例,請參考 Aave 子圖儲存庫 (opens in a new tab)

完整的說明文件可以在這裡 (opens in a new tab)查看。

1specVersion: 0.0.1
2description: 在以太坊上下注
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
  • 字串
  • 布林值
  • 整數
  • 大整數
  • 大十進位數

您也可以使用實體作為型別來定義關聯。 在我們的範例中,我們定義了從玩家到下注的一對多關聯。 驚嘆號 (!) 代表該值不可為空。 完整的說明文件可以在這裡 (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 實體。 這個實體的 ID 會是 event.transaction.hash.toHex() + "-" + event.logIndex.toString(),以確保其值永遠是唯一的。 只使用哈希是不夠的,因為有人可能會透過一份智能合約,在單一筆交易中多次呼叫 placeBet 函式。

最後,我們可以用所有資料來更新 Player 實體。 陣列無法直接推送,而需要如此處所示來更新。 我們使用 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 // 如果還不存在就建立
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 hooks 和 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)的指示來部署子圖。 對於許多專案,您其實可以在 explorer (opens in a new tab) 中找到現有的子圖。

The Graph Explorer

執行你自己的節點

或者,您也可以執行您自己的節點。 文件在這裡 (opens in a new tab)。 這樣做的一個原因可能是,您使用的網路不受託管服務支援。 目前支援的網路可在此處找到 (opens in a new tab)

去中心化的未來

GraphQL 也支援用於新傳入事件的串流。 The Graph 透過 Substreams (opens in a new tab) 支援這些功能,目前正在公開測試中。

2021 年 (opens in a new tab),The Graph 開始轉型為去中心化索引網路。 您可以在此處 (opens in a new tab)閱讀更多關於此去中心化索引網路的架構。

兩個關鍵面向是:

  1. 使用者為查詢向索引者付費。
  2. 索引者質押 Graph 代幣 (GRT)。

頁面最後更新時間: 2026年2月26日

這個使用教學對你有幫助嗎?