24 分钟
高级子图功能
概述
添加并实现高级子图功能,以增强子图的构建。
从 specVersion ``0.0.4
开始,子图特征必须使用它们的 camelCase
名称,在清单文件顶层的 features
部分中显式声明,如下表所列:
例如,如果子图使用 Full-Text Search 和 Non-fatal Errors 功能,则清单中的 features
字段应为:
1specVersion: 1.3.02description: Gravatar for Ethereum3features:4 - fullTextSearch5 - nonFatalErrors6dataSources: ……
请注意,在子图部署期间使用未声明的特性会导致验证错误,但如果声明了特性未使用,则不会出现错误。
时间序列和聚合
先决条件:
- 子图规范版本必须≥1.1.0。
时间序列和聚合使您的子图能够跟踪每日平均价格、每小时总转账等统计数据。
此功能引入了两种新型子图实体。时间序列实体用时间戳记录数据点。聚合实体每小时或每天对时间序列数据点执行预先声明的计算,然后存储结果,以便通过GraphQL轻松访问。
示例模式
1type Data @entity(timeseries: true) {2 id: Int8!3 timestamp: Timestamp!4 price: BigDecimal!5}67type Stats @aggregation(intervals: ["hour", "day"], source: "Data") {8 id: Int8!9 timestamp: Timestamp!10 sum: BigDecimal! @aggregate(fn: "sum", arg: "price")11}
如何实现时间序列和聚合
时间序列实体在GraphQL模式中用@entity(Timeseries:true)
定义。每个时间序列实体必须:
- 具有int8类型的唯一ID
- 具有timestamp类型的时间戳
- 包括将由聚合实体用于计算的数据
这些时序实体可以保存在常规触发器处理程序中,并作为聚合实体的“原始数据”。
聚合实体在GraphQL模式中用@aggregation
定义。每个聚合实体都定义了它将从中收集数据的源(必须是时间序列实体),设置间隔(例如小时、天),并指定它将使用的聚合函数(例如总和、计数、最小值、最大值、第一个、最后一个)。
聚合实体在所需间隔结束时根据指定的源自动计算。
可用聚合间隔
hour
:按小时设置时间序列周期。day
:设置每天的时间序列周期,从00:00开始到00:00结束。
可用聚合功能
sum
:所有值的总和。count
:值的数量。min
:最小值。max
:最大值。first
:周期中的第一个值。last
:周期中的最后一个值。
示例聚合查询
1{2 stats(interval: "hour", where: { timestamp_gt: 1704085200 }) {3 id4 timestamp5 sum6 }7}
阅读更多关于时间序列和聚合的信息。
非致命错误
在默认情况下,已同步子图上的索引错误会导致子图失败并停止同步。 子图也可以配置为忽略引发错误的处理程序所做的更改, 在出现错误时继续同步。 这使子图作者有时间更正他们的子图,同时继续针对最新区块提供查询,尽管由于导致错误的代码问题,结果可能会不一致。 请注意,某些错误仍然总是致命的,要成为非致命错误,首先需要确定相应的错误是确定性的错误。
注意: The Graph 网络尚不支持非致命错误,开发人员不应通过工作室将使用该功能的子图部署到网络。
启用非致命错误需要在子图清单上设置以下功能标志:
1specVersion: 1.3.02description: Gravatar for Ethereum3features:4 - nonFatalErrors5 …
查询还必须通过 subgraphError
参数选择查询可能存在不一致的数据。 还建议查询 _meta
以检查子图是否跳过错误,如示例:
1foos(first: 100, subgraphError: allow) {2 id3}45_meta {6 hasIndexingErrors7}
如果子图遇到错误,则查询将返回数据和带有消息 "indexing_error"
的 graphql 错误,如以下示例响应所示:
1"data": {2 "foos": [3 {4 "id": "0xdead"5 }6 ],7 "_meta": {8 "hasIndexingErrors": true9 }10},11"errors": [12 {13 "message": "indexing_error"14 }15]
IPFS/Arweave文件数据源
文件数据源是一种新的子图功能,用于以稳健、可扩展的方式在索引期间访问链下数据。文件数据源支持从IPFS和Arweave获取文件。
这也为链外数据的确定性索引以及引入任意HTTP源数据奠定了基础。
概述
这不是在处理程序执行期间“在线”获取文件,而是引入了可以作为给定文件标识符的新数据源生成的模板。这些新数据源获取文件,如果不成功则重试,找到文件后运行专用处理程序。
这类似于现有数据源模板,用于动态创建新的基于链的数据源。
这将替换现有的ipfs.cat
API
升级指南
更新graph-ts
和graph-cli
文件数据源需要graph-ts>=0.29.0和graph-cli>=0.33.1
添加新的实体类型,当找到文件时将更新该类型
文件数据源不能访问或更新基于链的实体,但必须更新特定于文件的实体。
这可能意味着将现有实体中的字段拆分为单独的实体,并链接在一起。
原始合并实体:
1type Token @entity {2 id: ID!3 tokenID: BigInt!4 tokenURI: String!5 externalURL: String!6 ipfsURI: String!7 image: String!8 name: String!9 description: String!10 type: String!11 updatedAtTimestamp: BigInt12 owner: User!13}
新拆分实体:
1type Token @entity {2 id: ID!3 tokenID: BigInt!4 tokenURI: String!5 ipfsURI: TokenMetadata6 updatedAtTimestamp: BigInt7 owner: String!8}910type TokenMetadata @entity {11 id: ID!12 image: String!13 externalURL: String!14 name: String!15 description: String!16}
如果母实体与生成的文件数据源实体之间的关系为1:1,则最简单的模式是通过使用IPFS CID作为查找将母实体链接到生成的文件实体。如果您在建模新的基于文件的实体时遇到困难,请联系Discord!
您可以使用嵌套筛选器根据这些嵌套实体过滤父实体。
添加一个新的模板数据源,使用kind: file/ipfs
或kind: file/arweave
。
这是在识别出感兴趣的文件时生成的数据源。
1templates:2 - name: TokenMetadata3 kind: file/ipfs4 mapping:5 apiVersion: 0.0.96 language: wasm/assemblyscript7 file: ./src/mapping.ts8 handler: handleMetadata9 entities:10 - TokenMetadata11 abis:12 - name: Token13 file: ./abis/Token.json
目前需要abis
,但无法从文件数据源中调用合约。
文件数据源必须特别提到它将在实体
下与之交互的所有实体类型。有关详细信息,请参阅限制 。
创建新处理程序以处理文件
此处理程序应接受一个Bytes
参数,当找到文件时,该参数将是文件的内容,然后可以对其进行处理。这通常是一个JSON文件,可以用graph-ts
助手(文件)处理。
文件的CID作为可读字符串可通过数据源
访问,如下所示:
1const cid = dataSource.stringParam()
示例处理程序:
1import { json, Bytes, dataSource } from '@graphprotocol/graph-ts'2import { TokenMetadata } from '../generated/schema'34export function handleMetadata(content: Bytes): void {5 let tokenMetadata = new TokenMetadata(dataSource.stringParam())6 const value = json.fromBytes(content).toObject()7 if (value) {8 const image = value.get('image')9 const name = value.get('name')10 const description = value.get('description')11 const externalURL = value.get('external_url')1213 if (name && image && description && externalURL) {14 tokenMetadata.name = name.toString()15 tokenMetadata.image = image.toString()16 tokenMetadata.externalURL = externalURL.toString()17 tokenMetadata.description = description.toString()18 }1920 tokenMetadata.save()21 }22}
需要时生成文件数据源
现在,您可以在执行基于链的处理程序期间创建文件数据源:
- 从自动生成的模板导入
模板
- 从映射中调用
TemplateName.create(cid:string)
,其中cid是有效的IPFS或Arweave内容标识符
对于IPFS,Graph Node支持v0 和 v1内容标识符,以及带有目录的内容标识符(例如bafyreighykzv2we26wfrbzkcdw37sbrby4upq7ae3aqobbq7i4er3tnxci/metadata.json
)。
对于Arweave,从0.33.0版本开始,Graph Node可以根据其交易ID获取存储在Arweave上的文件来自Arweave网关(示例文件). Arweave支持通过Irys(以前是Bundlr)上传的交易,Graph Node还可以根据Irys清单获取文件。
例子:
1import { TokenMetadata as TokenMetadataTemplate } from '../generated/templates'23const ipfshash = 'QmaXzZhcYnsisuue5WRdQDH6FDvqkLQX1NckLqBYeYYEfm'4//This example code is for a Crypto coven Subgraph. The above ipfs hash is a directory with token metadata for all crypto coven NFTs.56export function handleTransfer(event: TransferEvent): void {7 let token = Token.load(event.params.tokenId.toString())8 if (!token) {9 token = new Token(event.params.tokenId.toString())10 token.tokenID = event.params.tokenId1112 token.tokenURI = '/' + event.params.tokenId.toString() + '.json'13 const tokenIpfsHash = ipfshash + token.tokenURI14 //This creates a path to the metadata for a single Crypto coven NFT. It concats the directory with "/" + filename + ".json"1516 token.ipfsURI = tokenIpfsHash1718 TokenMetadataTemplate.create(tokenIpfsHash)19 }2021 token.updatedAtTimestamp = event.block.timestamp22 token.owner = event.params.to.toHexString()23 token.save()24}
这将创建一个新的文件数据源,该数据源将轮询Graph Node配置的IPFS或Arweave端点,如果未找到文件,则进行重试。当找到文件时,文件数据源处理程序将被执行。
此示例使用 CID 作为母 代币
实体和生成的 TokenMetadata
实体之间的查找。
以前,子图开发人员会在此时调用ipfs.cat(CID)
来获取文件。
祝贺您,您正在使用文件数据源!
部署子图
现在,您可以将子图构建
并部署
到任何Graph Node>=v0.30.0-rc.0。
限制
文件数据源处理程序和实体与其他子图实体隔离,确保它们在执行时是确定的,并确保基于链的数据源不受污染。具体来说:
- 文件数据源创建的实体是不可变的,不能更新
- 文件数据源处理程序无法访问其他文件数据源中的实体
- 基于链的处理程序无法访问与文件数据源关联的实体
虽然这个约束对于大多数用例不应该是有问题的,但是对于某些用例,它可能会引入复杂性。如果您在子图中基于文件数据建模时遇到问题,请通过 Discord 与我们联系!
此外,不可能从文件数据源创建数据源,无论是线上数据源还是其他文件数据源。这项限制将来可能会取消。
最佳实践
如果要将 NFT 元数据链接到相应的代币,请使用元数据的 IPFS hash从代币实体引用元数据实体。使用 IPFS hash作为 ID 保存元数据实体。
在创建文件数据源时,您可以使用DataSource上下文传递额外的信息,这些信息将可供文件数据源处理程序使用。
如果您有多次刷新的实体,请使用 IPFS 一的基于文件的实体。实体 ID,并使用基于链的实体中的派生字段引用它们。
我们正在努力改进上述建议,因此查询只返回“最新”版本。
已知问题
文件数据源目前需要ABI,即使不使用ABI(问题))。解决方法是添加任何ABI。
文件数据源的处理程序不能在导入 eth _ call
契约绑定的文件中,如果“未知导入: etherum:: etherum.call
尚未定义”(问题) 则失败。解决办法是在专用文件中创建文件数据源处理程序。
例子
参考
索引参数过滤器 / 主题过滤器
要求: 规范版本 >= 1.2.0
主题过滤器,也称为索引参数过滤器,是子图中的一个强大功能,允许用户根据其索引参数的值精确过滤区块链事件。
-
这些过滤器有助于将感兴趣的特定事件与区块链上的大量事件流隔离开来,通过只关注相关数据,使子图能够更有效地运行。
-
这对于创建跟踪特定地址及其与区块链上各种智能合约交互的个人子图非常有用。
主题过滤器的工作原理
当智能合约发出事件时,任何标记为索引的参数都可以用作子图清单中的过滤器。这允许子图有选择地监听与这些索引参数匹配的事件。
- 事件的第一个索引参数对应
topic1
,第二个对应topic2
,以此类推,直到topic3
,因为以太坊虚拟机(EVM)允许每个事件最多三个索引参数。
1// SPDX-License-Identifier: MIT2pragma solidity ^0.8.0;34contract Token {5 // Event declaration with indexed parameters for addresses6 event Transfer(address indexed from, address indexed to, uint256 value);78 // Function to simulate transferring tokens9 function transfer(address to, uint256 value) public {10 // Emitting the Transfer event with from, to, and value11 emit Transfer(msg.sender, to, value);12 }13}
在这个例子中:
Transfer
事件用于记录地址之间的代币交易。from
和to
参数被索引,允许事件侦听器过滤和监视涉及特定地址的传输。传输
函数是代币传输操作的简单表示,每当调用时都会发出传输事件。
子图中的配置
主题过滤器直接在子图清单的事件处理程序配置中定义。以下是它们的配置方式:
1eventHandlers:2 - event: SomeEvent(indexed uint256, indexed address, indexed uint256)3 handler: handleSomeEvent4 topic1: ['0xValue1', '0xValue2']5 topic2: ['0xAddress1', '0xAddress2']6 topic3: ['0xValue3']
在此设置中:
topic1
对应于事件的第一个索引参数,topic2
对应于第二个,topic3
对应于第三个。- 每个主题可以有一个或多个值,只有当事件与每个指定主题中的一个值匹配时,才会处理该事件。
过滤器逻辑
- 在单个主题中:逻辑功能充当OR条件。如果事件与给定主题中列出的任何一个值匹配,则将对其进行处理。
- 不同主题之间:逻辑作为AND条件发挥作用。事件必须满足不同主题的所有指定条件,才能触发关联的处理程序。
示例1:跟踪从地址A到地址B的直接传输
1eventHandlers:2 - event: Transfer(indexed address,indexed address,uint256)3 handler: handleDirectedTransfer4 topic1: ['0xAddressA'] # Sender Address5 topic2: ['0xAddressB'] # Receiver Address
在此配置中:
topic1
被配置为过滤0xAddressA
为发送方的传输
事件。topic2
被配置为过滤0xAddressB
为接收方的传输
事件。- 子图将仅索引直接从
0xAddressA
到0xAddressB
发生的交易。
示例2:跟踪两个或多个地址之间的双向交易
1eventHandlers:2 - event: Transfer(indexed address,indexed address,uint256)3 handler: handleTransferToOrFrom4 topic1: ['0xAddressA', '0xAddressB', '0xAddressC'] # Sender Address5 topic2: ['0xAddressB', '0xAddressC'] # Receiver Address
在此配置中:
topic1
被配置为过滤0xAddressA
、0xAddressB
、0xAddressC
为发送方的传输
事件。topic2
被配置为过滤0xAddressB
和0xAddressC
为接收方的传输
事件。- 子图将对多个地址之间双向发生的交易进行索引,从而全面监控涉及所有地址的交互。
已声明eth_call
注意:这是一个实验性功能,目前尚未在稳定的Graph Node版本中提供。您只能在Subgraph Studio或自托管节点中使用它。
声明性eth_calls
是一个有价值的子图特性,它允许eth_call
提前执行,使graph-node
能够并行执行它们。
此功能执行以下操作:
- 通过减少多次调用的总时间和优化子图的整体效率,显著提高了从以太坊区块链获取数据的性能。
- 允许更快的数据获取,从而获得更快的查询响应和更好的用户体验。
- 减少需要聚合来自多个以太坊调用的数据的应用程序的等待时间,使数据检索过程更加高效。
关键概念
- 声明性
eth_calls
:定义为并行执行而非顺序执行的以太坊调用。 - 并行执行:可以同时发起多个调用,而不是在开始下一个调用之前等待一个调用完成。
- 时间效率:所有呼叫所花费的总时间从单个呼叫时间的总和(连续)变为最长呼叫所花费时间(并行)。
没有声明性eth_calls
的场景
想象一下,你有一个子图,需要进行三次以太坊调用来获取有关用户交易、余额和代币持有量的数据。
传统上,这些调用可能会按顺序进行:
- 调用 1 (交易): 需要3 秒
- 调用 2 (余额): 需要2 秒
- 调用 3 (代币持有): 需要4 秒
总耗时 = 3 + 2 + 4 = 9 秒
带有声明性eth_calls
的场景
使用此功能,您可以声明这些调用并行执行:
- 调用 1 (交易): 需要3 秒
- 调用 2 (余额): 需要2 秒
- 调用 3 (代币持有): 需要4 秒
由于这些调用是并行执行的,因此所花费的总时间等于最长调用所花费的时间。
总耗时 = max (3, 2, 4) = 4 秒
它是如何工作的
- 声明性定义:在子图清单中,你以一种表明以太坊调用可以并行执行的方式声明它们。
- 并行执行引擎:The Graph节点的执行引擎识别这些声明并同时运行调用。
- 结果聚合:一旦所有调用完成,结果将被聚合并由子图用于进一步处理。
子图清单中的示例配置
声明的eth_calls
可以访问底层事件的event.address
以及所有event.params
。
使用event.address
的subgraph.yaml
:
1eventHandlers:2event: Swap(indexed address,indexed address,int256,int256,uint160,uint128,int24)3handler: handleSwap4calls:5 global0X128: Pool[event.address].feeGrowthGlobal0X128()6 global1X128: Pool[event.address].feeGrowthGlobal1X128()
上述示例的详细信息:
global0X128
是声明的eth_call
。- 文本(
global0X128
)是此eth_call
的标签,用于记录错误。 - 文本(
Pool[event.address].feeGrowthGlobal0X128()
)是将要执行的实际eth_call
,其形式为Contract[address].function(arguments
)。 地址
和参数
可以用执行处理程序时可用的变量替换。
使用 event.params
的Subgraph.yaml
:
1calls:2 - ERC20DecimalsToken0: ERC20[event.params.token0].decimals()
嫁接到现有子图
**注意:**不建议在最初升级到The Graph网络时使用嫁接。了解更多信息此处。
首次部署子图时,它会在相应链的启动区块(或每个数据源定义的 startBlock
处)开始索引事件。在某些情况下,可以使用现有子图已经索引的数据并在更晚的区块上开始索引。 这种索引模式称为Grafting。 例如,嫁接在开发过程中非常有用,可以快速克服映射中的简单错误,或者在现有子图失败后暂时恢复工作。
当 subgraph.yaml
中的子图清单在顶层包含 graft
区块时,子图被嫁接到基础子图:
1description: ...2graft:3 base: Qm... # Subgraph ID of base Subgraph4 block: 7345624 # Block number
当部署其清单包含 graft
区块的子图时,Graph 节点将复制 base
子图的数据,直到并包括给定的 区块
,然后继续从该区块开始索引新子图。 基础子图必须存在于目标Graph Node实例上,并且必须至少索引到给定区块。 由于这个限制,嫁接只能在开发期间或紧急情况下使用,以加快生成等效的非嫁接子图。
因为嫁接是拷贝而不是索引基础数据,所以子图同步到所需区块比从头开始索引要快得多,尽管对于非常大的子图,初始数据拷贝仍可能需要几个小时。 在初始化嫁接子图时,Graph 节点将记录有关已复制的实体类型的信息。
嫁接子图可以使用一个与基础子图不同的GraphQL 模式,但仅与之兼容。它本身必须是一个有效的子图模式,但是可以通过以下方式偏离基础子图的模式:
- 添加或删除实体类型
- 从实体类型中删除属性
- 将可为空的属性添加到实体类型
- 将不可为空的属性转换为可空的属性
- 将值添加到枚举类型中
- 添加或删除接口
- 改变了实现接口的实体类型
特征管理: grafting
必须在子图清单中的features
下声明。