这一节我们按照官方示例构建一个简单的subGraph
智能合约
// SPDX-License-Identifier: MIT
pragma solidity >0.4.0;
contract GravatarRegistry {
event NewGravatar(uint id, address owner, string displayName, string imageUrl);
event UpdatedGravatar(uint id, address owner, string displayName, string imageUrl);
struct Gravatar {
address owner;
string displayName;
string imageUrl;
}
Gravatar[] public gravatars;
mapping (uint => address) public gravatarToOwner;
mapping (address => uint) public ownerToGravatar;
function createGravatar(string calldata _displayName, string calldata _imageUrl) public {
require(ownerToGravatar[msg.sender] == 0);
gravatars.push(Gravatar(msg.sender, _displayName, _imageUrl));
uint id = gravatars.length - 1;
gravatarToOwner[id] = msg.sender;
ownerToGravatar[msg.sender] = id;
emit NewGravatar(id, msg.sender, _displayName, _imageUrl);
}
function getGravatar(address owner) public view returns (string memory, string memory) {
uint id = ownerToGravatar[owner];
return (gravatars[id].displayName, gravatars[id].imageUrl);
}
function updateGravatarName(string calldata _displayName) public {
require(ownerToGravatar[msg.sender] != 0);
require(msg.sender == gravatars[ownerToGravatar[msg.sender]].owner);
uint id = ownerToGravatar[msg.sender];
gravatars[id].displayName = _displayName;
emit UpdatedGravatar(id, msg.sender, _displayName, gravatars[id].imageUrl);
}
function updateGravatarImage(string calldata _imageUrl) public {
require(ownerToGravatar[msg.sender] != 0);
require(msg.sender == gravatars[ownerToGravatar[msg.sender]].owner);
uint id = ownerToGravatar[msg.sender];
gravatars[id].imageUrl = _imageUrl;
emit UpdatedGravatar(id, msg.sender, gravatars[id].displayName, _imageUrl);
}
// the gravatar at position 0 of gravatars[]
// is fake
// it's a mythical gravatar
// that doesn't really exist
// dani will invoke this function once when this contract is deployed
// but then no more
function setMythicalGravatar() public {
require(msg.sender == 0xBA8B604410ca76AF86BDA9B00Eb53B65AC4c41AC);
gravatars.push(Gravatar(address(0x0), " ", " "));
}
}
以上是官方提供的示例,这里做了些简单的修改,主要是适配了高版本的solidity。核心的方法有三个createGravatar,updateGravatarImage,updateGravatarName。逻辑很简单,就不多加解释了!合约的部署需要读者自行完成。
需求目标
首先确定subgraph所要完成的功能,就是需要支持合约中所有Gravatar的查询,支持字段过滤。合约中并没有提供这么复杂的查询函数。接下来我们通过构建一个简单的subgraph来完成此功能
创建subGraph
首先打开subGraph studio,连结钱包,进入My Subgraphs,点击Create按钮
![](https://i-blog.csdnimg.cn/blog_migrate/af52f538c0981cb469db4ec59ac082dc.png)
填写subGraph的基本信息,这里的网络我们选goerli
![](https://i-blog.csdnimg.cn/blog_migrate/b687b24a19f885abf46d86bd16589555.png)
选择一个分类点击save,这个时候会出现deploy key
![](https://i-blog.csdnimg.cn/blog_migrate/72efaca02463d435d678d6859cb6f049.png)
本地构建
创建空文件夹,并在文件夹中运行以下命令。
npm install -g @graphprotocol/graph-cli
yarn global add @graphprotocol/graph-cli
graph init --studio subgraph-example
--交互输出如下
√ Protocol · ethereum
√ Subgraph slug · subgraph-example
√ Directory to create the subgraph in · subgraph-example
? Ethereum network ...
? Ethereum network ...
? Ethereum network ...
√ Ethereum network · goerli
√ Contract address · 0x964F658FC863BAceFC719b85e8730fbc11c86ce4
× Failed to fetch ABI from Etherscan: request to https://api-goerli.etherscan.io/api?module=contract&action=getabi&address=0x964F658FC863BAceF
C719b85e8730fbc11c86ce4 failed, reason: connect ETIMEDOUT 69.63.178.13:443
√ ABI file (path) · ./GravatarRegistry.json //需要提前准备好abi
√ Contract Name · Gravatar
√ Index contract events as entities (Y/n) · true
构建的过程有可能会报错
fatal: unable to access 'https://hub.fastgit.org/edgeandnode/gluegun.git/': OpenSSL SSL_connect: SSL_ERROR_SYSCALL in connection to hub.fastgit.org:443
这个时候先进入subgraph-example文件夹,手动install
cd subgraph-example
yarn install
这里还有可能会出现info There appears to be trouble with your network connection. Retrying...的问题,可尝试如下方案
https://www.cnblogs.com/fmixue/p/16375938.html
这里生成了几个关键的文件
![](https://i-blog.csdnimg.cn/blog_migrate/61fbd96df78ea8256926f805e2efa00d.png)
subgraph.yaml
subgraph.yaml文件是上一篇概述中提到的subgraph manifest,是subgraph的起点文件,定义了subgraph索引的智能合约,这些合约中需要关注的事件,以及如何将事件数据映射到 Graph 节点存储的实体。具体如下(关键部分做了注释)
specVersion: 0.0.4
description: Gravatar for Ethereum #subgraph的描述。当subgraph部署到托管服务时,Graph Explorer 会显示此描述。
repository: https://github.com/graphprotocol/example-subgraph #subgraph的代码仓库。Graph Explorer会显示
schema:
file: ./schema.graphql #entities定义所在的文件
dataSources:
- kind: ethereum/contract
name: Gravity
network: mainnet
source:
address: '0x2E645469f354BB4F5c8a05B3b30A929361cf77eC'
abi: Gravity
startBlock: 6175244 #开始收集数据的区块。这里建议使用创建合约的区块。
mapping:
kind: ethereum/events
apiVersion: 0.0.6
language: wasm/assemblyscript
entities: #写入graph存储的实体。每个实体的数据结构在schema.graphql文件中定义。
- Gravatar
abis:
- name: Gravity
file: ./abis/Gravity.json
eventHandlers: #对智能合约事件的处理handler,示例中为./src/gravatar-registry.ts—会将这些事件转换为存储中的实体。
- event: NewGravatar(uint256,address,string,string)
handler: handleNewGravatar
- event: UpdatedGravatar(uint256,address,string,string)
handler: handleUpdatedGravatar
callHandlers: #对智能合约函数调用的处理handler,此handle可以获取函数的输入参数。
- function: createGravatar(string,string)
handler: handleCreateGravatar
blockHandlers: #当一个新的block产生时调用的handler,如果没有filter,此handler将在每个block中运行。
- handler: handleBlock
- handler: handleBlockWithCall
filter:
kind: call
file: ./src/gravatar-registry.ts #handler函数所在的文件位置
本例的subgraph.yaml如下
specVersion: 0.0.5
schema:
file: ./schema.graphql
dataSources:
- kind: ethereum
name: GravatarRegistry
network: goerli
source:
address: "0x964F658FC863BAceFC719b85e8730fbc11c86ce4"
abi: GravatarRegistry
mapping:
kind: ethereum/events
apiVersion: 0.0.7
language: wasm/assemblyscript
entities:
- Gravatar
abis:
- name: GravatarRegistry
file: ./abis/GravatarRegistry.json
eventHandlers:
- event: NewGravatar(uint256,address,string,string)
handler: handleNewGravatar
- event: UpdatedGravatar(uint256,address,string,string)
handler: handleUpdatedGravatar
file: ./src/gravatar-registry.ts
schema.graphql
实体的定义文件。在定义实体之前,重要的是要退后一步,思考数据的结构和链接方式。 所有查询都将针对schema.graphql中定义的数据模型和subgraph的索引的实体进行。 因此,最好以符合 dapp 需求的方式定义子图模式。 将实体想象为“包含数据的对象”,而不是事件或函数。
通过命令生成的schema如下:
type NewGravatar @entity(immutable: true) {
id: Bytes!
id: BigInt! # uint256
owner: Bytes! # address
displayName: String! # string
imageUrl: String! # string
blockNumber: BigInt!
blockTimestamp: BigInt!
transactionHash: Bytes!
}
type UpdatedGravatar @entity(immutable: true) {
id: Bytes!
id: BigInt! # uint256
owner: Bytes! # address
displayName: String! # string
imageUrl: String! # string
blockNumber: BigInt!
blockTimestamp: BigInt!
transactionHash: Bytes!
}
这里的entity是完全按照abi当中定义的事件来生成的,我们需要做一些修改。
按照上述,数据模型应该根据dapp最终展示的结果来定义,而不是完全照搬event,而我们的需求是展示合约中存储的Gravatar,所以并不需要按照create和updated分成两个entity。
还有一个是字段重复的问题,我们观察到自动生成的实体当中包含了两个id字段,原因是每一个实体都要包含一个默认的id字段,而我们的event中也定义了一个id字段,所以重复了,引用官方文档的一段话:
Each entity must have an id field, which must be of type Bytes! or String!. It is generally recommended to use Bytes!, unless the id contains human-readable text, since entities with Bytes! id's will be faster to write and query as those with a String! id. The id field serves as the primary key, and needs to be unique among all entities of the same type. For historical reasons, the type ID! is also accepted and is a synonym for String!.
每个实体必须有一个id字段,它的类型必须是Bytes!或String!通常建议使用Bytes!,除非id包含可读文本。Bytes类型的id会比String类型的id拥有更快的读写速度。id字段作为主键,在相同类型的所有实体中必须是唯一的。由于历史原因,类型ID!也是可以接受的,并且是String!的同义词。
合约里面存储的id是递增的数字,这里先使用String
immutable: true 的意思是当前的实体是不可变的。在本例中,我们需要在处理UpdatedGravatar事件的时候根据id更新存储在graph node上的实体。所以应当设为可变。
修改后的schema.graphql如下:
type Gravatar @entity(immutable: false) {
id: String!
owner: Bytes! # address
displayName: String! # string
imageUrl: String! # string
blockNumber: BigInt!
blockTimestamp: BigInt!
transactionHash: Bytes!
}
字段类型后面跟!代表非空。
gravatar-registry.ts
import {
NewGravatar as NewGravatarEvent,
UpdatedGravatar as UpdatedGravatarEvent
} from "../generated/GravatarRegistry/GravatarRegistry"
import { NewGravatar, UpdatedGravatar } from "../generated/schema"
export function handleNewGravatar(event: NewGravatarEvent): void {
let entity = new NewGravatar(
event.transaction.hash.concatI32(event.logIndex.toI32())
)
entity.id = event.params.id
entity.owner = event.params.owner
entity.displayName = event.params.displayName
entity.imageUrl = event.params.imageUrl
entity.blockNumber = event.block.number
entity.blockTimestamp = event.block.timestamp
entity.transactionHash = event.transaction.hash
entity.save()
}
export function handleUpdatedGravatar(event: UpdatedGravatarEvent): void {
let entity = new UpdatedGravatar(
event.transaction.hash.concatI32(event.logIndex.toI32())
)
entity.id = event.params.id
entity.owner = event.params.owner
entity.displayName = event.params.displayName
entity.imageUrl = event.params.imageUrl
entity.blockNumber = event.block.number
entity.blockTimestamp = event.block.timestamp
entity.transactionHash = event.transaction.hash
entity.save()
}
由于我们对schema.graphql做了修改,这里的handler需要和schema.graphql里面定义的entity配套使用,所以也要做相应的改动。需要注意的是对id的处理,event里面的id定义为uint,而Gravatar实体里定义的id是String,不可以直接赋值,需要做一下转换,我们编写handler方法时用到的api可以参考AssemblyScript API,修改完成后的代码如下。
import {
NewGravatar as NewGravatarEvent,
UpdatedGravatar as UpdatedGravatarEvent
} from "../generated/GravatarRegistry/GravatarRegistry"
import { Gravatar} from "../generated/schema"
export function handleNewGravatar(event: NewGravatarEvent): void {
let gravatar = new Gravatar(event.params.id.toString());
gravatar.owner = event.params.owner
gravatar.displayName = event.params.displayName
gravatar.imageUrl = event.params.imageUrl
gravatar.blockNumber = event.block.number
gravatar.blockTimestamp = event.block.timestamp
gravatar.transactionHash = event.transaction.hash
gravatar.save()
}
export function handleUpdatedGravatar(event: UpdatedGravatarEvent): void {
let id = event.params.id.toString();
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.blockNumber = event.block.number
gravatar.blockTimestamp = event.block.timestamp
gravatar.transactionHash = event.transaction.hash
gravatar.save()
}
这个时候开始还剩三步,代码生成,编译,和发布,命令如下
graph auth --studio {deploy key}
graph codegen
graph build
deploy key可以在这里获取
![](https://i-blog.csdnimg.cn/blog_migrate/454c3b675b388aaf3fe51c11d8f7215a.png)
codegen 和build会分别生成各自的文件夹,schema.graphql,subgraph.yaml,gravatar-registry.ts,这几个文件有任何改动都需要运行这两个命令,重新生成代码和编译。
![](https://i-blog.csdnimg.cn/blog_migrate/dffb451fdf83dd4dd369dca2de60ad56.png)
如果上述的命令都成功运行,最后执行yarn deploy命令发布subgraph
![](https://i-blog.csdnimg.cn/blog_migrate/e53485dd0c7515089e28e5d9e401939d.png)
验证
最后我们回到subgraph studio刷新一下页面,这个时候会多出来两个table页,选择Playground
![](https://i-blog.csdnimg.cn/blog_migrate/33545baf7c96015589beb2c178c56134.png)
进度条会显示同步区块的进度,等区块数据同步完成就可以开始查询了。
![](https://i-blog.csdnimg.cn/blog_migrate/29bfe591e547a7086242c42920b29755.png)
也可以做一些过滤查询
![](https://i-blog.csdnimg.cn/blog_migrate/65d35eef3a5bc883b22b1eda5bf118b1.png)
graphql的语法参照官方文档,功能还是很强大的,这里就不赘述了。