开发 > 创建子图

创建子图

子图从区块链中提取数据,对其进行处理并存储,以便通过 GraphQL 轻松查询。

定义子图

子图定义由几个文件组成:

为了在Graph的去中心化网络上使用子图,您需要创建一个API密钥。建议您将至少10000 GRT信号添加到子图中。

在详细了解清单文件的内容之前,您需要安装Graph CLI,以构建和部署子图。

安装 Graph CLI

链到本节

Graph CLI 是使用 JavaScript 编写的,您需要安装yarnnpm才能使用它;以下教程中假设您已经安装了 yarn。

一旦您安装了yarn,可以通过运行以下命令安装 Graph CLI

用 yarn 安装:

yarn global add @graphprotocol/graph-cli

用 npm 安装:

npm install -g @graphprotocol/graph-cli

graph init 命令可用于从任何公共以太坊网络上的现有合约或示例子图中设置新的子图项目。 此命令可用于通过 graph init --product subgraph-studio 命令在 Subgraph 工作室上创建子图。 如果你已经在你喜欢的网络上部署了一个智能合约,那么从该合约中引导一个新的子图可能是一个很好的开始方式。

基于现有合约

链到本节

以下命令创建一个索引现有合约的所有事件的子图。 它尝试从 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>

示例子图基于 Dani Grant 的 Gravity 合约,该合约管理用户头像并在创建或更新头像时发出 NewGravatarUpdateGravatar 事件。 子图通过将 Gravatar 实体写入 Graph Node存储并确保根据事件更新这些事件来处理这些事件。 以下部分将介绍构成此示例的子图清单的文件。

将新数据源添加到现有子图

链到本节

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 路径),并创建一个新的 dataSourcegraph init 命令创建 dataSource --from-contract 的方式相同,相应地更新架构和映射。

--merge-实体选项标识开发人员希望如何处理实体事件名称冲突:

  • 如果为true:新的数据源应该使用现有的事件处理程序& 和实体
  • 如果为false:应使用${dataSourceName}{EventName}创建新的实体& 和事件处理程序。

合约地址将写入相关网络的networks.json

**注意:**使用交互式cli时,在成功运行graph init后,将提示您添加新的dataSource

子图清单文件

链到本节

子图清单 subgraph.yaml 定义了您的子图索引的智能合约,这些合约中需要关注的事件,以及如何将事件数据映射到 Graph 节点存储并允许查询的实体。 子图清单的完整规范可以在这里找到。

对于示例子图,subgraph.yaml 的内容是:

specVersion: 0.0.4
description: Gravatar for Ethereum
repository: https://github.com/graphprotocol/graph-tooling
schema:
file: ./schema.graphql
dataSources:
- kind: ethereum/contract
name: Gravity
network: mainnet
source:
address: '0x2E645469f354BB4F5c8a05B3b30A929361cf77eC'
abi: Gravity
startBlock: 6175244
endBlock: 7175245
context:
foo:
type: Bool
data: true
bar:
type: String
data: 'bar'
mapping:
kind: ethereum/events
apiVersion: 0.0.6
language: wasm/assemblyscript
entities:
- Gravatar
abis:
- name: Gravity
file: ./abis/Gravity.json
eventHandlers:
- event: NewGravatar(uint256,address,string,string)
handler: handleNewGravatar
- event: UpdatedGravatar(uint256,address,string,string)
handler: handleUpdatedGravatar
callHandlers:
- function: createGravatar(string,string)
handler: handleCreateGravatar
blockHandlers:
- handler: handleBlock
- handler: handleBlockWithCall
filter:
kind: call
file: ./src/mapping.ts

清单中要更新的重要条目是:

  • description: a human-readable description of what the subgraph is. This description is displayed by the Graph Explorer when the subgraph is deployed to the hosted service.

  • repository:可以找到子图清单的存储库的 URL。 这也由 Graph 浏览器显示。

  • features:所有使用的 功能 名称的列表。

  • 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 like Bool, String, Int, Int8, BigDecimal, Bytes, List, and BigInt. Each variable needs to specify its type and data. 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 数组的合约添加一个条目,单个子图可以索引来自多个智能合约的数据。

