创建一个子图(一)

创建一个子图(一)

原文作者 :The Graph 基金会

翻译 :The Graph 社区成员


本文介绍如何创建一个子图

在能够使用 Graph CLI 之前,你需要在 Subgraph Studio 或 Legacy Explorer 中创建你的子图。然后,你将能够设置你的子图项目并将其部署到你选择的平台上。

graph init 命令可用于设置一个新的子图项目,可以从任何一个公共以太坊网络上的现有合同,或从一个示例子图。该命令可用于为传统资源管理器或我们新的 Subgraph Studio 创建一个子图,通过传递两个不同的参数 graph init --product hosted-service 用于传统资源管理器,graph init --product subgraph-studio 用于 Subgraph Studio。

从现有的合约

如果你已经有一个智能合约部署在 Ethereum 主网或其中一个测试网,从这个合约引导一个新的子图可以是一个很好的开始。

下面的命令创建了一个子图,索引了现有合约的所有事件。它试图从 Etherscan 中获取合同 ABI,并退回到请求一个本地文件路径。如果缺少任何一个可选参数,它就会带你进入一个交互式表格。

Legacy Explorer:

graph init \  --product hosted-service  --from-contract  \  [--network ] \  [--abi ] \  / []

是你的 github 用户或组织名称,是你的子图的名称,是可选的目录名称,graph init 将把示例子图清单放在其中。

是您现有合同的地址。是合约所在的以太坊网络的名称。是一个合约 ABI 文件的本地路径。--network 和 --abi 都是可选的。

Subgraph Studio:

graph init \  --product subgraph-studio  --from-contract  \  [--network ] \  [--abi ] \   []

是你的子图在 Subgraph Studio 中的 ID,它可以在你的子图详情页上找到。

支持的网络:

The Graph 网络支持子图对主网以太坊的索引:

  • mainnet

在托管服务上支持其他网络:

  • mainnet
  • kovan
  • rinkeby
  • ropsten
  • goerli
  • poa-core
  • poa-sokol
  • xdai
  • matic
  • mumbai
  • fantom
  • bsc
  • chapel
  • clover
  • avalanche
  • fuji
  • celo
  • celo-alfajores
  • fuse
  • mbase
  • arbitrum-one
  • arbitrum-rinkeby
  • optimism
  • optimism-kovan

从一个例子的子图

graph init 支持的第二种模式是从一个例子的子图创建一个新项目。下面的命令可以做到这一点:

Legacy Explorer:

graph init --from-example --product hosted-service / []

Subgraph Studio:

graph init --from-example --product subgraph-studio  []

这个例子的子图是基于 Dani Grant 的 Gravity 合约,该合约管理用户的头像,每当头像被创建或更新时都会发出 NewGravatar 或 UpdateGravatar 事件。该子图通过将 Gravatar 实体写入 Graph Node 存储并确保这些实体根据事件被更新来处理这些事件。下面几节将介绍构成本例中子图清单的文件。

子图清单

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

对于例子中的子图,subgraph.yaml 是:

