Explore multi-chain NFTs with KodaDot

Viki Val
KodaDot Frontier
Published in
7 min readMay 29, 2023

--

In the context of non-fungible tokens, the Polkadot ecosystem is special. Each parachain has its own implementation and uses a different approach.
But the ecosystem grows only when other builders make awesome apps on top of it. We took it as a challenge in KodaDot to create a one-stop shop API for the multi-chain NFTs.

Fetching data with Uniquery is super easy 💜

The heart of KodaDot’s data storage is Subsquid, the fastest blockchain indexer for Substrate and EVM, which offers GraphQL API for the data.

Since a lot of work is based on external contributors and bounty systems, we found using GraphQL very difficult for many developers.

Let’s meet Uniquery 👋

Uniquery is a state-of-the-art API for Kusama-based NFTs.

The core features of our API are:

1. Client first

3. Simple to use

2. Extensible and modular

4. Typesafe

What does that all mean? We will go through it step-by-step.

Usually, when some provider offers API, we need a special authorization key. Data are requested via a centralized server that tracks us.

We took a different approach, and Uniquery communicates directly to the SubSquid GraphQL endpoint. The magic under the hood contains a builder that translates a function into the GraphQL query.

If we, for example, want to fetch NFTs that belong to collection 2305670031 we would need to write the query below. It does not look hard, but it gives us extra cognitive load if we are unfamiliar with the technology.

  query nftListByCollectionId {
nft: nftEntities(where: {
collection: {id_eq: "2305670031"}}
) {
id
metadata
currentOwner
issuer
}
}

Generating the same query for via uniquery contains three lines of code!

import { getClient } from '@kodadot1/uniquery'

const client = getClient()
const query = client.itemListByCollectionId('2305670031')

Uniquery ships as an npm package, written in Typescript, so a good IDE such as VSCode will always give you a good hint about arguments.
So first, we need to import getClient from the package.
Next, we need to create an instance of the client by calling the function we have imported.
Once our client instance is ready, we can explore many handy functions available. In this case, we will use the same procedure to get items from a particular collection.

The general rule of thumb for what queries are available are:
1. I need to know which entity I want — collection, nft (item), or event (what has happened with an item)
2. Should the query return more than one element? If yes, then its called [entity]List; otherwise, it’s just an entity
3. Do I need to filter by any field? It is then denoted by By[FieldIWantToFilter].
Summing up the naming: we want items (more NFTs) that belong to a particular collection: itemListByCollectionId.

So we finally got the query we wanted. Now how to get the data?
Luckily the client contains the function fetch that takes the query as an argument.

import { getClient } from '@kodadot1/uniquery'

const client = getClient('bsx')
const query = client.itemListByCollectionId('2305670031')

const result = await client.fetch(query)

Apart from the previous query, we changed our getClient function to take an argument. The argument represents a chain that we would like to use to query the data.
When we call the fetch function, we will get the result 🤗.

{
data: {
items: [
{
id: "2305670031-1",
createdAt: "2022-10-09T11:56:36.338000Z",
name: "Bubble #0",
metadata: "ipfs://ipfs/bafkreids5pfs2xpgnoaccgd56rgd66jijgb5v5hdahiybmslwwmcpmbgza",
currentOwner: "bXhUWXbffHMJk2FoTriLixXjQY36RPDkX5Tugy5WYSmafJsGi",
issuer: "bXhUWXbffHMJk2FoTriLixXjQY36RPDkX5Tugy5WYSmafJsGi"
},
{
id: "2305670031-8",
createdAt: "2022-10-09T12:14:12.271000Z",
name: "Bubble #7",
metadata: "ipfs://ipfs/bafkreiho6blouuiwjc26vjunq5tc3pn7dtmz73p4j7tt6tuzsiqnqrw6zi",
currentOwner: "bXhUWXbffHMJk2FoTriLixXjQY36RPDkX5Tugy5WYSmafJsGi",
issuer: "bXhUWXbffHMJk2FoTriLixXjQY36RPDkX5Tugy5WYSmafJsGi"
}
]
}
}

Testing in prod.

As we ship this awesome API, we wanted to ensure that API is friendly enough; we decided to build an NFT viewer from scratch with Uniquery.

We took the latest tech: Deno and Fresh, to make this more challenging.

Why not Node.js? We always wanted to try the new cutting-edge tech (that’s why we built on top of the Kusama Network 😉).

Fandom shop is available for any parachain supported by KodaDot. For this tutorial we will build a simple fandom shop for a collection minted on the Basilisk parachain.

Fandom shop for NFTs

