Creating A Simple Subgraph

A subgraph is an open API to track and query an event recorded on a blockchain

This tutorial will go through the creation of a simple subgraph that tracks a single event of a token. For this tutorial we will be using USDT(Tether)

The first place to start is the Graph Subgraph Studio which is accessed from the Graph website

Here you will need to create a new Subgraph

Once your subgraph is created all the instructions you need to initialize the subgraph on your computer and deploy the finished subgraph can be found on the page containing your Subgraph details.

If you have not done so already, the first thing you will need to do is install the Graph CLI tool on your computer. That is done by running the following code

npm install -g @graphprotocol/graph-cli

or

yarn global add @graphprotocol/graph-cli

Once the Graph CLI is installed you are ready to initialize a subgraph. A Subgraph is initialized on your local system with this command.

graph init --studio simple-subgraph

You will face various prompts as the process starts. The first thing you will have to specify is what protocol is the smart contract you wish to track is deployed on. For this tutorial we are tracking USDT(Tether) erc-20, deployed on ethereum

You will then be prompted for the slug of the subgraph, which should be the same as the slug in the subgraph studio, followed by the name of the folder that the subgraph will be initialized in. This can be the same as the slug or you may choose to change it.

At this point you will need to specify which ethereum network the smart contract is on.

You will be required to provide the deployment address of the smart contract.

If its a contract that you deployed you already have this address from Hardhat or which ever tool you used to deploy your smart contract, when you completed the deployment. For a contract deployed by a third party you can easily get the address from a block explorer. In this case, we are using etherscan to get the smart contract address for USDT(Tether)

Once you have retrieved the contract address from your block explorer of choice you paste it and continue the process.

After pasting the contract address the final prompt would be for the starting block number.

Depending on the purpose of your subgraph, you can accept the default value of the starting block number or you can change it. The starting block number can also be manually modified in the subgraph manifest. The Graph CLI will go ahead and scaffold a subgraph for you.

It will ask if you want to add another smart contract, for our purpose, the answer is no

That's it! You have created a subgraph. You can deploy this subgraph as is, however, we will be editing 3 files before we deploy the subgraph.

The 3 files are

  1. Subgraph.yaml

  2. Schema.graphql

  3. contracts.ts

Before we proceed any further, we should authenticate our project to the Graph studio with the graph cli

Next we change directory into the created subgraph folder

cd simple-subgraph

We open the subgraph.yaml file in our code editor.

specVersion: 1.0.0
indexerHints:
  prune: auto
schema:
  file: ./schema.graphql
dataSources:
  - kind: ethereum
    name: Contract
    network: mainnet
    source:
      address: "0xdAC17F958D2ee523a2206206994597C13D831ec7"
      abi: Contract
      startBlock: 4634748
    mapping:
      kind: ethereum/events
      apiVersion: 0.0.7
      language: wasm/assemblyscript
      entities:
        - Issue
        - Redeem
        - Deprecate
        - Params
        - DestroyedBlackFunds
        - AddedBlackList
        - RemovedBlackList
        - Approval
        - Transfer
        - Pause
        - Unpause
      abis:
        - name: Contract
          file: ./abis/Contract.json
      eventHandlers:
        - event: Issue(uint256)
          handler: handleIssue
        - event: Redeem(uint256)
          handler: handleRedeem
        - event: Deprecate(address)
          handler: handleDeprecate
        - event: Params(uint256,uint256)
          handler: handleParams
        - event: DestroyedBlackFunds(address,uint256)
          handler: handleDestroyedBlackFunds
        - event: AddedBlackList(address)
          handler: handleAddedBlackList
        - event: RemovedBlackList(address)
          handler: handleRemovedBlackList
        - event: Approval(indexed address,indexed address,uint256)
          handler: handleApproval
        - event: Transfer(indexed address,indexed address,uint256)
          handler: handleTransfer
        - event: Pause()
          handler: handlePause
        - event: Unpause()
          handler: handleUnpause
      file: ./src/contract.ts

We will be editing the start block of the subgraph from the one created

We also will be modifying the entities and the event handlers

We are only going to be tracking one entity, Transfers, and we will only require the event handlers for this entity

The subgraph.yaml file should look like this

specVersion: 1.0.0
indexerHints:
  prune: auto
schema:
  file: ./schema.graphql
dataSources:
  - kind: ethereum
    name: Contract
    network: mainnet
    source:
      address: "0xdAC17F958D2ee523a2206206994597C13D831ec7"
      abi: Contract
      startBlock: 19582569
    mapping:
      kind: ethereum/events
      apiVersion: 0.0.7
      language: wasm/assemblyscript
      entities:

        - Transfer

      abis:
        - name: Contract
          file: ./abis/Contract.json
      eventHandlers:

        - event: Transfer(indexed address,indexed address,uint256)
          handler: handleTransfer

      file: ./src/contract.ts

After this we will be editing the schema.graphql file

type Issue @entity(immutable: true) {
  id: Bytes!
  amount: BigInt! # uint256
  blockNumber: BigInt!
  blockTimestamp: BigInt!
  transactionHash: Bytes!
}

