Transfer
, BoredApe
, Property
), map them via AssemblyScript to on-chain events, and handle mint or transfer events to store relevant NFT and metadata details.npm
or yarn
.
To install using npm:
npm
or yarn
, depending on what is installed in your system.
A single subgraph can be used to index data from multiple smart contracts. After the Graph CLI is done installing all the dependencies for the BAYC smart contract, it will ask you if you want to add another smart contract within the same project.
Select No
to exit the UI.
Let us go over what we do here:
graph init
, but to do that we need to pass it a bunch of parameters.cd ChainstackSubgraph
to move the terminal into the subdirectory.
Let us go over the main files in a subgraph project:
subgraph.yaml
at the root defines all the major parameters of a subgraph project. It contains a list of all the smart contracts being indexed, as well as a list of entities and their corresponding handler functions. We will be adding more properties like startBlock
and description
to the YAML file in the tutorial. You can read about all the specifications in detail in the Graph docs, though that is not necessary to go through this tutorial.
schema.graphql
file at the root of our project contains all our entities. These entities define what data our subgraph indexes.
Here is what an entity could look like:
src
folder. If you followed the instructions above, you should see a file named bayc.ts
inside the src
folder.
subgraph.yaml
subgraph.yaml
file and paste the following:
eventHandlers
object. In practice, what this means is that a function named handleTransfer
will run every time an event named Transfer
is triggered from the smart contract we are indexing. We are indexing the Bored Apes smart contract, and the transfer event is emitted every time a Bored Ape NFT is transferred from one address to another. You can check out the code on Etherscan.features
object. Since we will be using The Graph’s IPFS API, we need to declare it as such within the features object.startBlock
, that will allow us to define the block number from which we want our subgraph to start indexing data. This could potentially save us from having to index millions of blocks, so it makes sense to configure this. We can define the start block as the block in which our smart contract was created since any block before that is irrelevant to us.
To find the start block for the BAYC smart contract:
graph-cli
added a feature where The Graph fetches the start block for smart contracts by default during project initialization. You can use the default value, but it is always better to know how to fetch the start block yourselves.schema.graphql
file.
A subgraph schema is a collection of entities. An entity defines the kind of data we want to store and also the structure of the request query when we query data from our subgraph.
@entity
directive. Also, each entity must have an ID field, which must have a unique value for all entities of the same type. We will look more into this while defining our mapping functions.
As you can see, each object in an entity has a scalar type (akin to data types) specified. The Graph protocol supports the following scalar types in its 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. |
schema.graphql
file in your subgraph project, and delete everything.
We will define three entities for our subgraph. Paste the following code into the schema file:
Transfer
event in the Bored Apes smart contract looks like this:
Transfer
entity to store all of this data whenever the Transfer
event is triggered so that we have a complete history of ownership for all the Bored Ape NFTs.Transfer
events will have a unique transaction hash. Also, the id
field needs to be unique for all instances of the Transfer
entity. Thus, we can use the transaction hash of every transfer event to generate a unique ID every time.Transfer
entity will have unique IDs, we will never need to overwrite an existing instance of the Transfer
entity. Thus, we should mark the entity as immutable. Entities marked immutable are faster to query, and should be marked as such unless we expect our entities to be overwritten with new values.tokenURI
of an NFT from its tokenID
, along with the block in which it last changed ownership.BoredApe
entity will be distinguished by the tokenID
, which we will use as the ID for the entity. Since an NFT can be transferred multiple times, the entity will have to be mutable to reflect this fact.Transfer
event is emitted only when an ape is transferred from one address to another. How will we find out the creator of an NFT with its help?Transfer
event such that minting an ape to Alice is like transferring an ape from the null address to Alice. How cool is that! This means that all the instances of our Transfer
entity where the value of from
is the null address are actually a recording of the minting of a new NFT:
Property
entity right below the previous one:
Property
entity to store valid values for all the attributes that actually have a value. We want it to store null
for all the others.Property
entity are not suffixed with an exclamation mark (!
). This is because fields marked with an !
must have a valid value. They cannot be null
. We, however, expect many of our NFTs to have multiple attributes with the value null
. Moreover, sometimes an IPFS node is not available, and we might thus not receive a response at all. Thus, we must ensure that all fields that store our metadata can store null
as a valid value.generated
directory. You should not change any files inside the generated
directory, unless you know exactly what you are doing.src/bayc.ts
and delete everything inside it. Paste the following code at the top of the file to import all the AssemblyScript types we need from the generated folder:
generated
directory has two files, schema.ts
and BAYC.ts
schema.ts
contains AssemblyScript types generated from our schema file. We import AssemblyScript classes for our entities directly from this file.BAYC.ts
contains AssemblyScript types generated from our contract ABI. The TransferEvent
class allows us to work with the Transfer
event from the smart contract, while the BAYC
class is an abstraction of the smart contract itself. We can use this latter class to read data and call functions from the smart contract.handleTransfer
as follows:
subgraph.yaml
, we need to create an exported function of the same name in our mapping file. Each event handler should accept a parameter called event
with a type corresponding to the name of the event which is being handled. In our case, we are handling the Transfer
event.Transfer
event is emitted.Transfer
entity every time this function runs. Paste the following code inside the function:
Transfer
entity. We ensure that each instance of the Transfer
entity has a unique ID by concatenating the transaction hash with the log index of the event. This is what will be returned as the id
when we query the Transfer
entity.Transfer
event.event.block
and event.transaction
are part of the Ethereum API of the graph-ts library. You can refer to this page for a complete reference. We can leverage this library to get back all sorts of data.save()
method. Using this method, we can save new instances of the Transfer
entity to our database.bind
method allows us to access the address of the smart contract that emitted the event, which in our case is the address of the BAYC smart contract. This will come in handy later on.BoredApe
entity. Let us say our subgraph comes across a Transfer
event for a particular token ID. We can then use the load method to check if any instance of the BoredApe
entity exists with that particular ID.contractAddress
object.owner
field and the blockNumber
field. We don’t have to change the other fields because they will remain constant. After that, we save the entity to our database.baseURI
function on the bored apes smart contract.handleTransfer
function however will run every time the transfer event is emitted. Thus, we run the entire metadata indexing procedure only if that particular instance of the Property
entity doesn’t exist..cat()
method can be used to retrieve the entire metadata of an NFT by passing the complete IPFS hash path to it. Please note that you need to perform a null check at every step while querying IPFS data.Property
entity directly after converting it to a string.attributes
array to store those values to the respective fields. The attributes that don’t return a value will be marked null
, since that is the default value..save()
method.graph codegen
and graph build
before deploying your subgraph.
To deploy a subgraph to Chainstack:
Transfer
entity:
orderBy
attribute to sort the returned data with respect to a particular data field. If we modify the previous query like this:
Transfer
entity sorted according to the token ID.
What if we want to get the transaction hashes of all transactions when a Bored Ape NFT was minted? How do we do that?
Recall that any instance of the Transfer
entity that has the null address as its from
value represents an NFT being minted. Modify the previous query to look like this: