Transfer events emitted by the BAYC token contract and derived some information on tokens and their owners from that data. In this part we enrich token data with information obtained from contract state calls, IPFS and regular HTTP URLs.
Prerequisites: Node.js, Squid CLI, Docker, a project folder with the code from the second part (this commit).
Exploring token metadata
Now that we have a record for each BAYC NFT, let’s explore how we can retrieve more data for each token. EIP-721 suggests that token metadata contracts may make token data available in a JSON referred to by the output of thetokenURI() contract function. Upon examining the generated src/abi/bayc module, we find that the BAYC token contract implements this function. Also, the public ABI has no obvious contract methods that may set token URI or events that may be emitted on its change. In other words, it appears that the only way to retrieve this data is by querying the contract state.
State queries are the one thing that still requires an RPC endpoint of an archival Ethereum node: the Portal that streams our indexing data does not serve eth_call requests. Let us add an RPC client to the squid. Install the client package
src/processor.ts
'<my_eth_rpc_url>' is a public RPC endpoint we chose to use in this example. Please use a private endpoint or SQD Cloud’s RPC addon in production.
The next step is to prepare for retrieving and parsing the metadata proper. For this, we need to understand the protocols used in the URIs and the structure of metadata JSONs. To learn that, we retrieve and inspect some URIs ahead of the main squid sync. The most straightforward way to achieve this is by adding the following to the batch handler:
src/main.ts
Contract class provided by the generated src/abi/bayc module. Like every contract class made by squid-evm-typegen, it extends ContractBase from @subsquid/evm-abi and its constructor takes three arguments:
- A chain access object of the shape
{_chain: {client}}, whereclientcan be any object with acall(method: string, params?: unknown[])method that performs JSON-RPC calls. OurRpcClientinstance fits this interface, so we simply wrap it. - A block header. Contract state will be queried at the height taken from it. We use the header of the last block of each batch.
- The address of the contract to be queried.
Contract instance, we call tokenURI() for each token mentioned in the batch and print the retrieved URI.
This simple approach is rather slow: the modified squid needs about three to eight hours to get a reasonably sized sample of URIs (figure out of date). A faster yet more complex alternative will be discussed in the next part of the tutorial.
Running the modified squid reveals that some metadata URIs point to HTTPS and some point to IPFS. Here is one of the metadata JSONs:
- BAYC metadata URIs can point to HTTPS or IPFS — we need to be able to retrieve both.
- Metadata JSONs have two fields:
"image", a string, and"attributes", an array of pairs{"trait_type": string, "value": string}.
Extending the Token entity
We will save both image and attributes metadata fields and the metadata URI to the database. To do this, we need to add some new fields to the existing Token entity:
Attribute is a non-entity type that we use to type the attributes field.
Once schema.graphql is updated, we regenerate the TypeORM data model code::
createTokens():
PartialToken stores the incomplete Token information obtained purely from blockchain events and function calls, before any state queries or enrichment with external data.
The function completeTokens() is responsible for filling Token fields that are missing in PartialTokens. This involves IO operations, so both the function and its caller createTokens() have to be asynchronous. The functions also take the batch blocks: state queries must be pinned to a block height, and we will use the height of the last block in the batch. We modify the createTokens() call in the batch handler to accommodate these changes:
completeTokens():
Contract object and use it to call the tokenURI() method of the BAYC token contract. The retrieved URIs are then used by the fetchTokenMetadata() function, which is responsible for HTTPS/IPFS metadata retrieval and parsing. Once we have its output, we can create and return the final Token entity instances.
Retrieving external resources
In thefetchTokenMetadata() implementation we first classify the URIs depending on the protocol. For IPFS links we replace 'ipfs://' with an address of an IPFS gateway, then retrieve the metadata from all links using a regular HTTPS client. Here for the demonstration purposes we use the public ipfs.io gateway, which is slow and prone to dropping requests due to rate-limiting. For production squids we recommend using a dedicated gateway, e.g. from Filebase.
src/metadata.ts. Since it is a module of its own, it gets its own logger. Here are its full contents:
src/metadata.ts
src/metadata.ts
src/metadata.ts
src/main.ts:
src/main.ts
npx squid-graphql-server and visiting the local GraphiQL playground. It is now possible to retrieve image URLs and attributes for each token.
