The Graph监听合约事件:从部署到查询

  • 0xwu
  • 发布于 11小时前
  • 阅读 13

使用The Graph官方的Subgraph Studio在线平台来监控合约事件

1. 前言

The Graph 是一个去中心化的区块链数据索引协议,让你可以像查询数据库一样,用 GraphQL 查询区块链数据。

  1. 为什么需要?

区块链上的数据虽然公开,但查询效率低、结构复杂,想查某个合约的交互历史,要从成千上万个区块中找,太麻烦了。 而The Graph的作用就是提前监听区块链事件,把你关心的数据抓出来、处理好、存到数据库,然后开放查询接口。

  1. 使用场景
    • Uniswap 查询用户交易记录
    • Lens Protocol 查用户发布的帖子
    • ENS 查某地址注册的域名
    • NFT 市场查某个藏品的历史买卖记录

image.png

  1. 大概流程:

    • 开发者设计Subgraph子图,即定义需要监听的合约和监听规则,比如监听哪些事件?怎么处理数据?存什么字段?怎么查询? 然后定义好的规则上传到IPFS;
    • GraphNode 是一个区块链索引节点,它负责扫描区块,并读取IPFS中规则来解析区块数据,然后存储到 PostgreSQL数据库中;
    • 用户通过GraphNode提供的GraphQL接口,来查询感兴趣的数据;
  2. 便于理解:

    • 区块链数据 = 河水中的鱼
    • SubGraph = 你设计捞网的形状,决定打捞大鱼还是小鱼;
    • GraphNode = 打捞的工人,用捞网去小河中捞鱼,捞到的鱼放到箱子里;
    • GraphQL查询 = 从箱子中找到你想要的鱼;

2. 在线部署

Subgraph Studio 是 The Graph 官方提供的图形化工具,用来创建、测试、部署和管理 Subgraph 的 Web 平台。

官方教程:https://thegraph.com/docs/zh/subgraphs/quick-start/

我们在此平台上来监听Counter合约的事件,监听事件包括:SetNumber和Increment,来监听number的变化;

准备条件

  1. 加密钱包:为了登录Web平台
  2. 已部署的合约地址:用于监控合约的事件,注意该合约存在事件;我已在Ethereum Sepolia网络上成功部署和验证的Counter合约
  3. 安装Node.js:推荐安装v23.11.0。用于安装Graph 客户端或相关依赖包;安装工具支持npm,yarn和 pnpm

2.1 创建subgraph

  1. 使用metamask钱包登录网站:https://thegraph.com/studio/
  2. 创建一个子图:Counter子图。创建成功后,在右栏提供了具体的创建步骤;

image.png

2.2 安装graph客户端

在本地电脑上安装graph客户端

# npm安装graph客户端
$ npm install -g @graphprotocol/graph-cli@latest
npm warn deprecated inflight@1.0.6: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
npm warn deprecated rimraf@2.7.1: Rimraf versions prior to v4 are no longer supported
npm warn deprecated glob@7.2.3: Glob versions prior to v9 are no longer supported
added 422 packages in 22s
86 packages are looking for funding
  run `npm fund` for details

# 查看客户端版本
$ graph --version
@graphprotocol/graph-cli/0.97.0 win32-x64 node-v23.11.0

2.3 初始化subgraph

根据执行输入参数

  1. 选择sepolia网络,回车,回车,回车
  2. 输入合约地址:该合约需要验证通过,才能解析成功其中的ABI信息,否则要自行执行ABI的绝对路径。这是我验证通过的Counter合约地址
  3. 一路回车,使用默认参数;
$ graph init counter

√ Network · Ethereum Sepolia Testnet · sepolia · https://sepolia.etherscan.io  # 完整输入Ethereum Sepolia Testnet,选择sepolia网络
√ Source · Smart Contract · ethereum                # 回车
√ Subgraph slug · counter                   # 回车
√ Directory to create the subgraph in · counter         # 回车
√ Contract address · 0xB78473a261797b9Fa4a18adff4Ad89B2b94B8856 # 输入验证通过的合约地址
√ Fetching ABI from Sourcify API...
√ Fetching ABI from Contract API...
√ Fetching start block from Contract API...
√ Fetching contract name from Contract API...
√ Start block · 8310332                     # 回车,确认部署合约的区块号
? Contract name » counter                   # 回车
√ Contract name · counter
√ Index contract events as entities (Y/n) · true        # 回车
  Generate subgraph
  Write subgraph to directory
√ Create subgraph scaffold
√ Initialize networks config
√ Initialize subgraph repository
√ Install dependencies with npm install
√ Generate ABI and schema types with npm run codegen
√ Add another contract? (y/N) · false               # 回车

Subgraph counter created in counter
Next steps:
  1. Run `graph auth` to authenticate with your deploy key.
  2. Type `cd counter` to enter the subgraph.
  3. Run `npm run deploy` to deploy the subgraph.
Make sure to visit the documentation on https://thegraph.com/docs/ for further information.

2.4 授权

授权码在web端中查找 deploy key

