Index & Query Berachain Data with Goldsky šŸ§®

Beary Cucumber
Beary Cucumber
9 min read
Index & Query Berachain Data with Goldsky šŸ§®

What is Goldsky and Why Should I Care about Indexing?

Goldsky lets developers build GraphQL APIs that store and query data from the blockchain. This data is highly customizable and can be used to uncover trends such as token supply growth over time, or instantaneous data such as user token balances.

A subgraph contains logic for indexing data from a blockchain, transforming raw data and storing it in a form that can be easily queried.

In this tutorial, we teach you to develop a subgraph which queries usersā€™ ERC20 balances on the Berachain network.

Pre-requisites šŸ“‹

  • Nodejs v20.11.0 or greater
  • pnpm
  • An IDE e.g. VSCode, Replit, etc.

For those with existing subgraphs deployed on The Graph who want to deploy to Berchain/Goldsky, skip to the section Getting Set up on Goldsky. Goldsky and The Graph subgraphs are fully compatible with one another!

Building your Subgraph šŸ› ļø

To begin, enter in your terminal:

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

In the root of your project, create the following project structure (empty files are fine for now):

# FROM: ./goldsky-subgraph; 
 
. 
ā”œā”€ā”€ abis 
ā”‚   ā””ā”€ā”€ Erc20.json 
ā”œā”€ā”€ package.json 
ā”œā”€ā”€ schema.graphql 
ā”œā”€ā”€ src 
ā”‚   ā”œā”€ā”€ mapping.ts 
ā”‚   ā”œā”€ā”€ utils.ts 
ā””ā”€ā”€ subgraph.yaml

In ./abis/Erc20.json paste in the contents below, which define the ERC20 contract interface:

[ 
  { 
    "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" 
  } 
]

Configuring the Subgraph

The primary step in creating a subgraph is to define the data sources we read from, and the data structures (entities) we index this data into. This is accomplished in the subgraph.yaml, or the subgraph manifest.

In ./subgraph.yaml paste the following:

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

In this manifest, a few things are worth pointing out:

  1. source: The address is the bHONEY token, which employs the Erc20 interface, to be indexed from the startBlock it was deployed
  2. entities can be thought of as JavaScript objects which can be queried. Entities can have relational mappings between one another and are defined in a GraphQL schema (below)
  3. eventHandlers: Every time a tokenTransfer event is emitted, the handleTransfer method in ./src/mapping.ts is called to perform indexing logic

Writing the Schema

In ./schema.graphql we define our schema, containing properties and relationships of the different entities existing within our subgraph:

# 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! 
}

The Token entity is straightforward, containing well-known properties of ERC20 tokens.

The Account entity contains an id of the wallet address and more interestingly, a list of balances of the type TokenBalance. The @derivedFrom directive means that an Accountā€™s balances property is defined by reverse lookup, based on the account property in TokenBalance entities.

TokenBalance leverages both Account and Token entities, to define the token balance of each particular user.

For this tutorial, itā€™s wasnā€™t absolutely necessary to have both Token and TokenBalance entities. Usersā€™ MIM balances could conceivably have been captured in just the Account entity. However, this design makes it possible to extend the subgraph to capture multiple token balances.

Creating Mappings

The mapping file is where everything comes together āœØ Blockchain data is associated with the entities we have defined in our schema. Below, we define the interations that happen when the handleTransfer event handler is invoked.

In ./src/mapping.ts add the following code:

//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); 
}

The handleTransfer takes the Transfer event as a parameter, containing the information (fromAddress, toAddress, transferAmount). With this info, the handler is empowered to perform the following functions:

  1. Fetch token details
  2. Fetch account details
  3. Update account balances

At a high level, this handler code will dutifuly update ERC20 account balances every time a Transfer event is emitted.

Working With Entities

You may have noticed that the code in mapping.ts is rather simpleā€Šā€”ā€Šthatā€™s because the heavy lifting of interacting with entities has been abstracted into a utility file. Letā€™s go over the nuts and bolts of working with subgraph entities now.

In ./src/utils.ts add the following code:

//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() returns an Account entity. We first see whether an Account entity with the passed in address exists. If it doesnā€™t, we create a new one
  • fetchTokenDetails() returns a Token entity. If no Tokenexists, we instantiate a new one by binding the token address with the ERC20 interface, letting us access the public read functions from the token contract. This allows us to retrieve and set token properties such as name, symbol and decimals
  • updateTokenBalance() is perhaps the most important, as it updates usersā€™ TokenBalance entities with each transfer. Referring back to the mapping.ts file, this function is invoked twice with each Transfer eventā€Šā€”ā€Ša negative amount is passed in for the transferer, denoting a loss of token balance, and conversely, a positive amount for the recipient. This way, an accurate accounting of token balances is maintained

Building the Subgraph

In the root of your project directory, run the following in your terminal:

# FROM: ./goldsky-subgraph; 
 
pnpm codegen; 
pnpm build;

These commands generate TypeScript class files from your contract ABIs, compiles your code, and creates build outputs in the /build directory.

Before we deploy, we have to get you set up on Goldsky.

Getting Set up on Goldsky

Goldsky will host your subgraph and do all the necessary indexing. Follow the instructions below to get your account set up:

  1. Create an account at app.goldsky.com
  2. Create an API key on the Settings page
  3. Install the Goldsky CLI:
curl https://goldsky.com | sh

4. Log in with the API key created earlier:

goldsky login

Deploy your Subgraph šŸš€

In the root of your project, run the following:

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

Once successfully deployed, view your deployed subgraph. It wonā€™t be usable right away, because the indexing process requires going over every block to update token balances.

Goldsky Subgraph Dashboard

Querying your Data

In your dashboard, you will see a ā€œPublic GraphQL linkā€ which provides a nice interface for you to write queries. Now letā€™s give our new subgraph a spin with this query:

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

If you want an example to play along with, use this live subgraph.

Cross checking usersā€™s MIM balances against Berachainā€™s block explorer, we see that our subgraph has accurately indexed usersā€™ token balances āœ…

Comparison of Subgraph and Block Explorer Token Balances

Recap

And that, frens, is how you use Goldsky subgraphs to index token balances of Berachain wallets. Goldsky is a data-availability platform, providing a way for developers to easily store and query customized blockchain data.


šŸ» Full Code Repository

If you want to see the final code and see other guides, check out Berachain Goldsky Guide Code.

guides/apps/goldsky-subgraph at feat/goldsky-subgraph Ā· berachain/guides
A demonstration of different contracts, languages, and libraries that work with Berachain EVM. ā€¦

šŸ› ļø Want To Build More?

Want to build more on Berachain and see more examples. Take a look at our Berachain GitHub Guides Repo for a wide variety of implementations that include NextJS, Hardhat, Viem, Foundry, and more.

GitHub - berachain/guides: A demonstration of different contracts, languages, and libraries thatā€¦
A demonstration of different contracts, languages, and libraries that work with Berachain EVM. - berachain/guides

If youā€™re looking to dive deeper into the details, take a look at our Berachain Docs.

Berachain Docs
Berachain protocol core docs for learning how the blockchain works, developer guides, and understanding how to manageā€¦

Looking For Dev Support?

Make sure to join our Berachain Discord server and check out our developer channels to ask questions.

ā¤ļø Donā€™t forget to show some love for this article šŸ‘šŸ¼