官方文档|托管服务之定义子图

子图定义了The Graph从以太坊中索引哪些数据以及如何存储。一旦部署,它将成为全局区块链数据图的一部分。

官方文档|托管服务之定义子图

GRT Fans修改版
原文作者:The Graph
译者:depil sun
译文修改:smalloranges
原文出处https://thegraph.com/docs/define-a-subgraph
翻译出处:https://depilme.medium.com/定义subgraph-8868dcc1c88f


定义子图

子图定义了The Graph从以太坊中索引哪些数据以及如何存储。一旦部署,它将成为全局区块链数据图的一部分。

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

  • subgraph.yaml:包含子图清单的YAML文件
  • schema.graphql:一个GraphQL模式,该模式定义为子图存储的数据以及如何通过GraphQL查询数据
  • AssemblyScript Mappings:从以太坊中的事件数据转换为架构中定义的实体的AssemblyScript代码(例如mapping.ts,本教程中的)

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

安装The Graph CLI

The Graph CLI是用JavaScript编写的,您将需要安装 yarnnpm才能使用它;假定您具备yarn以下条件。详细的安装说明yarn 可以在 graph-cli仓库中找到

完成后yarn,通过运行以下命令安装Graph CLI:

yarn global add @graphprotocol/graph-cli

创建一个子图项目

graph init命令可用于通过任何公共以太坊网络上的现有合约或示例子图来建立新的子图项目 。

从现有合约开始

如果您已经将智能合约部署到以太坊主网或其中一个测试网,则从该合约引导新的子图可能是入门的好方法。

以下命令创建一个子图,该子图索引现有合同的所有事件。它尝试从Etherscan获取合同ABI,然后回退至请求本地文件路径。如果缺少任何可选参数,它将带您进入交互式表格。

graph init \  
--from-contract <CONTRACT_ADDRESS> \  
[--network <ETHEREUM_NETWORK>] \  
[--abi <FILE>] \  
<GITHUB_USER>/<SUBGRAPH_NAME> [<DIRECTORY>]

<GITHUB_USER>是你的GitHub用户或组织名称,<SUBGRAPH_NAME>是您的子图的名称,<DIRECTORY> (可选)是所在的目录的名称,graph init将把子图清单放在该目录下。

<CONTRACT_ADDRESS>是您现有合同的地址。 <ETHEREUM_NETWORK>是以太坊网络上的合约名称。<FILE>是合约ABI文件的本地路径。--network--abi都是可选项。

托管服务支持的网络有:

  • mainnet
  • kovan
  • rinkeby
  • ropsten
  • goerli
  • poa-core
  • xdai
  • poa-sokol
  • matic
  • mumbai
  • fantom
  • bsc
  • clover

从一个子图示例开始

第二种模式graph init支持从示例子图创建一个新项目 。下面的命令执行此操作:

graph init --from-example <GITHUB_USER>/<SUBGRAPH_NAME> [<DIRECTORY>]

该示例子图基于Dani GrantGravity合约,该合约管理用户头像和分发事件NewGravatar,以及头像创建或更新事件UpdateGravatarGravatar。子图通过将Gravatar写入图节点存储并确保根据事件更新这些实体来处理这些事件。以下各节将介绍构成此示例的子图清单的文件。

子图清单

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

对于示例子图,subgraph.yaml为:

specVersion: 0.0.1
description: Gravatar for Ethereum
repository: https://github.com/graphprotocol/example-subgraph
schema:
  file: ./schema.graphql
