子图开发

在前面的章节中,已经一步步地了解了子图项目的创建、构成和部署。子图部署成功之后,HyperGraph 后端节点就会根据子图索引数据。本章再基于上一节构建好的例子,比较详细地讲解子图的开发。

/abis

./abis/Contract.json 合约的接口定义文件,根据指定的合约地址、网络,graph 程序自动获得,有了 abi 才可以知道有哪些事件可以处理,这是原始数据源定义

./schema.graphql 示例的数据实体定义文件,在其中放置要操作的数据对象和属性定义,当然也不仅仅是跟智能合约中的智能合约事件对象直接关联的实体,也可以有一些中间和结果实体和查询实现

./subgraph.yaml 子图的数据源、处理过程、方式定义,包括合约、起始区块(默认没有)、事件、处理程序等

./yarn.lock yarn yarn 工具的依赖更新文件

./package.json npm 和 yarn 所使用项目依赖以及定义

./src AssemblyScript 目录,用于存放解析逻辑代码,定义了将数据从事件转化成为能存储的实体的过程

./src/mapping.ts 事件处理程序文件,针对每一个合约事件,均可以在这里编写相应的处理程序,对数据进行统计

一、ABI 文件

ABI 是Application Binary Interface的简写,以JSON文件格式定义了智能合约的接口,包括对智能合约公开函数的名称、参数、返回类型等的定义。一般会伴随着智能合约的部署而生成。可以通过以下方式获得一个智能合约的ABI 文件。

1、使用 truffle compile

如果是自有项目,在编译合约之后,在build/contracts 目录下就可以得到一个JSON文件,里面就有 ABI

2、通过 solc或者 solcjs 生成

如果是有合约代码,可以用 Solidity Compiler 取得合约 ABI,使用 JavaScript 版本的 Compiler 为例。

安装solcjs:

npm install solc -g

或者安装solc-select

pip3 install solc-select

来安装 solc

取得合约 ABI:

在取得ABI 之前,如果是复杂合约,可以先运行 truffle-flattener 将合约整合成一个文件,

执行:

solcjs xxx.sol --abi

solc xxx.sol --abi

就可以得到 ABI 文件。

3、使用Remix 编译生成

如果在Remix 中编译,在编译成功后,在artifacts 目录下,可以看到智能合约编译成功后,同步生成的JSON文件,打开文件,其中第一部分就是 ABI。

4、通过已验证合约

对于在区块链浏览器上已经验证的智能合约,可以直接使用智能合约获取ABI。比如

https://hecoinfo.com/address/0x0bb480582ecae1d22bbaeaccfbb849b441450026#code

5、API 方式获取

如果是在程序中获取ABI,也可以使用区块浏览器所提供的API获取方式。

比如 https://hecoinfo.com/apis#contracts graph-cli 中间的脚手架程序,根据合约地址自动获取ABI 就是用的这种方式。

注意请使用正确版本的ABI,或者在数据解析过程中,可能会失败。

二、子图声明文件 subgraph.yaml

subgraph.yaml 是一个 YAML 格式的文件,使用 YAML 来声明子图的作用是方便读写,相应的支持库完善,数据也不冗余。

在 subgraph.yaml 中,定义了在当前这个子图项目中,需要索引哪些合约的数据、在相应的合约中,又需要重点分析哪些事件,并且建立了一个映射关系,将事件和相应的处理程序关联起来,以存到Graph Node 来存储,并提供查询。当然,存储与查询在远程的实现,子图开发者不用关心,但是子图开发者需要在子图映射程序中描述相应的逻辑关系。依据上一节的示例,subgraph.yaml 的内容如下:

specVersion: 0.0.2

schema:

file: ./schema.graphql

dataSources:

- kind: ethereum/contract

name: Contract

network: mainnet

source:

address: "0x0bb480582ecae1d22bbaeaccfbb849b441450026"

startBlock: 2095189

abi: Contract

mapping:

kind: ethereum/events

apiVersion: 0.0.4

