Skip to main content
Version: Next

How to Create an NFT Project on Flow

This tutorial dives into the technical steps required to craft an NFT on the Flow blockchain, providing developers with a clear roadmap from setup to deployment.

What are NFTs

NFTs, or Non-Fungible Tokens, represent a unique digital asset verified using blockchain technology. Unlike cryptocurrencies such as Bitcoin, which are fungible and can be exchanged on a one-for-one basis, NFTs are distinct and cannot be exchanged on a like-for-like basis. This uniqueness and indivisibility make them ideal for representing rare and valuable items like art, collectibles, tickets and even real estate. Their blockchain-backed nature ensures the authenticity and ownership of these digital assets.

Setting Up a Project

To start creating an NFT on the Flow blockchain, you'll first need some tools and configurations in place.

Installing Flow CLI

The Flow CLI (Command Line Interface) provides a suite of tools that allow developers to interact seamlessly with the Flow blockchain.

If you haven't installed the Flow CLI yet and have Homebrew installed, you can run brew install flow-cli. If you don’t have Homebrew, please follow the installation guide here.

Initializing a New Project

💡 Note: Here is a link to the completed code if you want to skip ahead or reference as you follow along.

Once you have the Flow CLI installed, you can set up a new project using the flow setup command. This command initializes the necessary directory structure and a flow.json configuration file (a way to configure your project for contract sources, deployments, accounts, and more):


_10
flow setup foobar-nft

Upon execution, the command will generate the following directory structure:


_10
/cadence
_10
/contracts
_10
/scripts
_10
/transactions
_10
/tests
_10
flow.json

Now, navigate into the project directory:


_10
cd foobar-nft

To begin, let's create a contract file named FooBar for the FooBar token, which will be the focus of this tutorial:


_10
touch cadence/contracts/FooBar.cdc

With the contract file in place, you can now set up the basic contract structure:


_10
pub contract FooBar {
_10
init() {}
_10
}

Setting Up Our NFT on the Contract

Understanding Resources

On the Flow blockchain, "Resources" are a key feature of the Cadence programming language. They represent unique, non-duplicable assets, ensuring that they can only exist in one place at a time. This concept is crucial for representing NFTs on Flow, as it guarantees their uniqueness.

To begin, let's define a basic NFT resource. This resource requires an init method, which is invoked when the resource is instantiated:


_10
pub contract FooBar {
_10
_10
pub resource NFT {
_10
init() {}
_10
}
_10
_10
init() {}
_10
}

Every resource in Cadence has a unique identifier assigned to it. We can use it to set an ID for our NFT. Here's how you can do that:


_12
pub contract FooBar {
_12
_12
pub resource NFT {
_12
pub let id: UInt64
_12
_12
init() {
_12
self.id = self.uuid
_12
}
_12
}
_12
_12
init() {}
_12
}

We also need to keep track of the total supply of NFTs in existance. To do this let’s create a totalSupply variable on our contract and increase it by one whenever a new NFT is created. We can set this on the initialization of the NFT using the resource init function:


_16
pub contract FooBar {
_16
pub var totalSupply: UInt64
_16
_16
pub resource NFT {
_16
pub let id: UInt64
_16
_16
init() {
_16
self.id = self.uuid
_16
FooBar.totalSupply = FooBar.totalSupply + 1
_16
}
_16
}
_16
_16
init() {
_16
self.totalSupply = 0
_16
}
_16
}

To control the creation of NFTs, it's essential to have a mechanism that restricts their minting. This ensures that not just anyone can create an NFT and inflate its supply. To achieve this, you can introduce an NFTMinter resource that contains a createNFT function:


_16
pub contract FooBar {
_16
_16
// ...[previous code]...
_16
_16
pub resource NFTMinter {
_16
pub fun createNFT(): @NFT {
_16
return <-create NFT()
_16
}
_16
_16
init() {}
_16
}
_16
_16
init() {
_16
self.totalSupply = 0
_16
}
_16
}

In this example, the NFTMinter resource will be stored on the contract account's storage. This means that only the contract account will have the ability to mint new NFTs. To set this up, add the following line to the contract's init function:


