Skip to main content

Simple NFT Viewer

This example project will show you how to build a simple viewer that will allow you to view NFTs that conform to the NFT and MetadataViews standards.

This tutorial will mostly ignore the C# code that actually displays the NFTs and focus on a high level summary of the steps used.

Overview

When querying the blockchain we utilize four scripts:


_10
* [GetCollections.cdc](Cadence/GetCollections.cdc) - Gets a list of Collections that conform to NFT.Collection for a given address
_10
* [GetNftIdsForCollection.cdc](Cadence/GetNftIdsForCollection.cdc) - Gets a list of all NFT IDs that are contained in a given collection
_10
* [GetDisplayDataForIDs.cdc](Cadence/GetDisplayDataForIDs.cdc) - Gets just the display data for a given NFT
_10
* [GetFullDataForID.cdc](Cadence/GetFullDataForID.cdc) - Gets a more comprehensive set of data for a single NFT.

While we could use a single script to query for all the data, larger collections will cause the script to time out. Instead we query for just the data we need to reduce the chances of a timeout occurring.

Finding Collections

First we need to get a list of all collections on an account that are a subtype of NFT.Collection.


_25
import NonFungibleToken from 0x1d7e57aa55817448
_25
_25
access(all) fun main(addr: Address) : [StoragePath] {
_25
//Get the AuthAccount for the given address.
_25
//The AuthAccount is needed because we're going to be looking into the Storage of the user
_25
var acct = getAuthAccount(addr)
_25
_25
//Array that we will fill with all valid storage paths
_25
var paths : [StoragePath] = []
_25
_25
//Uses the storage iteration API to iterate through all storage paths on the account
_25
acct.forEachStored(fun (path: StoragePath, type:Type): Bool {
_25
//Check to see if the resource at this location is a subtype of NonFungibleToken.Collection.
_25
if type.isSubtype(of: Type<@NonFungibleToken.Collection>()) {
_25
//Add this path to the array
_25
paths.append(path)
_25
}
_25
_25
//returning true tells the iterator to continue to the next entry
_25
return true
_25
});
_25
_25
//Return the array that we built
_25
return paths
_25
}

We use the Storage Iteration API to look at everything the account has in it's storage and see if it is an NFT Collection. We return a list of all found NFT Collections.

Getting NFT IDs Contained in a Collection

We use this to create a list of collection paths a user can pick from. When the user selects a path to view, we fetch a list of IDs contained in that collection:


_13
import NonFungibleToken from 0x1d7e57aa55817448
_13
_13
access(all) fun main(addr: Address, path: StoragePath) : [UInt64] {
_13
//Get the AuthAccount for the given address.
_13
//The AuthAccount is needed because we're going to be looking into the Storage of the user
_13
var acct = getAuthAccount(addr)
_13
_13
//Get a reference to an interface of type NonFungibleToken.Collection public backed by the resource located at path
_13
var ref = acct.borrow<&{NonFungibleToken.CollectionPublic}>(from: path)!
_13
_13
//Return the list of NFT IDs contained in this collection
_13
return ref!.getIDs()
_13
}

Getting Display Data for an NFT

After we get a list of the available NFT IDs, we need to get some basic data about the NFT to display the thumbnail icon.


_30
import NonFungibleToken from 0x1d7e57aa55817448
_30
import MetadataViews from 0x1d7e57aa55817448
_30
_30
access(all) fun main(addr: Address, path: StoragePath, ids: [UInt64]) : {UInt64:AnyStruct?} {
_30
//Array to hold the NFT display data that we will return
_30
//We use AnyStruct? because that is the type that is returned by resolveView.
_30
var returnData: {UInt64:AnyStruct?} = {}
_30
_30
//Get account for address
_30
var acct = getAuthAccount(addr)
_30
_30
//Get a reference to a capability to the storage path as a NonFungibleToken.CollectionPublic
_30
var ref = acct.borrow<&{NonFungibleToken.CollectionPublic}>(from: path)!
_30
_30
//Loop through the requested IDs
_30
for id in ids {
_30
//Get a reference to the NFT we're interested in
_30
var nftRef = ref.borrowNFT(id: id)
_30
_30
//If for some reason we couldn't borrow a reference, continue onto the next NFT
_30
if nftRef == nil {
_30
continue
_30
}
_30
_30
//Fetch the information we're interested in and store it in our NFT structure
_30
returnData[id] = nftRef.resolveView(Type<MetadataViews.Display>())
_30
}
_30
_30
return returnData
_30
}

