跳转至主要内容

The Graph:修复 Web3 数据查询

Solidity
智能合同
查询中
the graph
react
中级
Markus Waas
2020年9月6日
12 分钟阅读

这次我们将仔细研究 The Graph,它在去年基本上成为了开发去中心化应用程序的标准堆栈的一部分。 我们先来看看传统方法的处理方式……

没有 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});
显示全部

现在,对于我们的简单示例来说,这在某种程度上还是不错的。 但是假设我们现在只想显示当前玩家输/赢的赌注金额。 嗯,我们运气不好,你最好部署一份新的合约来存储这些值,并将它们提取出来。 现在想象一个更复杂的智能合约和去中心化应用程序,事情可能很快就会变得一团糟。

One Does Not Simply Query

你可以看到这并不是最优的:

  • 不适用于已经部署的合约。
  • 存储这些值需要额外的燃料成本。
  • 需要另一次调用来获取以太坊节点的数据。

Thats not good enough

现在让我们看看更好的解决方案。

让我向你介绍 GraphQL

首先我们来谈谈 GraphQL,它最初是由 Facebook 设计和实现的。 你可能熟悉传统的 REST API 模型。 现在设想一下,你可以编写一个查询来精确查找你想要的数据:

GraphQL API vs. REST API

这两张图片基本上抓住了 GraphQL 的精髓。 通过右边的查询,我们可以精确地定义我们想要的数据,这样我们就可以在一个请求中得到所有的东西,而且只得到我们需要的东西。 GraphQL 服务器处理所有所需数据的获取,因此前端用户端使用起来非常简单。 如果你感兴趣,可以在这篇不错的文章中opens in a new tab了解服务器如何处理查询。

现在有了这些知识,让我们最终进入区块链空间和 The Graph。

什么是 The Graph?

区块链是一个去中心化数据库,但与通常情况不同的是,我们没有适用于这个数据库的查询语言。 检索数据的解决方案是痛苦的,或者是完全不可能的。 The Graph 是一种用于为区块链数据建立索引并进行查询的去中心化协议。 你可能已经猜到了,它使用 GraphQL 作为查询语言。

The Graph

示例总是理解事物的最好方式,所以让我们使用 The Graph 来讲解我们的 GameContract 示例。

如何创建子图

有关如何为数据建立索引的定义称为子图。 它需要三个组件:

  1. 清单 (subgraph.yaml)
  2. 模式 (schema.graphql)
  3. 映射 (mapping.ts)

清单 (subgraph.yaml)

清单是我们的配置文件,定义了:

  • 要为哪些智能合约建立索引(地址、网络、ABI 等)
  • 要侦听哪些事件
  • 其他要侦听的东西,如函数调用或区块
  • 所调用的映射函数(见下文 mapping.ts

你可以在此处定义多个合约和处理程序。 典型的设置是在 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
  • 字符串
  • 布尔值
  • 整数
  • 大整数
  • 大十进制数

你还可以使用实体作为类型来定义关系。 在我们的示例中,我们定义了从玩家到投注的一对多关系。 ! 表示该值不能为空。 完整的文档可以在这里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])
显示全部

Magic

但我们缺少了这个拼图的最后一块,那就是服务器。 你可以自行运行,也可以使用托管服务。

The Graph 服务器

Graph 浏览器:托管服务

最简单的方法是使用托管服务。 按照此处opens in a new tab的说明部署子图。 对于许多项目,你实际上可以在浏览器opens in a new tab中找到现有的子图。

The Graph-Explorer

运行你自己的节点

或者,你也可以运行自己的节点。 文档在此opens in a new tab。 这样做的一个原因可能是使用托管服务不支持的网络。 目前支持的网络可在此处找到opens in a new tab

去中心化的未来

GraphQL 也支持新传入事件的流。 它们通过 Substreamsopens in a new tab 在图上得到支持,目前其处于公开测试阶段。

2021opens in a new tab 年,The Graph 开始向去中心化索引网络转型。 你可以在此处opens in a new tab阅读更多有关该去中心化索引网络的体系结构。

两个关键方面是:

  1. 用户为查询向索引器付费。
  2. 索引器质押 Graph 代币 (GRT)。

页面最后更新: 2025年6月24日

本教程对你有帮助吗?