_10
pub contract FooBar {
_10
_10
// ...[previous code]...
_10
_10
init() {
_10
self.totalSupply = 0
_10
self.account.save(<- create NFTMinter(), to: /storage/NFTMinter)
_10
}
_10
}

Setting Up an NFT Collection

Storing individual NFTs directly in an account's storage can cause issues, especially if you want to store multiple NFTs. Instead, it's required to create a collection that can hold multiple NFTs. This collection can then be stored in the account's storage.

Start by creating a new resource named Collection. This resource will act as a container for your NFTs, storing them in a dictionary indexed by their IDs. Additionally, to ensure that all NFTs within a collection are destroyed when the collection itself is destroyed, you can add a destroy function:


_18
pub contract FooBar {
_18
_18
// ...[NFT resource code]...
_18
_18
pub resource Collection {
_18
pub var ownedNFTs: @{UInt64: NFT}
_18
_18
init() {
_18
self.ownedNFTs <- {}
_18
}
_18
_18
destroy () {
_18
destroy self.ownedNFTs
_18
}
_18
}
_18
_18
// ...[NFTMinter code]...
_18
}

To allow accounts to create their own collections, add a function in the main contract that creates a new Collection and returns it:


_10
pub contract FooBar {
_10
_10
pub var ownedNFTs: @{UInt64: NFT}
_10
_10
pub fun createEmptyCollection(): @Collection {
_10
return <-create Collection()
_10
}
_10
_10
// ...[following code]...
_10
}

To manage the NFTs within a collection, you'll need functions to deposit and withdraw NFTs. Here's how you can add a deposit function:


_11
pub resource Collection {
_11
_11
pub var ownedNFTs: @{UInt64: NFT}
_11
_11
pub fun deposit(token: @NFT) {
_11
let tokenID = token.id
_11
self.ownedNFTs[token.id] <-! token
_11
}
_11
_11
// ...[following code]...
_11
}

Similarly, you can add a withdraw function to remove an NFT from the collection:


_10
pub resource Collection {
_10
// ...[deposit code]...
_10
_10
pub fun withdraw(withdrawID: UInt64): @NFT {
_10
let token <- self.ownedNFTs.remove(key: withdrawID) ?? panic("Token not in collection")
_10
return <- token
_10
}
_10
_10
// ...[createEmptyCollection code]...
_10
}

To facilitate querying, you'll also want a function to retrieve all the NFT IDs within a collection:


_10
pub resource Collection {
_10
// ...[withdraw code]...
_10
_10
pub fun getIDs(): [UInt64] {
_10
return self.ownedNFTs.keys
_10
}
_10
_10
// ...[createEmptyCollection code]...
_10
}

For security reasons, you might not want to expose all the functions of the Collection to everyone. Instead, you can create an interface that exposes only the methods you want to make public. In Cadence, interfaces act as a blueprint for resources and structures, ensuring that certain methods or properties exist. By leveraging these interfaces, you establish clear boundaries and standardized interactions. In this case, you might want to expose only the deposit and getIDs methods. This interface can then be used to create capabilities, ensuring that only the allowed methods are accessible.


_15
pub contract FooBar {
_15
_15
// ...[previous code]...
_15
_15
pub resource interface CollectionPublic {
_15
pub fun deposit(token: @NFT)
_15
pub fun getIDs(): [UInt64]
_15
}
_15
_15
pub resource Collection: CollectionPublic {
_15
// ...[Collection code]...
_15
}
_15
_15
// ...[following code]...
_15
}

Fitting the Flow NFT Standard

To ensure compatibility and interoperability within the Flow ecosystem, it's crucial that your NFT contract adheres to the Flow NFT standard. This standard defines the events, functions, resources, and other elements that a contract should have. By following this standard, your NFTs will be compatible with various marketplaces, apps, and other services within the Flow ecosystem.

Applying the Standard

To start, you need to inform the Flow blockchain that your contract will implement the NonFungibleToken standard. Since it's a standard, there's no need for deployment. It's already available on the Emulator, Testnet, and Mainnet for the community's benefit.

