- Illustrates how to use subgraphs to index all the ERC-20 token balances for every account on Ethereum.
- Demonstrates mapping and schema design to track token, account, and token balance data comprehensively.
- Retrieves token details from an ERC-20 contract using the generated AssemblyScript code for the
transfer()
event. - Wraps up with sample GraphQL queries on Chainstack Subgraphs to explore indexed account balances.
Introduction
Before the Bored Apes and decentralized loans, there was a time when people were obsessed with creating tokens for everything that moved. This phase turned the Ethereum ecosystem into the Wild West of the cryptocurrency world—with new tokens popping up faster than you can say “to the moon.” The main ingredient of this frenzy was an implementation standard for creating fungible tokens, the ERC-20. ERC-20 was designed to provide a consistent set of rules and standards for developers to follow when creating new tokens on the Ethereum blockchain. This made it easier for people to understand how to create new tokens and for users to understand how to interact with them. Since its introduction in 2015, the ERC-20 has become the most widely used standard for creating new tokens on the Ethereum blockchain. So, it means that if you have dabbled in Ethereum, chances are you have either used, created, or even hoarded tokens made using the ERC-20 standard, and this article is aimed at the hoarders… I mean HODLers of ERC-20 tokens. In this article, you will use subgraphs to index Ethereum accounts and their ERC-20 token balances.Check out A beginner’s guide to getting started with The Graph and Indexing Uniswap data with Subgraphs to learn more about developing Subgraphs.
The modus operandi
Indexing the ERC-20 token balances presents quite a bit of a challenge compared to other exercises using subgraphs. You see while trying to gather this data, you won’t be focusing on a particular account or a contract. Here, you are trying to get the details of all the accounts that house any and all ERC-20 tokens. So where do we start? We know that every ERC-20 token is controlled by a smart contract that implements the ERC-20 standard on the Ethereum blockchain. The ERC-20 standard defines a set of mandatory and optional functions that a contract must implement in order to be considered an ERC-20 token. Of the many mandatory functions that should be implemented while creating an ERC-20 token, thetransfer()
function directly affects the token balance of an account.
Here’s a complete overview of the ERC-20 token standard: OpenZeppelin ERC-20 Doc
transfer()
function facilitates the transfer of tokens from one account to another. By mandate, the function must also emit a Transfer
event that carries the details of the accounts involved in the transfer and the value of the tokens that were transferred.
By capturing and processing the Transfer
events that were emitted, we could access all the above-mentioned transfer information plus the token’s address (contract). With all data, I say we could create a nice index of token balances. So, let’s get to it.
Prerequisites
Before we start scanning the Ethereum network for token balances, make sure you have installed the following on your computer:- Node (version ≥ 16) and the corresponding npm
- A reasonably useful code editor
Setting up the project
To write the code used in this article, the following Graph protocol repository was referred to:graphprotocol/erc20-subgraph.
- Create a new directory.
- Open a terminal in the directory.
- Use the following command:
Also note that the graph-cli will pick up the beginning block number automatically, but you can edit it to start indexing from the block you want.
transfer()
function and the associated event are mandatory, meaning that every ERC-20 token contract would have to implement them, and they would do so in a uniform format.
The graph-cli uses the contract address to fetch the contract ABI, which is required for accessing the contract functions. Since every ERC-20 token contract will have the transfer()
function and the Transfer
event, we can use the ABI of any given ERC-20 token contract for accessing them.
You can get the token contract address from Etherscan. To avoid confusion, you can set the contract name as ERC20
, as a nod to the generic nature in which we will be using its ABI.
Here’s a list of ERC-20 tokens to choose from Etherscan Token Tracker.
/abis
directory, the contract ABI is saved as ERC20.json
(based on the contract name that we provided).
Writing the schema
Now that we have the base template for our project, we can start working on our schema file,schema.graphql
. Within this file, we will define the data objects or entities we need. When you analyze our use case, you can see that in order to access the token balance, we also need information regarding the account and the token involved. Based on this requirement, let’s model our schema file:
Token
and Account
entities, we have also modeled the token balance as a separate entity, TokenBalance
. Since the token balance is associated with an account, we have declared a balances
field inside the Account
entity and declared it as a list of token balances; [TokenBalance!]
. This represents the fact that a single account can have multiple token balances. The @derivedFrom
directive in the field is used to declare it as a reverse lookup. By doing so, we have created a virtual field on the Account
entity (Balances
) that is derived from the relationship defined on the TokenBalance
entity (account
). Thus, the Balances
field need not be set manually using the mapping file.
Now that we are done with the schema let’s work on the manifest file.
Learn more about the schema by reading Explaining Subgraph schemas.
Modifying the manifest
If you open the auto-generated manifest filesubgraph.yaml
in your code editor, you will see that it is populated with many details pertaining to the contract that we mentioned while setting up the project. For our project, we won’t be needing many of these details as most of them are specific to that contract. So, edit the file in the following way:
entities
with the ones that we have defined. We have also removed all the eventHandlers
except the one for the Transfer
event and in the source
section, we have removed the contract address, as our subgraph is not directed towards any particular address.
We have also added the startBlock
parameter in the source
section. This will specify the block from which we wish to start the indexing.
And with that, we have our new and improved manifest file.
Code generation
Once you have the schema and the manifest files, you can use the following command to generate the required AssemblyScript code:/generated
directory:
Creating the mappings
The mapping file is where we actually associate the blockchain data to the entities that we have defined in our schema. They contain the code for theeventHandlers
, mentioned in our manifest file. As with the manifest file, if you open the auto-generated mapping file erc-20.ts
inside the /src
directory, you will find out that it contains the code for various eventHandlers
, but we can remove most of them and work on the handleTransfer
event handler.
Now, before we start editing our core mapping file, I would like you to create a separate file in the /src
directory. Within this file, we will be defining the code for:
- Fetching the token details
- Fetching the account details
- Getting the token balance
You can include all these functions in the core mapping file itself, but keeping it in a separate file promotes modularity and makes the code more readable.
utils.ts
inside our /src
directory and add the following code:
fetchTokenDetails()
gets us the details of a token. It does so by accessing the token address from the event
parameter and binding it with the ERC20
(token contract ABI) class. This will let us access all the public, read-only functions from the token contract, and using those functions we can retrieve the details like token name, symbol, and decimals. The fetchAccount()
function fetches the details of the account.
The fetchBalance()
function retrieves the balance of a particular token in a given account. To fetch the balance, the function uses the token address, which is given as a function parameter, and binds it with the ERC20
class. This allows you to access the balanceOf()
function, which takes the account address as the parameter and returns the token balance. The balanceOf()
is a read-only function that is implemented as part of the ERC-20 token standard.
In the code, we also use the graph-ts library to import certain useful data types. This library is automatically installed when we set up the project.
Once you add the new code, your project structure should look like this:
/src/erc-20.ts
.
Handling Transfer
event
Within the mapping file, you will be writing the code for the handleTransfer
event handler. The code should be defined as a function of the same name, handleTransfer()
:
handleTransfer()
function takes the Transfer
event as the parameter. From the event, the code retrieves the address of the token and the addresses of accounts involved in the transfer. These addresses are used to fetch the details of the token and the accounts respectively. For retrieving the details, we make use of the functions that we defined in the utils.ts
file. Once we have all the information, we use it to set the token balances of both accounts involved in the transfer.
With that, we have everything that we need to deploy our subgraph, so let’s build our code and deploy the subgraph using Chainstack.
Building the code
To build the code, open a terminal in the root directory of the project and type:.wasm
format. The build outputs will be stored inside the /build
directory.
Now, let’s deploy the subgraph.
Deploying the subgraph
To deploy your subgraph:- Head over to Chainstack.
- Create a new project.
- Go to the Subgraphs section.
- Click Add subgraph. The Add subgraph page is displayed.
-
In the Create subgraph section:
- Enter a Name for the subgraph
- Select the Project you created
- Click Add subgraph. The details page of the new subgraph is displayed.
- Scroll down to the part where it shows the Subgraph deployment command.
- Copy the command.
- Open a terminal in your project directory.
- Paste and run the deployment command.
Given the scale of the data that we are trying to index, the subgraph can take multiple hours/days to synchronize.
Querying the data
The GraphQL UI URL provides a neat interface for querying and viewing the data. For fetching the list of all the accounts and their token balances, use the following query:first
— specifies the maximum number of results to return.skip
— specifies the number of results to skip.
orderBy
and orderDirection
parameters.
orderBy
— specifies the field by which to sort the results.orderDirection
— specifies the direction of the sort, eitherasc
for ascending ordesc
for descending.