type Redeem @entity(immutable: true) {
  id: Bytes!
  amount: BigInt! # uint256
  blockNumber: BigInt!
  blockTimestamp: BigInt!
  transactionHash: Bytes!
}

type Deprecate @entity(immutable: true) {
  id: Bytes!
  newAddress: Bytes! # address
  blockNumber: BigInt!
  blockTimestamp: BigInt!
  transactionHash: Bytes!
}

type Params @entity(immutable: true) {
  id: Bytes!
  feeBasisPoints: BigInt! # uint256
  maxFee: BigInt! # uint256
  blockNumber: BigInt!
  blockTimestamp: BigInt!
  transactionHash: Bytes!
}

type DestroyedBlackFunds @entity(immutable: true) {
  id: Bytes!
  _blackListedUser: Bytes! # address
  _balance: BigInt! # uint256
  blockNumber: BigInt!
  blockTimestamp: BigInt!
  transactionHash: Bytes!
}

type AddedBlackList @entity(immutable: true) {
  id: Bytes!
  _user: Bytes! # address
  blockNumber: BigInt!
  blockTimestamp: BigInt!
  transactionHash: Bytes!
}

type RemovedBlackList @entity(immutable: true) {
  id: Bytes!
  _user: Bytes! # address
  blockNumber: BigInt!
  blockTimestamp: BigInt!
  transactionHash: Bytes!
}

type Approval @entity(immutable: true) {
  id: Bytes!
  owner: Bytes! # address
  spender: Bytes! # address
  value: BigInt! # uint256
  blockNumber: BigInt!
  blockTimestamp: BigInt!
  transactionHash: Bytes!
}

type Transfer @entity(immutable: true) {
  id: Bytes!
  from: Bytes! # address
  to: Bytes! # address
  value: BigInt! # uint256
  blockNumber: BigInt!
  blockTimestamp: BigInt!
  transactionHash: Bytes!
}

type Pause @entity(immutable: true) {
  id: Bytes!

  blockNumber: BigInt!
  blockTimestamp: BigInt!
  transactionHash: Bytes!
}

type Unpause @entity(immutable: true) {
  id: Bytes!

  blockNumber: BigInt!
  blockTimestamp: BigInt!
  transactionHash: Bytes!
}

Like in the case of the subgraph.yaml, we are removing the queries of all the entities we are not tracking and leaving just the transfer query.

We are going to edit this transfer query further as such

We will change the following

  • from to sender

  • to to receiver

  • value to amount

The schema.graphql file should look like this

type Transfer @entity(immutable: true) {
  id: Bytes!
  sender: Bytes! # address
  receiver: Bytes! # address
  amount: BigInt! # uint256
  blockNumber: BigInt!
  blockTimestamp: BigInt!
  transactionHash: Bytes!
}

The final file we will be editing will be the contract.ts file which can be found in the src folder inside the subgraph folder

import {
  Issue as IssueEvent,
  Redeem as RedeemEvent,
  Deprecate as DeprecateEvent,
  Params as ParamsEvent,
  DestroyedBlackFunds as DestroyedBlackFundsEvent,
  AddedBlackList as AddedBlackListEvent,
  RemovedBlackList as RemovedBlackListEvent,
  Approval as ApprovalEvent,
  Transfer as TransferEvent,
  Pause as PauseEvent,
  Unpause as UnpauseEvent
} from "../generated/Contract/Contract"
import {
  Issue,
  Redeem,
  Deprecate,
  Params,
  DestroyedBlackFunds,
  AddedBlackList,
  RemovedBlackList,
  Approval,
  Transfer,
  Pause,
  Unpause
} from "../generated/schema"

export function handleIssue(event: IssueEvent): void {
  let entity = new Issue(
    event.transaction.hash.concatI32(event.logIndex.toI32())
  )
  entity.amount = event.params.amount

  entity.blockNumber = event.block.number
  entity.blockTimestamp = event.block.timestamp
  entity.transactionHash = event.transaction.hash

  entity.save()
}

export function handleRedeem(event: RedeemEvent): void {
  let entity = new Redeem(
    event.transaction.hash.concatI32(event.logIndex.toI32())
  )
  entity.amount = event.params.amount

  entity.blockNumber = event.block.number
  entity.blockTimestamp = event.block.timestamp
  entity.transactionHash = event.transaction.hash

  entity.save()
}

export function handleDeprecate(event: DeprecateEvent): void {
  let entity = new Deprecate(
    event.transaction.hash.concatI32(event.logIndex.toI32())
  )
  entity.newAddress = event.params.newAddress

  entity.blockNumber = event.block.number
  entity.blockTimestamp = event.block.timestamp
  entity.transactionHash = event.transaction.hash

  entity.save()
}

export function handleParams(event: ParamsEvent): void {
  let entity = new Params(
    event.transaction.hash.concatI32(event.logIndex.toI32())
  )
  entity.feeBasisPoints = event.params.feeBasisPoints
  entity.maxFee = event.params.maxFee

  entity.blockNumber = event.block.number
  entity.blockTimestamp = event.block.timestamp
  entity.transactionHash = event.transaction.hash

  entity.save()
}

