Creating a Subgraph
Please confirm that you have completed the helloworld case.
Now, we use WMEER subgraph to demonstrate. WMEER sugbraph source code:
https://github.com/biibot/graph-example/tree/main/wmeer
A subgraph extracts data from a blockchain, processing it and storing it so that it can be easily queried via GraphQL.
Creating a Subgraph
The subgraph definition consists of a few files:
subgraph.yaml
: a YAML file containing the subgraph manifestschema.graphql
: a GraphQL schema that defines what data is stored for your subgraph, and how to query it via GraphQLAssemblyScript Mappings
: AssemblyScript code that translates from the event data to the entities defined in your schema (e.g.mapping.ts
in this tutorial)
Before you go into detail about the contents of the manifest file, you need to install the Graph CLI which you will need to build and deploy a subgraph.
Install the Graph CLI
The Graph CLI is written in JavaScript, and you will need to install either yarn
or npm
to use it; it is assumed that you have yarn in what follows.
Once you have yarn
, install the Graph CLI by running
Install with yarn:
yarn global add @graphprotocol/graph-cli
Install with npm:
npm install -g @graphprotocol/graph-cli
Once installed, the graph init
command can be used to set up a new subgraph project, either from an existing contract on any of the public Ethereum networks or from an example subgraph. This command can be used to create a subgraph on the Subgraph Studio by passing in graph init --product subgraph-studio
. If you already have a smart contract deployed to Ethereum mainnet or one of the testnets, bootstrapping a new subgraph from that contract can be a good way to get started.
The Subgraph Manifest
The subgraph manifest subgraph.yaml
defines the smart contracts your subgraph indexes, which events from these contracts to pay attention to, and how to map event data to entities that Graph Node stores and allows to query. The full specification for subgraph manifests can be found here.
For the WMEER subgraph, subgraph.yaml
is:
specVersion: 0.0.5
schema:
file: ./schema.graphql
dataSources:
- kind: ethereum
name: Wmeer
network: meer
source:
address: "0x470cBFB236860eb5257bBF78715FB5bd77119C2F"
abi: wmeer
startBlock: 400801
mapping:
kind: ethereum/events
apiVersion: 0.0.7
language: wasm/assemblyscript
entities:
- Deposit
- Withdrawal
abis:
- name: wmeer
file: ./abis/wmeer.json
eventHandlers:
- event: Deposit(indexed address,uint256)
handler: handleDeposit
- event: Withdrawal(indexed address,uint256)
handler: handleWithdrawal
file: ./src/mapping.ts
The important entries to update for the manifest are:
description
: a human-readable description of what the subgraph is. This description is displayed by the Graph Explorer when the subgraph is deployed to the Hosted Service.repository
: the URL of the repository where the subgraph manifest can be found. This is also displayed by The Graph Explorer.features
: a list of all used feature names.dataSources.source
: the address of the smart contract the subgraph sources, and the ABI of the smart contract to use. The address is optional; omitting it allows to index matching events from all contracts.dataSources.source.startBlock
: the optional number of the block that the data source starts indexing from. In most cases, we suggest using the block in which the contract was created.dataSources.mapping.entities
: the entities that the data source writes to the store. The schema for each entity is defined in the schema.graphql file.dataSources.mapping.abis
: one or more named ABI files for the source contract as well as any other smart contracts that you interact with from within the mappings.dataSources.mapping.eventHandlers
: lists the smart contract events this subgraph reacts to and the handlers in the mapping—./src/mapping.ts in the example—that transform these events into entities in the store.dataSources.mapping.callHandlers
: lists the smart contract functions this subgraph reacts to and handlers in the mapping that transform the inputs and outputs to function calls into entities in the store.dataSources.mapping.blockHandlers
: lists the blocks this subgraph reacts to and handlers in the mapping to run when a block is appended to the chain. Without a filter, the block handler will be run every block. An optional call-filter can be provided by adding afilter
field withkind: call
to the handler. This will only run the handler if the block contains at least one call to the data source contract.
A single subgraph can index data from multiple smart contracts. Add an entry for each contract from which data needs to be indexed to the dataSources
array.
The triggers for a data source within a block are ordered using the following process:
- Event and call triggers are first ordered by transaction index within the block.
- Event and call triggers within the same transaction are ordered using a convention: event triggers first then call triggers, each type respecting the order they are defined in the manifest.
- Block triggers are run after event and call triggers, in the order they are defined in the manifest.
These ordering rules are subject to change.
Getting The ABIs
The ABI file(s) must match your contract(s). There are a few ways to obtain ABI files:
- If you are building your own project, you will likely have access to your most current ABIs.
- If you are building a subgraph for a public project, you can download that project to your computer and get the ABI by using
truffle compile
or using solc to compile. - You can also find the ABI on Etherscan, but this isn't always reliable, as the ABI that is uploaded there may be out of date. Make sure you have the right ABI, otherwise running your subgraph will fail.
The GraphQL Schema
The schema for your subgraph is in the file schema.graphql
. GraphQL schemas are defined using the GraphQL interface definition language. If you've never written a GraphQL schema, it is recommended that you check out this primer on the GraphQL type system.
Defining Entities
Before defining entities, it is important to take a step back and think about how your data is structured and linked. All queries will be made against the data model defined in the subgraph schema and the entities indexed by the subgraph. Because of this, it is good to define the subgraph schema in a way that matches the needs of your dapp. It may be useful to imagine entities as "objects containing data", rather than as events or functions.
With The Graph, you simply define entity types in schema.graphql
, and Graph Node will generate top level fields for querying single instances and collections of that entity type. Each type that should be an entity is required to be annotated with an @entity
directive. By default, entities are mutable, meaning that mappings can load existing entities, modify them and store a new version of that entity. Mutability comes at a price, and for entity types for which it is known that they will never be modified, for example, because they simply contain data extracted verbatim from the chain, it is recommended to mark them as immutable with @entity(immutable: true)
. Mappings can make changes to immutable entities as long as those changes happen in the same block in which the entity was created. Immutable entities are much faster to write and to query, and should therefore be used whenever possible.
WMEER Schema
The Deposit
entity below is structured around a Deposit object and is a good example of how an entity could be defined.
type Deposit @entity(immutable: true) {
id: Bytes!
dst: Bytes! # address
wad: BigInt! # uint256
blockNumber: BigInt!
blockTimestamp: BigInt!
transactionHash: Bytes!
}
type Withdrawal @entity(immutable: true) {
id: Bytes!
src: Bytes! # address
wad: BigInt! # uint256
blockNumber: BigInt!
blockTimestamp: BigInt!
transactionHash: Bytes!
}
Optional and Required Fields
Entity fields can be defined as required or optional. Required fields are indicated by the !
in the schema. If a required field is not set in the mapping, you will receive this error when querying the field:
Null value resolved for non-null field 'name'
Each entity must have an id
field, which must be of type Bytes!
or String!
. It is generally recommended to use Bytes!
, unless the id
contains human-readable text, since entities with Bytes!
id's will be faster to write and query as those with a String!
id
. The id
field serves as the primary key, and needs to be unique among all entities of the same type. For historical reasons, the type ID!
is also accepted and is a synonym for String!
.
For some entity types the id
is constructed from the id's of two other entities; that is possible using concat
, e.g., let id = left.id.concat(right.id)
to form the id from the id's of left
and right
. Similarly, to construct an id from the id of an existing entity and a counter count
, let id = left.id.concatI32(count)
can be used. The concatenation is guaranteed to produce unique id's as long as the length of left
is the same for all such entities, for example, because left.id
is an Address
.
Built-In Scalar Types
GraphQL Supported Scalars
We support the following scalars in our GraphQL API:
Type | Description |
---|---|
Bytes | Byte array, represented as a hexadecimal string. Commonly used for Ethereum hashes and addresses. |
String | Scalar for string values. Null characters are not supported and are automatically removed. |
Boolean | Scalar for boolean values. |
Int | The GraphQL spec defines Int to have a size of 32 bytes. |
BigInt | Large integers. Used for Ethereum's uint32 , int64 , uint64 , ..., uint256 types. Note: Everything below uint32 , such as int32 , uint24 or int8 is represented as i32 . |
BigDecimal | BigDecimal High precision decimals represented as a significand and an exponent. The exponent range is from −6143 to +6144. Rounded to 34 significant digits. |
Enums
You can also create enums within a schema. Enums have the following syntax:
enum TokenStatus {
OriginalOwner
SecondOwner
ThirdOwner
}
Once the enum is defined in the schema, you can use the string representation of the enum value to set an enum field on an entity. For example, you can set the tokenStatus
to SecondOwner
by first defining your entity and subsequently setting the field with entity.tokenStatus = "SecondOwner"
. The example below demonstrates what the Token entity would look like with an enum field:
More detail on writing enums can be found in the GraphQL documentation.
Entity Relationships
An entity may have a relationship to one or more other entities in your schema. These relationships may be traversed in your queries. Relationships in The Graph are unidirectional. It is possible to simulate bidirectional relationships by defining a unidirectional relationship on either "end" of the relationship.
Relationships are defined on entities just like any other field except that the type specified is that of another entity.
One-To-One Relationships
Define a Transaction
entity type with an optional one-to-one relationship with a TransactionReceipt
entity type:
type Transaction @entity(immutable: true) {
id: Bytes!
transactionReceipt: TransactionReceipt
}
type TransactionReceipt @entity(immutable: true) {
id: Bytes!
transaction: Transaction
}
One-To-Many Relationships
Define a TokenBalance
entity type with a required one-to-many relationship with a Token entity type:
type Token @entity(immutable: true) {
id: Bytes!
}
type TokenBalance @entity {
id: Bytes!
amount: Int!
token: Token!
}
Reverse Lookups
Reverse lookups can be defined on an entity through the @derivedFrom
field. This creates a virtual field on the entity that may be queried but cannot be set manually through the mappings API. Rather, it is derived from the relationship defined on the other entity. For such relationships, it rarely makes sense to store both sides of the relationship, and both indexing and query performance will be better when only one side is stored and the other is derived.
For one-to-many relationships, the relationship should always be stored on the 'one' side, and the 'many' side should always be derived. Storing the relationship this way, rather than storing an array of entities on the 'many' side, will result in dramatically better performance for both indexing and querying the subgraph. In general, storing arrays of entities should be avoided as much as is practical.
Example
We can make the balances for a token accessible from the token by deriving a tokenBalances
field:
type Token @entity(immutable: true) {
id: Bytes!
tokenBalances: [TokenBalance!]! @derivedFrom(field: "token")
}
type TokenBalance @entity {
id: Bytes!
amount: Int!
token: Token!
}
Many-To-Many Relationships
For many-to-many relationships, such as users that each may belong to any number of organizations, the most straightforward, but generally not the most performant, way to model the relationship is as an array in each of the two entities involved. If the relationship is symmetric, only one side of the relationship needs to be stored and the other side can be derived.
Example
Define a reverse lookup from a User
entity type to an Organization
entity type. In the example below, this is achieved by looking up the members
attribute from within the Organization
entity. In queries, the organizations
field on User
will be resolved by finding all Organization
entities that include the user's ID.
type Organization @entity {
id: Bytes!
name: String!
members: [User!]!
}
type User @entity {
id: Bytes!
name: String!
organizations: [Organization!]! @derivedFrom(field: "members")
}
A more performant way to store this relationship is through a mapping table that has one entry for each User
/ Organization
pair with a schema like
type Organization @entity {
id: Bytes!
name: String!
members: [UserOrganization!]! @derivedFrom(field: "organization")
}
type User @entity {
id: Bytes!
name: String!
organizations: [UserOrganization!] @derivedFrom(field: "user")
}
type UserOrganization @entity {
id: Bytes! # Set to `user.id.concat(organization.id)`
user: User!
organization: Organization!
}
This approach requires that queries descend into one additional level to retrieve, for example, the organizations for users:
query usersWithOrganizations {
users {
organizations {
# this is a UserOrganization entity
organization {
name
}
}
}
}
This more elaborate way of storing many-to-many relationships will result in less data stored for the subgraph, and therefore to a subgraph that is often dramatically faster to index and to query.
Adding comments to the schema
As per GraphQL spec, comments can be added above schema entity attributes using double quotations ""
. This is illustrated in the example below:
type MyFirstEntity @entity {
"unique identifier and primary key of the entity"
id: Bytes!
address: Bytes!
}
Writing Mappings
The mappings transform the Ethereum data your mappings are sourcing into entities defined in your schema. Mappings are written in a subset of TypeScript called AssemblyScript which can be compiled to WASM (WebAssembly). AssemblyScript is stricter than normal TypeScript, yet provides a familiar syntax.
For each event handler that is defined in subgraph.yaml
under mapping.eventHandlers
, create an exported function of the same name. Each handler must accept a single parameter called event
with a type corresponding to the name of the event which is being handled.
In the WMEER subgraph, src/mapping.ts
contains handlers for the Deposit
and Withdrawal
events:
export function handleDeposit(event: DepositEvent): void {
let entity = new Deposit(
event.transaction.hash.concatI32(event.logIndex.toI32())
);
entity.dst = event.params.dst;
entity.wad = event.params.wad;
entity.blockNumber = event.block.number;
entity.blockTimestamp = event.block.timestamp;
entity.transactionHash = event.transaction.hash;
entity.save();
}
export function handleWithdrawal(event: WithdrawalEvent): void {
let entity = new Withdrawal(
event.transaction.hash.concatI32(event.logIndex.toI32())
);
entity.src = event.params.src;
entity.wad = event.params.wad;
entity.blockNumber = event.block.number;
entity.blockTimestamp = event.block.timestamp;
entity.transactionHash = event.transaction.hash;
entity.save();
}
The first handler takes a Deposit
event and creates a new Deposit
entity with new Deposit(event.logIndex.toI32())
, populating the entity fields using the corresponding event parameters. This entity instance is represented by the variable entity
, with an hash value of event.logIndex.toI32()
.
Recommended IDs for Creating New Entities
Every entity has to have an id
that is unique among all entities of the same type. An entity's id
value is set when the entity is created. Below are some recommended id
values to consider when creating new entities. NOTE: The value of id
must be a string
.
event.params.id.toHex()
event.transaction.from.toHex()
event.transaction.hash.toHex() + "-" + event.logIndex.toString()
We provide the Graph Typescript Library which contains utilies for interacting with the Graph Node store and conveniences for handling smart contract data and entities. You can use this library in your mappings by importing @graphprotocol/graph-ts
in mapping.ts
.
Code Generation
In order to make it easy and type-safe to work with smart contracts, events and entities, the Graph CLI can generate AssemblyScript types from the subgraph's GraphQL schema and the contract ABIs included in the data sources.
This is done with
graph codegen [--output-dir <OUTPUT_DIR>] [<MANIFEST>]
but in most cases, subgraphs are already preconfigured via package.json
to allow you to simply run one of the following to achieve the same:
# Yarn
yarn codegen
# NPM
npm run codegen
This will generate an AssemblyScript class for every smart contract in the ABI files mentioned in subgraph.yaml
, allowing you to bind these contracts to specific addresses in the mappings and call read-only contract methods against the block being processed. It will also generate a class for every contract event to provide easy access to event parameters, as well as the block and transaction the event originated from. All of these types are written to <OUTPUT_DIR>/<DATA_SOURCE_NAME>/<ABI_NAME>.ts
. In the example subgraph, this would be generated/Wmeer/wmeer.ts
, allowing mappings to import these types with.
// the events classes
import {
Deposit as DepositEvent,
Withdrawal as WithdrawalEvent,
} from "../generated/Wmeer/wmeer";
In addition to this, one class is generated for each entity type in the subgraph's GraphQL schema. These classes provide type-safe entity loading, read and write access to entity fields as well as a save()
method to write entities to store. All entity classes are written to <OUTPUT_DIR>/schema.ts
, allowing mappings to import them with
// the entities classes
import { Deposit, Withdrawal } from "../generated/schema";
Note: The code generation must be performed again after every change to the GraphQL schema or the ABIs included in the manifest. It must also be performed at least once before building or deploying the subgraph.
Code generation does not check your mapping code in src/mapping.ts
. If you want to check that before trying to deploy your subgraph to the Graph Explorer, you can run yarn build
and fix any syntax errors that the TypeScript compiler might find.
Start Blocks
The startBlock
is an optional setting that allows you to define from which block in the chain the data source will start indexing. Setting the start block allows the data source to skip potentially millions of blocks that are irrelevant. Typically, a subgraph developer will set startBlock
to the block in which the smart contract of the data source was created.
dataSources:
- kind: ethereum
name: Wmeer
network: meer
source:
address: "0x470cBFB236860eb5257bBF78715FB5bd77119C2F"
abi: wmeer
startBlock: 400801
mapping:
kind: ethereum/events
apiVersion: 0.0.7
language: wasm/assemblyscript
entities:
- Deposit
- Withdrawal
abis:
- name: wmeer
file: ./abis/wmeer.json
eventHandlers:
- event: Deposit(indexed address,uint256)
handler: handleDeposit
- event: Withdrawal(indexed address,uint256)
handler: handleWithdrawal
file: ./src/mapping.ts
Call Handlers
While events provide an effective way to collect relevant changes to the state of a contract, many contracts avoid generating logs to optimize gas costs. In these cases, a subgraph can subscribe to calls made to the data source contract. This is achieved by defining call handlers referencing the function signature and the mapping handler that will process calls to this function. To process these calls, the mapping handler will receive an ethereum.Call
as an argument with the typed inputs to and outputs from the call. Calls made at any depth in a transaction's call chain will trigger the mapping, allowing activity with the data source contract through proxy contracts to be captured.
Call handlers will only trigger in one of two cases: when the function specified is called by an account other than the contract itself or when it is marked as external in Solidity and called as part of another function in the same contract.
Note: Call handlers currently depend on the Parity tracing API. Certain networks, such as BNB chain and Arbitrum, does not support this API. If a subgraph indexing one of these networks contain one or more call handlers, it will not start syncing. Subgraph developers should instead use event handlers. These are far more performant than call handlers, and are supported on every evm network.
Defining a Call Handler
To define a call handler in your manifest, simply add a callHandlers
array under the data source you would like to subscribe to.
dataSources:
- kind: ethereum
name: Wmeer
network: meer
source:
address: "0x470cBFB236860eb5257bBF78715FB5bd77119C2F"
abi: wmeer
startBlock: 400801
mapping:
kind: ethereum/events
apiVersion: 0.0.7
language: wasm/assemblyscript
entities:
- Deposit
- Withdrawal
abis:
- name: wmeer
file: ./abis/wmeer.json
eventHandlers:
- event: Deposit(indexed address,uint256)
handler: handleDeposit
- event: Withdrawal(indexed address,uint256)
handler: handleWithdrawal
file: ./src/mapping.ts
The function
is the normalized function signature to filter calls by. The handler
property is the name of the function in your mapping you would like to execute when the target function is called in the data source contract.
Mapping Function
Each call handler takes a single parameter that has a type corresponding to the name of the called function. In the example subgraph above, the mapping contains a handler for when the handleDeposit
function is called and receives a DepositEvent
parameter as an argument:
import {
Deposit as DepositEvent,
Withdrawal as WithdrawalEvent,
} from "../generated/Wmeer/wmeer";
import { Deposit, Withdrawal } from "../generated/schema";
export function handleDeposit(event: DepositEvent): void {
let entity = new Deposit(
event.transaction.hash.concatI32(event.logIndex.toI32())
);
entity.dst = event.params.dst;
entity.wad = event.params.wad;
entity.blockNumber = event.block.number;
entity.blockTimestamp = event.block.timestamp;
entity.transactionHash = event.transaction.hash;
entity.save();
}
The handleDeposit
function takes a new DepositEvent
which is a subclass of ethereum.Call
, provided by @graphprotocol/graph-ts
, that includes the typed inputs and outputs of the call. The DepositEvent
type is generated for you when you run graph codegen
.
Transaction Receipts in Event Handlers
Starting from specVersion
0.0.5
and apiVersion
0.0.7
, event handlers can have access to the receipt for the transaction which emitted them.
To do so, event handlers must be declared in the subgraph manifest with the new receipt: true
key, which is optional and defaults to false.
eventHandlers:
- event: Deposit(indexed address,uint256)
handler: handleDeposit
receipt: true
Inside the handler function, the receipt can be accessed in the Event.receipt
field. When the receipt
key is set to false
or omitted in the manifest, a null
value will be returned instead.