dataSources:
  - 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: ./src/mapping.ts

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

  • description:子图是什么的可读描述。当子图部署到托管服务时,图资源管理器将显示此描述。
  • repository:可以在其中找到子图清单库的URL。The Graph浏览器也会显示此内容。
  • dataSources.sourceaddress子图来源的智能合约以及abi要使用的智能合约。address是可选的;省略它将允许索引所有合约的匹配事件。
  • dataSources.source.startBlock:数据源从其开始索引的块的可选编号。在大多数情况下,我们建议使用创建合同的区块。
  • dataSources.mapping.entities:数据源写入存储的实体。schema.graphql文件中定义了每个实体的架构。
  • dataSources.mapping.abis:一个或多个命名ABI文件,用于源合同以及您在映射中与之交互的任何其他智能合约。
  • dataSources.mapping.eventHandlers:列出此子图所响应的智能合约事件以及映射./src/mapping.ts中的处理程序(在示例中),这些处理程序将这些事件转换为商店中的实体。
  • dataSources.mapping.callHandlers:列出该子图对智能合约功能的反应以及映射中的处理程序,这些处理程序将输入和输出转换为功能调用,并将其转换为存储中的实体。
  • dataSources.mapping.blockHandlers:列出该子图对应的区块,并在将块附加到链中时运行映射中的处理程序。没有过滤器,块处理程序将在每个块上运行。可以提供以下几种可选过滤器:call。如果一个区块包含至少一个对数据源合约的请求,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 API部分也提供了GraphQL模式的参考文档 。

定义实体

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

使用The Graph,您只需定义实体类型schema.graphql,并且Graph节点将生成顶级字段,用于查询该实体类型的单个实例和集合。每种应为实体​​的类型都必须使用@entity指令进行注释。

正确示例

Gravatar下面的实体围绕Gravatar对象构建,是一个很好的关于定义实体的例子。

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

错误示例

下面的示例GravatarAcceptedGravatarDeclined实体基于事件。不建议将事件或函数调用映射到实体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字段用作主键,并且在相同类型的所有实体之间必须唯一。

内置标量类型

GraphQL支持的标量

我们的GraphQL API支持以下标量:

类型 描述
Bytes 字节数组,以十六进制字符串表示。常用于以太坊哈希和地址。
ID 储存为string
String string值的标量。不支持空字符,它们会被自动删除。
Boolean boolean的标量。
Int GraphQL规范定义Int为具有32个字节的大小。
BigInt 大整数。用于以太坊的uint32int64uint64,……,uint256类型。注意:下面所提的所有uint32,例如int32uint24或者int8,均表示为i32
BigDecimal BigDecimal高精度小数表示为符号和指数。指数范围是−6143至+6144。舍入到34位有效数字。

枚举

您还可以在架构中创建枚举。枚举具有以下语法:

enum TokenStatus {
  OriginalOwner
  SecondOwner
  ThirdOwner
}

在架构中定义枚举后,就可以使用枚举值的字符串表示形式在实体上设置枚举字段。例如,您可以设置tokenStatusSecondOwner通过先定义你的实体,并随后与设置现场entity.tokenStatus = "SecondOwner。下面的示例Token通过枚举字段演示实体的外观:

type Token @entity {
  id: ID!
  name: String!
  symbol: String!
  decimals: Int!
  tokenStatus: TokenStatus!
}

可以在GraphQL文档中找到有关编写枚举的更多详细信息

实体关系

实体可能与架构中的一个或多个其他实体有关系。您可以在查询中遍历这些关系。图中的关系是单向的。通过在关系的“两端”定义单向关系,可以模拟双向关系。

关系与其他字段一样,在实体上定义,所指定的类型是另一个实体的类型。

一对一关系

定义一个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流转额度:

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

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

多对多关系

对于多对多关系,例如每个用户可能属于任意数量的组织的用户,建立关系模型的最直接但通常不是最有效的方法是在涉及的两个实体中的每一个中作为一个数组。如果关系是对称的,则仅需要存储关系的一侧,而可以导出另一侧。

示例

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

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!
  }

定义全文搜索字段

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

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

要添加全文查询,需要一个在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: ID!
    name: String!
    description: String!
    bio: String
    wallet: Address
    labels: [Label!]!
    discography: [Album!]!
    members: [Musician!]!
}

这个例子bandSearch字段可以在查询过滤器中使用Band基于文本的文档实体namedescriptionbio领域。跳转至GraphQL API-查询全文搜索API的说明以及更多示例用法。

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