This gives us a dictionary that maps NFT IDs to Display structs ({UInt64:MetadataViews.Display}). Because accessing this information can be tedious in C#, we can define some C# classes to make our lives easier:


_13
public class File
_13
{
_13
public string url;
_13
public string cid;
_13
public string path;
_13
}
_13
_13
public class Display
_13
{
_13
public String name;
_13
public String description;
_13
public File thumbnail;
_13
}

This will allow us to use Cadence.Convert to convert from the CadenceBase that the script returns into a Display class.

This line in NFTViewer.cs is an example of converting using Cadence.Convert:


_10
Dictionary<UInt64, Display> displayData = Convert.FromCadence<Dictionary<UInt64, Display>>(scriptResponseTask.Result.Value);

You might ask whey we don't combine GetNftIdsForCollection.cdc and GetDisplayDataForIDs.cdc to get the Display data at the same time we get the list of IDs. This approach would work in many cases, but when an account contains large numbers of NFTs, this could cause a script timeout. Getting the list of IDs is a cheap call because the NFT contains this list in an array already. By getting just the NFT IDs, we could implement paging and use multiple script calls to each fetch a portion of the display data. This example doesn't currently do this type of paging, but could do so without modifying the cadence scripts.

Getting Complete NFT Data

When a user selects a particular NFT to view in more detail, we need to fetch that detail.


_82
import NonFungibleToken from 0x1d7e57aa55817448
_82
import MetadataViews from 0x1d7e57aa55817448
_82
_82
//Structure that will hold all the data we want for an NFT
_82
access(all) struct NFTData {
_82
access(all) var NFTView: AnyStruct?
_82
access(all) var Display : AnyStruct?
_82
access(all) var HTTPFile: AnyStruct?
_82
access(all) var IPFSFile: AnyStruct?
_82
access(all) var Edition: AnyStruct?
_82
access(all) var Editions: AnyStruct?
_82
access(all) var Serial: AnyStruct?
_82
access(all) var Royalty: AnyStruct?
_82
access(all) var Royalties: AnyStruct?
_82
access(all) var Media: AnyStruct?
_82
access(all) var Medias: AnyStruct?
_82
access(all) var License: AnyStruct?
_82
access(all) var ExternalURL: AnyStruct?
_82
access(all) var NFTCollectionDisplay: AnyStruct?
_82
access(all) var Rarity: AnyStruct?
_82
access(all) var Trait: AnyStruct?
_82
access(all) var Traits: AnyStruct?
_82
_82
init() {
_82
self.NFTView = nil
_82
self.Display = nil
_82
self.HTTPFile = nil
_82
self.IPFSFile = nil
_82
self.Edition = nil
_82
self.Editions = nil
_82
self.Serial = nil
_82
self.Royalty = nil
_82
self.Royalties = nil
_82
self.Media = nil
_82
self.Medias = nil
_82
self.License = nil
_82
self.ExternalURL = nil
_82
self.NFTCollectionDisplay = nil
_82
self.Rarity = nil
_82
self.Trait = nil
_82
self.Traits = nil
_82
}
_82
}
_82
_82
access(all) fun main(addr: Address, path: StoragePath, id: UInt64) : NFTData? {
_82
//Get account for address
_82
var acct = getAuthAccount(addr)
_82
_82
//Get a reference to a capability to the storage path as a NonFungibleToken.CollectionPublic
_82
var ref = acct.borrow<&{NonFungibleToken.CollectionPublic}>(from: path)!
_82
_82
//Get a reference to the NFT we're interested in
_82
var nftRef = ref.borrowNFT(id: id)
_82
_82
//If for some reason we couldn't borrow a reference, continue onto the next NFT
_82
if nftRef == nil {
_82
return nil
_82
}
_82
_82
var nftData : NFTData = NFTData()
_82
_82
//Fetch the information we're interested in and store it in our NFT structure
_82
nftData.Display = nftRef.resolveView(Type<MetadataViews.Display>())
_82
nftData.NFTView = nftRef.resolveView(Type<MetadataViews.NFTView>())
_82
nftData.HTTPFile = nftRef.resolveView(Type<MetadataViews.HTTPFile>())
_82
nftData.IPFSFile = nftRef.resolveView(Type<MetadataViews.IPFSFile>())
_82
nftData.Edition = nftRef.resolveView(Type<MetadataViews.Edition>())
_82
nftData.Editions = nftRef.resolveView(Type<MetadataViews.Editions>())
_82
nftData.Serial = nftRef.resolveView(Type<MetadataViews.Serial>())
_82
nftData.Media = nftRef.resolveView(Type<MetadataViews.Media>())
_82
nftData.Rarity = nftRef.resolveView(Type<MetadataViews.Rarity>())
_82
nftData.Trait = nftRef.resolveView(Type<MetadataViews.Trait>())
_82
nftData.Traits = nftRef.resolveView(Type<MetadataViews.Traits>())
_82
nftData.Medias = nftRef.resolveView(Type<MetadataViews.Medias>())
_82
nftData.ExternalURL = nftRef.resolveView(Type<MetadataViews.ExternalURL>())
_82
nftData.Royalty = nftRef.resolveView(Type<MetadataViews.Royalty>())
_82
nftData.Royalties = nftRef.resolveView(Type<MetadataViews.Royalties>())
_82
nftData.License = nftRef.resolveView(Type<MetadataViews.License>())
_82
nftData.NFTCollectionDisplay = nftRef.resolveView(Type<MetadataViews.NFTCollectionDisplay>())
_82
_82
return nftData
_82
}