Begin by importing the token standard into your contract:


_10
import "NonFungibleToken"
_10
_10
pub contract FooBar: NonFungibleToken {
_10
_10
// ...[rest of code]...
_10
_10
}

Adding Standard Events

To ensure interoperability, the Flow NFT standard requires certain events to be emitted during specific operations.

Adding ContractInitialized Event

For instance, when the contract is initialized, a ContractInitialized event should be emitted:


_14
import "NonFungibleToken"
_14
_14
pub contract FooBar: NonFungibleToken {
_14
_14
pub event ContractInitialized()
_14
_14
// ...[rest of code]...
_14
_14
init() {
_14
self.totalSupply = 0
_14
emit ContractInitialized()
_14
self.account.save(<- create NFTMinter(), to: /storage/NFTMinter)
_14
}
_14
}

Adding Withdraw and Deposit Events

Additionally, when NFTs are withdrawn or deposited, corresponding events should be emitted:


_10
import "NonFungibleToken"
_10
_10
pub contract FooBar: NonFungibleToken {
_10
_10
pub event ContractInitialized()
_10
pub event Withdraw(id: UInt64, from: Address?)
_10
pub event Deposit(id: UInt64, to: Address?)
_10
_10
// ...[rest of code]...
_10
}

You can then update your deposit and withdraw functions to emit these events:


_11
pub fun deposit(token: @NFT) {
_11
let tokenID = token.id
_11
self.ownedNFTs[token.id] <-! token
_11
emit Deposit(id: tokenID, to: self.owner?.address) // new
_11
}
_11
_11
pub fun withdraw(withdrawID: UInt64): @NFT {
_11
let token <- self.ownedNFTs.remove(key: withdrawID) ?? panic("Token not in collection")
_11
emit Withdraw(id: token.id, from: self.owner?.address) // new
_11
return <- token
_11
}

Update NFT Resource

The NFT resource should also be updated to implement the NonFungibleToken.INFT interface:


_10
pub resource NFT: NonFungibleToken.INFT {
_10
pub let id: UInt64
_10
_10
init() {
_10
self.id = self.uuid
_10
FooBar.totalSupply = FooBar.totalSupply + 1
_10
}
_10
}

Adding Provider, Receiver, CollectionPublic

Your Collection resource should also implement the Provider, Receiver, and CollectionPublic interfaces from the standard:


_10
pub resource Collection: NonFungibleToken.Provider, NonFungibleToken.Receiver, NonFungibleToken.CollectionPublic {
_10
// ...[rest of code]...
_10
}

With these implementations, you can now remove your custom CollectionPublic interface since the standard already provides it.

To ensure users can access a read-only reference to an NFT in the collection without actually removing it, introduce the borrowNFT function.


_10
pub resource Collection: NonFungibleToken.Provider, NonFungibleToken.Receiver, NonFungibleToken.CollectionPublic {
_10
_10
// ...[getIDs code]...
_10
_10
pub fun borrowNFT(id: UInt64): &NonFungibleToken.NFT {
_10
return (&self.ownedNFTs[id] as &NonFungibleToken.NFT?)!
_10
}
_10
_10
// ...[rest of code]...
_10
}

Lastly, update the ownedNFTs, deposit, and withdraw variables/methods to use the NonFungibleToken.NFT type:


_10
pub var ownedNFTs: @{UInt64: NonFungibleToken.NFT}
_10
_10
pub fun deposit(token: @NonFungibleToken.NFT) {
_10
//...[deposit code]...
_10
}
_10
_10
pub fun withdraw(withdrawID: UInt64): @NonFungibleToken.NFT {
_10
//...[withdraw code]...
_10
}

Deploying the Contract

With your contract ready, it's time to deploy it. First, add the FooBar contract to the flow.json configuration file:


_10
flow config add contract

When prompted, enter the following name and location (press Enter to skip alias questions):


_10
Enter name: FooBar
_10
Enter contract file location: cadence/contracts/FooBar.cdc

Next, configure the deployment settings by running the following command:


_10
flow config add deployment

