如何使用 Goldsky 索引和查询 Berachain 数据 🧮 | 开发者必备

本篇文章详细介绍了如何使用 Goldsky 子图在 Berachain 网络上索引 ERC20 代币的用户余额,涵盖了从项目目录结构、子图配置到映射文件的编写及部署过程。文章还提供了详细的代码示例、指南和 Berachain 文档链接,帮助开发者轻松查询和存储定制化的区块链数据。

如何使用 Goldsky 索引和查询 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

在这个清单中,有几点值得指出:

  1. sourceaddress 是 bHONEY 代币,它采用 ERC20 接口,从其部署的 startBlock 开始进行索引。
  2. entities:可以将实体视为可查询的 JavaScript 对象。实体之间可以有关系映射,并在 GraphQL 模式(如下所示)中定义。
  3. 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 指令表示 Accountbalances 属性是通过反向查找定义的,基于 TokenBalance 实体中的 account 属性。

TokenBalance 利用 AccountToken 实体来定义每个特定用户的代币余额。

对于本教程来说,实际上并不一定需要同时拥有 TokenTokenBalance 实体。用户的 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 事件作为参数,包含以下信息(fromAddresstoAddresstransferAmount)。借助这些信息,处理程序能够执行以下功能:

  • 获取代币详情
  • 获取账户详情
  • 更新账户余额

从整体上看,每当发出 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 将托管你的子图并执行所有必要的索引。请按照以下说明设置你的账户:

  1. app.goldsky.com 创建一个账户
  2. “设置”页面创建一个 API 密钥
  3. 安装 Goldsky CLI:
curl https://goldsky.com | sh

4. 使用之前创建的 API 密钥登录:

goldsky login

部署你的子图 🚀

在项目根目录下,运行以下命令:

# FROM: ./goldsky-subgraph; 
 
goldsky subgraph deploy erc20-subgraph/1.0.0 --path .

成功部署后,查看你已部署的子图。它不会立即可用,因为索引过程需要遍历每个区块以更新代币余额。

如何使用 Goldsky 索引和查询 Berachain 数据 🧮 | 开发者必备

Goldsky 子图仪表板

查询你的数据

在你的仪表板上,你会看到一个“Public GraphQL link”,它为你提供了一个友好的界面来编写查询。现在让我们用这个查询来测试一下我们新的子图:

{   
  accounts {   
    id   
    balances {   
      id   
      token {   
        id   
        name   
        symbol   
        decimals   
      }   
      amount   
    }   
  }   
}

如果你想要一个示例来练习,可以使用这个实时子图

通过与 Berachain 的区块浏览器对比用户的 MIM 余额,我们可以看到子图已准确索引了用户的代币余额 ✅

如何使用 Goldsky 索引和查询 Berachain 数据 🧮 | 开发者必备

子图与区块浏览器代币余额的比较

总结

各位朋友,这就是你如何使用 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 文档。

https://docs.berachain.com

需要开发支持?

确保加入我们的 Berachain Discord 服务器,并查看我们的开发者频道来提问。

❤️ 不要忘记为这篇文章点赞 👏🏼

原创文章,作者:Rama Ai,如若转载,请注明出处:https://www.dappchaser.com/index-query-berachain-data-with-goldsky/

发表评论

邮箱地址不会被公开。 必填项已用*标注

联系我们

邮件:contact@dappchaser.com

QR code