支持的语言

选择不同的语言将对全文搜索API产生确定的(尽管有时是微妙的)影响。全文查询字段覆盖的字段是在所选语言的上下文中检查的,因此分析和搜索查询生成的词素会因语言而异。例如:当支持土耳其语词典时,“ token”被认为是“ toke”,当然,英语词典会译为“ token”。

支持的语言词典:

代码 词典
简写 通用
da 丹麦语
nl 荷兰语
en 英语
fi 芬兰语
fr 法语
de 德语
hu 匈牙利语
it 意大利语
no 挪威语
pt 葡萄牙语
ro 罗马尼亚语
ru 俄语
es 西班牙语
sv 瑞典语
tr 土耳其语

排名算法

支持的排序结果算法:

算法 描述
排名 Rank 使用全文查询的匹配质量(0–1)对结果进行排序。
近距离排名 proximityRank 类似于rank,但也考虑接近情况。

编写映射

映射将映射所获取的以太坊数据转换为架构中定义的实体。映射以TypeScript的子集( 称为 AssemblyScript)编写,可以将其编译为WASM(WebAssembly)。AssemblyScript比普通的TypeScript严格,但提供了熟悉的语法。

每个在subgraph.yaml中定义的事件都在mapping.eventHandlers中创建具有相同名称的导出函数。每个处理程序必须接受一个调用的参数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.toHex())
  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.toHex()
  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事件,并使用创建一个新Gravatar 实体new Gravatar(event.params.id.toHex()),使用相应的事件参数填充实体字段。该实体实例由变量表示gravatarid值为event.params.id.toHex()

第二个处理程序尝试Gravatar从Graph节点存储中加载现有文件。如果尚不存在,则按需创建。然后,在使用将该实体保存回商店之前,对该实体进行更新以匹配新的事件参数gravatar.save()

创建新实体的推荐ID

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

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

我们提供了 Graph Typescript库,其中包含用于与Graph节点存储进行交互的实用程序,以及用于处理智能合约数据和实体的便利工具。您可以通过导入来@graphprotocol/graph-ts在映射中使用此库mapping.ts

代码生成

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

这是用以下代码实现的:

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

但是在大多数情况下,子图已经通过package.json进行了预配置,以使您可以简单地运行以下其中一项来实现相同的目的:

# Yarn
yarn codegen

# NPM
npm run codegen

这将为中提到的在subgraph.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浏览器之前进行检查,则可以运行yarn build并修复TypeScript编译器可能发现的所有语法错误。

如果您合约的只读方法可以还原,则应通过调用以开头的已生成合约方法来处理该问题try_。例如,Gravity合约公开了gravatarToOwner方法。此代码将能够处理该方法中的还原:

 let gravity = Gravity.bind(event.address)
  let callResult = gravity.try_gravatarToOwner(gravatar)
  if (callResult.reverted) {
    log.info("getGravatar reverted", [])
  } else {
    let owner = callResult.value
  }

请注意,连接到Geth或Infura客户端的Graph节点可能无法检测到所有还原,如果您依靠它,我们建议使用连接到Parity客户端的Graph节点。

在下一节中,我们将描述如何使用Graph浏览器部署子图。

数据源模板

以太坊智能合约的一种常见模式是使用注册表或工厂合约,当一个合约被创建时,管理或引用任意数量的其他合约,每个合约都有各自的状态和事件。这些分包合约的地址可能事先知道,也可能不知道,随着时间的推移,可能会创建并且(或者)添加许多这种合约。因此,在这种情况下,不可能定义单个数据源或固定数量的数据源,而需要一种更动态的方法:数据源模板

主合约的数据源

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

dataSources:
  - kind: ethereum/contract
    name: Factory
    network: mainnet
    source:
      address: '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95'
      abi: Factory
    mapping:
      kind: ethereum/events
      apiVersion: 0.0.2
      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.1
      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")

getter和setter就像setStringgetString ,覆盖所有类型值。

初始区块

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