Choose the emulator for the network and emulator-account for the account to deploy to. Then, select the FooBar contract (you may need to scroll down). This will update your flow.json configuration. After that, you can select No when asked to deploy another contract.

To start the Flow emulator, run (you may need to approve a prompt to allow connection the first time):


_10
flow emulator start

In a separate terminal or command prompt, deploy the contract:


_10
flow project deploy

You’ll then see a message that says All contracts deployed successfully.

Creating an NFTCollection

To manage multiple NFTs, you'll need an NFT collection. Start by creating a transaction file for this purpose:


_10
touch cadence/transactions/CreateCollection.cdc

Transactions, on the other hand, are pieces of Cadence code that can mutate the state of the blockchain. Transactions need to be signed by one or more accounts, and they can have multiple phases, represented by different blocks of code.

In this file, import the necessary contracts and define a transaction to create a new collection, storing it in the account's storage. Additionally, for the CollectionPublic interface, create a capability that allows others to read from its methods. This capability ensures secure, restricted access to specific functionalities or information within a resource.


_13
import "FooBar"
_13
import "NonFungibleToken"
_13
_13
transaction {
_13
prepare(acct: AuthAccount) {
_13
acct.save(<- FooBar.createEmptyCollection(), to: /storage/FooBarCollection)
_13
acct.link<&FooBar.Collection{NonFungibleToken.CollectionPublic}>(/public/FooBarCollection, target: /storage/FooBarCollection)
_13
}
_13
_13
execute {
_13
log("NFT collection created")
_13
}
_13
}

To store this new NFT collection, create a new account:


_10
flow accounts create

Name it test-acct and select emulator as the network. Then, using the Flow CLI, run the transaction:


_10
flow transactions send cadence/transactions/CreateCollection.cdc --signer test-acct --network emulator

Congratulations! You've successfully created an NFT collection for the test-acct.

Get an Account's NFTs

To retrieve the NFTs associated with an account, you'll need a script. Scripts are read-only operations that allow you to query the blockchain. They don't modify the blockchain's state, and therefore, they don't require gas fees or signatures (read more about scripts here).

Start by creating a script file:


_10
touch cadence/scripts/GetNFTs.cdc

In this script, import the necessary contracts and define a function that retrieves the NFT IDs associated with a given account:


_10
import "FooBar"
_10
import "NonFungibleToken"
_10
_10
pub fun main(account: Address): [UInt64] {
_10
let publicReference = getAccount(account).getCapability(/public/FooBarCollection)
_10
.borrow<&FooBar.Collection{NonFungibleToken.CollectionPublic}>()
_10
?? panic("Could not borrow public reference to FooBar")
_10
_10
return publicReference.getIDs()
_10
}

To check the NFTs associated with the test-acct, run the script (note: replace 0x123 with the address for test-acct from flow.json):


_10
flow scripts execute cadence/scripts/GetNFTs.cdc 0x123

Since you haven't added any NFTs to the collection yet, the result will be an empty array.

Minting and Depositing an NFT to a Collection

To mint and deposit an NFT into a collection, create a new transaction file:


_10
touch cadence/transactions/DepositNFT.cdc

In this file, define a transaction that takes a recipient's address as an argument. This transaction will borrow the minting capability from the contract account, borrow the recipient's collection capability, create a new NFT using the minter, and deposit it into the recipient's collection:


_15
import "FooBar"
_15
import "NonFungibleToken"
_15
transaction(recipient: Address) {
_15
prepare(acct: AuthAccount) {
_15
let nftMinter = acct.borrow<&FooBar.NFTMinter>(from: /storage/NFTMinter)
_15
?? panic("Could not borrow a reference to the NFTMinter")
_15
let recipientReference = getAccount(recipient).getCapability(/public/FooBarCollection)
_15
.borrow<&FooBar.Collection{NonFungibleToken.CollectionPublic}>()
_15
?? panic("Could not borrow a reference to the recipient's collection")
_15
recipientReference.deposit(token: <- nftMinter.createNFT())
_15
}
_15
execute {
_15
log("New NFT deposited into collection")
_15
}
_15
}