language: wasm/assemblyscript

entities:

- Packetstarted

- PacketClaimed

- Packetended

- ClaimedTokens

- ClaimedPacketTokens

- OwnershipTransferred

abis:

- name: Contract

file: ./abis/Contract.json

eventHandlers:

- event: Packetstarted(uint256,address)

handler: handlePacketstarted

- event: PacketClaimed(uint256,address,uint32,address)

handler: handlePacketClaimed

- event: Packetended(uint256,address)

handler: handlePacketended

- event: ClaimedTokens(address,address,uint256)

handler: handleClaimedTokens

- event: ClaimedPacketTokens(uint32,address,address,uint256)

handler: handleClaimedPacketTokens

- event: OwnershipTransferred(address,address)

handler: handleOwnershipTransferred

file: ./src/mapping.ts

下面比较详情地讲解一下这个文件各个条目的构成:

description:子图项目的描述,如果子图和控制台添加的子图关联起来,此描述会保存起来,以展示在子图的详情页。

repository:子图所在的git 地址,通过这里可以找到子图代码,包括声明文件。也会展示在子图的详情页。

dataSources.source: 子图数据源的智能合约地址,以及智能合约对应的abi 定义。此合约地址不一定要写,如果没有指定合约地址,就有可能需要索引所有地址的所有事件。

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 的方式来提供,如果一个区块中数据源合约,包含了一个指定的调用,那么调用过滤器将运行相应的处理程序。

区块中数据源触发顺序,是根据区块中的事务顺序而决定的。

三、Schema 文件

GraphQL 数据实体定义

子图项目的数据实体定义在了 chema.graphql 文件中。GraphQL 是一种用于API查询的语言,对 API 中的数据提供了一套易于理解的完整描述,使得客户端能够准确地获得它需要的数据。因而可以用于描述数据实体,也可以用于请求想要的数据,得到结果。

子图中的数据实体就是使用 GraphQL 接口定义语言(IDL)来描述和定义。如果您是GraphQL 新手,或者想了解更多GraphQL 开发的基本知识,可以参看GraphQL API 一节。

定义实体

正像传统数据库设计的数据库表,面向对象编译的对象设计一样,实体的设计也跟业务紧密相关,所以在定义实体之前,必须对DApp的业务和要分析的数据有一个整理和关联分析。

由于所有的查询都基于子图中定义的数据模型实体,而子图本身的索引也是基于实体。所以实体的定义是基于DApp的需求而出发的,所以我们在定义实体的时候,把实体认为是包含了数据的对象,而不仅是从事件和智能合约函数的角度来考虑。

当在schema.graphql中定义了各种实体类型,然后HyperGraph 将为查询某一个实体的一个实例或者集合,生成字段。在定义实体的时候,每一个应该是实体的类型,都必须用 @entity 修饰符去标注。

举两个实体定义的样例,还以上节中的示例项目为例,一个推荐的,一个不推荐的:

首先来看一下事件定义:

{

"anonymous": false,

"inputs": [

{ "indexed": false, "name": "total", "type": "uint256" },

{ "indexed": false, "name": "tokenAddress", "type": "address" }

],

"name": "Packetstarted",

"type": "event"

}

Packetstarted 事件是在红包(空投计划)成功设置之后记录到链上的,有两个参数,一个total 记录要发送的Token的数量,tokenAddress 是发送的Token。

好的做法是定义一个Packet,为了统计某一个Token发放的全部量,也定义了一下PacketToken

type Packet @entity {

id: ID!

count: BigInt!

total: BigInt! # uint256

tokenAddress: Bytes! # address

}

type PacketToken @entity {

id: ID!

total: BigInt! # uint256

}

不好的做法就是把事件,原封不动地定义为实体,如:

type Packetstarted @entity {

id: ID!

count: BigInt!

total: BigInt! # uint256

tokenAddress: Bytes! # address

}

要根据实际的业务需要,而不是将事件或者调用1:1 地在shema.graphql中定义为实体。

Last updated