dataSources:
 - kind: ethereum/contract
   name: ExampleSource
   network: mainnet
   source:
     address: '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95'
     abi: ExampleContract
     startBlock: 6627917
   mapping:
     kind: ethereum/events
     apiVersion: 0.0.2
     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部分中的创建交易哈希。3.加载交易明细页面,您将在其中找到该合同的初始区块。

调用处理程序

事件为合约状态更改提供了一种有效的方法,但许多合约都避免生成日志以解约交易Gas成本。在这些情况下,子图可以预订对数据源合同的调用。这是通过定义引用函数签名的调用处理程序以及将处理该函数调用的映射处理程序来实现的。为了处理这些调用,映射处理程序将接收一个ethereum.Callas作为参数,并带有该调用的类型输入和输出。任何深度调用都将触发映射,从而允许捕获通过代理合约进行的数据源合约使用。

调用处理程序将仅在以下两种情况之一中触发:指定的功能由合约本身以外的帐户调用,或者在Solidity中被标记为外部功能,并在同一合同中作为另一个功能的一部分被调用。

注意: Rinkeby和Ganache不支持调用处理程序。调用处理程序取决于奇偶校验跟踪API,Rinkeby和Ganache均不支持此功能(Rinkeby仅适用于Geth)。

定义调用处理程序

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

dataSources:
  - kind: ethereum/contract
    name: Gravity
    network: mainnet
    source:
      address: '0x731a10897d267e19b34503ad902d0a29173ba4b1'
      abi: Gravity
    mapping:
      kind: ethereum/events
      apiVersion: 0.0.2
      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.toHex()
  let transaction = new Transaction(id)
  transaction.displayName = call.inputs._displayName
  transaction.imageUrl = call.inputs._imageUrl
  transaction.save()
}

handleCreateGravatar函数采用new CreateGravatarCall,它是的的子类ethereum.Call,由所提供@graphprotocol/graph-ts,其中包括调用的类型化输入和输出。在CreateGravatarCall当您运行类型为您生成graph codegen

区块处理程序

除了订阅合约事件或函数调用外,子图可能还需要在新区块上链时更新其数据。为此,子图可以在每个区块之后、或与预定义过滤器匹配的区块之后运行函数。

支持的过滤器

filter:
  kind: call

每个包含对定义了该处理程序的合约(数据源)调用的区块都会对其定义的处理程序调用一次。

块处理程序缺少过滤器将确保该处理程序被每个区块调用。对于每种过滤器类型,一个数据源只能包含一个区块处理程序。

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

映射功能

映射函数将收到一个ethereum.Block作为其唯一参数。类似于事件的映射功能,此功能可以访问商店中的现有子图实体,调用智能合约并创建或更新实体。

import { ethereum } from '@graphprotocol/graph-ts'

export function handleBlock(block: ethereum.Block): void {
  let id = block.hash.toHex()
  let entity = new Block(id)
  entity.save()
}

匿名事件

如果您需要在Solidity中处理匿名事件,可以通过提供事件的主题0来实现,如示例中所示:

eventHandlers:
  - event: LogNote(bytes4,address,bytes32,bytes32,uint256,bytes)
    topic0: "0xbaa8529c00000000000000000000000000000000000000000000000000000000"
    handler: handleGive

仅当签名和主题0都匹配时才触发事件。默认情况下,topic0等于事件签名的哈希值。

IPFS固定

结合使用IPFS和以太坊的一个常见用例是将数据存储在IPFS上,这对于链上的维护来说太昂贵了,并在以太坊合约中引用IPFS哈希。

给定这样的IPFS散列,子图可以使用ipfs.cat和从IPFS读取相应的文件ipfs.map。但是,为了可靠地执行此操作,需要将这些文件固定在索引子图的图节点所连接的IPFS节点上。对于托管服务,这就是 https://api.thegraph.com/ipfs/

为了使子图开发变得简单,The Graph团队编写了一个在IPFS节点之间传输文件的工具:IPF同步