跳转至主要内容

The Graph:修复Web3数据查询

solidity智能合约查询中The Graphcreate-eth-appreact
中级
Markus Waas
soliditydeveloper.com(opens in a new tab)
2020年9月6日
11 分钟阅读 minute read

这次我们将仔细研究 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 // event fired
6})
7.on('changed', function(event) {
8 // event was removed again
9})
10.on('error', function(error, receipt) {
11 // tx rejected
12});
显示全部
复制

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

一个不简单的查询

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

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

是不够好的

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

让我向您介绍一下 GraphQL

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

GraphQL API与REST API

(opens in a new tab)

这两张图片基本上抓住了 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 subgraph repo(opens in a new tab) 示例。

完整的文档可以在这里看到:https://thegraph.com/docs/define-a-subgraph#the-subgraph-manifest。(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
  • 字符串
  • 布尔值
  • 整数
  • 大整数
  • 大十进制数

您还可以使用实体作为类型来定义关系。 在我们的示例中,我们定义了从玩家到投注的一对多关系。 感叹号(!) 表示值不能为空。 完整的文档可以在这里看到:https://thegraph.com/docs/define-a-subgraph#the-graphql-schema。(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/define-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 // create if doesn't exist yet
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 // update array like this
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// See all subgraphs: 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 Explorer:托管服务

最简单的方法是使用托管服务。 按照此处(opens in a new tab)的说明来部署子图。 许多项目,您实际上可以在浏览器中找到现有的子图:https://thegraph.com/explorer/。(opens in a new tab)

The Graph 浏览器

运行您自己的节点

或者,您可以运行自己的节点:https://github.com/graphprotocol/graph-node#quick-start。(opens in a new tab) 这样做的一个原因可能是使用托管服务不支持的网络。 目前支持的有主网、Kovan、Rinkeby、Ropsten、Goerli、PoA-Core、xDAI 和 Sokol。

去中心化的未来

GraphQL 也支持新传入事件的流。 The Graph 还没有完全支持这一点,但很快就会发布。

然而,一个缺失的方面仍然是去中心化。 The Graph 计划最终成为一个完全去中心化的协议。 这两篇文章更详细地解释了这个计划:

以下有两个关键的方面:

  1. 用户将为查询支持索引器费用。
  2. 索引器将权益质押图形通证(GRT)。

上次修改时间: @sumitvekariya(opens in a new tab), 2024年8月29日

本教程对你有帮助吗?