区块内数据源的触发器使用以下流程进行排序:

  1. 事件和调用触发器首先按区块内的交易索引排序。
  2. 同一交易中的事件和调用触发器使用约定进行排序:首先是事件触发器,然后是调用触发器,每种类型都遵循它们在清单中定义的顺序。
  3. 区块触发器按照它们在清单中定义的顺序,在事件和调用触发器之后运行。

这些排序规则可能会发生变化。

获取 ABI

链到本节

ABI 文件必须与您的合约相匹配。 获取 ABI 文件的方法有以下几种:

  • 如果您正在构建自己的项目,您可以获取最新的 ABI。
  • 如果您正在为公共项目构建子图,则可以将该项目下载到您的计算机,并通过使用 truffle compile,或使用 solc 进行编译来获取 ABI。
  • 您还可以在 Etherscan 上找到 ABI,但这并不总是可靠的,因为在那里上传的 ABI 可能已过期。 请确保您拥有正确的 ABI,否则您的子图将会运行失败。

GraphQL 模式

链到本节

您子图的模式定义位于文件 schema.graphql 中。 GraphQL 模式是使用 GraphQL 接口定义语言定义的。 如果您从未编写过 GraphQL 模式,建议您在 GraphQL 类型系统上查看入门教程。 GraphQL 模式的参考文档可以在 GraphQL API 部分中找到。

定义实体

链到本节

在定义实体之前,重要的是要退后一步,思考数据的结构和链接方式。 所有查询都将针对子图模式中定义的数据模型和子图索引的实体进行。 因此,最好以符合 dapp 需求的方式定义子图模式。 将实体想象为“包含数据的对象”,而不是事件或函数,可能很有用。

使用 Graph,您只需在 schema.Graphql 中定义实体类型,Graph 节点将生成顶级字段,用于查询该实体类型的单个实例和集合。应该是一个实体的每个类型都需要用@entity 指令进行注释。默认情况下,实体是可变的,这意味着映射可以加载现有实体,修改它们并存储该实体的新版本。可变性是有代价的,对于已知永远不会被修改的实体类型,例如,因为它们只是包含从链中逐字提取的数据,建议使用@entity (immutable: true)将它们标记为不可变的。只要这些更改发生在创建实体的同一区块中,映射就可以对不可变实体进行更改。不可变实体的写入和查询速度要快得多,因此应尽可能使用它们。

好代码的例子

链到本节

下面的 Gravatar 实体围绕 Gravatar 对象构建,是如何定义实体的一个很好的示例。

type Gravatar @entity {
id: Id!
owner: Bytes
displayName: String
imageUrl: String
accepted: Boolean
}

坏榜样

链到本节

下面的示例中,GravatarAcceptedGravatarDeclined 实体都基于事件。 不建议将事件或函数调用以 1:1 的方式映射到实体。

type GravatarAccepted @entity {
id: Bytes!
owner: Bytes
displayName: String
imageUrl: String
}
type GravatarDeclined @entity {
id: Bytes!
owner: Bytes
displayName: String
imageUrl: 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 支持的标量

链到本节

我们在 GraphQL API 中支持以下标量:

类型描述
字节字节数组,表示为十六进制字符串。 通常用于以太坊hash和地址。
字符串string 值的标量。 不支持空字符,并会自动进行删除。
Booleanboolean 值的标量。
IntGraphQL 规范将 Int 定义为 32 字节的大小。
Int8An 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大整数。 用于以太坊的 uint32int64uint64、...、uint256 类型。 注意:uint32以下的所有类型,例如int32uint24int8都表示为i32
BigDecimalBigDecimal 表示为有效数字和指数的高精度小数。 指数范围是 -6143 到 +6144。 四舍五入到 34 位有效数字。

枚举类型

链到本节

您还可以在模式中创建枚举类型。 枚举类型具有以下语法:

enum TokenStatus {
OriginalOwner
SecondOwner
ThirdOwner
}

在模式中定义枚举后,您可以使用枚举值的字符串表示形式在实体上设置枚举字段。 例如,您可以将 tokenStatus 设置为 SecondOwner,方法是首先定义您的实体,然后使用 entity.tokenStatus = "SecondOwner 设置字段。 下面的示例演示了带有枚举字段的 Token 实体:

有关编写枚举的更多详细信息,请参阅 GraphQL 文档

实体关系

链到本节

一个实体可能与模式中的一个或多个其他实体发生联系。 您可以在您的查询中遍历这些联系。 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 entity
organization {
name
}
}
}
}

