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):
_10flow setup foobar-nft
Upon execution, the command will generate the following directory structure:
_10/cadence_10 /contracts_10 /scripts_10 /transactions_10 /tests_10flow.json
Now, navigate into the project directory:
_10cd foobar-nft
To begin, let's create a contract file named FooBar
for the FooBar
token, which will be the focus of this tutorial:
_10touch cadence/contracts/FooBar.cdc
With the contract file in place, you can now set up the basic contract structure:
_10pub 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:
_10pub 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:
_12pub 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:
_16pub 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:
_16pub 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:
_10pub 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:
_18pub 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:
_10pub 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:
_11pub 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:
_10pub 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:
_10pub 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.
_15pub 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:
_10import "NonFungibleToken"_10_10pub 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:
_14import "NonFungibleToken"_14_14pub 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:
_10import "NonFungibleToken"_10_10pub 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:
_11pub 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_11pub 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:
_10pub 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:
_10pub 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.
_10pub 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:
_10pub var ownedNFTs: @{UInt64: NonFungibleToken.NFT}_10_10pub fun deposit(token: @NonFungibleToken.NFT) {_10 //...[deposit code]..._10}_10_10pub 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:
_10flow config add contract
When prompted, enter the following name and location (press Enter
to skip alias questions):
_10Enter name: FooBar_10Enter contract file location: cadence/contracts/FooBar.cdc
Next, configure the deployment settings by running the following command:
_10flow 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):
_10flow emulator start
In a separate terminal or command prompt, deploy the contract:
_10flow 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:
_10touch 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.
_13import "FooBar"_13import "NonFungibleToken"_13_13transaction {_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:
_10flow accounts create
Name it test-acct
and select emulator
as the network. Then, using the Flow CLI, run the transaction:
_10flow 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:
_10touch 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:
_10import "FooBar"_10import "NonFungibleToken"_10_10pub 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
):
_10flow 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:
_10touch 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:
_15import "FooBar"_15import "NonFungibleToken"_15transaction(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
):
_10flow 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
):
_10flow 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:
_10touch 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:
_15import "FooBar"_15_15transaction(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:
_10flow accounts create
Name it test-acct-2
and select Emulator
as the network. Next, create a collection for this new account:
_10flow 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):
_10flow 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
):
_10flow 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:
_10import "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
:
_10pub 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:
_18pub 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.
_10pub 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.
_10import "NonFungibleToken"_10import "MetadataViews"_10import "ViewResolver"_10_10pub 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:
_29pub contract FooBar: NonFungibleToken, ViewResolver {_29_29//...[all code above contract init]..._29_29pub 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
:
_11pub 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
- Explore an example NFT repository
- Watch a video tutorial on creating an NFT project in the Flow Playground
- Dive into the details of the NFT Standard
- For a deeper dive into
MetadataViews
, consult the API documentation or the FLIP that introduced this feature. - Use a no code tool for creating NFT projects on Flow