创建子图
Reading time: 61 min
子图从区块链中提取数据,对其进行处理并存储,以便通过 GraphQL 轻松查询。
子图定义由几个文件组成:
-
subgraph.yaml
: 包含子图清单的 YAML 文件 -
schema.graphql
: 一个 GraphQL 模式文件,它定义了为您的子图存储哪些数据,以及如何通过 GraphQL 查询这些数据
In order to use your subgraph on The Graph's decentralized network, you will need to . It is recommended that you to your subgraph with at least .
Before you go into detail about the contents of the manifest file, you need to install the which you will need to build and deploy a subgraph.
Graph CLI 是使用 JavaScript 编写的,您需要安装yarn
或 npm
才能使用它;以下教程中假设您已经安装了 yarn。
一旦您安装了yarn
,可以通过运行以下命令安装 Graph CLI
用 yarn 安装:
yarn global add @graphprotocol/graph-cli
用 npm 安装:
npm install -g @graphprotocol/graph-cli
Once installed, the graph init
command can be used to set up a new subgraph project, either from an existing contract or from an example subgraph. This command can be used to create a subgraph in Subgraph Studio by passing in graph init --product subgraph-studio
. If you already have a smart contract deployed to your preferred network, bootstrapping a new subgraph from that contract can be a good way to get started.
以下命令创建一个索引现有合约的所有事件的子图。 它尝试从 Etherscan 获取合约 ABI 并回退到请求本地文件路径。 如果缺少任何可选参数,它会带您进入交互式表单。
graph init \--product subgraph-studio--from-contract <CONTRACT_ADDRESS> \[--network <ETHEREUM_NETWORK>] \[--abi <FILE>] \<SUBGRAPH_SLUG> [<DIRECTORY>]
<SUBGRAPH_SLUG>
是您在 Subgraph Studio 中的子图 ID,可以在您的子图详细信息页面上找到。
graph init
支持的第二种模式是从示例子图创建新项目。 以下命令执行此操作:
graph init --studio <SUBGRAPH_SLUG>
The is based on the Gravity contract by Dani Grant that manages user avatars and emits NewGravatar
or UpdateGravatar
events whenever avatars are created or updated. The subgraph handles these events by writing Gravatar
entities to the Graph Node store and ensuring these are updated according to the events. The following sections will go over the files that make up the subgraph manifest for this example.
从v0.31.0
开始,graph cli
支持通过graph add
命令向现有子图添加新的数据源。
graph add <address> [<subgraph-manifest default: "./subgraph.yaml">]Options:--abi <path> Path to the contract ABI (default: download from Etherscan)--contract-name Name of the contract (default: Contract)--merge-entities Whether to merge entities with the same name (default: false)--network-file <path> Networks config file path (default: "./networks.json")
add
命令将从 Etherscan 获取 ABI(除非使用 --abi
选项指定 ABI 路径),并创建一个新的 dataSource
与 graph init
命令创建 dataSource
--from-contract
的方式相同,相应地更新架构和映射。
--merge-实体
选项标识开发人员希望如何处理实体
和事件
名称冲突:
- 如果为
true
:新的数据源
应该使用现有的事件处理程序
& 和实体
。 - 如果为
false
:应使用${dataSourceName}{EventName}
创建新的实体& 和事件处理程序。
合约地址
将写入相关网络的networks.json
。
**注意:**使用交互式cli时,在成功运行graph init
后,将提示您添加新的dataSource
。
子图清单 subgraph.yaml
定义了您的子图索引的智能合约,这些合约中需要关注的事件,以及如何将事件数据映射到 Graph 节点存储并允许查询的实体。 子图清单的完整规范可以在找到。
对于示例子图,subgraph.yaml
的内容是:
specVersion: 0.0.4description: Gravatar for Ethereumrepository: https://github.com/graphprotocol/graph-toolingschema:file: ./schema.graphqlindexerHints:prune: autodataSources:- kind: ethereum/contractname: Gravitynetwork: mainnetsource:address: '0x2E645469f354BB4F5c8a05B3b30A929361cf77eC'abi: GravitystartBlock: 6175244endBlock: 7175245context:foo:type: Booldata: truebar:type: Stringdata: 'bar'mapping:kind: ethereum/eventsapiVersion: 0.0.6language: wasm/assemblyscriptentities:- Gravatarabis:- name: Gravityfile: ./abis/Gravity.jsoneventHandlers:- event: NewGravatar(uint256,address,string,string)handler: handleNewGravatar- event: UpdatedGravatar(uint256,address,string,string)handler: handleUpdatedGravatarcallHandlers:- function: createGravatar(string,string)handler: handleCreateGravatarblockHandlers:- handler: handleBlock- handler: handleBlockWithCallfilter:kind: callfile: ./src/mapping.ts
清单中要更新的重要条目是:
-
specVersion
: a semver version that identifies the supported manifest structure and functionality for the subgraph. The latest version is1.2.0
. See section to see more details on features & releases. -
description
: a human-readable description of what the subgraph is. This description is displayed in Graph Explorer when the subgraph is deployed to Subgraph Studio. -
repository
: the URL of the repository where the subgraph manifest can be found. This is also displayed in Graph Explorer. -
indexerHints.prune
: Defines the retention of historical block data for a subgraph. See in section. -
dataSources.source
:智能合约子图源的地址,以及要使用的智能合约的ABI。 地址是可选的; 省略它允许索引来自所有合约的匹配事件。 -
dataSources.source.startBlock
:数据源开始索引的区块的可选编号。 在大多数情况下,我们建议使用创建合约的区块。 -
dataSources.source.endBlock
: The optional number of the block that the data source stops indexing at, including that block. Minimum spec version required:0.0.9
. -
dataSources.context
: key-value pairs that can be used within subgraph mappings. Supports various data types likeBool
,String
,Int
,Int8
,BigDecimal
,Bytes
,List
, andBigInt
. Each variable needs to specify itstype
anddata
. These context variables are then accessible in the mapping files, offering more configurable options for subgraph development. -
dataSources.mapping.entities
:数据源写入存储的实体。 每个实体的模式在 schema.graphql 文件中定义。 -
dataSources.mapping.abis
:源合约以及您在映射中与之交互的任何其他智能合约的一个或多个命名 ABI 文件。 -
dataSources.mapping.eventHandlers
:列出此子图响应的智能合约事件,映射中的处理程序—示例中为./src/mapping.ts—也将这些事件转换为存储中的实体。 -
dataSources.mapping.callHandlers
:列出此子图响应的智能合约函数以及映射中的处理程序,该映射将输入和输出转换为函数调用到存储中的实体。 -
dataSources.mapping.blockHandlers
:列出此子图响应的区块以及映射中的处理程序,以便在将区块附加到链时运行。 如果没有过滤器,区块处理程序将在每个区块中运行。 可以通过向处理程序添加为以下类型字段提供可选的调用过滤器
:call
。 如果区块包含至少一个对数据源合约的调用,则调用过滤器将运行处理程序。
通过为每个需要将数据索引到 dataSources
数组的合约添加一个条目,单个子图可以索引来自多个智能合约的数据。
区块内数据源的触发器使用以下流程进行排序:
- 事件和调用触发器首先按区块内的交易索引排序。
- 同一交易中的事件和调用触发器使用约定进行排序:首先是事件触发器,然后是调用触发器,每种类型都遵循它们在清单中定义的顺序。
- 区块触发器按照它们在清单中定义的顺序,在事件和调用触发器之后运行。
这些排序规则可能会发生变化。
Note: When new are created, the handlers defined for dynamic data sources will only start processing after all existing data source handlers are processed, and will repeat in the same sequence whenever triggered.
Topic filters, also known as indexed argument filters, are a powerful feature in subgraphs that allow users to precisely filter blockchain events based on the values of their indexed arguments.
-
These filters help isolate specific events of interest from the vast stream of events on the blockchain, allowing subgraphs to operate more efficiently by focusing only on relevant data.
-
This is useful for creating personal subgraphs that track specific addresses and their interactions with various smart contracts on the blockchain.
When a smart contract emits an event, any arguments that are marked as indexed can be used as filters in a subgraph's manifest. This allows the subgraph to listen selectively for events that match these indexed arguments.
- The event's first indexed argument corresponds to
topic1
, the second totopic2
, and so on, up totopic3
, since the Ethereum Virtual Machine (EVM) allows up to three indexed arguments per event.
// SPDX-License-Identifier: MITpragma solidity ^0.8.0;contract Token {// Event declaration with indexed parameters for addressesevent Transfer(address indexed from, address indexed to, uint256 value);// Function to simulate transferring tokensfunction transfer(address to, uint256 value) public {// Emitting the Transfer event with from, to, and valueemit Transfer(msg.sender, to, value);}}
In this example:
- The
Transfer
event is used to log transactions of tokens between addresses. - The
from
andto
parameters are indexed, allowing event listeners to filter and monitor transfers involving specific addresses. - The
transfer
function is a simple representation of a token transfer action, emitting the Transfer event whenever it is called.
Topic filters are defined directly within the event handler configuration in the subgraph manifest. Here is how they are configured:
eventHandlers:- event: SomeEvent(indexed uint256, indexed address, indexed uint256)handler: handleSomeEventtopic1: ['0xValue1', '0xValue2']topic2: ['0xAddress1', '0xAddress2']topic3: ['0xValue3']
In this setup:
topic1
corresponds to the first indexed argument of the event,topic2
to the second, andtopic3
to the third.- Each topic can have one or more values, and an event is only processed if it matches one of the values in each specified topic.
- Within a Single Topic: The logic functions as an OR condition. The event will be processed if it matches any one of the listed values in a given topic.
- Between Different Topics: The logic functions as an AND condition. An event must satisfy all specified conditions across different topics to trigger the associated handler.
eventHandlers:- event: Transfer(indexed address,indexed address,uint256)handler: handleDirectedTransfertopic1: ['0xAddressA'] # Sender Addresstopic2: ['0xAddressB'] # Receiver Address
In this configuration:
topic1
is configured to filterTransfer
events where0xAddressA
is the sender.topic2
is configured to filterTransfer
events where0xAddressB
is the receiver.- The subgraph will only index transactions that occur directly from
0xAddressA
to0xAddressB
.
eventHandlers:- event: Transfer(indexed address,indexed address,uint256)handler: handleTransferToOrFromtopic1: ['0xAddressA', '0xAddressB', '0xAddressC'] # Sender Addresstopic2: ['0xAddressB', '0xAddressC'] # Receiver Address
In this configuration:
topic1
is configured to filterTransfer
events where0xAddressA
,0xAddressB
,0xAddressC
is the sender.topic2
is configured to filterTransfer
events where0xAddressB
and0xAddressC
is the receiver.- The subgraph will index transactions that occur in either direction between multiple addresses allowing for comprehensive monitoring of interactions involving all addresses.
Declarative eth_calls
are a valuable subgraph feature that allows eth_calls
to be executed ahead of time, enabling graph-node
to execute them in parallel.
This feature does the following:
- Significantly improves the performance of fetching data from the Ethereum blockchain by reducing the total time for multiple calls and optimizing the subgraph's overall efficiency.
- Allows faster data fetching, resulting in quicker query responses and a better user experience.
- Reduces wait times for applications that need to aggregate data from multiple Ethereum calls, making the data retrieval process more efficient.
- Declarative
eth_calls
: Ethereum calls that are defined to be executed in parallel rather than sequentially. - Parallel Execution: Instead of waiting for one call to finish before starting the next, multiple calls can be initiated simultaneously.
- Time Efficiency: The total time taken for all the calls changes from the sum of the individual call times (sequential) to the time taken by the longest call (parallel).
Imagine you have a subgraph that needs to make three Ethereum calls to fetch data about a user's transactions, balance, and token holdings.
Traditionally, these calls might be made sequentially:
- Call 1 (Transactions): Takes 3 seconds
- Call 2 (Balance): Takes 2 seconds
- Call 3 (Token Holdings): Takes 4 seconds
Total time taken = 3 + 2 + 4 = 9 seconds
With this feature, you can declare these calls to be executed in parallel:
- Call 1 (Transactions): Takes 3 seconds
- Call 2 (Balance): Takes 2 seconds
- Call 3 (Token Holdings): Takes 4 seconds
Since these calls are executed in parallel, the total time taken is equal to the time taken by the longest call.
Total time taken = max (3, 2, 4) = 4 seconds
- Declarative Definition: In the subgraph manifest, you declare the Ethereum calls in a way that indicates they can be executed in parallel.
- Parallel Execution Engine: The Graph Node's execution engine recognizes these declarations and runs the calls simultaneously.
- Result Aggregation: Once all calls are complete, the results are aggregated and used by the subgraph for further processing.
Declared eth_calls
can access the event.address
of the underlying event as well as all the event.params
.
Subgraph.yaml
using event.address
:
eventHandlers:event: Swap(indexed address,indexed address,int256,int256,uint160,uint128,int24)handler: handleSwapcalls:global0X128: Pool[event.address].feeGrowthGlobal0X128()global1X128: Pool[event.address].feeGrowthGlobal1X128()
Details for the example above:
global0X128
is the declaredeth_call
.- The text before colon(
global0X128
) is the label for thiseth_call
which is used when logging errors. - The text (
Pool[event.address].feeGrowthGlobal0X128()
) is the actualeth_call
that will be executed, which is in the form ofContract[address].function(arguments)
- The
address
andarguments
can be replaced with variables that will be available when the handler is executed.
Subgraph.yaml
using event.params
calls:- ERC20DecimalsToken0: ERC20[event.params.token0].decimals()
ABI 文件必须与您的合约相匹配。 获取 ABI 文件的方法有以下几种:
- 如果您正在构建自己的项目,您可以获取最新的 ABI。
- 如果您正在为公共项目构建子图,则可以将该项目下载到您的计算机,并通过使用 ,或使用 solc 进行编译来获取 ABI。
- 您还可以在 上找到 ABI,但这并不总是可靠的,因为在那里上传的 ABI 可能已过期。 请确保您拥有正确的 ABI,否则您的子图将会运行失败。
您子图的模式定义位于文件 schema.graphql
中。 GraphQL 模式是使用 GraphQL 接口定义语言定义的。 如果您从未编写过 GraphQL 模式,建议您在 GraphQL 类型系统上查看入门教程。 GraphQL 模式的参考文档可以在 部分中找到。
在定义实体之前,重要的是要退后一步,思考数据的结构和链接方式。 所有查询都将针对子图模式中定义的数据模型和子图索引的实体进行。 因此,最好以符合 dapp 需求的方式定义子图模式。 将实体想象为“包含数据的对象”,而不是事件或函数,可能很有用。
使用 Graph,您只需在 schema.Graphql
中定义实体类型,Graph 节点将生成顶级字段,用于查询该实体类型的单个实例和集合。应该是一个实体的每个类型都需要用@entity
指令进行注释。默认情况下,实体是可变的,这意味着映射可以加载现有实体,修改它们并存储该实体的新版本。可变性是有代价的,对于已知永远不会被修改的实体类型,例如,因为它们只是包含从链中逐字提取的数据,建议使用@entity (immutable: true)
将它们标记为不可变的。只要这些更改发生在创建实体的同一区块中,映射就可以对不可变实体进行更改。不可变实体的写入和查询速度要快得多,因此应尽可能使用它们。
下面的 Gravatar
实体围绕 Gravatar 对象构建,是如何定义实体的一个很好的示例。
type Gravatar @entity {id: Id!owner: BytesdisplayName: StringimageUrl: Stringaccepted: Boolean}
下面的示例中,GravatarAccepted
和 GravatarDeclined
实体都基于事件。 不建议将事件或函数调用以 1:1 的方式映射到实体。
type GravatarAccepted @entity {id: Bytes!owner: BytesdisplayName: StringimageUrl: String}type GravatarDeclined @entity {id: Bytes!owner: BytesdisplayName: StringimageUrl: String}
实体字段可以定义为必选或可选。 必选字段由模式中的 !
指示。 如果映射中未设置必选字段,则在查询该字段时会收到此错误:
Null value resolved for non-null field 'name'
每个实体必须有一个 id
字段,其类型必须是 Bytes!
或者String!
。通常建议使用Bytes!
,除非 id
包含人类可读的文本,因为有Bytes!
id的试题比使用String!
id
的写入和查询速度会更快!id
字段充当主钥,并且需要在同一类型的所有实体中是唯一的。由于历史原因,类型 ID!
也被接受,是 String!
的同义词!
对于某些实体类型,id
是由另外两个实体的 id 构成的; 这可以使用 concat
,例如,let id = left t.id.concat (right id)
来从左边
和右边
的 id 构成 id。类似地,要从现有实体的 id 和计数器count
构造 id,可以使用 id = left t.id.concatI32(count)
。只要左边
的长度对于所有这样的实体都是相同的,这种串联就一定会产生唯一的 id,例如,因为 left. id
是一个 Address
。
我们在 GraphQL API 中支持以下标量:
类型 | 描述 |
---|---|
字节 | 字节数组,表示为十六进制字符串。 通常用于以太坊hash和地址。 |
字符串 | string 值的标量。 不支持空字符,并会自动进行删除。 |
Boolean | boolean 值的标量。 |
Int | The GraphQL spec defines Int to be a signed 32-bit integer. |
Int8 | An 8-byte signed integer, also known as a 64-bit signed integer, can store values in the range from -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807. Prefer using this to represent i64 from ethereum. |
BigInt | 大整数。 用于以太坊的 uint32 、int64 、uint64 、...、uint256 类型。 注意:uint32 以下的所有类型,例如int32 、uint24 或int8 都表示为i32 。 |
BigDecimal | BigDecimal 表示为有效数字和指数的高精度小数。 指数范围是 -6143 到 +6144。 四舍五入到 34 位有效数字。 |
Timestamp | It is an i64 value in microseconds. Commonly used for timestamp fields for timeseries and aggregations. |
您还可以在模式中创建枚举类型。 枚举类型具有以下语法:
enum TokenStatus {OriginalOwnerSecondOwnerThirdOwner}
在模式中定义枚举后,您可以使用枚举值的字符串表示形式在实体上设置枚举字段。 例如,您可以将 tokenStatus
设置为 SecondOwner
,方法是首先定义您的实体,然后使用 entity.tokenStatus = "SecondOwner
设置字段。 下面的示例演示了带有枚举字段的 Token 实体:
一个实体可能与模式中的一个或多个其他实体发生联系。 您可以在您的查询中遍历这些联系。 Graph 中的联系是单向的。 可以通过在关系的任一“端”上定义单向关系来模拟双向关系。
关系是在实体上定义的,就像任何其他字段一样,除了指定的类型是另一个实体类型。
使用TransactionReceipt
实体类型,它与Transaction
实体类型具有可选的一对一关系:
type Transaction @entity(immutable: true) {id: Bytes!transactionReceipt: TransactionReceipt}type TransactionReceipt @entity(immutable: true) {id: Bytes!transaction: Transaction}
定义一个 TokenBalance
实体类型,它与 Token 实体类型具有一对多关系:
type Token @entity(immutable: true) {id: Bytes!}type TokenBalance @entity {id: Bytes!amount: Int!token: Token!}
可以通过 @derivedFrom
字段在实体上定义反向查找。 这会在实体上创建一个虚拟字段,它可以被查询,但不能通过映射 API 手动设置。 相反的,它是从在另一个实体上定义的关系派生的。 对于这样的关系,将关系的两边都存储起来几乎没有意义,并且当只存储一侧而导出另一侧时,索引和查询性能都会更好。
对于一对多关系,关系应始终存储在“一”端,而“多”端应始终派生。 以这种方式存储关系,而不是在“多”端存储实体数组,将大大提高索引和查询子图的性能。 通常,应尽可能避免存储实体数组。
我们可以通过派生 tokenBalances
字段,来使代币的余额可以从代币中访问:
type Token @entity(immutable: true) {id: Bytes!tokenBalances: [TokenBalance!]! @derivedFrom(field: "token")}type TokenBalance @entity {id: Bytes!amount: Int!token: Token!}
对于多对多关系,例如每个可能属于任意数量的组织的用户,对关系建模的最直接,但通常不是最高效的方法,是在所涉及的两个实体中的每一个中定义数组。 如果关系是对称的,则只需要存储关系的一侧联系,就可以导出另一侧。
定义从 User
实体类型到 Organization
实体类型的反向查找。 在下面的示例中,这是通过从 Organization
实体中查找 members
属性来实现的。 在查询中,User
上的 organizations
字段将通过查找包含用户 ID 的所有 Organization
实体来解析。
type Organization @entity {id: Bytes!name: String!members: [User!]!}type User @entity {id: Bytes!name: String!organizations: [Organization!]! @derivedFrom(field: "members")}
存储这种关系的一种更高效的方法是通过一个映射表,其中每个 User
/ Organization
对都有一个条目,其模式如下
type Organization @entity {id: Bytes!name: String!members: [UserOrganization!]! @derivedFrom(field: "organization")}type User @entity {id: Bytes!name: String!organizations: [UserOrganization!] @derivedFrom(field: "user")}type UserOrganization @entity {id: Bytes! # Set to `user.id.concat(organization.id)`user: User!organization: Organization!}
这种方法要求查询下降一个额外的级别来检索,例如,用户的组织:
query usersWithOrganizations {users {organizations {# this is a UserOrganization entityorganization {name}}}}
这种存储多对多关系的更精细的方式将导致为子图存储的数据更少,因此子图的索引和查询速度通常会大大加快。
As per GraphQL spec, comments can be added above schema entity attributes using the hash symble #
. This is illustrated in the example below:
type MyFirstEntity @entity {# unique identifier and primary key of the entityid: Bytes!address: Bytes!}
全文搜索查询根据文本搜索输入来过滤和排列实体。 通过在与索引文本数据进行比较之前,将查询文本输入处理到词干中,全文查询能够返回相似词的匹配项。
全文查询定义包括查询名称、用于处理文本字段的语言词典、用于对结果进行排序的排序算法,以及搜索中包含的字段。 每个全文查询可能跨越多个字段,但所有包含的字段必须来自单个实体类型。
要添加全文查询,请在 GraphQL 模式中包含带有全文指令的 _Schema_
类型。
type _Schema_@fulltext(name: "bandSearch"language: enalgorithm: rankinclude: [{ entity: "Band", fields: [{ name: "name" }, { name: "description" }, { name: "bio" }] }])type Band @entity {id: Bytes!name: String!description: String!bio: Stringwallet: Addresslabels: [Label!]!discography: [Album!]!members: [Musician!]!}
示例 bandSearch
字段可用于查询,以根据 name
、description
、bio
字段中的文本文档,来过滤 Band
实体。 请跳转到 ,了解全文搜索 API 的描述和更多示例用法。
query {bandSearch(text: "breaks & electro & detroit") {idnamedescriptionwallet}}
选择不同的语言将对全文搜索 API 产生明确的(尽管有时是微妙的)影响。 全文查询字段涵盖的字段将会在所选语言的内容中进行检查,因此分析和搜索查询产生的词位因语言而异。 例如:当使用支持的土耳其语词典时,“token”的词干为“toke”,而英语词典当然会认为其词干为“token”。
支持的语言词典:
代码 | 词典 |
---|---|
simple | 通用 |
da | 丹麦语 |
nl | 荷兰语 |
en | 英语 |
fi | 芬兰语 |
fr | 法语 |
de | 德语 |
hu | 匈牙利语 |
it | 意大利语 |
no | 挪威语 |
pt | 葡萄牙语 |
ro | 罗马尼亚语 |
ru | 俄语 |
es | 西班牙语 |
sv | 瑞典语 |
tr | 土耳其语 |
支持的排序结果算法:
算法 | 描述 |
---|---|
rank | 使用全文查询的匹配质量 (0-1) 对结果进行排序。 |
proximityRank | 与 rank 类似,但也包括匹配的接近程度。 |
映射将获取的以太坊数据转换为您的模式文件中定义的实体。 映射是用 的子集编写的,称为 。 AssemblyScript 可以编译成 WASM ()。 AssemblyScript 比普通的 TypeScript 更严格,但提供了开发者熟悉的语法。
对于在 mapping.eventHandlers
下的 subgraph.yaml
中定义的每个事件处理程序,都会创建一个同名的导出函数。 每个处理程序必须接受一个名为 event
的参数,其类型对应于正在处理的事件的名称。
在示例子图中,src/mapping.ts
包含 NewGravatar
和 UpdatedGravatar
事件的处理程序:
import { NewGravatar, UpdatedGravatar } from '../generated/Gravity/Gravity'import { Gravatar } from '../generated/schema'export function handleNewGravatar(event: NewGravatar): void {let gravatar = new Gravatar(event.params.id)gravatar.owner = event.params.ownergravatar.displayName = event.params.displayNamegravatar.imageUrl = event.params.imageUrlgravatar.save()}export function handleUpdatedGravatar(event: UpdatedGravatar): void {let id = event.params.idlet gravatar = Gravatar.load(id)if (gravatar == null) {gravatar = new Gravatar(id)}gravatar.owner = event.params.ownergravatar.displayName = event.params.displayNamegravatar.imageUrl = event.params.imageUrlgravatar.save()}
第一个处理程序接受 NewGravatar
事件,而且使用 new Gravatar(event.params.id.toHex())
创建一个新的 Gravatar
实体,使用相应的事件参数填充实体字段。 该实体实例由变量 gravatar
表示,id 值为 event.params.id.toHex()
。
第二个处理程序尝试从 Graph 节点存储加载现有的 Gravatar
。 如果尚不存在,则会按需创建。 然后更新实体以匹配新的事件参数,并使用 gravatar.save()
将其保存。
It is highly recommended to use Bytes
as the type for id
fields, and only use String
for attributes that truly contain human-readable text, like the name of a token. Below are some recommended id
values to consider when creating new entities.
-
transfer.id = event.transaction.hash
-
let id = event.transaction.hash.concatI32(event.logIndex.toI32())
-
For entities that store aggregated data, for e.g, daily trade volumes, the
id
usually contains the day number. Here, using aBytes
as theid
is beneficial. Determining theid
would look like
let dayID = event.block.timestamp.toI32() / 86400let id = Bytes.fromI32(dayID)
- Convert constant addresses to
Bytes
.
const id = Bytes.fromHexString('0xdead...beef')
There is a which contains utilities for interacting with the Graph Node store and conveniences for handling smart contract data and entities. It can be imported into mapping.ts
from @graphprotocol/graph-ts
.
When creating and saving a new entity, if an entity with the same ID already exists, the properties of the new entity are always preferred during the merge process. This means that the existing entity will be updated with the values from the new entity.
If a null value is intentionally set for a field in the new entity with the same ID, the existing entity will be updated with the null value.
If no value is set for a field in the new entity with the same ID, the field will result in null as well.
为了使与智能合约、事件和实体的代码编写工作变得简单且类型安全,Graph CLI 可以从子图的 GraphQL 模式和数据源中包含的合约 ABI 生成 AssemblyScript 类型。
这可以通过以下命令实现
graph codegen [--output-dir <OUTPUT_DIR>] [<MANIFEST>]
但在大多数情况下,子图已经通过 package.json
进行了预配置,以允许您简单地运行以下命令之一来实现相同的目的:
# Yarnyarn codegen# NPMnpm run codegen
这将为 subgrap.yaml
中提到的 ABI 文件中的每个智能合约生成一个 AssemblyScript 类,允许您将这些合约绑定到映射中的特定地址,并针对正在处理的区块调用只读合约方法。它还将为每个合约事件生成一个类,以便于访问事件参数以及事件源自的区块和交易。所有这些类型都写入到<OUTPUT_DIR>/<DATA_SOURCE_NAME>/<ABI_NAME>.ts
。在示例子图中,这将generated/Gravity/Gravity.ts
,允许映射导入这些类型。
import {// The contract class:Gravity,// The events classes:NewGravatar,UpdatedGravatar,} from '../generated/Gravity/Gravity'
除此之外,还会为子图的 GraphQL 模式中的每个实体类型生成一个类。 这些类提供类型安全的实体加载、对实体字段的读写访问以及一个 save()
方法来写入要存储的实体。 所有实体类都写入 <OUTPUT_DIR>/schema.ts
,允许映射导入它们
import { Gravatar } from '../generated/schema'
注意: 每次更改 GraphQL 模式文件或清单中包含的 ABI 后,都必须再次执行代码生成。 在构建或部署子图之前,它还必须至少执行一次。
Code generation does not check your mapping code in src/mapping.ts
. If you want to check that before trying to deploy your subgraph to Graph Explorer, you can run yarn build
and fix any syntax errors that the TypeScript compiler might find.
EVM兼容智能合约中的一种常见模式是使用注册表或工厂合约,其中一个合约创建、管理或引用任意数量的其他合约,每个合约都有自己的状态和事件。
这些子合约的地址可能事先知道,也可能不知道,其中许多合约可能会随着时间的推移而创建和/或添加。这就是为什么在这种情况下,定义单个数据源或固定数量的数据源是不可能的,需要一种更动态的方法:数据源模板。
首先,您需要为主合约定义一个常规数据源。 下面的代码片段显示了 交换工厂合约的简化示例数据源。 注意 NewExchange(address,address)
事件处理程序。 当工厂合约在链上创建新交换合约时,会发出此消息。
dataSources:- kind: ethereum/contractname: Factorynetwork: mainnetsource:address: '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95'abi: Factorymapping:kind: ethereum/eventsapiVersion: 0.0.6language: wasm/assemblyscriptfile: ./src/mappings/factory.tsentities:- Directoryabis:- name: Factoryfile: ./abis/factory.jsoneventHandlers:- event: NewExchange(address,address)handler: handleNewExchange
然后,将 数据源模板 添加到清单中。 它们与常规数据源相同,只是在 source
下缺少预先定义的合约地址。 通常,您需要为母合约管理或引用的每种类型的子合约定义一个模板。
dataSources:- kind: ethereum/contractname: Factory# ... other source fields for the main contract ...templates:- name: Exchangekind: ethereum/contractnetwork: mainnetsource:abi: Exchangemapping:kind: ethereum/eventsapiVersion: 0.0.6language: wasm/assemblyscriptfile: ./src/mappings/exchange.tsentities:- Exchangeabis:- name: Exchangefile: ./abis/exchange.jsoneventHandlers:- event: TokenPurchase(address,uint256,uint256)handler: handleTokenPurchase- event: EthPurchase(address,uint256,uint256)handler: handleEthPurchase- event: AddLiquidity(address,uint256,uint256)handler: handleAddLiquidity- event: RemoveLiquidity(address,uint256,uint256)handler: handleRemoveLiquidity
在最后一步中,您可以更新主合约映射,以便从其中一个模板创建动态数据源实例。 在此示例中,您将更改主合约映射以导入 Exchange
模板,并在其上调用 Exchange.create(address)
方法,从而开始索引新交换合约。
import { Exchange } from '../generated/templates'export function handleNewExchange(event: NewExchange): void {// Start indexing the exchange; `event.params.exchange` is the// address of the new exchange contractExchange.create(event.params.exchange)}
注意: 新的数据源只会处理创建它的区块和所有后续区块的调用和事件,而不会处理历史数据,也就是包含在先前区块中的数据。
如果先前的区块包含与新数据源相关的数据,最好通过读取合约的当前状态,并在创建新数据源时创建表示该状态的实体来索引该数据。
数据源背景允许在实例化模板时传递额外的配置。 在我们的示例中,假设交易所与特定的交易对相关联,该交易对包含在 NewExchange
事件中。 该信息可以传递到实例化的数据源中,如下所示:
import { Exchange } from '../generated/templates'export function handleNewExchange(event: NewExchange): void {let context = new DataSourceContext()context.setString('tradingPair', event.params.tradingPair)Exchange.createWithContext(event.params.exchange, context)}
在 Exchange
模板的映射中,可以访问背景:
import { dataSource } from '@graphprotocol/graph-ts'let context = dataSource.context()let tradingPair = context.getString('tradingPair')
对于所有的值类型,都有像 setString
和 getString
这样的 setter 和 getter。
startBlock
是一个可选配置,允许您定义数据源从区块链中的哪个区块开始索引。 设置起始区块允许数据源跳过潜在的数百万个不相关的区块。 通常,子图开发人员会将 startBlock
设置为创建数据源智能合约的区块。
dataSources:- kind: ethereum/contractname: ExampleSourcenetwork: mainnetsource:address: '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95'abi: ExampleContractstartBlock: 6627917mapping:kind: ethereum/eventsapiVersion: 0.0.6language: wasm/assemblyscriptfile: ./src/mappings/factory.tsentities:- Userabis:- name: ExampleContractfile: ./abis/ExampleContract.jsoneventHandlers:- event: NewEvent(address,address)handler: handleNewEvent
注意: 合约创建区块可以在 Etherscan 上快速查找:
- 通过在搜索栏中输入合约地址来搜索合约。
- 单击
Contract Creator
部分中的创建交易hash。 - 加载交易详情页面,您将在其中找到该合约的起始区块。
The indexerHints
setting in a subgraph's manifest provides directives for indexers on processing and managing a subgraph. It influences operational decisions across data handling, indexing strategies, and optimizations. Presently, it features the prune
option for managing historical data retention or pruning.
This feature is available from specVersion: 1.0.0
indexerHints.prune
: Defines the retention of historical block data for a subgraph. Options include:
"never"
: No pruning of historical data; retains the entire history."auto"
: Retains the minimum necessary history as set by the indexer, optimizing query performance.- A specific number: Sets a custom limit on the number of historical blocks to retain.
indexerHints:prune: auto
The term "history" in this context of subgraphs is about storing data that reflects the old states of mutable entities.
History as of a given block is required for:
- , which enable querying the past states of these entities at specific blocks throughout the subgraph's history
- Using the subgraph as a in another subgraph, at that block
- Rewinding the subgraph back to that block
If historical data as of the block has been pruned, the above capabilities will not be available.
Using "auto"
is generally recommended as it maximizes query performance and is sufficient for most users who do not require access to extensive historical data.
For subgraphs leveraging , it's advisable to either set a specific number of blocks for historical data retention or use prune: never
to keep all historical entity states. Below are examples of how to configure both options in your subgraph's settings:
To retain a specific amount of historical data:
indexerHints:prune: 1000 # Replace 1000 with the desired number of blocks to retain
To preserve the complete history of entity states:
indexerHints:prune: never
You can check the earliest block (with historical state) for a given subgraph by querying the :
{indexingStatuses(subgraphs: ["Qm..."]) {subgraphsyncedhealthchains {earliestBlock {number}latestBlock {number}chainHeadBlock { number }}}}
Note that the earliestBlock
is the earliest block with historical data, which will be more recent than the startBlock
specified in the manifest, if the subgraph has been pruned.
Event handlers in a subgraph react to specific events emitted by smart contracts on the blockchain and trigger handlers defined in the subgraph's manifest. This enables subgraphs to process and store event data according to defined logic.
An event handler is declared within a data source in the subgraph's YAML configuration. It specifies which events to listen for and the corresponding function to execute when those events are detected.
dataSources:- kind: ethereum/contractname: Gravitynetwork: devsource:address: '0x731a10897d267e19b34503ad902d0a29173ba4b1'abi: Gravitymapping:kind: ethereum/eventsapiVersion: 0.0.6language: wasm/assemblyscriptentities:- Gravatar- Transactionabis:- name: Gravityfile: ./abis/Gravity.jsoneventHandlers:- event: Approval(address,address,uint256)handler: handleApproval- event: Transfer(address,address,uint256)handler: handleTransfertopic1: ['0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', '0xc8dA6BF26964aF9D7eEd9e03E53415D37aA96325'] # Optional topic filter which filters only events with the specified topic.
虽然事件提供了一种收集合约状态相关变换的有效方法,但许多合约避免生成日志以优化 gas 成本。 在这些情况下,子图可以订阅对数据源合约的调用。 这是通过定义引用函数签名的调用处理程序,及处理对该函数调用的映射处理程序来实现的。 为了处理这些调用,映射处理程序将接收一个 ethereum.Call
作为参数,其中包含调用的类型化输入和输出。 在交易调用链中的任何深度进行的调用都会触发映射,从而捕获通过代理合约与数据源合约的交互活动。
调用处理程序只会在以下两种情况之一触发:当指定的函数被合约本身以外的账户调用时,或者当它在 Solidity 中被标记为外部,并作为同一合约中另一个函数的一部分被调用时。
注意: 调用处理程序目前依赖于 Parity 跟踪 API。某些网络,如 BNB 链和 Arbitrum,不支持此 API。如果索引其中一个网络的子图包含一个或多个调用处理程序,它将不会开始同步。子图开发人员应该使用事件处理程序。它们比调用处理程序性能好得多,并且在每个 evm 网络上都受到支持。
要在清单中定义调用处理程序,只需在您要订阅的数据源下添加一个 callHandlers
数组。
dataSources:- kind: ethereum/contractname: Gravitynetwork: mainnetsource:address: '0x731a10897d267e19b34503ad902d0a29173ba4b1'abi: Gravitymapping:kind: ethereum/eventsapiVersion: 0.0.6language: wasm/assemblyscriptentities:- Gravatar- Transactionabis:- name: Gravityfile: ./abis/Gravity.jsoncallHandlers:- function: createGravatar(string,string)handler: handleCreateGravatar
function
是用于过滤调用的规范化函数签名。 handler
属性是映射中您希望在数据源合约中调用目标函数时执行的函数名称。
每个调用处理程序都有一个参数,该参数的类型对应于被调用函数的名称。 在上面的示例子图中,映射包含一个处理程序,用于调用 createGravatar
函数并接收 CreateGravatarCall
参数作为参数:
import { CreateGravatarCall } from '../generated/Gravity/Gravity'import { Transaction } from '../generated/schema'export function handleCreateGravatar(call: CreateGravatarCall): void {let id = call.transaction.hashlet transaction = new Transaction(id)transaction.displayName = call.inputs._displayNametransaction.imageUrl = call.inputs._imageUrltransaction.save()}
handleCreateGravatar
函数接受一个新的 CreateGravatarCall
,它是 @graphprotocol/graph-ts
提供的ethereum.Call
的子类,包括调用的输入和输出。 CreateGravatarCall
类型是在您运行 graph codegen
时为您生成的。
除了订阅合约事件或函数调用之外,子图可能还希望在将新区块附加到链上时更新其数据。 为了实现这一点,子图可以在每个区块之后,或匹配预定义过滤器的区块之后,运行一个函数。
filter:kind: call
对于每个包含对定义处理程序的合约(数据源)调用的区块,相应的处理程序都会被调用一次。
注意: 调用
处理程序目前依赖于 Parity 跟踪 API。某些网络,如 BNB 链和 Arbitrum,不支持此 API。如果索引其中一个网络的子图包含一个或多个带过滤器的区块调用
处理程序,它将不会开始同步。
块处理程序没有过滤器将确保每个块都调用处理程序。对于每种过滤器类型,一个数据源只能包含一个块处理程序。
dataSources:- kind: ethereum/contractname: Gravitynetwork: devsource:address: '0x731a10897d267e19b34503ad902d0a29173ba4b1'abi: Gravitymapping:kind: ethereum/eventsapiVersion: 0.0.6language: wasm/assemblyscriptentities:- Gravatar- Transactionabis:- name: Gravityfile: ./abis/Gravity.jsonblockHandlers:- handler: handleBlock- handler: handleBlockWithCallToContractfilter:kind: call
Requires specVersion
>= 0.0.8
注意: 投票筛选器仅适用于kind: ethereum
的数据源。
blockHandlers:- handler: handleBlockfilter:kind: pollingevery: 10
所定义的处理程序将在每n
个块上被调用一次,其中n
的值由every
字段提供。这种配置允许子图以固定的区块间隔执行特定的操作。
Requires specVersion
>= 0.0.8
注意: 一次性筛选器仅适用于kind: ethereum
的数据源。
blockHandlers:- handler: handleOncefilter:kind: once
带有 "once filter" 的所定义处理程序将在所有其他处理程序运行之前仅被调用一次。这种配置允许子图将该处理程序用作初始化处理程序,在索引开始时执行特定任务。
export function handleOnce(block: ethereum.Block): void {let data = new InitialData(Bytes.fromUTF8('initial'))data.data = 'Setup data here'data.save()}
映射函数将接收 ethereum.Block
作为其唯一参数。 与事件的映射函数一样,此函数可以访问存储中现有的子图实体、调用智能合约、以及创建或更新实体。
import { ethereum } from '@graphprotocol/graph-ts'export function handleBlock(block: ethereum.Block): void {let id = block.hashlet entity = new Block(id)entity.save()}
如果您需要在 Solidity 中处理匿名事件,可以通过提供事件的主题 0 来实现,如示例所示:
eventHandlers:- event: LogNote(bytes4,address,bytes32,bytes32,uint256,bytes)topic0: '0x644843f351d3fba4abcd60109eaff9f54bac8fb8ccf0bab941009c21df21cf31'handler: handleGive
只有当签名和主题 0 都匹配时才会触发事件。 默认情况下,topic0
等于事件签名的hash值。
从 specVersion`` 0.0.5
和 apiVersion`` 0.0.7
开始,事件处理程序可以访问发出它们的交易接收。
要做到这一点,事件处理程序必须在子图清单中用新的receipt: true
密钥声明,该密钥是可选的,默认为 false。
eventHandlers:- event: NewGravatar(uint256,address,string,string)handler: handleNewGravatarreceipt: true
在处理程序函数内部,可以在 Event.Receipt
字段中访问收据。当接收
密钥设置为 false
或在清单中省略时,将返回空
值。
从 specVersion
0.0.4
开始,子图特征必须使用它们的 camelCase
名称,在清单文件顶层的 features
部分中显式声明,如下表所列:
例如,如果子图使用 Full-Text Search 和 Non-fatal Errors 功能,则清单中的 features
字段应为:
specVersion: 0.0.4description: Gravatar for Ethereumfeatures:- fullTextSearch- nonFatalErrorsdataSources: ...
请注意,在子图部署期间使用未声明的特性会导致验证错误,但如果声明了特性未使用,则不会出现错误。
Timeseries and aggregations enable your subgraph to track statistics like daily average price, hourly total transfers, etc.
This feature introduces two new types of subgraph entity. Timeseries entities record data points with timestamps. Aggregation entities perform pre-declared calculations on the Timeseries data points on an hourly or daily basis, then store the results for easy access via GraphQL.
type Data @entity(timeseries: true) {id: Int8!timestamp: Timestamp!price: BigDecimal!}type Stats @aggregation(intervals: ["hour", "day"], source: "Data") {id: Int8!timestamp: Timestamp!sum: BigDecimal! @aggregate(fn: "sum", arg: "price")}
Timeseries entities are defined with @entity(timeseries: true)
in schema.graphql. Every timeseries entity must have a unique ID of the int8 type, a timestamp of the Timestamp type, and include data that will be used for calculation by aggregation entities. These Timeseries entities can be saved in regular trigger handlers, and act as the “raw data” for the Aggregation entities.
Aggregation entities are defined with @aggregation
in schema.graphql. Every aggregation entity defines the source from which it will gather data (which must be a Timeseries entity), sets the intervals (e.g., hour, day), and specifies the aggregation function it will use (e.g., sum, count, min, max, first, last). Aggregation entities are automatically calculated on the basis of the specified source at the end of the required interval.
hour
: sets the timeseries period every hour, on the hour.day
: sets the timeseries period every day, starting and ending at 00:00.
sum
: Total of all values.count
: Number of values.min
: Minimum value.max
: Maximum value.first
: First value in the period.last
: Last value in the period.
{stats(interval: "hour", where: { timestamp_gt: 1704085200 }) {idtimestampsum}}
Note:
To use Timeseries and Aggregations, a subgraph must have a spec version ≥1.1.0. Note that this feature might undergo significant changes that could affect backward compatibility.
about Timeseries and Aggregations.
在默认情况下,已同步子图上的索引错误会导致子图失败并停止同步。 子图也可以配置为忽略引发错误的处理程序所做的更改, 在出现错误时继续同步。 这使子图作者有时间更正他们的子图,同时继续针对最新区块提供查询,尽管由于导致错误的代码问题,结果可能会不一致。 请注意,某些错误仍然总是致命的,要成为非致命错误,首先需要确定相应的错误是确定性的错误。
注意: Graph 网络尚不支持非致命错误,开发人员不应通过工作室将使用该功能的子图部署到网络。
启用非致命错误需要在子图清单上设置以下功能标志:
specVersion: 0.0.4description: Gravatar for Ethereumfeatures:- nonFatalErrors...
查询还必须通过 subgraphError
参数选择查询可能存在不一致的数据。 还建议查询 _meta
以检查子图是否跳过错误,如示例:
foos(first: 100, subgraphError: allow) {id}_meta {hasIndexingErrors}
如果子图遇到错误,则查询将返回数据和带有消息 "indexing_error"
的 graphql 错误,如以下示例响应所示:
"data": {"foos": [{"id": "0xdead"}],"_meta": {"hasIndexingErrors": true}},"errors": [{"message": "indexing_error"}]
首次部署子图时,它会在相应链的启动区块(或每个数据源定义的 startBlock
处)开始索引事件。在某些情况下,可以使用现有子图已经索引的数据并在更晚的区块上开始索引。 这种索引模式称为Grafting。 例如,嫁接在开发过程中非常有用,可以快速克服映射中的简单错误,或者在现有子图失败后暂时恢复工作。
当 subgraph.yaml
中的子图清单在顶层包含 graft
区块时,子图被嫁接到基础子图:
description: ...graft:base: Qm... # Subgraph ID of base subgraphblock: 7345624 # Block number
当部署其清单包含 graft
区块的子图时,Graph 节点将复制 base
子图的数据,直到并包括给定的 区块
,然后继续从该区块开始索引新子图。 基础子图必须存在于目标图节点实例上,并且必须至少索引到给定区块。 由于这个限制,嫁接只能在开发期间或紧急情况下使用,以加快生成等效的非嫁接子图。
因为嫁接是拷贝而不是索引基础数据,所以子图同步到所需区块比从头开始索引要快得多,尽管对于非常大的子图,初始数据拷贝仍可能需要几个小时。 在初始化嫁接子图时,Graph 节点将记录有关已复制的实体类型的信息。
嫁接子图可以使用一个GraphQL模式,该模式与某个基本子图不同,但仅与基本子图兼容。它本身必须是一个有效的子图模式,但是可以通过以下方式偏离基本子图的模式:
- 它添加或删除实体类型
- 它从实体类型中删除属性
- 它将可为空的属性添加到实体类型
- 它将不可为空的属性转换为可空的属性
- 它将值添加到枚举类型中
- 它添加或删除接口
- 它改变了实现接口的实体类型
文件数据源是一种新的子图功能,用于以稳健、可扩展的方式在索引期间访问链下数据。文件数据源支持从IPFS和Arweave获取文件。
这也为链外数据的确定性索引以及引入任意HTTP源数据奠定了基础。
Rather than fetching files "in line" during handler execution, this introduces templates which can be spawned as new data sources for a given file identifier. These new data sources fetch the files, retrying if they are unsuccessful, running a dedicated handler when the file is found.
This is similar to the , which are used to dynamically create new chain-based data sources.
这将替换现有的ipfs.cat
API
文件数据源需要graph-ts>=0.29.0和graph-cli>=0.33.1
文件数据源不能访问或更新基于链的实体,但必须更新特定于文件的实体。
这可能意味着将现有实体中的字段拆分为单独的实体,并链接在一起。
原始合并实体:
type Token @entity {id: ID!tokenID: BigInt!tokenURI: String!externalURL: String!ipfsURI: String!image: String!name: String!description: String!type: String!updatedAtTimestamp: BigIntowner: User!}
新拆分实体:
type Token @entity {id: ID!tokenID: BigInt!tokenURI: String!ipfsURI: TokenMetadataupdatedAtTimestamp: BigIntowner: String!}type TokenMetadata @entity {id: ID!image: String!externalURL: String!name: String!description: String!}
如果母实体与生成的文件数据源实体之间的关系为1:1,则最简单的模式是通过使用IPFS CID作为查找将母实体链接到生成的文件实体。如果您在建模新的基于文件的实体时遇到困难,请联系Discord!
这是在识别出感兴趣的文件时生成的数据源。
templates:- name: TokenMetadatakind: file/ipfsmapping:apiVersion: 0.0.7language: wasm/assemblyscriptfile: ./src/mapping.tshandler: handleMetadataentities:- TokenMetadataabis:- name: Tokenfile: ./abis/Token.json
目前需要abis
,但无法从文件数据源中调用合同
The file data source must specifically mention all the entity types which it will interact with under entities
. See for more details.
This handler should accept one Bytes
parameter, which will be the contents of the file, when it is found, which can then be processed. This will often be a JSON file, which can be processed with graph-ts
helpers ().
文件的CID作为可读字符串可通过数据源访问
,如下所示:
const cid = dataSource.stringParam()
示例处理程序:
import { json, Bytes, dataSource } from '@graphprotocol/graph-ts'import { TokenMetadata } from '../generated/schema'export function handleMetadata(content: Bytes): void {let tokenMetadata = new TokenMetadata(dataSource.stringParam())const value = json.fromBytes(content).toObject()if (value) {const image = value.get('image')const name = value.get('name')const description = value.get('description')const externalURL = value.get('external_url')if (name && image && description && externalURL) {tokenMetadata.name = name.toString()tokenMetadata.image = image.toString()tokenMetadata.externalURL = externalURL.toString()tokenMetadata.description = description.toString()}tokenMetadata.save()}}
现在,您可以在执行基于链的处理程序期间创建文件数据源:
- 从自动生成的
模板
导入模板 - 从映射中调用
TemplateName.create(cid:string)
,其中cid是有效的IPFS或Arweave内容标识符
对于IPFS,Graph Node支持,以及带有目录的内容标识符(例如bafyreighykzv2we26wfrbzkcdw37sbrby4upq7ae3aqobbq7i4er3tnxci/metadata.json
)。
For Arweave, as of version 0.33.0 Graph Node can fetch files stored on Arweave based on their from an Arweave gateway (). Arweave supports transactions uploaded via Irys (previously Bundlr), and Graph Node can also fetch files based on .
例子:
import { TokenMetadata as TokenMetadataTemplate } from '../generated/templates'const ipfshash = 'QmaXzZhcYnsisuue5WRdQDH6FDvqkLQX1NckLqBYeYYEfm'//This example code is for a Crypto coven subgraph. The above ipfs hash is a directory with token metadata for all crypto coven NFTs.export function handleTransfer(event: TransferEvent): void {let token = Token.load(event.params.tokenId.toString())if (!token) {token = new Token(event.params.tokenId.toString())token.tokenID = event.params.tokenIdtoken.tokenURI = '/' + event.params.tokenId.toString() + '.json'const tokenIpfsHash = ipfshash + token.tokenURI//This creates a path to the metadata for a single Crypto coven NFT. It concats the directory with "/" + filename + ".json"token.ipfsURI = tokenIpfsHashTokenMetadataTemplate.create(tokenIpfsHash)}token.updatedAtTimestamp = event.block.timestamptoken.owner = event.params.to.toHexString()token.save()}
这将创建一个新的文件数据源,该数据源将轮询Graph Node配置的IPFS或Arweave端点,如果未找到文件,则进行重试。当找到文件时,文件数据源处理程序将被执行。
此示例使用 CID 作为母 Token
实体和生成的 TokenMetadata
实体之间的查找。
以前,子图开发人员会在此时调用 ipfs.cat (CID)
来获取文件。
祝贺您,您正在使用文件数据源!
现在,您可以将子图构建
并部署
到任何Graph Node>=v0.30.0-rc.0。
文件数据源处理程序和实体与其他子图实体隔离,确保它们在执行时是确定的,并确保基于链的数据源不受污染。具体来说:
- 文件数据源创建的实体是不可变的,不能更新
- 文件数据源处理程序无法访问其他文件数据源中的实体
- 基于链的处理程序无法访问与文件数据源关联的实体
虽然这个约束对于大多数用例不应该是有问题的,但是对于某些用例,它可能会引入复杂性。如果您在子图中基于文件数据建模时遇到问题,请通过 Discord 与我们联系!
此外,不可能从文件数据源创建数据源,无论是线上数据源还是其他文件数据源。这项限制将来可能会取消。
如果要将 NFT 元数据链接到相应的代币,请使用元数据的 IPFS hash从代币实体引用元数据实体。使用 IPFS hash作为 ID 保存元数据实体。
You can use when creating File Data Sources to pass extra information which will be available to the File Data Source handler.
如果您有多次刷新的实体,请使用 IPFS & 一的基于文件的实体。实体 ID,并使用基于链的实体中的派生字段引用它们
我们正在努力改进上述建议,因此查询只返回“最新”版本。
文件数据源目前需要 ABI,即使没有使用 ABI ()。解决方法是添加任何 ABI。
Handlers for File Data Sources cannot be in files which import eth_call
contract bindings, failing with "unknown import: ethereum::ethereum.call
has not been defined" (). Workaround is to create file data source handlers in a dedicated file.