For demonstration purposes, we used the collection minted by Deepologic. So let’s get started.
When we open the index.tsx file, we will see a very strange import of our Uniquery. One of the advantages of the Deno linter is that it forcibly recommends you use the specific version of the package. In this case, we are using the latest one. Which one is the latest? We can always check it on npmjs.

import { getClient } from 'https://esm.sh/@kodadot1/uniquery@0.2.1-rc.0'

After we successfully imported our uniquery function, let’s fetch some data. We must create and export a constant handler with a GET procedure to do this in Fresh. The name of this function is not random; it tells Fresh that if we visit this page, this handler should be called.

As the snippet below shows, we spawn an instance of our Uniquery client inside the GET function by calling the getClient function.
Since we want to fetch all NFTs that belong to a particular collection, we need to call a method called itemListByCollectionId.
The first parameter it takes is the id of a collection, we want to use.
The second parameter, option, let us customize the query as we wish. We only wanted to fetch fields id, name, price, and meta.
The result should be ordered by name ascending (meaning from A to Z).
The last thing to obtain our data is to call the fetch method on the client with the query parameter. We are ready to render our data using the built-in Fresh render method.

export const handler: Handlers<Data> = {
async GET(_req, ctx) {
const client = getClient('bsx')
const query = client.itemListByCollectionId('2551182625', {
fields: ['id', 'name', 'price', 'meta'],
orderBy: 'name_ASC'
})
const data = await client.fetch<Data>(query);
return ctx.render(data);
},
};

Now how to render the data? In our Home component, we get data from the context argument. Then we will assign data.items to the products variable. How do we know there is an items attribute? Luckily there is a logical answer to that. Since we called the method on Uniquery, whose name starts with itemList it means that the output will be a list of item elements, therefore, items.
Similarly, it works for also for collections and events.

export default function Home(ctx: PageProps<Data>) {
const { data } = ctx;
const products = data.items;

return (
<div>
{products.map((item) => <ProductCard product={item} />)}
</div>
);
}

Once we have the products variable, we can render that by mapping each product into the <ProductCard /> component. product={item} means that we will assign the item variable to the prop product.

Now the biggest chunk. Render the ProductCard ! As we mentioned previously, the props we obtain in the ProductCard is a product that we will use to render our data. The standard approach in NFT data is that the image field is in the form of ipfs://ipfs/<CID>. Unfortunately, the <image /> tag can’t show this URL. Therefore, we must transform it to a format-friendly image tag. Luckily KodaDot has a package called minipfs that can help us with that. Now we need to wrap the $purify function inside the useComputed . It is a cool special syntax for Preact similar to computed in Vue.js. It basically means that the image variable assigns the computed value from this function.

Why we can’t call purify directly? Because this way, we are avoiding unnecessary re-renders.

In the same style, we will format the price using the polkadot-js library that works perfectly in Deno. Because blockchains do not have floating numbers like 1.69, we need to represent a number that denotes balance as a huge number. In Kusama Network, we have 12 decimals, so 1 KSM is saved in the blockchain as 1000000000000 (1 and 12 zeros after). That is why we need to use a formatBalance function, and the arguments you see make perfect sense now ❤.

import { useComputed } from "@preact/signals";
import { $purify } from "https://esm.sh/@kodadot1/minipfs@0.2.0-rc.0";
import { formatBalance } from "https://deno.land/x/polkadot@0.2.29/util/mod.ts";

function ProductCard(props: { product: Item }) {
const { product } = props;
const image = useComputed(() => $purify(product.meta?.image).at(0));
const price = useComputed(() =>
formatBalance(product.price, { decimals: 12, withUnit: "KSM" })
);

return (
<a key={product.id} href={`/products/${product.id}`}>
<div>
{image && (
<img
src={image.value}
alt={product.name}
/>
)}
</div>
<div>
<h3>
{product.name}
</h3>
<strong>
{price.value}
</strong>
</div>
</a>
);
}

Now return some HTML from the ProductCard function, and voila! Our code works. It is worth noting that we removed all CSS classes we use for simplicity.

Sum Up.

It’s time to celebrate! We have successfully created a collection viewer for the NFTs. Uniquery makes it easy to query important data from the indexer without GraphQL knowledge. Fresh as a framework is super simple and fast to iterate on simple UI.
You can check the working demo. The code for this project is open-source, and we would be very happy if you check it.
Do not forget to follow us on Twitter and star on our main KodaDot repository.

What’s next?

We want to see a new wild implementation of KodaDot. You can build command line interfaces, chatbots, or alternative UIs. While you are building, visit our KodaDot UI to inspire yourself and flip some JPEGs.
Our Discord is the best way to communicate, so see you there!

--

--