export function handleDestroyedBlackFunds(
  event: DestroyedBlackFundsEvent
): void {
  let entity = new DestroyedBlackFunds(
    event.transaction.hash.concatI32(event.logIndex.toI32())
  )
  entity._blackListedUser = event.params._blackListedUser
  entity._balance = event.params._balance

  entity.blockNumber = event.block.number
  entity.blockTimestamp = event.block.timestamp
  entity.transactionHash = event.transaction.hash

  entity.save()
}

export function handleAddedBlackList(event: AddedBlackListEvent): void {
  let entity = new AddedBlackList(
    event.transaction.hash.concatI32(event.logIndex.toI32())
  )
  entity._user = event.params._user

  entity.blockNumber = event.block.number
  entity.blockTimestamp = event.block.timestamp
  entity.transactionHash = event.transaction.hash

  entity.save()
}

export function handleRemovedBlackList(event: RemovedBlackListEvent): void {
  let entity = new RemovedBlackList(
    event.transaction.hash.concatI32(event.logIndex.toI32())
  )
  entity._user = event.params._user

  entity.blockNumber = event.block.number
  entity.blockTimestamp = event.block.timestamp
  entity.transactionHash = event.transaction.hash

  entity.save()
}

export function handleApproval(event: ApprovalEvent): void {
  let entity = new Approval(
    event.transaction.hash.concatI32(event.logIndex.toI32())
  )
  entity.owner = event.params.owner
  entity.spender = event.params.spender
  entity.value = event.params.value

  entity.blockNumber = event.block.number
  entity.blockTimestamp = event.block.timestamp
  entity.transactionHash = event.transaction.hash

  entity.save()
}

export function handleTransfer(event: TransferEvent): void {
  let entity = new Transfer(
    event.transaction.hash.concatI32(event.logIndex.toI32())
  )
  entity.from = event.params.from
  entity.to = event.params.to
  entity.value = event.params.value

  entity.blockNumber = event.block.number
  entity.blockTimestamp = event.block.timestamp
  entity.transactionHash = event.transaction.hash

  entity.save()
}

export function handlePause(event: PauseEvent): void {
  let entity = new Pause(
    event.transaction.hash.concatI32(event.logIndex.toI32())
  )

  entity.blockNumber = event.block.number
  entity.blockTimestamp = event.block.timestamp
  entity.transactionHash = event.transaction.hash

  entity.save()
}

export function handleUnpause(event: UnpauseEvent): void {
  let entity = new Unpause(
    event.transaction.hash.concatI32(event.logIndex.toI32())
  )

  entity.blockNumber = event.block.number
  entity.blockTimestamp = event.block.timestamp
  entity.transactionHash = event.transaction.hash

  entity.save()
}

Like in the case of the previous 2 files we are removing all functions that are related to the entities we are not tracking

import {
  Transfer as TransferEvent
} from "../generated/Contract/Contract"
import {
  Transfer
} from "../generated/schema"


export function handleTransfer(event: TransferEvent): void {
  let entity = new Transfer(
    event.transaction.hash.concatI32(event.logIndex.toI32())
  )
  entity.from = event.params.from
  entity.to = event.params.to
  entity.value = event.params.value

  entity.blockNumber = event.block.number
  entity.blockTimestamp = event.block.timestamp
  entity.transactionHash = event.transaction.hash

  entity.save()
}

We will further modify the handleTransfer function in the following way.

We start off by creating a variable id and assign this value to it event.transaction.hash.concatI32(event.logIndex.toI32())

Instead of `entity` variable that was in the function.

We will also create another variable transfer which has a value of Transferwith the id loaded as a parameter. If a transfer does not exist, it will be tracked using a newTransfer

The entity requires to be changed to transfer on the values, and we will change the values of from to sender, to to receiver and value to amount because this is what is in our schema.graphql file. The contract.ts file should look like this

import {

  Transfer as TransferEvent,

} from "../generated/Contract/Contract"
import {
  Transfer,

} from "../generated/schema"


export function handleTransfer(event: TransferEvent): void {
  let id = event.transaction.hash.concatI32(event.logIndex.toI32())

  let transfer = Transfer.load(id);

  if (!transfer){
    transfer = new Transfer(id);
  }

  transfer.sender = event.params.from
  transfer.receiver = event.params.to
  transfer.amount = event.params.value

  transfer.blockNumber = event.block.number
  transfer.blockTimestamp = event.block.timestamp
  transfer.transactionHash = event.transaction.hash

  transfer.save()
}

At this point we need to rebuild our subgraph. We do this by running this command in the graph cli

graph codegen && graph build

Once the code has been rebuilt successfully the subgraph is ready to be deployed. To do this from the graph cli, you run this command

graph deploy --studio simple-subgraph

You will be prompted to specify a version number for your subgraph. Subsequent deployments will also require a version number.

Your subgraph will be deployed, if you open the subgraph studio you should see is that your subgraph is syncing

Once the subgraph is synced, you can run queries on it, using the playground tab in the graph studio