Naveen

Posted on Feb 15, 2022Read on Mirror.xyz

The Graph Tutorial: Creating a Subgraph

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 the 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 the Ethereum Mainnet network, with an already deployed Gravity contract. Make sure your input match the 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 a 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 the 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, the data source is the Gravity contract on Ethereum Mainnet. Replace the contents of the 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. 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 no filter it will react every time new block is appended to chain. However, a call 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 the mappings file (in ./src/mapping.ts) is to convert data coming from the 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 are 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!

Recommended Reading