To run this transaction, use the Flow CLI. Remember, the contract account (which has the minting resource) should be the one signing the transaction. Pass the test account's address (from the flow.json file) as the recipient argument (note: replace 0x123 with the address for test-acct from flow.json):


_10
flow transactions send cadence/transactions/DepositNFT.cdc 0x123 --signer emulator-account --network emulator

After executing the transaction, you can run the earlier script to verify that the NFT was added to the test-acct's collection (remember to replace 0x123):


_10
flow scripts execute cadence/scripts/GetNFTs.cdc 0x123

You should now see a value in the test-acct's collection array!

Transferring an NFT to Another Account

To transfer an NFT to another account, create a new transaction file:


_10
touch cadence/transactions/TransferNFT.cdc

In this file, define a transaction that takes a recipient's address and the ID of the NFT you want to transfer as arguments. This transaction will borrow the sender's collection, get the recipient's capability, withdraw the NFT from the sender's collection, and deposit it into the recipient's collection:


_15
import "FooBar"
_15
_15
transaction(recipient: Address, id: UInt64) {
_15
prepare(acct: AuthAccount) {
_15
let collection = acct.borrow<&FooBar.Collection>(from: /storage/FooBarCollection)!
_15
let recipientReference = getAccount(recipient).getCapability(/public/FooBarCollection)
_15
.borrow<&FooBar.Collection{FooBar.CollectionPublic}>()
_15
?? panic("Could not borrow a reference to the recipient's collection")
_15
recipientReference.deposit(token: <- collection.withdraw(withdrawID: id))
_15
}
_15
_15
execute {
_15
log("NFT transferred to another collection")
_15
}
_15
}

To transfer the NFT, first create a new account:


_10
flow accounts create

Name it test-acct-2 and select Emulator as the network. Next, create a collection for this new account:


_10
flow transactions send cadence/transactions/CreateCollection.cdc --signer test-acct-2 --network emulator

Now, run the transaction to transfer the NFT from test-acct to test-acct-2 using the addresses from the flow.json file (replace 0x124 with test-acct-2's address. Also note that 0 is the id of the NFT we'll be transferring):


_10
flow transactions send cadence/transactions/TransferNFT.cdc 0x124 0 --signer test-acct --network emulator

To verify the transfer, you can run the earlier script for test-acct-2 (replace 0x124):


_10
flow scripts execute cadence/scripts/GetNFTs.cdc 0x123

Adding MetadataViews

Many NFT projects include metadata associated with the NFT, such as a name, description, or image. However, different projects might store this metadata in various formats. To ensure compatibility across the Flow ecosystem, Flow uses MetadataViews to standardize the representation of this metadata.

There are two types of Metadata Views: NFT level and contract level. In this guide, we’ll show you how to implement the most basic display, but for a deeper dive into what is possible, check out the MetadataViews API doc.

NFT Metadata

For the NFT metadata, you'll add a simple MetadataView called Display, which includes a name, description, and thumbnail. This format is common for many NFT projects. (For more details, refer to the Display documentation).

Start by importing the MetadataViews contract into your FooBar contract:


_10
import "MetadataViews"

Because this is already deployed to Emulator and our flow setup command added it to our flow.json, there is no more configuration we need to do.

