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
Subgraph.yaml
Schema.graphql
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
tosender
to
toreceiver
value
toamount
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 Transfer
with 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
Subscribe to my newsletter
Read articles from Temitope Hassan directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Temitope Hassan
Temitope Hassan
I am a developer from Nigeria, celebrating 40+ years of coding(1983 till date)