$ graph auth 635702bb8e772ac010c612105bd4b2f4
Deploy key set for https://api.studio.thegraph.com/deploy/

image.png

2.5 生成和编译

三个核心文件:

  1. schema.graphql:定义解析数据的结构,即作为存储到PostgreSQL的实体结构,也是前端查询接口的传参;
  2. subgraph.yaml:定义数据来源和处理规则;告诉GraphNode监听哪些合约,从哪个区块开始,匹配哪些事件,调用哪个处理函数等;
  3. mapping.ts:定义事件的处理逻辑,将解析数据转换为实体存入PostgreSQL;
# 进入初始化项目目录
$ cd counter/

# 生成和编译
$ graph codegen && graph build
  Skip migration: Bump mapping apiVersion from 0.0.1 to 0.0.2
  Skip migration: Bump mapping apiVersion from 0.0.2 to 0.0.3
  Skip migration: Bump mapping apiVersion from 0.0.3 to 0.0.4
  Skip migration: Bump mapping apiVersion from 0.0.4 to 0.0.5
  Skip migration: Bump mapping apiVersion from 0.0.5 to 0.0.6
  Skip migration: Bump manifest specVersion from 0.0.1 to 0.0.2
  Skip migration: Bump manifest specVersion from 0.0.2 to 0.0.4
√ Apply migrations
√ Load subgraph from subgraph.yaml                      # 数据来源和处理规则
  Load contract ABI from abis\counter.json
√ Load contract ABIs
  Generate types for contract ABI: counter (abis\counter.json)
  Write types to generated\counter\counter.ts
√ Generate types for contract ABIs
√ Generate types for data source templates
√ Load data source template ABIs
√ Generate types for data source template ABIs
√ Load GraphQL schema from schema.graphql                   # 解析数据的实体
  Write types to generated\schema.ts
√ Generate types for GraphQL schema

Types generated successfully                            # 生成成功

  Skip migration: Bump mapping apiVersion from 0.0.1 to 0.0.2
  Skip migration: Bump mapping apiVersion from 0.0.2 to 0.0.3
  Skip migration: Bump mapping apiVersion from 0.0.3 to 0.0.4
  Skip migration: Bump mapping apiVersion from 0.0.4 to 0.0.5
  Skip migration: Bump mapping apiVersion from 0.0.5 to 0.0.6
  Skip migration: Bump manifest specVersion from 0.0.1 to 0.0.2
  Skip migration: Bump manifest specVersion from 0.0.2 to 0.0.4
√ Apply migrations
√ Load subgraph from subgraph.yaml
  Compile data source: counter => build\counter\counter.wasm
√ Compile subgraph
  Copy schema file build\schema.graphql
  Write subgraph file build\counter\counter.json
  Write subgraph manifest build\subgraph.yaml
√ Write compiled subgraph to build\

Build completed: build\subgraph.yaml                        # 编译完成

2.5.1 schema.graphql文件

生成的schema.graphql文件内容

定义解析数据的结构,即作为存储到PostgreSQL的实体结构,也是前端查询接口的传参;

type Increment @entity(immutable: true) {   # @entity 表示这是需要被GraphNode存储到PostgreSQL的数据
  id: Bytes!                    # 所有 Entity 都需要有一个 id 字段
  sender: Bytes! # address
  newValue: BigInt! # uint256
  blockNumber: BigInt!
  blockTimestamp: BigInt!
  transactionHash: Bytes!
}

type SetNumber @entity(immutable: true) {
  id: Bytes!
  sender: Bytes! # address
  newNumber: BigInt! # uint256
  blockNumber: BigInt!
  blockTimestamp: BigInt!
  transactionHash: Bytes!
}

2.5.2 subgraph.yaml文件

定义数据来源和处理规则;告诉GraphNode监听哪些合约,从哪个区块开始,匹配哪些事件,调用哪个处理函数等;

specVersion: 1.3.0
indexerHints:
  prune: auto
schema:
  file: ./schema.graphql
dataSources:                    # 要监听的链上合约及其事件
  - kind: ethereum
    name: counter
    network: sepolia
    source:
      address: "0xB78473a261797b9Fa4a18adff4Ad89B2b94B8856"
      abi: counter              # 对应事件的 ABI 文件,用于解析日志
      startBlock: 8310332           # 指定从哪个区块开始监听(提升效率)
    mapping:
      kind: ethereum/events
      apiVersion: 0.0.9
      language: wasm/assemblyscript
      entities:
        - Increment
        - SetNumber
      abis:
        - name: counter
          file: ./abis/counter.json
        eventHandlers:              # 指定事件和处理函数的映射关系
        - event: Increment(indexed address,uint256)
          handler: handleIncrement
        - event: SetNumber(indexed address,uint256)
          handler: handleSetNumber
      file: ./src/counter.ts    # 指定映射逻辑所在的 TypeScript 文件

2.5.3 mapping.ts

该案例的mapping.ts 对应为 counter/src/counter.ts;

定义事件的处理逻辑,将解析数据转换为实体存入PostgreSQL;

import {
  Increment as IncrementEvent,
  SetNumber as SetNumberEvent
} from "../generated/counter/counter"
import { Increment, SetNumber } from "../generated/schema"