这种存储多对多关系的更精细的方式将导致为子图存储的数据更少,因此子图的索引和查询速度通常会大大加快。

向模式添加注释

链到本节

根据 GraphQL 规范,可以使用双引号 "" 在模式实体属性上方添加注释。 这在下面的示例中进行了说明:

type MyFirstEntity @entity {
"unique identifier and primary key of the entity"
id: Bytes!
address: Bytes!
}

定义全文搜索字段

链到本节

全文搜索查询根据文本搜索输入来过滤和排列实体。 通过在与索引文本数据进行比较之前,将查询文本输入处理到词干中,全文查询能够返回相似词的匹配项。

全文查询定义包括查询名称、用于处理文本字段的语言词典、用于对结果进行排序的排序算法,以及搜索中包含的字段。 每个全文查询可能跨越多个字段,但所有包含的字段必须来自单个实体类型。

要添加全文查询,请在 GraphQL 模式中包含带有全文指令的 _Schema_ 类型。

type _Schema_
@fulltext(
name: "bandSearch"
language: en
algorithm: rank
include: [{ entity: "Band", fields: [{ name: "name" }, { name: "description" }, { name: "bio" }] }]
)
type Band @entity {
id: Bytes!
name: String!
description: String!
bio: String
wallet: Address
labels: [Label!]!
discography: [Album!]!
members: [Musician!]!
}

示例 bandSearch 字段可用于查询,以根据 namedescriptionbio 字段中的文本文档,来过滤 Band 实体。 请跳转到 GraphQL API - 查询,了解全文搜索 API 的描述和更多示例用法。

query {
bandSearch(text: "breaks & electro & detroit") {
id
name
description
wallet
}
}

**功能管理:**从 specVersion 0.0.4 及以后,必须在子图清单的 features 部分下声明 fullTextSearch

支持的语言

链到本节

选择不同的语言将对全文搜索 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 类似,但也包括匹配的接近程度。

编写映射

链到本节

映射将获取的以太坊数据转换为您的模式文件中定义的实体。 映射是用 TypeScript 的子集编写的,称为 [AssemblyScript]。 AssemblyScript 可以编译成 WASM (WebAssembly)。 AssemblyScript 比普通的 TypeScript 更严格,但提供了开发者熟悉的语法。

对于在 mapping.eventHandlers 下的 subgraph.yaml 中定义的每个事件处理程序,都会创建一个同名的导出函数。 每个处理程序必须接受一个名为 event 的参数,其类型对应于正在处理的事件的名称。

在示例子图中,src/mapping.ts 包含 NewGravatarUpdatedGravatar 事件的处理程序:

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.owner
gravatar.displayName = event.params.displayName
gravatar.imageUrl = event.params.imageUrl
gravatar.save()
}
export function handleUpdatedGravatar(event: UpdatedGravatar): void {
let id = event.params.id
let gravatar = Gravatar.load(id)
if (gravatar == null) {
gravatar = new Gravatar(id)
}
gravatar.owner = event.params.owner
gravatar.displayName = event.params.displayName
gravatar.imageUrl = event.params.imageUrl
gravatar.save()
}

第一个处理程序接受 NewGravatar 事件,而且使用 new Gravatar(event.params.id.toHex()) 创建一个新的 Gravatar 实体,使用相应的事件参数填充实体字段。 该实体实例由变量 gravatar 表示,id 值为 event.params.id.toHex()

第二个处理程序尝试从 Graph 节点存储加载现有的 Gravatar。 如果尚不存在,则会按需创建。 然后更新实体以匹配新的事件参数,并使用 gravatar.save() 将其保存。

用于创建新实体的推荐 ID

链到本节

每个实体都必须有一个在所有相同类型的实体中唯一的 id。 实体的 id 值在创建实体时设置。 以下是创建新实体时要考虑的一些推荐 id 值。 注意:id 的值必须是 string

  • event.params.id.toHex()
  • event.transaction.from.toHex()
  • event.transaction.hash.toHex() + "-" + event.logIndex.toString()

