Indexing events with SubQuery

This guide outlines the process of indexing all transfers and approval events from the Wrapped ETH on ZKsync Era Network.

This guide outlines the process of indexing all transfers and approval events from the Wrapped ETH on ZKsync Era Network.

What you'll learn:

  • How to use Subquery
  • How to create an indexer
  • How to index smart contract events
GitHub

Tools:

  • - SubQuery
  • - GraphQL
guidesubquery
Last Updated: May 09, 2024

The SubQuery Network indexes and services data to the global community in an incentivized and verifiable way. After publishing your project to the SubQuery Network, anyone can index and host it — providing data to users around the world faster and reliably.

Prerequisites

  • Node.js: If not installed, download it here.
  • Docker: If not installed, download it here.
  • Tooling: This guide utilizes @subql/cli. Ensure you have it accessible or installed in your environment.

Understanding SubQuery

A SubQuery extracts data from a blockchain, processing it and storing it so that it can be easily queried via GraphQL. Make advanced, flexible, but simple queries over GraphQL from any website or app. SubQuery supports advanced features like aggregate functions and subscriptions. dApps often need to fetch data from chain. Instead of querying the chain directly, which can be slow and expensive, dApps can use SubQuery to quickly retrieve the data they need.

Key components:

  • project.yaml: a YAML file containing the subgraph manifest.
  • schema.graphql: a GraphQL schema that defines what data is stored for your subgraph, and how to query it via GraphQL.
  • Mappings: Translates from the event data to the entities defined in your schema.

Environment setup

Install the SubQuery CLI using NPM (avoid using yarn global due to dependency issues):

npm install -g @subql/cli
subql --help  # Verify installation

Run subql init in the desired directory, follow the prompts to set up your project:

subql init
# Follow the interactive prompts

After initialization, navigate to your project directory and install dependencies:

cd PROJECT_NAME
npm install

Configure Your Project Manifest File

The project.yaml file serves as the entry point to your ZKsync project, defining how SubQuery will index and transform the chain data. For ZKsync Era, three types of mapping handlers are available:

  • BlockHandlers: Executes a mapping function on every block.
  • TransactionHandlers: Executes a mapping function on each transaction matching optional filter criteria.
  • LogHandlers: Executes a mapping function on each log matching optional filter criteria.

Start by:

Import the contract ABI definition from any standard ERC-20 contract, save it as erc20.abi.json in the /abis directory.

Update the dataSources section in project.yaml as shown below:

project.yaml
dataSources:
  - kind: ethereum/Runtime # We use ethereum runtime since Zksync is a layer-2 that is compatible
    startBlock: 10456259 # This is the block that the contract was deployed on
    options:
      # Must be a key of assets
      abi: erc20
      address: "0x3355df6D4c9C3035724Fd0e3914dE96A5a83aaf4" # This is the contract address for wrapped ether https://explorer.zksync.io/address/0x3355df6D4c9C3035724Fd0e3914dE96A5a83aaf4
    assets:
      erc20:
        file: "./abis/erc20.abi.json"
    mapping:
      file: "./dist/index.js"
      handlers:
        - handler: handleTransaction
          kind: ethereum/TransactionHandler # We use ethereum handlers since Zksync is a layer-2 that is compatible
          filter:
            ## The function can either be the function fragment or signature
            # function: '0x095ea7b3'
            # function: '0x7ff36ab500000000000000000000000000000000000000000000000000000000'
            function: approve(address to, uint256 value)
        - handler: handleLog
          kind: ethereum/LogHandler # We use ethereum handlers since Zksync is a layer-2 that is compatible
          filter:
            topics:
              ## Follows standard log filters https://docs.ethers.io/v5/concepts/events/
              - Transfer(address indexed from, address indexed to, uint256 amount)
              # address: "0x60781C2586D68229fde47564546784ab3fACA982"

Update Your GraphQL Schema File

Remove existing entities and update schema.graphql file to index block information, transfers, and approvals as shown below:

type Transfer @entity {
  id: ID!
  blockHeight: BigInt
  to: String!
  from: String!
  value: BigInt!
  contractAddress: String!
}

type Approval @entity {
  id: ID!
  blockHeight: BigInt
  owner: String!
  spender: String!
  value: BigInt!
  contractAddress: String!
}

Generate entity classes and ABI types:

npm run-script codegen

Import the generated types in your project:

import { Approval, Transfer } from "../types";
import { ApproveTransaction, TransferLog } from "../types/abi-interfaces/Erc20Abi";

Add Mapping Functions

Navigate to src/mappings directory, you'll find handleLog and handleTransaction functions.

Update the functions to process and store the desired data as shown below:

export async function handleLog(log: TransferLog): Promise<void> {
  logger.info(`New transfer transaction log at block ${log.blockNumber}`);
  assert(log.args, "No log.args");

  const transaction = Transfer.create({
    id: log.transactionHash,
    blockHeight: BigInt(log.blockNumber),
    to: log.args.to,
    from: log.args.from,
    value: log.args.value.toBigInt(),
    contractAddress: log.address,
  });

  await transaction.save();
}

export async function handleTransaction(tx: ApproveTransaction): Promise<void> {
  logger.info(`New Approval transaction at block ${tx.blockNumber}`);
  assert(tx.args, "No tx.args");

  const approval = Approval.create({
    id: tx.hash,
    owner: tx.from,
    spender: await tx.args[0],
    value: BigInt(await tx.args[1].toString()),
    contractAddress: tx.to,
  });

  await approval.save();
}

These functions process transaction and log data, extracting necessary information to save in the database. Ensure to check the Mappings documentation for more insights.

Step 6 — Build, Run, and Query Your Project

Build your project:

npm run build

Run your project locally with Docker:

npm run start:docker

Open your browser and head to http://localhost:3000. Explore the GraphQL playground and use the Docs tab to understand the available queries.

Try the following query to understand how it works for your new SubQuery starter project:

{
  query {
    transfers(first: 5, orderBy: VALUE_DESC) {
      totalCount
      nodes {
        id
        blockHeight
        from
        to
        value
        contractAddress
      }
    }
  }
  approvals(first: 5, orderBy: BLOCK_HEIGHT_DESC) {
    nodes {
      id
      blockHeight
      owner
      spender
      value
      contractAddress
    }
  }
}

You should now see the results displayed below in the GraphQL playground, and you're ready to continue developing your SubQuery project.

Conclusion

Congratulations! You have successfully set up a SubQuery project running locally, ready to handle GraphQL API requests pertaining to data transfers.

To ensure your SubQuery project operates efficiently and to sidestep common pitfalls, delve into the Project Optimization section. Happy querying!


Made with ❤️ by the ZKsync Community