Rapid Development of Bored Ape Yacht Club Subgraph
You have two ways to quickly experience our hosted subgraph feature:
- Visit our example code repository directly, build and compile, then deploy the subgraph to our cloud service.
- Follow our tutorial to develop a simple BAYC smart contract subgraph from scratch.
Install the dependencies in your development environment.
➜ yarn global add @graphprotocol/graph-cli
1. Example Code Repository
# 1. Visit and clone our GitHub code repository
➜ git clone https://github.com/chainbase-labs/subgraph-example.git
# 2. cd subgraph-example
➜ cd subgraph-example/BAYC
# 3. Compile and build
➜ yarn && yarn install (or: npm install)
➜ graph codegen && graph build
# 3. Deploy (please refer to our documentation to create a BAYC project in advan: https://docs.chainbase.com/docs/host-subgraph)
# (⚠️⚠️ After creating the subgraph, copy your exclusive deployment command )
➜ graph deploy bayc --node https://api.chainbase.online/v1/subgraph/xxx/deploy --ipfs https://api.chainbase.online/v1/subgraph/xxx/ipfs
2. Developing a Subgraph from Scratch
2.1 Create a Subgraph Project
After installing the graph-cli tool, create a new directory locally. Then, use the command graph init
in the terminal to initialize a new subgraph project
The graph-cli client will guide you step by step in creating it. Customizable
parameters might vary based on the client version, but they generally include:
➜ subgraph-example git:(main) ✗ graph init
✔ Protocol · ethereum
✔ Product for which to initialize · subgraph-studio
✔ Subgraph slug · BAYC
✔ Directory to create the subgraph in · BAYC
✔ Ethereum network · mainnet
✔ Contract address · 0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d
✖ Failed to fetch ABI from Etherscan: request to https://api.etherscan.io/api?module=contract&action=getabi&address=0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d failed, reason: read ECONNRESET
✖ Failed to fetch Start Block: Failed to fetch contract creation transaction hash
✔ ABI file (path) · ./abi/abi # retrieve the ABI file.
✔ Start Block · 12287507 # Obtain the block where the contract was deployed
✔ Contract Name · BAYC
✔ Index contract events as entities (Y/n) · true
Generate subgraph
Write subgraph to directory
✔ Create subgraph scaffold
✔ Initialize networks config
✔ Initialize subgraph repository
Error: Couldn't find match for "feat/smaller" in "refs/heads/1.3.x,refs/heads/1.4.x,refs/heads/master,refs/tags/v1.3.2,refs/tags/v1.4.10,refs/tags/v1.4.11,refs/tags/v1.4.7,refs/tags/v1.4.8,refs/tags/v1.4.9,refs/tags/v1.5.0,refs/tags/v1.5.1,refs/tags/v1.5.2,refs/tags/v1.6.0,refs/tags/v1.6.1,refs/tags/v1.6.2,refs/tags/v2.0.0" for "https://github.com/hugomrdias/concat-stream.git".
at MessageError.ExtendableBuiltin (/opt/homebrew/Cellar/yarn/1.22.19/libexec/lib/cli.js:721:66)
retrieving the ABI file, which can be done by viewing the corresponding blockchain browser and copying the ABI file to a local directory
Obtaining the block height at which the contract was deployed
Finally, either yarn
or npm
will install specific project dependencies during the initialization process. You may
encounter an error related to the non-existence of a specific branch, feat/smaller
, in the concat-stream
GitHub repository. You can redirect the problematic branch to the correct one using the resolutions
field inside the package.json
file
"resolutions": {
"concat-stream": "https://github.com/hugomrdias/concat-stream#1.4.x"
},
"dependencies": {
"@graphprotocol/graph-cli": "0.43.0",
"@graphprotocol/graph-ts": "0.29.1"
}
re-run yarn
to ensure the project's initialization dependencies are installed correctly
yarn && yarn install
2.2 Core File Code