We provide the Graph Typescript Library which contains utilities for interacting with the Graph Node store and conveniences for handling smart contract data and entities. You can use this library in your mappings by importing @graphprotocol/graph-ts in mapping.ts.

代码生成

链到本节

为了使与智能合约、事件和实体的代码编写工作变得简单且类型安全,Graph CLI 可以从子图的 GraphQL 模式和数据源中包含的合约 ABI 生成 AssemblyScript 类型。

这可以通过以下命令实现

graph codegen [--output-dir <OUTPUT_DIR>] [<MANIFEST>]

但在大多数情况下,子图已经通过 package.json 进行了预配置,以允许您简单地运行以下命令之一来实现相同的目的:

# Yarn
yarn codegen
# NPM
npm 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 后,都必须再次执行代码生成。 在构建或部署子图之前,它还必须至少执行一次。

代码生成不会检查 src/mapping.ts 中的映射代码。 如果您想在尝试将子图部署到 Graph Explorer 之前进行检查,您可以运行 yarn build,并修复 TypeScript 编译器可能发现的任何语法错误。

数据源模板

链到本节

EVM兼容智能合约中的一种常见模式是使用注册表或工厂合约,其中一个合约创建、管理或引用任意数量的其他合约,每个合约都有自己的状态和事件。

这些子合约的地址可能事先知道,也可能不知道,其中许多合约可能会随着时间的推移而创建和/或添加。这就是为什么在这种情况下,定义单个数据源或固定数量的数据源是不可能的,需要一种更动态的方法:数据源模板

主合约的数据源

链到本节

首先,您需要为主合约定义一个常规数据源。 下面的代码片段显示了 Uniswap 交换工厂合约的简化示例数据源。 注意 NewExchange(address,address) 事件处理程序。 当工厂合约在链上创建新交换合约时,会发出此消息。

dataSources:
- kind: ethereum/contract
name: Factory
network: mainnet
source:
address: '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95'
abi: Factory
mapping:
kind: ethereum/events
apiVersion: 0.0.6
language: wasm/assemblyscript
file: ./src/mappings/factory.ts
entities:
- Directory
abis:
- name: Factory
file: ./abis/factory.json
eventHandlers:
- event: NewExchange(address,address)
handler: handleNewExchange

动态创建合约的数据源模板

链到本节

然后,将 数据源模板 添加到清单中。 它们与常规数据源相同,只是在 source 下缺少预先定义的合约地址。 通常,您需要为母合约管理或引用的每种类型的子合约定义一个模板。

dataSources:
- kind: ethereum/contract
name: Factory
# ... other source fields for the main contract ...
templates:
- name: Exchange
kind: ethereum/contract
network: mainnet
source:
abi: Exchange
mapping:
kind: ethereum/events
apiVersion: 0.0.6
language: wasm/assemblyscript
file: ./src/mappings/exchange.ts
entities:
- Exchange
abis:
- name: Exchange
file: ./abis/exchange.json
eventHandlers:
- 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 contract
Exchange.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')

对于所有的值类型,都有像 setStringgetString 这样的 setter 和 getter。

起始区块

链到本节

startBlock 是一个可选配置,允许您定义数据源从区块链中的哪个区块开始索引。 设置起始区块允许数据源跳过潜在的数百万个不相关的区块。 通常,子图开发人员会将 startBlock 设置为创建数据源智能合约的区块。

dataSources:
- kind: ethereum/contract
name: ExampleSource
network: mainnet
source:
address: '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95'
abi: ExampleContract
startBlock: 6627917
mapping:
kind: ethereum/events
apiVersion: 0.0.6
language: wasm/assemblyscript
file: ./src/mappings/factory.ts
entities:
- User
abis:
- name: ExampleContract
file: ./abis/ExampleContract.json
eventHandlers:
- event: NewEvent(address,address)
handler: handleNewEvent

注意: 合约创建区块可以在 Etherscan 上快速查找:

  1. 通过在搜索栏中输入合约地址来搜索合约。
  2. 单击 Contract Creator 部分中的创建交易hash。
  3. 加载交易详情页面,您将在其中找到该合约的起始区块。

调用处理程序

链到本节