export function handleIncrement(event: IncrementEvent): void {
  let entity = new Increment(           // 监听到 IncrementEvent 后,创建一个entity实体。
    event.transaction.hash.concatI32(event.logIndex.toI32())
  )
  entity.sender = event.params.sender       // 利用事件参数填充字段
  entity.newValue = event.params.newValue

  entity.blockNumber = event.block.number
  entity.blockTimestamp = event.block.timestamp
  entity.transactionHash = event.transaction.hash

  entity.save()                 // 然后 .save() 保存进数据库。
}

export function handleSetNumber(event: SetNumberEvent): void {
  let entity = new SetNumber(
    event.transaction.hash.concatI32(event.logIndex.toI32())
  )
  entity.sender = event.params.sender
  entity.newNumber = event.params.newNumber

  entity.blockNumber = event.block.number
  entity.blockTimestamp = event.block.timestamp
  entity.transactionHash = event.transaction.hash

  entity.save()
}

2.6 部署

将三个核心文件上传IPFS,并部署Graph node

$ graph deploy counter

# 正确结果
Which version label to use? (e.g. "v0.0.1") · v0.0.1        # 输入版本标签,用于标识本次部署的子图版本
  Skip migration: Bump mapping apiVersion from 0.0.1 to 0.0.2
  Skip migration: Bump mapping apiVersion from 0.0.2 to 0.0.3
  Skip migration: Bump mapping apiVersion from 0.0.3 to 0.0.4
  Skip migration: Bump mapping apiVersion from 0.0.4 to 0.0.5
  Skip migration: Bump mapping apiVersion from 0.0.5 to 0.0.6
  Skip migration: Bump manifest specVersion from 0.0.1 to 0.0.2
  Skip migration: Bump manifest specVersion from 0.0.2 to 0.0.4

  # 编译输出的关键文件(已准备部署)
  Write subgraph file build\counter\counter.json    # subgraph.yaml 编译后的 JSON 格式配置
  Write subgraph manifest build\subgraph.yaml       # 拷贝原始 manifest 文件用于 build 目录中的部署引用
√ Write compiled subgraph to build\         # 编译完成,所有核心文件写入 build 目录

  # 将核心文件上传到 IPFS(去中心化内容地址存储)
  Add file to IPFS build\schema.graphql         # GraphQL 实体结构定义(决定可查询字段)
                .. QmezFjzFxkKzgjYZzNgM67kEbwFWFqYSCjrGmXwLSsfas5# IPFS文件的唯一哈希值,保证唯一防篡改
  Add file to IPFS build\counter\counter.json       # 索引说明书(合约地址、处理事件、mapping 指向等)
                .. QmUp4K2wbtfL6BS6RkcPmqMrEgzDJFbiG8spBtVFY7cPCX
  Add file to IPFS build\counter\counter.wasm       # 编译后的 mapping.ts 文件(事件处理逻辑)
                .. QmU7rqqieWR3v8rcxwtEvDkWM12ymA4E5bK5eHtpf2zCUa
√ Upload subgraph to IPFS                                                           # 上传成功!

Build completed: QmQ3iiVTZByx28jUuN1nXPAHr2JcwxgjfAVjDa9gYvZhyc # 整个子图的 IPFS 根哈希(代表本次部署的唯一标识)

- Deploying to Graph node https://api.studio.thegraph.com/depl
\ Deploying to Graph node https://api.studio.thegraph.com/depl
| Deploying to Graph node https://api.studio.thegraph.com/depl
/ Deploying to Graph node https://api.studio.thegraph.com/depl
- Deploying to Graph node https://api.studio.thegraph.com/depl
\ Deploying to Graph node https://api.studio.thegraph.com/depl
| Deploying to Graph node https://api.studio.thegraph.com/depl
/ Deploying to Graph node https://api.studio.thegraph.com/depl
- Deploying to Graph node https://api.studio.thegraph.com/depl
\ Deploying to Graph node https://api.studio.thegraph.com/depl
| Deploying to Graph node https://api.studio.thegraph.com/depl
/ Deploying to Graph node https://api.studio.thegraph.com/depl
- Deploying to Graph node https://api.studio.thegraph.com/depl
\ Deploying to Graph node https://api.studio.thegraph.com/depl
| Deploying to Graph node https://api.studio.thegraph.com/depl
/ Deploying to Graph node https://api.studio.thegraph.com/depl
- Deploying to Graph node https://api.studio.thegraph.com/depl    # 部署到graph node节点
Deployed to https://thegraph.com/studio/subgraph/counter      # 部署到studio中 

2.7 验证

部署完成后,在回到web端就会发生变化

当我调用Counter合约的两个函数时,etherscan 和 subgraph studio上都能看到发生的事件;

合约事件:https://sepolia.etherscan.io/address/0xB78473a261797b9Fa4a18adff4Ad89B2b94B8856#events

Subgraph查询:https://thegraph.com/studio/subgraph/counter/playground

image.png

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
0xwu
0xwu
0x919C...8e48
hello