Update the NFT resource to implement the [ViewResolver interface](https://github.com/onflow/flow-nft/blob/master/contracts/MetadataViews.cdc#L20) provided by the MetadataViews contract. This interface specifies that a getViews function and a resolveView function should exist. Then, add fields for name, thumbnail, and description:


_10
pub resource NFT: NonFungibleToken.INFT, MetadataViews.ViewResolver {
_10
pub let id: UInt64
_10
pub let name: String
_10
pub let description: String
_10
pub let thumbnail: String
_10
_10
// ...[rest of NFT code]...
_10
}

Now, add the methods from the ViewResolver interface to the NFT resource. These methods will return the metadata in the standardized Display format:


_18
pub resource NFT: NonFungibleToken.INFT, ViewResolver {
_18
// ...[NFT code]...
_18
_18
pub fun getViews(): [Type] {
_18
return [Type<MetadataViews.Display>()]
_18
}
_18
_18
pub fun resolveView(_ view: Type): AnyStruct? {
_18
if (view == Type<MetadataViews.Display>()) {
_18
return MetadataViews.Display(
_18
name: self.name,
_18
thumbnail: self.thumbnail,
_18
description: self.description
_18
)
_18
}
_18
return nil
_18
}
_18
}

Finally, to retrieve our NFT along with its metadata, we currently have a borrowNFT function. However, this function only returns a NonFungibleToken.NFT with an id field. To address this, let's introduce a new function in our collection that borrows the NFT and returns it as a FooBar NFT. We'll utilize the auth syntax to downcast the NonFungibleToken.NFT to our specific type.


_10
pub fun borrowFooBarNFT(id: UInt64): &FooBar.NFT? {
_10
if self.ownedNFTs[id] != nil {
_10
let ref = (&self.ownedNFTs[id] as auth &NonFungibleToken.NFT?)!
_10
return ref as! &FooBar.NFT
_10
}
_10
_10
return nil
_10
}

Contract Metadata

For the contract level metadata, we need to create an interface that defines the required methods for the contract. Luckily, there is already a commonly used contract interface called ViewResolver deployed both to the Emulator and other networks. This interface requires a getViews and a resolveViews method. It is also deployed on Testnet and Mainnet. You can find its address in the flow.json we generated with the setup command. To use it, return to your FooBar contract, import this new contract, and specify that FooBar should implement it.


_10
import "NonFungibleToken"
_10
import "MetadataViews"
_10
import "ViewResolver"
_10
_10
pub contract FooBar: NonFungibleToken, ViewResolver {
_10
//...[contract code]...
_10
}

Just like the NFT (except at a contract level), we’ll add functions for getView which returns the Display and resolveViews which tells it how to get the Display values:


_29
pub contract FooBar: NonFungibleToken, ViewResolver {
_29
_29
//...[all code above contract init]...
_29
_29
pub fun getViews(): [Type] {
_29
return [Type<MetadataViews.Display>()]
_29
}
_29
_29
pub fun resolveView(_ view: Type): AnyStruct? {
_29
switch view {
_29
case Type<MetadataViews.NFTCollectionData>():
_29
return MetadataViews.NFTCollectionData(
_29
storagePath: /storage/FooBarCollection,
_29
publicPath: /public/FooBarCollection,
_29
providerPath: /private/FooBarCollection,
_29
publicCollection: Type<&FooBar.Collection{NonFungibleToken.CollectionPublic}>(),
_29
publicLinkedType: Type<&FooBar.Collection{NonFungibleToken.CollectionPublic,NonFungibleToken.Receiver,MetadataViews.ResolverCollection}>(),
_29
providerLinkedType: Type<&FooBar.Collection{NonFungibleToken.CollectionPublic,NonFungibleToken.Provider,MetadataViews.ResolverCollection}>(),
_29
createEmptyCollectionFunction: (fun (): @NonFungibleToken.Collection {
_29
return <-FooBar.createEmptyCollection()
_29
})
_29
)
_29
}
_29
return nil
_29
}
_29
_29
//...[contract init code]...
_29
_29
}

Finally, we need a way to read this data like we did with the NFT. Let’s also make a borrowViewResolver function that we add below the borrowFooBarNFT method inside of the Collection:


_11
pub resource Collection: NonFungibleToken.Provider, NonFungibleToken.Receiver, NonFungibleToken.CollectionPublic, MetadataViews.ResolverCollection {
_11
_11
// ...[borrowFooBarNFT]...
_11
_11
pub fun borrowViewResolver(id: UInt64): &AnyResource{MetadataViews.Resolver} {
_11
let ref = (&self.ownedNFTs[id] as auth &NonFungibleToken.NFT?)!
_11
return ref as! &FooBar.NFT
_11
}
_11
_11
// ...[Collection init]...
_11
}

Congrats, you did it! You’re now ready to launch the next fun NFT project on Flow.

More