specVersion: 0.0.1description: Gravatar for Ethereumrepository: https://github.com/graphprotocol/example-subgraphschema:  file: ./schema.graphqldataSources:  - kind: ethereum/contract    name: Gravity    network: mainnet    source:      address: '0x2E645469f354BB4F5c8a05B3b30A929361cf77eC'      abi: Gravity      startBlock: 6175244    mapping:      kind: ethereum/events      apiVersion: 0.0.1      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:        - function: handleBlock        - function: handleBlockWithCall          filter:            kind: call      file: ./hide/mapping.ts

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

  • description:对子图内容的可读描述。当子图被部署到托管服务时,Graph 浏览器会显示此描述。
  • repository:可以找到子图清单的存储库的 URL。这也会由 Graph Explorer 显示。
  • dataSources.source:子图来源的智能合约的地址,以及要使用的智能合约的 ABI。地址是可选的;省略它可以索引所有合约的匹配事件。
  • dataSources.source.startBlock:数据源开始索引的块的可选数字。在大多数情况下,我们建议使用创建合同的区块。
  • dataSources.mapping.entities:数据源写入存储的实体。每个实体的模式在 schema.graphql 文件中定义。
  • dataSources.mapping.abis:一个或多个命名的 ABI 文件,用于源合同以及你在映射中与之互动的任何其他智能合同。
  • dataSources.mapping.eventHandlers:列出该子图反应的智能合约事件以及映射中的处理程序--例子中的 ./hide/mapping.ts--将这些事件转化为存储中的实体。
  • dataSources.mapping.callHandlers:列出了这个子图所反应的智能合约函数和映射中的处理程序,这些处理程序将函数调用的输入和输出转化为商店中的实体。
  • dataSources.mapping.blockHandlers:列出了这个子图所反应的区块和映射中的处理程序,当一个区块被附加到链上时运行。如果没有过滤器,块处理程序将被运行到每个块。一个可选的过滤器可以提供以下几种:如果块包含至少一个对数据源合约的调用,则调用 call` 过滤器将运行处理程序。

一个子图可以索引来自多个智能合约的数据。在 dataSources 数组中为每个需要索引数据的合约添加一个条目。

一个区块内的数据源的触发器使用以下过程排序:

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

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

获得 ABI

ABI 文件必须与你的合同相符。有几种方法可以获得 ABI 文件:

  • 如果你正在建立你自己的项目,你可能会有机会获得你最新的 ABI。
  • 如果你正在为一个公共项目构建一个子图,你可以将该项目下载到你的电脑上,通过使用 truffle 编译(https://truffleframework.com/docs/truffle/overview)或使用 solc 编译来获得 ABI。
  • 你也可以在 Etherscan 上找到 ABI,但这并不总是可靠的,因为那里上传的 ABI 可能已经过时了。请确保你有正确的 ABI,否则运行你的子图会失败。

GraphQL 模式

你的子图的模式在文件 schema.graphql 中。GraphQL 模式是使用 GraphQL 接口定义语言定义的。如果你从来没有写过 GraphQL 模式,建议你看看这个关于 GraphQL 类型系统的入门手册。GraphQL 模式的参考文档可以在 GraphQL API (https://thegraph.com/docs/developer/graphql-api)部分找到。

定义实体

在定义实体之前,重要的是退一步想一想你的数据是如何结构化和链接的。所有的查询都是针对子图模式中定义的数据模型和子图索引的实体进行的。正因为如此,以一种符合你的 dApp 需求的方式来定义子图模式是很好的。把实体想象成 " 包含数据的对象 ",而不是事件或函数,可能会很有用。

使用 The Graph,你只需在 schema.graphql 中定义实体类型,Graph Node 将生成顶层字段,用于查询该实体类型的单个实例和集合。每个应该成为实体的类型都需要用 @entity 指令来注解。

一个好例子

下面的 Gravatar 实体是围绕 Gravatar 对象进行结构化设计的,是一个很好的例子,说明如何定义实体。

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

不好的例子

下面的 GravatarAccepted 和 GravatarDeclined 实体的例子是基于事件的。我们不建议将事件或函数调用 1:1 地映射到实体。

type GravatarAccepted @entity {  id: ID!  owner: Bytes  displayName: String  imageUrl: String}

type GravatarDeclined @entity {  id: ID!  owner: Bytes  displayName: String  imageUrl: String}

可选和必需字段

实体字段可以被定义为必填或可选。必需字段在模式中以!表示。如果在映射中没有设置必填字段,在查询该字段时,你会收到这个错误:

Null value resolved for non-null field 'name'

每个实体必须有一个 id 字段,其类型为 ID! (字符串)。id 字段作为主键,并且在所有相同类型的实体中需要是唯一的。

内置的 Scalar 类型

GraphQL 支持的 Scalar

我们在 GraphQL API 中支持以下 Scalar:

枚举 Enums

你也可以在一个模式中创建枚举。枚举有以下语法:

enum TokenStatus {  OriginalOwner  SecondOwner  ThirdOwner}

一旦在模式中定义了枚举,你就可以使用枚举值的字符串表示在实体上设置一个枚举字段。例如,你可以通过首先定义你的实体,然后用 entity.tokenStatus = "SecondOwner " 来设置这个字段,将 tokenStatus 设置为 SecondOwner。下面的例子展示了带有枚举字段的 Token 实体是什么样子。

关于编写枚举的更多细节可以在 GraphQL 文档(https://graphql.org/learn/schema/)中找到。

实体关系

一个实体可能与你模式中的一个或多个其他实体有关系。这些关系可以在你的查询中被遍历。The Graph 中的关系是单向的。通过在关系的 " 一端 " 定义一个单向关系,可以模拟双向关系。

关系在实体上的定义就像其他字段一样,除了指定的类型是另一个实体的类型。

一对一的关系

定义一个 Transaction 实体类型,并与 TransactionReceipt 实体类型建立可选的一对一的关系:

type Transaction @entity {  id: ID!  transactionReceipt: TransactionReceipt}

type TransactionReceipt @entity {  id: ID!  transaction: Transaction}

一对多的关系

定义一个 TokenBalance 实体类型,该实体类型与 Token 实体类型有必要的一对多关系:

type Token @entity {  id: ID!} type TokenBalance @entity {  id: ID!  amount: Int!  token: Token!}

反向查找

反向查找可以通过 @derivedFrom 字段在实体上定义。这在实体上创建了一个可以被查询的虚拟字段,但不能通过映射 API 手动设置。相反,它是从另一个实体上定义的关系派生出来的。对于这样的关系,存储关系的两边很少有意义,当只有一边被存储而另一边被派生时,索引和查询性能都会更好。

对于一对多的关系,关系应该总是被存储在 " 一 " 边,而 " 多 " 边应该总是被派生。以这种方式存储关系,而不是在 " 多 " 边存储一个实体数组,将使索引和查询子图的性能大大提升。一般来说,应该尽可能地避免存储实体的数组。

例子

我们可以通过派生一个 tokenBalances 字段,使一个 token 的余额可以从 token 中访问。

type Token @entity {  id: ID!  tokenBalances: [TokenBalance!]! @derivedFrom(field: "token")}

type TokenBalance @entity {  id: ID!  amount: Int!  token: Token!}

多对多的关系

对于多对多的关系,如用户可能属于任何数量的组织,最直接的,但通常不是最有效的方式是将关系建模为两个相关实体中的一个数组。如果关系是对称的,那么只需要存储关系的一边,另一边可以被导出。

例子

定义从 User 实体类型到 Organization 实体类型的反向查找。在下面的示例中,这是通过 members 从 Organization 实体内查找属性来实现的。在查询中,organizations 字段 onUser 将通过查找 Organization 包含用户 ID 的所有实体来解析。

type Organization @entity {  id: ID!  name: String!  members: [User!]!}

type User @entity {  id: ID!  name: String!  organizations: [Organization!]! @derivedFrom(field: "members")}

存储这种关系的一个更有效的方法是通过一个映射表,为每个 User / Organization 对提供一个条目,其模式为:

type Organization @entity {  id: ID!  name: String!  members: [UserOrganization]! @derivedFrom(field: "user")}

type User @entity {  id: ID!  name: String!  organizations: [UserOrganization!] @derivedFrom(field: "organization")}

type UserOrganization @entity {  id: ID! # Set to `${user.id}-${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: ID!  address: Bytes!}