Before you continue, I'm assuming you know what The Graph is and what problems it tackles. If you haven't, I highly recommend checking out - Why The Graph.
Installation
First of all, you'll need Graph CLI tool. Install it via:
yarn
yarn global add @graphprotocol/graph-cli
or, npm
npm install -g @graphprotocol/graph-cli
In this tutorial I'll be using a simple Gravity contract as an example.
Before you can use the already installed Graph CLI, head over to the Subgraph Studio and connect your account. You'll need a wallet like MetaMask already installed on your browser. So make sure it is there.
Time to create a new subgraph in the studio now. Note that the Gravity contract is already deployed on Ethereum Mainnet. You can inspect it on Etherscan. It is deployed at the following address:
0x2E645469f354BB4F5c8a05B3b30A929361cf77eC
With this information known, go ahead and create a new subgraph in the studio. Select the Ethereum Mainnet as the network to index the contract data from. And fill-in a sensible name of subgraph like - Gravity
. Hit continue. This will take you to dashboard where you may fill optional fields like description, website, etc. I'm gonna skip it for sake of brevity.
Initializing Subgraph
The Graph CLI provides graph init
command to initialize the subgraph:
graph init \
--product subgraph-studio
--from-contract <CONTRACT_ADDRESS> \
[--network <ETHEREUM_NETWORK>] \
[--abi <FILE>] \
<SUBGRAPH_SLUG> [<DIRECTORY>]
I recommend checking what each option does by running graph init --help
in console.
Now copy the generated subgraph slug from the dashboard. It is gravity
for me. We need it for initializing this subgraph:
graph init --studio gravity
(Note that --studio
option above is nothing but short-hand for --product subgraph-studio
)
This will prompt for multiple inputs like network name, contract address, ABI etc. In this case, we're using Ethereum Mainnet network, with an already deployed Gravity contract. Make sure your input match following:
✔ Protocol · ethereum
✔ Subgraph slug · gravity
✔ Directory to create the subgraph in · gravity
✔ Ethereum network · mainnet
✔ Contract address · 0x2E645469f354BB4F5c8a05B3b30A929361cf77eC
✔ Fetching ABI from Etherscan
✔ Contract Name · Gravity
If not provided inline to graph init
, the cli will try to fetch the ABI of the contract from Etherscan. If not found, you'll need to provide path to the ABI file of the contract. ABI is easy to generate for your contract through dev tools like Hardhat or Truffle, one of which you might already be using. Or, you can simply copy it from IDE, if you're using Remix. In our case, ABI was already available on Etherscan.
Initialization creates a new directory containing multiple auto-generated files for you as a starting point. These include some configuration files specifically tailored for the provided contract through its ABI. Open this project in your favorite editor and let's start with next steps.
Configuring Subgraph Manifest
The subgraph manifest file - subgraph.yaml
is a YAML file located at the root path. This describes the data-sources your subgraph will index. In this case, data-source is Gravity contract on Ethereum Mainnet. Replace the contents of file with:
specVersion: 0.0.4
description: Gravatar for Ethereum
repository: https://github.com/graphprotocol/example-subgraph
schema:
file: ./schema.graphql
dataSources:
- kind: ethereum/contract
name: Gravity
network: mainnet
source:
address: '0x2E645469f354BB4F5c8a05B3b30A929361cf77eC'
abi: Gravity
startBlock: 6175244
mapping:
kind: ethereum/events
apiVersion: 0.0.6
language: wasm/assemblyscript
entities:
- Gravatar
abis:
- name: Gravity
file: ./abis/Gravity.json
eventHandlers:
- event: NewGravatar(uint256,address,string,string)
handler: handleNewGravatar
- event: UpdatedGravatar(uint256,address,string,string)
handler: handleUpdatedGravatar
file: ./src/mapping.ts
We're gonna keep this simple. You can find out about what each of the available fields mean at full-specification here. Though some notable ones are:
-
schema.file
: Path to a GraphQL schema file defining what data is stored for your subgraph. This also generated with initialization. -
dataSources
: List of sources from which to index data. You can use a single subgraph to index the data from multiple smart-contracts. -
dataSources.source
: Address and ABI of the source smart-contract.startBlock
indicates from which block of the blockchain to start indexing data from. Its value is usually the block at which the contract was deployed. -
dataSources.mapping.entities
: These are the entities that are written to The Graph network's storage. These entities are defined in a GraphQL schema file -schema.graphql
. -
dataSources.mapping.abis
: ABI files for the source contract as well as any other smart contracts that you interact with from within the mappings (mapping.ts
file that you will write). -
dataSources.mapping.eventHandlers
: List of the smart-contract events that this subgraph will react to with corresponding handlers that are defined in mappings file (./src/mapping.ts
). It is these handlers that will transform the events to proper entities to be saved. -
dataSources.mapping.callHandlers
(not used here): Lists of the smart-contract functions the subgraph reacts to with corresponding handlers. These handlers are defined in the mapping too. They transform the inputs and outputs to function calls into entities in the store. -
dataSources.mapping.blockHandlers
(not used here): Blocks with handlers the subgraph will react to. With nofilter
it will react every time new block is appended to chain. However, acall
filter will make the handler run only if the block contains at least one call to the data source contract.
Defining Entities
The entities are defined in a GraphQL schema file - schema.graphql
, also located the root path. These entities indicate what data is stored for this subgraph and how to query it. If you're new to GraphQL, check out at least a primer on it on official website or if you're feeling adventurous like me try this awesome course by Apollo team.
Alright, I assume you have at least basic knowledge of GraphQL by now. But before defining entities think about the structure of your data, like how you would've done while designing schemas for a DB. All the queries made from user-facing app will me made against this data model. Rather than imagining these entities in smart-contract lingo i.e. imagining entities as "events" or contract "functions", imagine entities as data "objects". Your app will query on these objects.
In our case, for example, it can be seen in subgraph.yaml
above that, the network is instructed to react to NewGravatar
/UpdateGravatar
through handlers - handleNewGravatar
/handleUpdateGravatar
(defined in mapping.ts
). These handlers eventually convert event data to a related entity (Gravatar
) defined in schema.graphql
, which will then be saved/updated in storage.
Here the entity is the Gravatar
object, not the individual events. A good schema would be:
type Gravatar @entity {
id: ID!
owner: Bytes
displayName: String
imageUrl: String
}
Open the schema.graphql
and paste above to finish defining the entity.
Writing Mappings
The job of mappings file (in ./src/mapping.ts
) is to convert data coming from blockchain (e.g. events) to an entity defined in the schema.graphql
and write it to store. The mappings are written in AssemblyScript which can be compiled to WASM. It is a stricter subset of TypeScript. If you've never written any TypeScript ever, I recommend getting familiar with the syntax here.
All the APIs available to write these mappings is described in AssemblyScript API docs. You might want to check out at least Built-in Types and Store API specifically to start.
If you inspect ./src/mapping.ts
, you can see already pre-filled code that was generated by the graph init
command ran earlier. But this is based off the subgraph.yaml
and schema.graphql
before editing. Since we changed contents of these two we need to generate types corresponding to updated entities. This is so we can use these generated types/classes to write our mapping.ts
. The Graph CLI provides the codegen
command for this purpose. Run:
graph codegen
(This is also pre-configured in package.json
so that you can run yarn codegen
or npm codegen
which does the same)
You can see the generated types/classes corresponding to schema as well as source contract(s) in the ./generated
directory.
Alright, now is the time to write the handlers - handleNewGravatar
and handleUpdateGravatar
that were specified at dataSources.mapping.eventHandlers
in the subgraph.manifest
. Open the mapping.ts
and replace with following:
// Import event classes
import {
NewGravatar,
UpdatedGravatar
} from "../generated/Gravity/Gravity";
// Import entity class
import { Gravatar } from "../generated/schema";
export function handleNewGravatar(event: NewGravatar): void {
// Use id field from emitted event as unique id for the entity
const id = event.params.id.toHex();
// Create a new Gravatar Entity with unique id
const gravatar = new Gravatar(id);
// Set Gravatar Entity fields
gravatar.owner = event.params.owner;
gravatar.displayName = event.params.displayName;
gravatar.imageUrl = event.params.imageUrl;
// Save entity to store
gravatar.save();
}
export function handleUpdatedGravatar(event: UpdatedGravatar): void {
// Use proper id to load an entity from store
const id = event.params.id.toHex();
// Load the entity to be updated
let gravatar = Gravatar.load(id);
// Create the entity if it doesn't already exist
if (!gravatar) {
gravatar = new Gravatar(id);
}
// Set updated fields to entity
gravatar.owner = event.params.owner;
gravatar.displayName = event.params.displayName;
gravatar.imageUrl = event.params.imageUrl;
// Save updated entity to store
gravatar.save();
}
Note the imports above are the same types/classes generated earlier by graph codegen
. The handlers receive event
of type NewGravatar
/UpdateGravatar
, which are subclass of a parent ethereum.Event
class. The event data fields is available in event.params
object.
Each entity requires a unique id assigned to it. Here, hex of event.params.id
(type BigInt
) from contract event is used. However, uniqueness can also be guaranteed by choosing id from event metadata like:
event.transaction.from.toHex()
event.transaction.hash.toHex() + "-" + event.logIndex.toString()
After setting/updating fields to entity, Store API provides methods like load
and save
on Entity
(inherited by Gravatar
) types to retrieve and save the entity to store.
Publishing the Subgraph
Now that all of the configuration is done, let's proceed to deploying this subgraph. But before doing that make sure that mapping.ts
is checked without any errors or bugs. It is not checked by code generation step. As a necessary precaution run the build command:
graph build
(Or yarn build
/ npm build
as this command must also be configured in package.json
too)
This compiles the subgraph to WebAssembly. If there is a syntax error somewhere, it should fail. Otherwise, build succeeds, creating a ./build
directory with built files.
For deploying the subgraph to Subgraph Studio, you'll need the deploy key of the subgraph. Go to the already created Gravity subgraph's dashboard in the studio and copy its deploy key. Now authenticate from cli with this key:
graph auth --studio <DEPLOY KEY>
This stores the access token to system's keychain.
Finally, deploy to studio by providing subgraph slug:
graph deploy --studio gravity
This will ask for a version label for current version of your subgraph. You can enter whatever version semantic you prefer or go with v0.0.1
.
✔ Version Label (e.g. v0.0.1) · v0.0.1
.
.
✔ Apply migrations
✔ Load subgraph from subgraph.yaml
.
✔ Compile subgraph
.
✔ Write compiled subgraph to build/
.
.
✔ Upload subgraph to IPFS
Note that the subgraph is not yet published! It is currently deployed to the studio. After deployment it will start syncing data from the chain and take some time, after which you can play around and test it. Then, if everything seems to be ok, hit Publish. This will publish your subgraph to the production on the Graph network and you'll get an API endpoint, like below, where you query the data:
https://api.studio.thegraph.com/query/<ID>/<SUBGRAPH_NAME>/<VERSION>
Note that every query will be charged in GRT tokens as fees.
However, for testing purposes the studio also provides a temporary, rate limited API endpoint. For example, for me it was - https://api.studio.thegraph.com/query/21330/gravity/v0.0.1
. Let's see if it works. You can use this online GraphQL client - GraphiQL for that. Enter your testing API endpoint and make the following query to get a gravatar by id and hit run:
query MyQuery {
gravatar(id: "0xa") {
displayName
id
imageUrl
owner
}
}
which outputs:
{
"data": {
"gravatar": {
"displayName": "duqd",
"id": "0xa",
"imageUrl": "https://ucarecdn.com/ddbebfc0-...",
"owner": "0x48c89d77ae34ae475e4523b25ab01e363dce5a78"
}
}
}
It worked!
This was a fairly simple example to familiarize with the workings of The Graph. You might define a more complex GraphQL schema, mappings and the subgraph manifest for a production application. Feel free to dive into the official docs for reference.
See full code at GitHub repo here.
Hope you learned some awesome stuff! 😎
Feel free to catch me here!
Top comments (0)