Here we define a struct NFTData that will contain all the different information we want and fill the struct via multiple resolveView calls.

C# Classes for Easy Converting

The end of NFTViewer.cs contains classes that we use to more easily convert from Cadence into C#. One thing to note is that the Cadence structs contain Optionals, like:

var IPFSFile: AnyStruct?

while the C# versions do not, such as

public IPFSFile IPFSFile;

This is because we are declaring them as Classes, not Structs. Classes in C# are reference types, which can automatically be null. We could have used Structs, in which case we'd have to use:

public IPFSFile? IPFSFile

This would wrap the IPFSFile struct in a Nullable, which would allow it to be null if the Cadence value was nil.

Another thing to note is the declaration of the C# File class:


_16
public class File
_16
{
_16
public string url;
_16
public string cid;
_16
public string path;
_16
_16
public string GetURL()
_16
{
_16
if (string.IsNullOrEmpty(url) && !string.IsNullOrEmpty(cid))
_16
{
_16
return $"https://ipfs.io/ipfs/{cid}";
_16
}
_16
_16
return url;
_16
}
_16
}

Compare this to the File interface in the MetadataViews contract:


_10
access(all) struct interface File {
_10
access(all) fun uri(): String
_10
}

The MetadataViews.File interface doesn't actually contain any fields, only a single method. Because only two things in MetadataViews implement the File interface (HTTPFile and IPFSFile), we chose to combine the possible fields into our File class.


_10
access(all) struct HTTPFile: File {
_10
access(all) let url: String
_10
}
_10
_10
access(all) struct IPFSFile: File {
_10
access(all) let cid: String
_10
access(all) let path: String?
_10
}

This allows Cadence.Convert to convert either an HTTPFile or an IPFSFile into a File object. We can then check which fields are populated to determine which it was initially.

This works fine for this simple viewer, but a more robust approach might be to create a ResolvedFile struct in the cadence script which has a single uri field and populates it by calling the uri() function on whatever File type was retrieved.