Rapid Development of Bored Ape Yacht Club Subgraph

You have two ways to quickly experience our hosted subgraph feature:

  1. Visit our example code repository directly, build and compile, then deploy the subgraph to our cloud service.
  2. 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.

  1. 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.
  1. 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.

TypeDescription
BytesByte array, represented as a hexadecimal string. Commonly used for Ethereum hashes and addresses.
StringScalar for string values. Null characters are not supported and are automatically removed.
BooleanScalar for boolean values.
IntThe GraphQL spec defines Int to have a size of 32 bytes.
BigIntLarge integers. Used for Ethereum's uint32, int64, uint64, ..., uint256 types. Note: Everything below uint32, such as int32, uint24, or int8 is represented as i32.
BigDecimalBigDecimal 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
}
  1. 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 as id.
  • event.block and event.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 the Transfer 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:

  1. 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 from scheme.graphql, and we can import and use it.
  • BAYC.ts is generated from the monthly ABI, and the TransferEvent class allows us to handle the Transfer event in the contract. The BAYC 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
  1. 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
  1. 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 🎉✌️.