虽然事件提供了一种收集合约状态相关变换的有效方法,但许多合约避免生成日志以优化 gas 成本。 在这些情况下,子图可以订阅对数据源合约的调用。 这是通过定义引用函数签名的调用处理程序,及处理对该函数调用的映射处理程序来实现的。 为了处理这些调用,映射处理程序将接收一个 ethereum.Call 作为参数,其中包含调用的类型化输入和输出。 在交易调用链中的任何深度进行的调用都会触发映射,从而捕获通过代理合约与数据源合约的交互活动。

调用处理程序只会在以下两种情况之一触发:当指定的函数被合约本身以外的账户调用时,或者当它在 Solidity 中被标记为外部,并作为同一合约中另一个函数的一部分被调用时。

注意: 调用处理程序目前依赖于 Parity 跟踪 API。某些网络,如 BNB 链和 Arbitrum,不支持此 API。如果索引其中一个网络的子图包含一个或多个调用处理程序,它将不会开始同步。子图开发人员应该使用事件处理程序。它们比调用处理程序性能好得多,并且在每个 evm 网络上都受到支持。

定义调用处理程序

链到本节

要在清单中定义调用处理程序,只需在您要订阅的数据源下添加一个 callHandlers 数组。

dataSources:
- kind: ethereum/contract
name: Gravity
network: mainnet
source:
address: '0x731a10897d267e19b34503ad902d0a29173ba4b1'
abi: Gravity
mapping:
kind: ethereum/events
apiVersion: 0.0.6
language: wasm/assemblyscript
entities:
- Gravatar
- Transaction
abis:
- name: Gravity
file: ./abis/Gravity.json
callHandlers:
- 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.hash
let transaction = new Transaction(id)
transaction.displayName = call.inputs._displayName
transaction.imageUrl = call.inputs._imageUrl
transaction.save()
}

handleCreateGravatar 函数接受一个新的 CreateGravatarCall,它是 @graphprotocol/graph-ts提供的ethereum.Call 的子类,包括调用的输入和输出。 CreateGravatarCall 类型是在您运行 graph codegen 时为您生成的。

区块处理程序

链到本节

除了订阅合约事件或函数调用之外,子图可能还希望在将新区块附加到链上时更新其数据。 为了实现这一点,子图可以在每个区块之后,或匹配预定义过滤器的区块之后,运行一个函数。

支持的过滤器

链到本节

调用筛选器

链到本节
filter:
kind: call

对于每个包含对定义处理程序的合约(数据源)调用的区块,相应的处理程序都会被调用一次。

注意: 调用处理程序目前依赖于 Parity 跟踪 API。某些网络,如 BNB 链和 Arbitrum,不支持此 API。如果索引其中一个网络的子图包含一个或多个带过滤器的区块调用处理程序,它将不会开始同步。

块处理程序没有过滤器将确保每个块都调用处理程序。对于每种过滤器类型,一个数据源只能包含一个块处理程序。

dataSources:
- kind: ethereum/contract
name: Gravity
network: dev
source:
address: '0x731a10897d267e19b34503ad902d0a29173ba4b1'
abi: Gravity
mapping:
kind: ethereum/events
apiVersion: 0.0.6
language: wasm/assemblyscript
entities:
- Gravatar
- Transaction
abis:
- name: Gravity
file: ./abis/Gravity.json
blockHandlers:
- handler: handleBlock
- handler: handleBlockWithCallToContract
filter:
kind: call

投票筛选器

链到本节

Requires specVersion >= 0.0.8

注意: 投票筛选器仅适用于kind: ethereum的数据源。

blockHandlers:
- handler: handleBlock
filter:
kind: polling
every: 10

所定义的处理程序将在每n个块上被调用一次,其中n的值由every字段提供。这种配置允许子图以固定的区块间隔执行特定的操作。

一次性筛选器

链到本节

Requires specVersion >= 0.0.8

注意: 一次性筛选器仅适用于kind: ethereum的数据源。

blockHandlers:
- handler: handleOnce
filter:
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.hash
let 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.5apiVersion`` 0.0.7开始,事件处理程序可以访问发出它们的交易接收。

要做到这一点,事件处理程序必须在子图清单中用新的receipt: true 密钥声明,该密钥是可选的,默认为 false。

