剖析DeFi借贷产品之Compound:Subgraph篇(二)

简介: 笔记

Subgraph 编码

我们先获取 Compound 的 Subgraph 项目源码,其实存在两份不同的源码,注意以下两个地址的区别:

第一个是 Compound 官方的 Github 地址,而第二个是 The Graph 的 Github 地址。第一个已经超过两年没有提交过代码,第二个则是 fork 自第一个的,增加扩展了较多内容,最后一次提交是 8 个月前。而第二个也是部署在了 The Graph 的 Hosted Service 上并公开展示的:

因此,从结果来看,第二个地址的源码项目更值得研究学习。下载源码之后,剔除掉一些不相关的文件之后,项目结构如下:

├── abis
│   ├── cERC20.json
│   ├── cETH.json
│   ├── comptroller.json
│   ├── ctoken.json
│   ├── erc20.json
│   ├── priceOracle.json
│   └── priceOracle2.json
├── src
│   ├── mappings
│   │   ├── comptroller.ts
│   │   ├── ctoken.ts
│   │   ├── helpers.ts
│   │   └── markets.ts
├── package.json
├── schema.graphql
├── subgraph.yaml
└── yarn.lock

abis 里都是需要用到的合约的 ABI 文件,当我们开发自己的 subgraph 项目时,也要将我们所需要的 ABI 文件放到该目录下。package.json 是项目的描述文件,我们最需要关心的是里面封装的脚本命令,在前面我们其实已经有讲到。yarn.lock 文件列出了项目所有的依赖库,这是自动生成的,我们不需要去改动它。

剩下的,subgraph.yaml、schema.graphql 和 src/mappings 下的文件,才是编码工作的核心。


Manifest

Manifest 文件就是 subgraph.yaml 文件,是核心的入口点,主要定义需要建立索引的合约、需要监听的合约事件,以及处理事件数据和保存到 Graph 节点的实体数据之间的映射函数,等等。最核心的配置就是数据源 dataSources,可以配置多个数据源,每个数据源指定名称、目标网络、对应的合约信息、映射信息等。另外,模板 templates 则可用于设置支持动态创建的合约。

以下文档有对 manifest 文件完整的描述:

Compound 的 subgraph.yaml 中就只设置了一个数据源和一个模板,数据源为 Comptroller,模板则是 CToken。有一点需要注意,因为 Comptroller 合约使用了代理模式,所以 Comptroller 数据源所绑定的合约地址其实是其代理合约 Unitroller 的。

Comptroller 数据源中设置监听了 MarketListed(address) 事件,该事件也是 CToken 合约添加进市场时触发的,因此,在该事件的处理函数中,就可以使用 CToken 模板初始化每个 CToken 数据源。


Schema

GraphQL schema 都是在 schema.graphql 文件中所定义,是使用 GraphQL IDL(Interface Definition Language) 来定义 schema 的。如果之前没了解过 GraphQL schema,可以学习下以下两篇文档:

在 schema.graphql 中,最核心的就是定义各种 entity,即实体,如下示例:

type Account @entity {
    "User ETH address"
    id: ID!
    "Array of CTokens user is in"
    tokens: [AccountCToken!]! @derivedFrom(field: "account")
    "Count user has been liquidated"
    countLiquidated: Int!
    "Count user has liquidated others"
    countLiquidator: Int!
    "True if user has ever borrowed"
    hasBorrowed: Boolean!
}

每种实体可以类比为 MySQL 数据库中的一张表,以上代码可以简单理解为定义了 Account 的表结构

下图则是 Compound 的 subgraph 中所定义的所有 schema:

11.png

其中,最后的两个,CTokenTransferUnderlyingTransferinterface,其他的则都是 entity


Mappings

mappings 是在一个 subgraph 项目中需要做最多编码工作的模块,是用 称为 AssemblyScriptTypeScript 子集编写的, 它可以编译为 WASM ( WebAssembly )。AssemblyScript 比普通的 TypeScript 更严格,但提供了常用的语法。

mappings 所映射的就是链上数据schema entities,通过编写各种 handler 函数来处理监听到的合约事件或函数调用等,从而得到链上数据,并将这些数据转化为实体数据进行存储。

比如,我们前面提到过,Comptroller 数据源会监听一个 MarketListed(address) 事件,并指定了处理该事件的 handler:

eventHandlers:
  - event: MarketListed(address)
    handler: handleMarketListed

另外,Comptroller 数据源中也指定了 mapping:

mapping:
  kind: ethereum/events
  apiVersion: 0.0.4
  language: wasm/assemblyscript
  file: ./src/mappings/comptroller.ts
  entities:
    - Comptroller
    - Market

其中,file 指定为 ./src/mappings/comptroller.ts,即是说 Comptroller 数据源中定义的所有 handler 函数都将在该文件中找到。另外,entities 指明了 Comptroller 和 Market,表示在 comptroller.ts 中将会用到这两个实体,不过,事实上,comptroller.ts 还用到了 Account 实体,这是配置中写少了的。

不过,在编写实际的 mappings 之前,需要先执行 graph codegen 自动生成一些 ts 代码。该命令也可以指定生成的代码存放的路径,如下:

graph codegen --output-dir src/types/

则会将生成的 ts 代码存放在当前项目的 src/types/ 目录下。如果不指定目录,默认为当前项目的 generated 目录。

compound-v2-subgraph 生成的该目录如下:

types
├── Comptroller
│   ├── CToken.ts
│   ├── Comptroller.ts
│   ├── ERC20.ts
│   ├── PriceOracle.ts
│   └── PriceOracle2.ts
├── schema.ts
├── templates
│   └── CToken
│       ├── CToken.ts
│       ├── ERC20.ts
│       ├── PriceOracle.ts
│       └── PriceOracle2.ts
└── templates.ts

