The Graph:修复Web3数据查询
这次我们将仔细研究 The Graph,它在去年基本上成为了开发去中心化应用程序的标准堆栈的一部分。 让我们先看看我们会如何用传统的方式做事…
没有 The Graph...
为了说明起见,让我们举一个简单的例子。 我们都喜欢游戏,所以想象一个用户下注的简单游戏:
1pragma solidity 0.7.1;23contract Game {4 uint256 totalGamesPlayerWon = 0;5 uint256 totalGamesPlayerLost = 0;6 event BetPlaced(address player, uint256 value, bool hasWon);78 function placeBet() external payable {9 bool hasWon = evaluateBetForPlayer(msg.sender);1011 if (hasWon) {12 (bool success, ) = msg.sender.call{ value: msg.value * 2 }('');13 require(success, "Transfer failed");14 totalGamesPlayerWon++;15 } else {16 totalGamesPlayerLost++;17 }1819 emit BetPlaced(msg.sender, msg.value, hasWon);20 }21}显示全部复制
现在假设在我们的去中心化应用程序中,我们想要显示总投注、输/赢的游戏总数,并且每当有人再次玩游戏时更新它。 该方法将是:
- 获取
totalGamesPlayerWon
。 - 获取
totalGamesPlayerLost
。 - 订阅
BetPlaced
事件。
我们可以侦听如右侧所示的Web3 中的事件(opens in a new tab),但它需要处理相当多的情况。
1GameContract.events.BetPlaced({2 fromBlock: 03}, function(error, event) { console.log(event); })4.on('data', function(event) {5 // event fired6})7.on('changed', function(event) {8 // event was removed again9})10.on('error', function(error, receipt) {11 // tx rejected12});显示全部复制
现在,对于我们的简单示例来说,这在某种程度上还是不错的。 但是假设我们现在只想显示当前玩家输/赢的赌注金额。 嗯,我们运气不好,您最好部署一份新的合约来存储这些值,并将它们提取出来。 现在想象一个更复杂的智能合约和去中心化应用程序,事情可能很快就会变得一团糟。
您可以看到这并不是最优的:
- 不适用于已经部署的合约。
- 存储这些值需要额外的 gas 成本。
- 需要另一个调用来获取以太坊节点的数据。
现在让我们看看更好的解决方案。
让我向您介绍一下 GraphQL
首先我们来谈谈 GraphQL,它最初是由 Facebook 设计和实现的。 您可能熟悉传统的 Rest API 模型。 现在设想一下,您可以编写一个查询来精确查找您想要的数据:
这两张图片基本上抓住了 GraphQL 的精髓。 通过右边的查询,我们可以精确地定义我们想要的数据,这样我们就可以在一个请求中得到所有的东西,而不仅仅是我们需要的东西。 GraphQL 服务器处理所有所需数据的获取,因此前端用户端使用起来非常简单。 如果您感兴趣,这是一个很好的解释(opens in a new tab),说明服务器是如何处理查询的。
现在有了这些知识,让我们最终进入区块链空间和 The Graph。
什么是 The Graph?
区块链是一个去中心化的数据库,但与通常情况不同的是,我们没有适用于这个数据库的查询语言。 检索数据的解决方案是痛苦的,或者是完全不可能的。 The Graph 是一种用于为区块链数据建立索引并进行查询的去中心化协议。 您可能已经猜到了,它使用 GraphQL 作为查询语言。
示例总是最容易理解的,所以让我们使用 The Graph 作为 GameContract 示例。
如何创建子图
有关如何建立数据索引的定义称为子图. 它需要三个组件:
- 清单 (
subgraph.yaml
) - 模式 (
schema.graphql
) - 映射 (
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.12description: Placing Bets on Ethereum3repository: - GitHub link -4schema:5 file: ./schema.graphql6dataSources:7 - kind: ethereum/contract8 name: GameContract9 network: mainnet10 source:11 address: '0x2E6454...cf77eC'12 abi: GameContract13 startBlock: 617524414 mapping:15 kind: ethereum/events16 apiVersion: 0.0.117 language: wasm/assemblyscript18 entities:19 - GameContract20 abis:21 - name: GameContract22 file: ../build/contracts/GameContract.json23 eventHandlers:24 - event: PlacedBet(address,uint256,bool)25 handler: handleNewBet26 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}78type Player @entity {9 id: ID!10 totalPlayedCount: Int11 hasWonCount: Int12 hasLostCount: Int13 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"34export function handleNewBet(event: PlacedBet): void {5 let player = Player.load(event.transaction.from.toHex())67 if (player == null) {8 // create if doesn't exist yet9 player = new Player(event.transaction.from.toHex())10 player.bets = new Array<string>(0)11 player.totalPlayedCount = 012 player.hasWonCount = 013 player.hasLostCount = 014 }1516 let bet = new Bet(17 event.transaction.hash.toHex() + "-" + event.logIndex.toString()18 )19 bet.player = player.id20 bet.playerHasWon = event.params.hasWon21 bet.time = event.block.timestamp22 bet.save()2324 player.totalPlayedCount++25 if (event.params.hasWon) {26 player.hasWonCount++27 } else {28 player.hasLostCount++29 }3031 // update array like this32 let bets = player.bets33 bets.push(bet.id)34 player.bets = bets3536 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})56ReactDOM.render(7 <ApolloProvider client={client}>8 <App />9 </ApolloProvider>,10 document.getElementById("root")11)显示全部
现在我们可以编写一个像这样的查询。 这个查询将帮助我们获取如下数据:
- 当前用户已经赢得游戏多少次
- 当前用户已经输了游戏多少次
- 他之前所有赌注的时间戳清单
所有这些都在一个向 GraphQL 服务器发出的请求中。
1const myGraphQlQuery = gql`2 players(where: { id: $currentUser }) {3 totalPlayedCount4 hasWonCount5 hasLostCount6 bets {7 time8 }9 }10`1112const { loading, error, data } = useQuery(myGraphQlQuery)1314React.useEffect(() => {15 if (!loading && !error && data) {16 console.log({ data })17 }18}, [loading, error, data])显示全部
但我们缺少了这个拼图的最后一块,那就是服务器。 您可以自行运行一个服务器或使用托管服务。
The Graph 服务器
Graph Explorer:托管服务
最简单的方法是使用托管服务。 按照此处(opens in a new tab)的说明来部署子图。 许多项目,您实际上可以在浏览器中找到现有的子图:https://thegraph.com/explorer/。(opens in a new tab)
运行您自己的节点
或者,您可以运行自己的节点: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 计划最终成为一个完全去中心化的协议。 这两篇文章更详细地解释了这个计划:
- https://thegraph.com/blog/the-graph-network-in-depth-part-1(opens in a new tab)
- https://thegraph.com/blog/the-graph-network-in-depth-part-2(opens in a new tab)
以下有两个关键的方面:
- 用户将为查询支持索引器费用。
- 索引器将权益质押图形通证(GRT)。
上次修改时间: @sumitvekariya(opens in a new tab), 2024年8月29日