eventHandlers:
- event: NewGravatar(uint256,address,string,string)
handler: handleNewGravatar
receipt: true

在处理程序函数内部,可以在 Event.Receipt字段中访问收据。当接收密钥设置为 false 或在清单中省略时,将返回值。

实验性特征

链到本节

specVersion 0.0.4 开始,子图特征必须使用它们的 camelCase 名称,在清单文件顶层的 features 部分中显式声明,如下表所列:

特征名称
非致命错误nonFatalErrors
全文搜索fullTextSearch
嫁接grafting
以太坊合约上的IPFSipfsOnEthereumContracts or nonDeterministicIpfs

例如,如果子图使用 Full-Text SearchNon-fatal Errors 功能,则清单中的 features 字段应为:

specVersion: 0.0.4
description: Gravatar for Ethereum
features:
- fullTextSearch
- nonFatalErrors
dataSources: ...

请注意,在子图部署期间使用未声明的特性会导致验证错误,但如果声明了特性未使用,则不会出现错误。

以太坊合约上的IPFS

链到本节

如果将这些数据在链上维护,成本会太高,所以将 IPFS 与以太坊结合的一个常见用例是将数据存储在 IPFS 上,并在以太坊合约中引用 IPFS hash。

给定这样的 IPFS hash,子图可以使用 ipfs.catipfs.map 从 IPFS 读取相应的文件。 但是,要可靠地执行此操作,需要将这些文件有效地锚定在 IPFS 节点上。这样 托管服务的IPFS 节点在索引过程中能够找到他们。

注意: Graph网络暂不支持ipfs.catipfs.map,开发者请勿部署使用该功能的子图通过 Studio 连接到网络。

特征管理 ipfsOnEthereumContracts 必须在子图清单中features 下声明。对非EVM链, 也可以使用nonDeterministicIpfs别名来实现相同的目的。

在运行本地Graph节点时,必须设置GRAPH_ALLOW_NON_DETERMINISTIC_IPFS环境变量,以便使用此实验性功能对子图进行索引。

非致命错误

链到本节

在默认情况下,已同步子图上的索引错误会导致子图失败并停止同步。 子图也可以配置为忽略引发错误的处理程序所做的更改, 在出现错误时继续同步。 这使子图作者有时间更正他们的子图,同时继续针对最新区块提供查询,尽管由于导致错误的代码问题,结果可能会不一致。 请注意,某些错误仍然总是致命的,要成为非致命错误,首先需要确定相应的错误是确定性的错误。

注意: Graph 网络尚不支持非致命错误,开发人员不应通过工作室将使用该功能的子图部署到网络。

启用非致命错误需要在子图清单上设置以下功能标志:

specVersion: 0.0.4
description: Gravatar for Ethereum
features:
- 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"
}
]

嫁接到现有子图

链到本节

注意: 在初次升级到The Graph Network时,不建议使用grafting。可以在这里了解更多信息。

首次部署子图时,它会在相应链的启动区块(或每个数据源定义的 startBlock 处)开始索引事件。在某些情况下,可以使用现有子图已经索引的数据并在更晚的区块上开始索引。 这种索引模式称为Grafting。 例如,嫁接在开发过程中非常有用,可以快速克服映射中的简单错误,或者在现有子图失败后暂时恢复工作。

subgraph.yaml 中的子图清单在顶层包含 graft 区块时,子图被嫁接到基础子图:

description: ...
graft:
base: Qm... # Subgraph ID of base subgraph
block: 7345624 # Block number

当部署其清单包含 graft 区块的子图时,Graph 节点将复制 base 子图的数据,直到并包括给定的 区块,然后继续从该区块开始索引新子图。 基础子图必须存在于目标图节点实例上,并且必须至少索引到给定区块。 由于这个限制,嫁接只能在开发期间或紧急情况下使用,以加快生成等效的非嫁接子图。

因为嫁接是拷贝而不是索引基础数据,所以子图同步到所需区块比从头开始索引要快得多,尽管对于非常大的子图,初始数据拷贝仍可能需要几个小时。 在初始化嫁接子图时,Graph 节点将记录有关已复制的实体类型的信息。

