As a data guy, I find The Graph super awesome for developing any dapp. But I had a slightly hard time getting my subgraph to work. So here's a little guide for anyone who may find the same struggles.
Contents:
Why The Graph
How to use the Graph
- Getting set up
- Build your subgraph
- Define event handlers
- Finishing Touches
Extras
- a more complex example
- how to deal with using proxy contracts
- how to deal with slow Studio performance
Why The Graph
The Graph is GREAT for developing the frontend of a decentralized app (dapp). Here's my take on why it's so awesome and how you can quickly get up and running with it.
When your dapp needs to display some information on the state or history of something going on in your smart contract, using events instead of state variables will help save gas and overall performance. Things like tracking transaction history of an ERC20 token, borrowing/lending activity or actions like voting. You only need to read this info.
So if you design your frontend to read from logs, you can potentially let your smart contract use less storage variables. Your frontend can query from some external source with the data all prepared in a clean format.
The problem is that you need some infrastructure to collect all the relevant events, clean them up a bit and then host them somewhere for you. The Graph solves these problems in one go. Your project of indexed events is referred to as a Subgraph.
How to use The Graph
At a high level you'll need 3 things:
- Set up a subgraph using their web interface and cli tool
- Design a schema for how to store your data
- Set up the logic to fit the event data into your schema
Part 1: Getting set up
- Go to thegraph's studio: thegraph.com/studio. You'll need to connect your metamask wallet to log in.
- Click the button "Create a Subgraph"
- Set things up The next screen shows you the commands you need to get set up
You'll be prompted to enter a bunch of info. Here's an example from thegraph's site:
When you enter a contract address, it will be able to fetch the ABI from etherscan if the contract has been verified. So if it's your own smart contract, take the steps to verify/publish your contract. Then this graph init
step will obtain the abi automatically.
It's easiest to just go with the default config by pressing enter all the way.
Part 2: Design a schema
Go into your subgraph folder. In my case, it'd be test123. Open your code editor in this folder.
The default schema generated by graph init
is called schema.graphql
. It creates an entity (data table) for each smart contract event but most often that will not be the schema you want to use.
How do you go about this?
Let's say your smart contract emits 2 events:
event GravatarAccepted(
address indexed owner,
bytes32 indexed id,
string displayName,
string imageUrl
);
event GravatarDeclined(
address indexed owner,
bytes32 indexed id,
string displayName,
string imageUrl
);
Instead of creating an entity for each event, you should try to think of the simplest way you can store this info in a way that makes sense to your front-end app.
We shouldn't need duplicate info for owner, id, displayName and imageUrl. You'd likely need just one additional field indicating whether a Gravatar was accepted or declined. So you can end up with this:
This is an over-simplified example. Later I'll show you a slightly more complex example that got me an award from The Graph (1st place in Best Use of Subgraph at ETH Global New York 2023).
Event handlers: Mapping events to your schema
This is the bulk of the work.
You'll see a ts
file in the src
folder named after your contract. It auto-generates a bunch of files to be able to import contract & event related event objects.
You just need to focus on the handler functions that specify what to do with each event.
In the Gravatar example above, I had 2 events:
- GravatarAccepted
- GravatarDeclined
Then my ts
file would have handler functions called
handleGravatarAccepted
handleGravatarDeclined
The default ones only show you how to create a new record in your entity.
But that's not what we want to do for GravatarAccepted
and GravatarDeclined
.
For these events, we should only need to indicate whether the gravatar was accepted or not. Consider the schema defined earlier:
We should only need to update the "accepted" field.
Let's assume the contract has an event 'GravatarCreated' indicating the details (owner, displayName, imageUrl). This can simply use the default function where it creates for the our entity every time this event is created.
Then our handler functions for GravatarAccepted
and GravatarDeclined
should only need to locate the existing record by id and update the value for the field 'accepted'.
export function handleGravatarAccepted(event: GravatarAccepted): void {
const gravatarId = event.params.id.toString()
const gravatar = Gravatar.load(gravatarId) //locate entry with matching Id
if (gravatar) {
gravatar.accepted = true // set to true for GravatarAccepted event
gravatar.save()
}
}
For GravatarDeclined
, the handler function would be identical except I'd just change the value to false.
That's all!
Finishing touches
One last thing before attempting to deploy your subgraph: make sure your subgraph.yaml
file specifies the correct handler functions. The initially auto-generated settings are likely wrong.
Under datasources
you'll see eventHandlers
. We should keep only the ones we need: GravatarAccepted & GravatarDeclined. Based on our assumption that we have another event for initially creating the records, we may have GravatarCreated. So in the end it should look like this:
Alright. Time to deploy. Just follow the cli instructions you see in The Graph's studio page and you should be good to go:
Upon successful deployment, you'll see a query url that you can use from a frontend app:
You can use the playground to put together a query you want:
Then from the frontend, you just need to make a request to the url using a graphql query.
const graphqlQuery =
{
gravatar(last: 5) {
id
owner
displayName
imageUrl
accepted
}
}
;
const graphQLRequest = {
method: 'post',
url: 'https://api.studio.thegraph.com/query/12345/test123/0.0.26',
data: {
query: graphqlQuery,
},
};
Extras:
A more complex example
In this post I've been using the Gravatar example seen on The Graph's documentation. Here's a slightly more complex example from my hackathon project at ETH Global New York. It won #1 Best Use of Subgraph.
It indexes some key info from Blend, which is Blur's lending contract for NFT-backed loans. (Blur is the largest NFT trading dex)
The events from Blend's smart contract are rather messy because they did some hacky things to optimize for gas. The result is some very unintuitive events. I simplified them into 2 entities that reflect the true context of NFT-backed lending.
My schema has a 1-to-many relationship between Liens and Loans. Each lien contains a collateralized NFT and can have its associated loan renewed/updated many times with new terms. This schema makes it much easier to work with for building an app that analyzes historical data.
I have many different events that map to these 2 entities. One of the events get re-used in many different situations so I have some complex logic for that event:
What if your smart contract is a proxy contract, and the implementation code is in another contract?
When setting up your project with graph init
, it'll get the abi of the contract. If it's a proxy contract, it'll get the abi of the proxy, NOT the implementation.
To deal with this, you should specify the address of the implementation contract containing the actual logic of the smart contract, including the events you're trying to index.
For my hackathon project, this was the target contract: 0x29469395eaf6f95920e59f858042f0e28d98a20b. On etherscan, I obtained the address of the implementation contract and used it when initializing my subgraph project (graph init
).
But when deploying the subgraph, you still need to point to the proxy contract, so just before deploying, you should specify the proxy contract in subgraph.yaml
.
Slow indexing on Studio
While The Graph's Studio is mostly great, it is possible for you to run into performance issues. In my case, it took countless hours to index my target smart contract.
To deal with this, I specified a startBlock that's very recent, just so I could obtain some data to test with during the hackathon, though incomplete.
Some time later, the slow Studio issue was resolved. But in the heat of the moment that's what I did.
Top comments (0)