Comptroller 目录对应于 Comptroller 数据源,其下是该数据源下所设置的 abis 所生成的,即将合约的 ABI 文件转成了 ts 文件。

schema.ts 则是 schema.graphql 文件中所定义的所有 schema 的转化结果了。

templates 目录下则是定义的每一个模板,因为只定义了一个 CToken 模板,所以就只有一个 CToken 子目录,而该目录下的这些 ts 文件则是 CToken 模板中所定义的 abi 的映射了。

templates.ts 就很简单,其代码如下:

import {
  Address,
  DataSourceTemplate,
  DataSourceContext
} from "@graphprotocol/graph-ts";
export class CToken extends DataSourceTemplate {
  static create(address: Address): void {
    DataSourceTemplate.create("CToken", [address.toHex()]);
  }
  static createWithContext(address: Address, context: DataSourceContext): void {
    DataSourceTemplate.createWithContext("CToken", [address.toHex()], context);
  }
}

其实就是根据模板创建 CToken 合约对象的函数封装。

接着,我们来看看 ./src/mappings/comptroller.ts 具体的 handler 函数 handleMarketListed 的实现:

export function handleMarketListed(event: MarketListed): void {
  // Dynamically index all new listed tokens
  CToken.create(event.params.cToken)
  // Create the market for this token, since it's now been listed.
  let market = createMarket(event.params.cToken.toHexString())
  market.save()
}

第一行会创建一个 CToken 合约对象,其中的 CToken 其实就是上面 templates.ts 所定义的 CToken 类。

第二行和第三行则会创建一个 market 对象并保存,这是一个 entity 实例,也可以理解为就是生成了 Market 表的一条新记录。

另外,每个 event 对象其实都继承自 ethereum.Event,其封装了一些基本属性,如下:

export class Event {
  address: Address
  logIndex: BigInt
  transactionLogIndex: BigInt
  logType: string | null
  block: Block
  transaction: Transaction
  parameters: Array<EventParam>
}

其中,Address、Block 和 Transaction 又封装了对应的一些基本属性,方便调用。

当然,最多的还是获取 event 的参数,可以通过 event.params.XXX 读取出对应的参数。

最后,可以看下以下文档学习如何编写 mappings:



GraphQL API


当将完整的 subgraph 开发完并成功部署到 Graph 节点之后,就可以实现查询操作了。而查询就是用 GraphQL API 进行查询的。 Dapp 前端页面所展示的数据,大部分都是可以用 GraphQL API 编写对应的查询语句实现的。

GraphQL API 的查询语句主要也是基于 schema 中所定义的各种实体。比如,以下面这个 entity 为例:

type Market @entity {
  id: ID!
  name: String!
  symbol: String!
  borrowRate: BigDecimal!
  supplyRate: BigDecimal!
}

现在,想要查出所有市场数据,那查询语句就可以这么写:

{
  markets {
    id
    name
    symbol
    borrowRate
    supplyRate
  }
}

而如果要进行条件查询,GraphQL API 提供了 where 参数,比如要查询 borrowRate 为 15 的,语句就可以这么写:

{
  markets(where: {borrowRate: 15}) {
    id
    name
    symbol
    borrowRate
    supplyRate
  }
}

而如果想要查询 borrowRate 大于 15 的,则是这么写:

{
  markets(where: {borrowRate_gt: 15}) {
    id
    name
    symbol
    borrowRate
    supplyRate
  }
}

即是在查询的字段名后面添加上 _gt 后缀。GraphQL API 提供了多个这种后缀:

  • _not
  • _gt
  • _lt
  • _gte
  • _lte
  • _in
  • _not_in
  • _contains
  • _not_contains
  • _starts_with
  • _ends_with
  • _not_starts_with
  • _not_ends_with

除了 where 参数,GraphQL API 还提供了其他参数,包括:

  • id:指定 id 查询
  • orderBy:指定排序的字段
  • orderDirection:排序方向,asc | desc
  • first:查询条数,比如设为 10,则最多只查出 10 条记录
  • skip:跳过不查询的条数
  • block:指定区块查询,可以指定区块 number 或 hash
  • text:全文检索

最后,GraphQL API 也同样有学习文档:


总结


Subgraph 涉及到的内容细节其实也比较多,但限于篇幅,我也无法都一一讲解,本篇文章更多只是为一些还不太懂 Subgraph 的小伙伴提供一些指引,要深入学习 Subgraph 还是需要多学习那些文档以及学习实际项目的源码。

下一篇,我将讲讲 Compound 清算服务该如何设计,这块 Compound 并没有开源项目,所以只能根据我自己的设计经验来讲解。

相关文章
|
区块链 数据安全/隐私保护 开发者
ptahdao普塔道系统开发|ptahdao普塔道质押模式系统开发
智能合约是区块链技术的一大创新,它是一种在区块链上运行的自动化合约。
|
存储 IDE 搜索推荐
NFT铸造质押借贷dapp系统开发|赋能功能模式定制
NFT铸造质押借贷dapp系统开发|赋能功能模式定制
|
11月前
|
存储 区块链
NFT+defi质押流动性系统开发技术分析
智能合约采用的是区块链技术,其中数据和程序的完整性得到了高度保障
|
资源调度 网络协议 关系型数据库
|
存储 安全 区块链
|
区块链
Defi/IDO代币预售借贷分红模式系统开发部署搭建
pragma solidity ^0.8.0; contract IDX { // 代币总量 uint256 public totalSupply;