嫁接子图可以使用一个GraphQL模式,该模式与某个基本子图不同,但仅与基本子图兼容。它本身必须是一个有效的子图模式,但是可以通过以下方式偏离基本子图的模式:

  • 它添加或删除实体类型
  • 它从实体类型中删除属性
  • 它将可为空的属性添加到实体类型
  • 它将不可为空的属性转换为可空的属性
  • 它将值添加到枚举类型中
  • 它添加或删除接口
  • 它改变了实现接口的实体类型

特征管理 grafting必须在子图清单中的features下声明。

文件数据源

链到本节

文件数据源是一种新的子图功能,用于以稳健、可扩展的方式在索引期间访问链下数据。文件数据源支持从IPFS和Arweave获取文件。

这也为链外数据的确定性索引以及引入任意HTTP源数据奠定了基础。

概述

链到本节

这不是在处理程序执行期间“在线”获取文件,而是引入了可以作为给定文件标识符的新数据源生成的模板。这些新数据源获取文件,如果不成功则重试,找到文件后运行专用处理程序。

这类似于现有的数据源模板,用于动态创建新的基于链的数据源。

这将替换现有的ipfs.cat API

升级指南

链到本节

更新graph-tsgraph-cli

链到本节

文件数据源需要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: BigInt
owner: User!
}

新拆分实体:

type Token @entity {
id: ID!
tokenID: BigInt!
tokenURI: String!
ipfsURI: TokenMetadata
updatedAtTimestamp: BigInt
owner: String!
}
type TokenMetadata @entity {
id: ID!
image: String!
externalURL: String!
name: String!
description: String!
}

如果母实体与生成的文件数据源实体之间的关系为1:1,则最简单的模式是通过使用IPFS CID作为查找将母实体链接到生成的文件实体。如果您在建模新的基于文件的实体时遇到困难,请联系Discord!

可以使用嵌套过滤器根据这些嵌套实体过滤母实体。

添加一个新的模板数据源,使用kind: file/ipfskind: file/arweave

链到本节

这是在识别出感兴趣的文件时生成的数据源。

templates:
- name: TokenMetadata
kind: file/ipfs
mapping:
apiVersion: 0.0.7
language: wasm/assemblyscript
file: ./src/mapping.ts
handler: handleMetadata
entities:
- TokenMetadata
abis:
- name: Token
file: ./abis/Token.json

目前需要abis,但无法从文件数据源中调用合同

文件数据源必须特别提到它将在实体下与之交互的所有实体类型。有关详细信息,请参阅限制

创建新处理程序以处理文件

链到本节

此处理程序应接受一个Bytes参数,当找到文件时,该参数将是文件的内容,然后可以对其进行处理。这通常是一个JSON文件,可以用graph-ts 助手(文件).处理。

文件的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支持v0和v1内容标识符,以及带有目录的内容标识符(例如bafyreighykzv2we26wfrbzkcdw37sbrby4upq7ae3aqobbq7i4er3tnxci/metadata.json)。

For Arweave, as of version 0.33.0 Graph Node can fetch files stored on Arweave based on their transaction ID from an Arweave gateway (example file). Arweave supports transactions uploaded via Bundlr, and Graph Node can also fetch files based on Bundlr manifests.

例子:

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.tokenId
token.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 = tokenIpfsHash
TokenMetadataTemplate.create(tokenIpfsHash)
}
token.updatedAtTimestamp = event.block.timestamp
token.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 保存元数据实体。

在创建文件数据源时,可以使用 DataSource context来传递文件数据源处理程序可以使用的额外信息。

如果您有多次刷新的实体,请使用 IPFS & 一的基于文件的实体。实体 ID,并使用基于链的实体中的派生字段引用它们

我们正在努力改进上述建议,因此查询只返回“最新”版本。

已知问题

链到本节

文件数据源目前需要 ABI,即使没有使用 ABI (问题)。解决方法是添加任何 ABI。

文件数据源的处理程序不能在导入 eth _ call 契约绑定的文件中,如果“未知导入: etherum:: etherum.call 尚未定义”(问题) 则失败。解决办法是在专用文件中创建文件数据源处理程序。

例子

链到本节

加密魔法师子图迁移

参考

链到本节

GIP文件数据源

编辑

上页
支持的网络
下页
AssemblyScript API
编辑