本篇文章详细介绍了如何使用 Goldsky 子图在 Berachain 网络上索引 ERC20 代币的用户余额,涵盖了从项目目录结构、子图配置到映射文件的编写及部署过程。文章还提供了详细的代码示例、指南和 Berachain 文档链接,帮助开发者轻松查询和存储定制化的区块链数据。
Goldsky 是什么,为什么我应该关心索引?
Goldsky 让开发者构建 GraphQL API,用于存储和查询来自区块链的数据。这些数据具有高度可定制性,可以用于揭示如代币供应增长等趋势,或即时查询用户代币余额等数据。
子图(Subgraph)包含从区块链索引数据的逻辑,将原始数据转换并存储为易于查询的形式。
在本教程中,我们将教你如何开发一个子图,查询用户在 Berachain 网络上的 ERC20 余额。
准备要求 📋
- Nodejs v20.11.0 或更高版本
- pnpm
- 一个 IDE(如 VSCode、Replit 等)
如果你已经在 The Graph 上部署了现有的子图,并希望部署到 Berachain/Goldsky,请跳到“在 Goldsky 上设置”部分。Goldsky 和 The Graph 的子图是完全兼容的!
构建你的子图 🛠️
首先,在终端中输入:
mkdir goldsky-subgraph;
cd goldsky-subgraph;
pnpm init;
pnpm install @graphprotocol/graph-cli @graphprotocol/graph-ts;
# Accept all of the defaults, hitting enter when prompted
# name: (project-name) project-name
# version: (0.0.0) 0.0.1
# description: The Project Description
# entry point: //leave empty
# test command: //leave empty
# git repository: //the repositories url
# keywords: //leave empty
# author: // your name
# license: N/A
# @graphprotocol dependencies provide subgraph development tooling
在项目的根目录下,创建以下项目结构(目前文件可以是空的):
# FROM: ./goldsky-subgraph;
.
├── abis
│ └── Erc20.json
├── package.json
├── schema.graphql
├── src
│ ├── mapping.ts
│ ├── utils.ts
└── subgraph.yaml
在 ./abis/Erc20.json
文件中粘贴以下内容,这些内容定义了 ERC20 合约接口:
[
{
"constant": true,
"inputs": [],
"name": "name",
"outputs": [{ "name": "", "type": "string" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{ "name": "_spender", "type": "address" },
{ "name": "_value", "type": "uint256" }
],
"name": "approve",
"outputs": [{ "name": "", "type": "bool" }],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "totalSupply",
"outputs": [{ "name": "", "type": "uint256" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{ "name": "_from", "type": "address" },
{ "name": "_to", "type": "address" },
{ "name": "_value", "type": "uint256" }
],
"name": "transferFrom",
"outputs": [{ "name": "", "type": "bool" }],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "decimals",
"outputs": [{ "name": "", "type": "uint8" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [{ "name": "_owner", "type": "address" }],
"name": "balanceOf",
"outputs": [{ "name": "balance", "type": "uint256" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "symbol",
"outputs": [{ "name": "", "type": "string" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{ "name": "_to", "type": "address" },
{ "name": "_value", "type": "uint256" }
],
"name": "transfer",
"outputs": [{ "name": "", "type": "bool" }],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [
{ "name": "_owner", "type": "address" },
{ "name": "_spender", "type": "address" }
],
"name": "allowance",
"outputs": [{ "name": "", "type": "uint256" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{ "payable": true, "stateMutability": "payable", "type": "fallback" },
{
"anonymous": false,
"inputs": [
{ "indexed": true, "name": "owner", "type": "address" },
{ "indexed": true, "name": "spender", "type": "address" },
{ "indexed": false, "name": "value", "type": "uint256" }
],
"name": "Approval",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{ "indexed": true, "name": "from", "type": "address" },
{ "indexed": true, "name": "to", "type": "address" },
{ "indexed": false, "name": "value", "type": "uint256" }
],
"name": "Transfer",
"type": "event"
}
]
配置子图
创建子图的主要步骤是定义我们要读取的数据源,以及我们将这些数据索引到的数据结构(实体)。这通过 subgraph.yaml
文件,即子图清单来完成。
在 ./subgraph.yaml
中粘贴以下内容:
specVersion: 0.0.4
description: ERC-20 subgraph with event handlers & entities
schema:
file: ./schema.graphql
dataSources:
- kind: ethereum/contract
name: Erc20
network: berachain-bartio
source:
address: "0x1306D3c36eC7E38dd2c128fBe3097C2C2449af64"
abi: Erc20
startBlock: 88948
mapping:
kind: ethereum/events
apiVersion: 0.0.7
language: wasm/assemblyscript
entities:
- Token
- Account
- TokenBalance
abis:
- name: Erc20
file: ./abis/Erc20.json
eventHandlers:
- event: Transfer(indexed address,indexed address,uint256)
handler: handleTransfer
file: ./src/mapping.ts
在这个清单中,有几点值得指出:
- source:
address
是 bHONEY 代币,它采用 ERC20 接口,从其部署的startBlock
开始进行索引。 - entities:可以将实体视为可查询的 JavaScript 对象。实体之间可以有关系映射,并在 GraphQL 模式(如下所示)中定义。
- eventHandlers:每当发出
tokenTransfer
事件时,./src/mapping.ts
文件中的handleTransfer
方法会被调用来执行索引逻辑。
编写模式
在 ./schema.graphql
文件中,我们定义了模式,包含我们子图中不同实体的属性和关系:
# Token details
type Token @entity {
id: ID!
#token name
name: String!
#token symbol
symbol: String!
#decimals used
decimals: BigDecimal!
}
# account details
type Account @entity {
#account address
id: ID!
#balances
balances: [TokenBalance!]! @derivedFrom(field: "account")
}
# token balance details
type TokenBalance @entity {
id: ID!
#token
token: Token!
#account
account: Account!
#amount
amount: BigDecimal!
}
Token 实体非常简单,包含 ERC20 代币的常见属性。
Account 实体包含一个钱包地址的 id
,更有趣的是,它还包含一个 TokenBalance
类型的余额列表。@derivedFrom
指令表示 Account
的 balances
属性是通过反向查找定义的,基于 TokenBalance
实体中的 account
属性。
TokenBalance 利用 Account 和 Token 实体来定义每个特定用户的代币余额。
对于本教程来说,实际上并不一定需要同时拥有 Token 和 TokenBalance 实体。用户的 MIM 余额完全可以仅通过 Account 实体来捕获。然而,这种设计使得可以扩展子图以捕获多个代币余额。
创建映射
映射文件是所有内容汇集的地方 ✨ 区块链数据与我们在模式中定义的实体相关联。下面,我们定义了 handleTransfer
事件处理程序被调用时发生的交互。
在 ./src/mapping.ts
中添加以下代码:
//import event class from generated files
import { Transfer } from "../generated/Erc20/Erc20";
//import the functions defined in utils.ts
import { fetchTokenDetails, fetchAccount, updateTokenBalance } from "./utils";
//import datatype
import { BigInt } from "@graphprotocol/graph-ts";
export function handleTransfer(event: Transfer): void {
// 1. Get token details
let token = fetchTokenDetails(event);
if (!token) {
return;
}
// 2. Get account details
let fromAddress = event.params.from.toHex();
let toAddress = event.params.to.toHex();
let fromAccount = fetchAccount(fromAddress);
let toAccount = fetchAccount(toAddress);
if (!fromAccount || !toAccount) {
return;
}
// 3. Update the token balances
// Setting the token balance of the 'from' account
updateTokenBalance(
token,
fromAccount,
BigInt.fromI32(0).minus(event.params.value)
);
// Setting the token balance of the 'to' account
updateTokenBalance(token, toAccount, event.params.value);
}
handleTransfer
方法以 Transfer
事件作为参数,包含以下信息(fromAddress
,toAddress
,transferAmount
)。借助这些信息,处理程序能够执行以下功能:
- 获取代币详情
- 获取账户详情
- 更新账户余额
从整体上看,每当发出 Transfer
事件时,该处理程序代码将忠实地更新 ERC20 账户余额。
与实体协作
你可能已经注意到,mapping.ts
文件中的代码相对简单——这是因为与实体交互的繁重工作已被抽象到一个实用文件中。现在,让我们了解如何具体操作子图实体的基础细节。
在 ./src/utils.ts
中添加以下代码:
//import smart contract class from generated files
import { Erc20 } from "../generated/Erc20/Erc20";
//import entities
import { Account, Token, TokenBalance } from "../generated/schema";
//import datatypes
import { BigDecimal, ethereum, BigInt } from "@graphprotocol/graph-ts";
const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
// Fetch token details
export function fetchTokenDetails(event: ethereum.Event): Token | null {
//check if token details are already saved
let token = Token.load(event.address.toHex());
if (!token) {
//if token details are not available
//create a new token
token = new Token(event.address.toHex());
//set some default values
token.name = "N/A";
token.symbol = "N/A";
token.decimals = BigDecimal.fromString("0");
//bind the contract
let erc20 = Erc20.bind(event.address);
//fetch name
let tokenName = erc20.try_name();
if (!tokenName.reverted) {
token.name = tokenName.value;
}
//fetch symbol
let tokenSymbol = erc20.try_symbol();
if (!tokenSymbol.reverted) {
token.symbol = tokenSymbol.value;
}
//fetch decimals
let tokenDecimal = erc20.try_decimals();
if (!tokenDecimal.reverted) {
token.decimals = BigDecimal.fromString(tokenDecimal.value.toString());
}
//save the details
token.save();
}
return token;
}
// Fetch account details
export function fetchAccount(address: string): Account | null {
//check if account details are already saved
let account = Account.load(address);
if (!account) {
//if account details are not available
//create new account
account = new Account(address);
account.save();
}
return account;
}
export function updateTokenBalance(
token: Token,
account: Account,
amount: BigInt
): void {
// Don't update zero address
if (ZERO_ADDRESS == account.id) return;
// Get existing account balance or create a new one
let accountBalance = getOrCreateAccountBalance(account, token);
let balance = accountBalance.amount.plus(bigIntToBigDecimal(amount));
// Update the account balance
accountBalance.amount = balance;
accountBalance.save();
}
function getOrCreateAccountBalance(
account: Account,
token: Token
): TokenBalance {
let id = token.id + "-" + account.id;
let tokenBalance = TokenBalance.load(id);
// If balance is not already saved
// create a new TokenBalance instance
if (!tokenBalance) {
tokenBalance = new TokenBalance(id);
tokenBalance.account = account.id;
tokenBalance.token = token.id;
tokenBalance.amount = BigDecimal.fromString("0");
tokenBalance.save();
}
return tokenBalance;
}
function bigIntToBigDecimal(quantity: BigInt, decimals: i32 = 18): BigDecimal {
return quantity.divDecimal(
BigInt.fromI32(10)
.pow(decimals as u8)
.toBigDecimal()
);
}
fetchAccount()
返回一个 Account 实体。首先检查传入的地址是否已存在对应的 Account 实体。如果不存在,则创建一个新的 Account 实体。fetchTokenDetails()
返回一个 Token 实体。如果没有现有的 Token,我们通过将代币地址与 ERC20 接口绑定来实例化一个新的 Token,这使我们能够访问代币合约的公开读取函数。这样可以检索和设置代币的属性,例如名称、符号和小数位数。updateTokenBalance()
可能是最重要的函数,因为它在每次转账时更新用户的 TokenBalance 实体。回顾mapping.ts
文件,这个函数在每次 Transfer 事件中调用两次——对于转出方传入负数金额,表示代币余额的减少;对于接收方则传入正数金额。通过这种方式,保持了代币余额的精确记录。
构建子图
在项目根目录下,在终端中运行以下命令:
# FROM: ./goldsky-subgraph;
pnpm codegen;
pnpm build;
这些命令会从你的合约 ABI 文件生成 TypeScript 类文件,编译你的代码,并在 /build
目录中创建构建输出。
在我们部署之前,首先需要在 Goldsky 上进行设置。
在 Goldsky 上进行设置
Goldsky 将托管你的子图并执行所有必要的索引。请按照以下说明设置你的账户:
- 在 app.goldsky.com 创建一个账户
- 在“设置”页面创建一个 API 密钥
- 安装 Goldsky CLI:
curl https://goldsky.com | sh
4. 使用之前创建的 API 密钥登录:
goldsky login
部署你的子图 🚀
在项目根目录下,运行以下命令:
# FROM: ./goldsky-subgraph;
goldsky subgraph deploy erc20-subgraph/1.0.0 --path .
成功部署后,查看你已部署的子图。它不会立即可用,因为索引过程需要遍历每个区块以更新代币余额。
Goldsky 子图仪表板
查询你的数据
在你的仪表板上,你会看到一个“Public GraphQL link”,它为你提供了一个友好的界面来编写查询。现在让我们用这个查询来测试一下我们新的子图:
{
accounts {
id
balances {
id
token {
id
name
symbol
decimals
}
amount
}
}
}
如果你想要一个示例来练习,可以使用这个实时子图。
通过与 Berachain 的区块浏览器对比用户的 MIM 余额,我们可以看到子图已准确索引了用户的代币余额 ✅
子图与区块浏览器代币余额的比较
总结
各位朋友,这就是你如何使用 Goldsky 子图来索引 Berachain 钱包的代币余额。Goldsky 是一个数据可用性平台,为开发者提供了一种轻松存储和查询定制化区块链数据的方式。
🐻 完整代码仓库
如果你想查看最终代码并查看更多指南,请查看 Berachain Goldsky 指南代码。
https://github.com/berachain/guides/tree/feat/goldsky-subgraph/apps/goldsky-subgraph
🛠️ 想构建更多项目?
想要在 Berachain 上构建更多项目并查看更多示例吗?请查看我们的 Berachain GitHub 指南仓库,里面包含各种实现方式,包括 NextJS、Hardhat、Viem、Foundry 等。
https://github.com/berachain/guides
如果你想深入了解更多细节,请查看我们的 Berachain 文档。
需要开发支持?
确保加入我们的 Berachain Discord 服务器,并查看我们的开发者频道来提问。
❤️ 不要忘记为这篇文章点赞 👏🏼
原创文章,作者:Rama Ai,如若转载,请注明出处:https://www.dappchaser.com/index-query-berachain-data-with-goldsky/