The Graph:解決 Web3 資料查詢問題
這次我們將深入探討 The Graph,它在過去一年中基本上已成為開發去中心化應用程式 (dapp) 的標準技術堆疊之一。讓我們先看看傳統上我們會怎麼做……
如果沒有 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,情況很快就會變得一團糟。
你可以看出這並非最佳方案:
- 對已部署的合約無效。
- 儲存這些值需要額外的燃料 (gas) 成本。
- 需要對以太坊節點進行另一次呼叫來獲取資料。
現在讓我們來看一個更好的解決方案。
讓我向你介紹 GraphQL
首先我們來談談 GraphQL,它最初是由臉書 (Facebook) 設計和實作的。你可能對傳統的 REST API 模型很熟悉。現在想像一下,你可以精確地為你想要的資料撰寫查詢:
這兩張圖片幾乎捕捉了 GraphQL 的精髓。透過右邊的查詢,我們可以精確定義我們想要的資料,因此我們可以在一次請求中獲得所有內容,而且不多不少正是我們需要的。GraphQL 伺服器會處理所有所需資料的獲取,因此對於前端消費者來說非常容易使用。如果你有興趣,這裡有一個很好的解釋 (opens in a new tab),說明伺服器究竟是如何處理查詢的。
現在有了這些知識,讓我們終於進入區塊鏈領域和 The Graph。
什麼是 The Graph?
區塊鏈是一個去中心化的資料庫,但與通常情況相反,我們沒有用於這個資料庫的查詢語言。檢索資料的解決方案要麼令人痛苦,要麼完全不可能。The Graph 是一個用於索引和查詢區塊鏈資料的去中心化協定。你可能已經猜到了,它使用 GraphQL 作為查詢語言。
範例總是理解事物的最佳方式,所以讓我們將 The Graph 用於我們的 GameContract 範例。
如何建立子圖 (Subgraph)
定義如何索引資料的配置稱為子圖 (subgraph)。它需要三個元件:
- 清單 (Manifest) (
subgraph.yaml) - 結構描述 (Schema) (
schema.graphql) - 映射 (Mapping) (
mapping.ts)
清單 (Manifest) (subgraph.yaml)
清單是我們的設定檔,它定義了:
- 要索引哪些智能合約(地址、網路、ABI 等)
- 要監聽哪些事件
- 要監聽的其他事物,例如函式呼叫或區塊
- 被呼叫的映射函式(請參閱下方的
mapping.ts)
你可以在這裡定義多個合約和處理常式 (handler)。典型的設定是在 Hardhat 專案內有一個帶有自己儲存庫的 subgraph 資料夾。這樣你就可以輕鬆地參考 ABI。
為了方便起見,你可能還會想使用像 mustache 這樣的模板工具。然後你建立一個 subgraph.template.yaml,並根據最新的部署插入地址。如需更進階的範例設定,請參閱 Aave 子圖儲存庫 (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) (schema.graphql)
結構描述是 GraphQL 的資料定義。它將允許你定義存在哪些實體 (entities) 及其類型。The Graph 支援的類型有:
- Bytes
- ID
- String
- Boolean
- Int
- BigInt
- BigDecimal
你也可以使用實體作為類型來定義關聯性。在我們的範例中,我們定義了從玩家到下注的一對多關聯。! 表示該值不能為空。完整的說明文件可以在此 (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) (mapping.ts)
The Graph 中的映射檔定義了我們將傳入事件轉換為實體的函式。它是用 AssemblyScript(TypeScript 的一個子集)撰寫的。這意味著它可以被編譯成 WASM (WebAssembly),以便更有效率且可攜地執行映射。
你需要定義在 subgraph.yaml 檔案中命名的每個函式,所以在我們的例子中只需要一個:handleNewBet。我們首先嘗試從發送者地址作為 id 來載入 Player 實體。如果它不存在,我們就建立一個新實體並填入初始值。
然後我們建立一個新的 Bet 實體。它的 id 將會是 event.transaction.hash.toHex() + "-" + event.logIndex.toString(),以確保永遠是一個唯一值。僅使用雜湊 (hash) 是不夠的,因為有人可能會透過智能合約在一次交易中多次呼叫 placeBet 函式。
最後,我們可以使用所有資料來更新 Player 實體。陣列不能直接推送 (push),而是需要如這裡所示進行更新。我們使用 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 這樣的工具,你可以輕鬆地將 The Graph 整合到你的 React dapp(或 Apollo-Vue)中。特別是在使用 React hooks 和 Apollo 時,獲取資料就像在你的元件中撰寫單一 GraphQL 查詢一樣簡單。典型的設定可能如下所示:
// 查看所有子圖:https://thegraph.com/explorer/
const client = new ApolloClient({
uri: "{{ subgraphUrl }}",
})
ReactDOM.render(
<ApolloProvider client={client}>
<App />
</ApolloProvider>,
document.getElementById("root")
)
現在我們可以撰寫例如這樣的查詢。這將為我們獲取:
- 目前使用者贏了多少次
- 目前使用者輸了多少次
- 包含他所有過往記錄的時間戳記清單
所有這些都在對 GraphQL 伺服器的單次請求中完成。
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])
但我們還缺少最後一塊拼圖,那就是伺服器。你可以自己執行它,或者使用託管服務。
The Graph 伺服器
Graph 瀏覽器:託管服務
最簡單的方法是使用託管服務。按照這裡 (opens in a new tab)的指示來部署子圖。對於許多專案,你實際上可以在瀏覽器 (opens in a new tab)中找到現有的子圖。
執行你自己的節點
或者,你可以執行自己的節點。說明文件在這裡 (opens in a new tab)。這樣做的一個原因可能是使用了託管服務不支援的網路。目前支援的網路可以在這裡找到 (opens in a new tab)。
去中心化的未來
GraphQL 也支援針對新傳入事件的串流 (streams)。The Graph 透過目前處於公開測試階段的 Substreams (opens in a new tab) 來支援這些功能。
在 2021 年 (opens in a new tab),The Graph 開始過渡到去中心化的索引網路。你可以在此 (opens in a new tab)閱讀更多關於這個去中心化索引網路架構的資訊。
兩個關鍵方面是:
- 使用者向索引器 (indexer) 支付查詢費用。
- 索引器質押 Graph 代幣 (GRT)。