Once graph init
is completed, it will automatically generate the framework code for us. We only need to modify a few files to achieve our goal of indexing the BAYC contract. Let's take it step by step and understand the function of each file and try to write a subgraph that can index BAYC contract data.
- subgraph.yaml
First Step: Define our data source, i.e., specify to the subgraph what smart contract to index, including the contract address, network, ABI, and handlers that trigger indexing.
specVersion: 0.0.5
schema:
file: ./schema.graphql
features:
- ipfsOnEthereumContracts
dataSources:
- kind: ethereum
name: BAYC
network: mainnet
source:
address: "0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d"
abi: BAYC
startBlock: 12287507
mapping:
kind: ethereum/events
apiVersion: 0.0.7
language: wasm/assemblyscript
entities:
- Transfer
- BoredApe
- Property
abis:
- name: BAYC
file: ./abis/BAYC.json
eventHandlers:
- event: Transfer(indexed address,indexed address,indexed uint256)
handler: handleTransfer
file: ./src/bayc.ts
The Graph allows us to define three types of handlers on EVM chains: event handlers, call handlers, and block handlers. You can refer to Subgraph Manifest for details.
Here, the core handler is eventHandlers
, defining how we index data from blockchain events. Take the Transfer event as an example:
- Whenever an NFT is transferred from one address to another, this event is triggered. It records the previous owner, the new owner, and the specific NFT TOKEN ID.
- We want to start recording transfers from the initial block, so we can capture the complete ownership history of BAYC NFTs.
- Moreover, if you mark the Transfer ID entity as immutable in later definitions, the query speed will be faster.
- schema.garphql
schema
defines the data types we need to store, i.e., the fields ultimately stored in PostgreSQL. You can also use these fields to create custom query statements.
type Transfer @entity(immutable: true) {
id: Bytes!
from: Bytes!
to: Bytes!
tokenId: BigInt!
blockNumber: BigInt!
transactionHash: Bytes!
}
There are a few points to note here:
Every entity needs an @entity
directive. There also needs to be an ID field, and the unique value of this field must be applicable to all entities of the same type. Below are some common data types, and you can refer to the documentation for details: Types.
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. |
Additionally, we hope to query a specific NFT's creator, current owner, and the specific block when the last ownership change occurred through the API. There are also some attributes related to NFT. Therefore, we can define two more entities.
type Transfer @entity(immutable: true) {
id: Bytes!
from: Bytes!
to: Bytes!
tokenId: BigInt!
blockNumber: BigInt!
transactionHash: Bytes!
}
type BoredApe @entity {
id: ID!
creator: Bytes!
newOwner: Bytes!
tokenURI: String!
blockNumber: BigInt!
}
type Property @entity {
id: ID!
image: String
background: String
clothes: String
earring: String
eyes: String
fur: String
hat: String
mouth: String
}
- src/bayc.ts
or each event handler defined in subgraph.yaml
, we need to create an exported function with the same name in the mapping file. Each event handler should accept a parameter named event
, and the type of this parameter needs to match the name of the event being handled. The mapping functions here are the individual handling functions defined in bayc.ts
, where we manipulate blockchain data and index it according to our needs:
import {
Transfer as TransferEvent,
BAYC,
} from "../generated/BAYC/BAYC"
import {
BoredApe,
Transfer,
Property
} from "../generated/schema"
import { ipfs, json } from '@graphprotocol/graph-ts'
function initializeTransfer(event: TransferEvent): Transfer {
return new Transfer(event.transaction.hash.concatI32(event.logIndex.toI32()));
}
function handleBoredApe(event: TransferEvent, contractAddress: BAYC): BoredApe {
let boredApe = BoredApe.load(event.params.tokenId.toString());
if (boredApe == null) {
boredApe = new BoredApe(event.params.tokenId.toString());
boredApe.creator = event.params.to;
boredApe.tokenURI = contractAddress.tokenURI(event.params.tokenId);
}
if (boredApe) {
boredApe.newOwner = event.params.to;
boredApe.blockNumber = event.block.number;
}
return boredApe
}
function handleProperty(event: TransferEvent, ipfshash: string): Property | null {
const fullURI = ipfshash + "/" + event.params.tokenId.toString();
let ipfsData = ipfs.cat(fullURI);
if (!ipfsData) return null;
let ipfsValues = json.fromBytes(ipfsData).toObject();
if (!ipfsValues) return null;
let property = Property.load(event.params.tokenId.toString()) || new Property(event.params.tokenId.toString());
if (property) {
let imageValue = ipfsValues.get('image');
if (imageValue) {
property.image = imageValue.toString();
}
let attributeArray = ipfsValues.get('attributes');
if (attributeArray) {
let attributes = attributeArray.toArray();
for (let i = 0; i < attributes.length; i++) {
let attributeObject = attributes[i].toObject();
let trait = attributeObject.get('trait_type');
let value = attributeObject.get('value');
if (trait && value) {
let traitString = trait.toString();
let valueString = value.toString();
if (traitString == "Background") {
property.background = valueString;
} else if (traitString == "Clothes") {
property.clothes = valueString;
} else if (traitString == "Earring") {
property.earring = valueString;
} else if (traitString == "Eyes") {
property.eyes = valueString;
} else if (traitString == "Fur") {
property.fur = valueString;
} else if (traitString == "Hat") {
property.hat = valueString;
} else if (traitString == "Mouth") {
property.mouth = valueString;
}
}
}
}
}
return property;
}
export function handleTransfer(event: TransferEvent): void {
const ipfshash = "QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq";
let transfer = initializeTransfer(event);
transfer.from = event.params.from
transfer.to = event.params.to
transfer.tokenId = event.params.tokenId
transfer.blockNumber = event.block.number
transfer.transactionHash = event.transaction.hash
transfer.save();
let contractAddress = BAYC.bind(event.address);
handleBoredApe(event, contractAddress).save();
let property = handleProperty(event, ipfshash);
if (property) {
property.save();
}
}
initializeTransfer
initializes a new Transfer entity object. We link the transaction hash value with the event's log index to ensure that each instance of the Transfer entity has a unique ID. When querying the Transfer entity, it will be returned asid
.event.block
andevent.transaction
are part of the Ethereum API in the graph-ts library. You can refer to the documentation for complete reference information. We can use this library to fetch various data.- The storage API is also part of the graph-ts library, allowing us to access the
save()
method. With this method, we can save new instances of theTransfer
entity to the database.
2.3 Compile and Build
At this point, we have fully developed a simple subgraph, and next, we can compile our code and deploy the subgraph:
- graph codegen
Compile with graph codegen
: After modifying the subgraph.yaml
and scheme.graphql
files, run codegen to generate corresponding AssemblyScript files in the generated
directory:
scheme.ts
is generated directly fromscheme.graphql
, and we can import and use it.BAYC.ts
is generated from the monthly ABI, and theTransferEvent
class allows us to handle the Transfer event in the contract. TheBAYC
class is an abstraction of the contract itself, from which we can read data and call functions.
➜ BAYC git:(main) ✗ graph codegen
Skip migration: Bump mapping apiVersion from 0.0.1 to 0.0.2
Skip migration: Bump mapping apiVersion from 0.0.2 to 0.0.3
Skip migration: Bump mapping apiVersion from 0.0.3 to 0.0.4
Skip migration: Bump mapping apiVersion from 0.0.4 to 0.0.5
Skip migration: Bump mapping apiVersion from 0.0.5 to 0.0.6
Skip migration: Bump manifest specVersion from 0.0.1 to 0.0.2
Skip migration: Bump manifest specVersion from 0.0.2 to 0.0.4
✔ Apply migrations
✔ Load subgraph from subgraph.yaml
Load contract ABI from abis/BAYC.json
✔ Load contract ABIs
Generate types for contract ABI: BAYC (abis/BAYC.json)
Write types to generated/BAYC/BAYC.ts
✔ Generate types for contract ABIs
✔ Generate types for data source templates
✔ Load data source template ABIs
✔ Generate types for data source template ABIs
✔ Load GraphQL schema from schema.graphql
Write types to generated/schema.ts
✔ Generate types for GraphQL schema
Types generated successfully
- graph build
Convert the subgraph into WebAssembly, ready for deployment
➜ BAYC git:(main) ✗ graph codegen
Skip migration: Bump mapping apiVersion from 0.0.1 to 0.0.2
Skip migration: Bump mapping apiVersion from 0.0.2 to 0.0.3
Skip migration: Bump mapping apiVersion from 0.0.3 to 0.0.4
Skip migration: Bump mapping apiVersion from 0.0.4 to 0.0.5
Skip migration: Bump mapping apiVersion from 0.0.5 to 0.0.6
Skip migration: Bump manifest specVersion from 0.0.1 to 0.0.2
Skip migration: Bump manifest specVersion from 0.0.2 to 0.0.4
✔ Apply migrations
✔ Load subgraph from subgraph.yaml
Load contract ABI from abis/BAYC.json
✔ Load contract ABIs
Generate types for contract ABI: BAYC (abis/BAYC.json)
Write types to generated/BAYC/BAYC.ts
✔ Generate types for contract ABIs
✔ Generate types for data source templates
✔ Load data source template ABIs
✔ Generate types for data source template ABIs
✔ Load GraphQL schema from schema.graphql
Write types to generated/schema.ts
✔ Generate types for GraphQL schema
Types generated successfully
- Deploy the Subgraph:
If you haven't created a subgraph yet, you can refer to our online documentation.
# replace xxx with your actual deployment address.
graph deploy demo --node https://api.chainbase.online/v1/subgraph/xxx/deploy --ipfs https://api.chainbase.online/v1/subgraph/xxx/ipfs
2.4 Query Your Subgraph Using Playground
You can log in to our Chainbase console, find your subgraph, open the playground, and explore your indexed data 🎉.
2.5 Query Your Subgraph Using GraphQL API
You can also directly integrate GraphQL into your Dapp and query via API calls. Congratulations on completing your data set parsing 🎉✌️.
Updated 3 months ago