NFT composability refers to the ability of different Non-Fungible Tokens (NFTs) to be combined, linked, and interact with each other in a meaningful way. This allows NFTs to be used as building blocks to create new and more complex NFTs, or to be integrated into various decentralized applications, marketplaces, games, and platforms.
In this guide, we will walk through the process of building a composable NFT contract. We will cover setting up the development environment, writing the contracts, supporting transactions and scripts, along with how to deploy the contract to testnet. By the end of this guide, you will have a solid understanding of how to build a composable NFT on Flow. This guide will assume that you have a beginner level understanding of cadence.
All of the resulting code from this guide is available here.
The starter code includes important standard contracts that we will use to build our NFT. Make sure you add them to your project in the contracts
The contracts are:
FungibleToken.cdc - This is a standard Flow smart contract that represents Fungible Tokens
NonFungibleToken.cdc - This is a standard Flow smart contract that represents NFTs. We will use this to implement our custom NFT
MetadataViews.cdc - This contract is used to make our NFT interoperable. We will implement the metadata views specified in this contract so any Dapp can interact with our NFT!
This code implements a fundamental flow of Non-Fungible Tokens (NFTs) by extending the NonFungibleToken.INFT resource and a basic collection through MyFunNFTCollectionPublic. The program includes essential events that can be emitted, global variables that determine the storage paths of NFTs in a user's account, and a public mint function to create NFTs. It is worth noting that public minting is typically reserved for distributing free NFTs, while minting for profit requires an admin or the integration of a payment mechanism within the function.
Although this is commendable progress, the current NFT implementation lacks data. To remedy this, we can introduce customizable data fields for each NFT. For instance, in this use case, we aim to incorporate editions, each with a unique name, description, and serial number, much like the TopShot platform
Firstly, we will introduce two global variables at the top of the code, alongside totalSupply:
pub var totalEditions: UInt64
access(self) let editions: {UInt64: Edition}
We need to update the init() function for this contract. Add
These variables will facilitate monitoring the overall count of editions and accessing a specific edition through its assigned identifier. The editionsdictionary will provide a means to extract particular information for each edition. Consequently, we will proceed to construct the Editionstruct that we refer to within our editionsobject.
pub struct Edition {
pub let id: UInt64
/// The number of NFTs minted in this edition.
///
/// This field is incremented each time a new NFT is minted.
///
pub var size: UInt64
/// The number of NFTs in this edition that have been burned.
///
/// This field is incremented each time an NFT is burned.
///
pub var burned: UInt64
pub fun supply(): UInt64 {
return self.size - self.burned
}
/// The metadata for this edition.
pub let metadata: Metadata
init(
id: UInt64,
metadata: Metadata
) {
self.id = id
self.metadata = metadata
self.size = 0
self.burned = 0
}
/// Increment the size of this edition.
///
access(contract) fun incrementSize() {
self.size = self.size + (1 as UInt64)
}
/// Increment the burn count for this edition.
///
access(contract) fun incrementBurned() {
self.burned = self.burned + (1 as UInt64)
}
}
This is a fundamental struct that we will employ to represent "Editions" within this NFT. It retains an id, the size, the burned count, and a bespoke Metadataobject defined below. Please include this struct in your code as well.
pub struct Metadata {
pub let name: String
pub let description: String
init(
name: String,
description: String
) {
self.name = name
self.description = description
}
}
To maintain simplicity, the Metadatain this instance consists solely of a name and a description. However, if necessary, you may include more complex data within this object
We will now proceed to modify the NFT resource to include additional fields that allow us to track which "edition" each NFT belongs to and its serial number. We will be storing this information in the NFT resource. Following are the steps to accomplish this:
Add the following fields below id in the NFT resource:
pub let editionID: UInt64
pub let serialNumber: UInt64
Update the init() function in the NFT resource:
init(
editionID: UInt64,
serialNumber: UInt64
) {
self.id = self.uuid
self.editionID = editionID
self.serialNumber = serialNumber
}
Update the mintNFT() function to adhere to the new init() and the Edition struct:
pub fun mintNFT(editionID: UInt64): @MyFunNFT.NFT {
let edition = MyFunNFT.editions[editionID]
?? panic("edition does not exist")
// Increase the edition size by one
edition.incrementSize()
let nft <- create MyFunNFT.NFT(editionID: editionID, serialNumber: edition.size)
MyFunNFT.totalSupply = MyFunNFT.totalSupply + (1 as UInt64)
return <- nft
}
The updated mintNFT()function now receives an edition ID for the NFT to be minted. It validates the ID and creates the NFT by incrementing the serial number. It then updates the global variables to reflect the new size and returns the new NFT.
Excellent progress! We can now mint new NFTs for a specific edition. However, we need to enable the creation of new editions. To accomplish this, we will add function that allows anyone to create an edition (although in a real-world scenario, this would typically be a capability reserved for admin-level users). Please note that for the purposes of this example, we will make this function public.
pub fun createEdition(
name: String,
description: String,
): UInt64 {
let metadata = Metadata(
name: name,
description: description,
)
MyFunNFT.totalEditions = MyFunNFT.totalEditions + (1 as UInt64)
Now that we have the contract we need to create transactions that can be called to call the functions createEdition and mintNFT. These transactions can be called by any wallet since the methods are public on the contract.
This transaction takes in a name and description and creates a new edition with it.
mintNFT.cdc
import MyFunNFT from "../contracts/MyFunNFT.cdc"
import MetadataViews from "../contracts/MetadataViews.cdc"
import NonFungibleToken from "../contracts/NonFungibleToken.cdc"
transaction(
editionID: UInt64,
) {
let MyFunNFTCollection: &MyFunNFT.Collection{MyFunNFT.MyFunNFTCollectionPublic,NonFungibleToken.CollectionPublic,NonFungibleToken.Receiver,MetadataViews.ResolverCollection}
prepare(signer: AuthAccount) {
if signer.borrow<&MyFunNFT.Collection>(from: MyFunNFT.CollectionStoragePath) == nil {
// Create a new empty collection
let collection <- MyFunNFT.createEmptyCollection()
// save it to the account
signer.save(<-collection, to: MyFunNFT.CollectionStoragePath)
let item <- MyFunNFT.mintNFT(editionID: editionID)
self.MyFunNFTCollection.deposit(token: <-item)
}
}
This transaction verifies whether a MyFunNFT collection exists for the user by checking the presence of a setup. If no such collection is found, the transaction sets it up. Subsequently, the transaction mints a new NFT and deposits it in the user's collection.
We have successfully implemented a simple Edition NFT and created transactions to create editions and mint NFTs. However, in order for other applications to build on top of or interface with our NFT, they would need to know that our NFT contains a Metadataobject with a nameand descriptionfield. Additionally, it is important to consider how each app would keep track of the individual metadata and its structure for each NFT, especially given that different developers may choose to implement metadata in entirely different ways.
In Cadence, MetadataViewsserve as a standardized way of accessing NFT metadata, regardless of the specific metadata implementation used in the NFT resource. By providing a consistent interface for accessing metadata, MetadataViewsenable developers to build applications that can work with any NFT that uses a MetadataViews, regardless of how that metadata is structured.
By using MetadataViews, we can facilitate interoperability between different applications and services that use NFTs, and ensure that the metadata associated with our NFTs can be easily accessed and used by other developers.
Now let’s unlock interoperability for our NFT…
Let’s start off by importing the MetadataViews contract to the top
import MetadataViews from "./MetadataViews.cdc”
Now we need to have our NFT resource extend the MetadataViews.Resolver interface.
Now we must implement getViews and resolveView. The function getViews tells anyone which views this NFT supports and resolveView takes in a view type and returns the view itself. Some common views are:
ExternalURL - A website / link for an NFT
NFT Collection Data - Data on how to setup this NFT collection in a users account
Display View - How to display this NFT on a website
Royalties View - Any royalties that should be adhered to for a marketplace transaction
NFT Collection Display View - How to display the NFT collection on a website
Let’s add the following getViews implementation to our NFT resource.
pub fun getViews(): [Type] {
let views = [
Type<MetadataViews.Display>(),
Type<MetadataViews.ExternalURL>(),
Type<MetadataViews.NFTCollectionDisplay>(),
Type<MetadataViews.NFTCollectionData>(),
Type<MetadataViews.Royalties>(),
Type<MetadataViews.Edition>(),
Type<MetadataViews.Serial>()
]
return views
}
These function helps inform what specific views this NFT supports. In the same NFT resource add the following method:
1. This creates a display view and takes in the edition data to populate the name and description. I included a dummy image but you would want to include a unique thumbnail
pub fun resolveExternalURL(): MetadataViews.ExternalURL {
let collectionURL = "www.flow-nft-catalog.com"
return MetadataViews.ExternalURL(collectionURL)
}
2. This is a link for the NFT. I’m putting in a placeholder site for now but this would be something for a specific NFT not an entire collection. So something like www.collection_site/nft_id
pub fun resolveNFTCollectionDisplay(): MetadataViews.NFTCollectionDisplay {
3. This is a view that indicates to apps on how to display information about the collection. The externalURL here would be the website for the entire collection. I have linked a temporary flow image but you could many image you want here.
pub fun resolveNFTCollectionData(): MetadataViews.NFTCollectionData {
4. This is a view that allows any Flow Dapps to have the information needed to setup a collection in any users account to support this NFT.
pub fun resolveRoyalties(): MetadataViews.Royalties {
return MetadataViews.Royalties([])
}
5. For now we will skip Royalties but here you can specify which addresses should receive royalties and how much.
pub fun resolveEditionView(serialNumber: UInt64, size: UInt64): MetadataViews.Edition {
return MetadataViews.Edition(
name: "Edition",
number: serialNumber,
max: size
)
}
pub fun resolveSerialView(serialNumber: UInt64): MetadataViews.Serial {
return MetadataViews.Serial(serialNumber)
}
6. These are some extra views we can support since this NFT has editions and serial numbers. Not all NFTs need to support this but it’s nice to have for our case.
Lastly we need our Collection resource to support MetadataViews.ResolverCollection
You should see an error that you need to implement borrowViewResolver. This is a method a Dapp can use on the collection to borrow an NFT that inherits to the MetadataViews.Resolver interface so that resolveView that we implemented earlier can be called.
pub fun borrowViewResolver(id: UInt64): &AnyResource{MetadataViews.Resolver} {
let nft = (&self.ownedNFTs[id] as auth &NonFungibleToken.NFT?)!
let nftRef = nft as! &MyFunNFT.NFT
return nftRef as &AnyResource{MetadataViews.Resolver}
}
Now your NFT is interoperable!
Your final NFT contract should look something like this.
Run flow keys generate and paste your public key on the site. Keep your private key handy for the future. I just created the account 0x503b9841a6e501eb on testnet.
Deploying this on testnet is simple. We need to populate our config file with the relevant contracts and there addresses as well as where we want to deploy any new contracts.
Copy and replace your flow.json with the following:
This is a file that is meant to be pushed so we don’t want to expose our private keys. Luckily we can reference environment variables so use the following command to update the "$MYFUNNFT_TESTNET_PRIVATEKEY" environment variable with your newly created private key.
This is telling Flow where to find the contracts NonFungibleToken, MetadataViews, FungibleToken. For MyFunNFT it’s specifying where to deploy it, being testnet-account. Run flow project deploy --network=testnet and your contract should be deployed on testnet!
Let’s mint an NFT to an account. We will run the transactions from before. I’m using my testnet blocto wallet with the address: 0xf5e9719fa6bba61a. The newly minted NFT will go into this account.
Now that we have minted some NFTs into an account and made our NFT interoperable let’s add it to the NFT catalog.
What is the Flow NFT Catalog?
The Flow NFT Catalog is a repository of NFT’s on Flow that adhere to the Metadata standard and implement at least the core views. The core views being
External URL
NFT Collection Data
NFT Collection Display
Display
Royalties
When proposing an NFT to the catalog, it will make sure you have implemented these views correctly. Once added your NFT will easily be discoverable and other ecosystem developers can feel confident that your NFT has implemented the core views and build on top of your NFT using the Metadata views we implemented earlier!
1. It starts off by asking for the NFT contract. This is where the contract is deployed so what is in your flow.json and what we created via the faucet.
2. Now we need to enter the storage path. In our NFT it is /storage/MyFunNFT_Collection as well as an account that holds the NFT. This is 0xf5e9719fa6bba61a for me.
3. Now you should screen that verifies that you have implemented the “core” nft views correctly and you can also see the actual data being returned from the chain.
In the last step you can submit your collection to the NFT catalog and voila, you have created an NFT on Flow that can easily be discovered and supported on any Dapp in the Flow ecosystem!
Conclusion
In this tutorial, we created a basic NFT that supports multiple editions and unlimited serials. Each edition has a name and description and each NFT has a unique serial belonging to a specific Edition. We then made our NFT interoperable by implementing MetadataViews. We minted an NFT and added our NFT collection to the catalog so it’s easily discoverable and ready to be built on top of!