5.2 Non-Fungible Token Tutorial Part 2

An introduction to NFTs on Cadence

In this tutorial, we're going to learn about a full implementation for Non-Fungible Tokens (NFTs).


Open the starter code for this tutorial in the Flow Playground:

https://play.onflow.org/f08e8e0d-d28e-4cbe-8d72-3afe2349c629


The tutorial will ask you to take various actions to interact with this code.

Instructions that require you to take action are always included in a callout box like this one. These highlighted actions are all that you need to do to get your code running, but reading the rest is necessary to understand the language's design.

Storing Multiple NFTs in a Collection

In the last tutorial, we created a simple NFT resource, stored in at a storage path, then used a multi-sig transaction to transfer it from one account to another.

It should hopefully be clear that the setup and operations that we used in the previous tutorial are not very scalable. Users need a way to manage all of their NFTs from a single place.

There are some different ways we could accomplish this.

  • We could store all of our NFTs in an array or dictionary, like so.
1
// Define a dictionary to store the NFTs in
2
let myNFTs: @{Int: BasicNFT.NFT} = {}
3
4
// Create a new NFT
5
let newNFT <- BasicNFT.createNFT(id: 1)
6
7
// Save the new NFT to the dictionary
8
myNFTs[newNFT.id] <- newNFT
9
10
// Save the NFT to a new storage path
11
account.save(<-myNFTs, to: /storage/basicNFTDictionary)

Dictionaries

This example uses a Dictionary: a mutable, unordered collection of key-value associations.

1
pub let myNFTs: @{Int: NFT}

In a dictionary, all keys must have the same type, and all values must have the same type. In this case, we are mapping integer (Int) IDs to NFT resource objects. Dictionary definitions don't usually have the @ symbol in the type specification, but because the myNFTs mapping stores resources, the whole field also has to become a resource type, which is why the field has the @ symbol indicating that it is a resource type.

This means that all the rules that apply to resources apply to this type.

Using a dictionary to store our NFTs would solve the problem of having to use different storage paths for each NFT, but it doesn't solve all the problems. This types are relatively opaque and doesn't have much useful functionality on its own.

Instead, we can use a powerful feature of Cadence, resources owning other resources! We'll define a new Collection resource as our NFT storage place to enable more-sophisticated ways to interact with our NFTs.

The next contract we look at is called ExampleNFT, it's stored in account 0x02. This contract expands on the BasicNFT we looked at by adding:

  1. An idCount contract field that tracks unique NFT ids.
  2. An NFTReceiver interface that exposes three functions for the collection
  3. Declares a resource called Collection that implements the NFTReceiver interface
  4. The Collection will declare fields and functions to interact with it, including ownedNFTs, init(), withdraw(), destroy(), and other important functions
  5. Next, the contract declares functions that create a new NFT (mintNFT()) and an empty collection (createEmptyCollection())
  6. Finally, the contract declares an init() function that initializes the path fields, creates an empty collection as well as a reference to it, and saves a minter resource to account storage.

This contract introduces a few new concepts, we'll look at the new contract, then break down all the new concepts this contract introduces.

Open Account 0x01 to see ExampleNFT.cdc.
Deploy the contract by clicking the Deploy button in the bottom right of the editor.
ExampleNFT.cdc should contain the code below. It contains what was already in BasicNFT.cdc plus additional resource declarations in the contract body.

1
// ExampleNFT.cdc
2
//
3
// This is a complete version of the ExampleNFT contract
4
// that includes withdraw and deposit functionalities, as well as a
5
// collection resource that can be used to bundle NFTs together.
6
//
7
// Learn more about non-fungible tokens in this tutorial: https://docs.onflow.org/docs/non-fungible-tokens
8
9
pub contract ExampleNFT {
10
11
// Declare Path constants so paths do not have to be hardcoded
12
// in transactions and scripts
13
14
pub let CollectionStoragePath: StoragePath
15
pub let CollectionPublicPath: PublicPath
16
pub let MinterStoragePath: StoragePath
17
18
// Tracks the unique IDs of the NFT
19
pub var idCount: UInt64
20
21
// Declare the NFT resource type
22
pub resource NFT {
23
// The unique ID that differentiates each NFT
24
pub let id: UInt64
25
26
// Initialize both fields in the init function
27
init(initID: UInt64) {
28
self.id = initID
29
}
30
}
31
32
// We define this interface purely as a way to allow users
33
// to create public, restricted references to their NFT Collection.
34
// They would use this to publicly expose only the deposit, getIDs,
35
// and idExists fields in their Collection
36
pub resource interface NFTReceiver {
37
38
pub fun deposit(token: @NFT)
39
40
pub fun getIDs(): [UInt64]
41
42
pub fun idExists(id: UInt64): Bool
43
}
44
45
// The definition of the Collection resource that
46
// holds the NFTs that a user owns
47
pub resource Collection: NFTReceiver {
48
// dictionary of NFT conforming tokens
49
// NFT is a resource type with an `UInt64` ID field
50
pub var ownedNFTs: @{UInt64: NFT}
51
52
// Initialize the NFTs field to an empty collection
53
init () {
54
self.ownedNFTs <- {}
55
}
56
57
// withdraw
58
//
59
// Function that removes an NFT from the collection
60
// and moves it to the calling context
61
pub fun withdraw(withdrawID: UInt64): @NFT {
62
// If the NFT isn't found, the transaction panics and reverts
63
let token <- self.ownedNFTs.remove(key: withdrawID)!
64
65
return <-token
66
}
67
68
// deposit
69
//
70
// Function that takes a NFT as an argument and
71
// adds it to the collections dictionary
72
pub fun deposit(token: @NFT) {
73
// add the new token to the dictionary with a force assignment
74
// if there is already a value at that key, it will fail and revert
75
self.ownedNFTs[token.id] <-! token
76
}
77
78
// idExists checks to see if a NFT
79
// with the given ID exists in the collection
80
pub fun idExists(id: UInt64): Bool {
81
return self.ownedNFTs[id] != nil
82
}
83
84
// getIDs returns an array of the IDs that are in the collection
85
pub fun getIDs(): [UInt64] {
86
return self.ownedNFTs.keys
87
}
88
89
destroy() {
90
destroy self.ownedNFTs
91
}
92
}
93
94
// creates a new empty Collection resource and returns it
95
pub fun createEmptyCollection(): @Collection {
96
return <- create Collection()
97
}
98
99
// mintNFT
100
//
101
// Function that mints a new NFT with a new ID
102
// and returns it to the caller
103
pub fun mintNFT(): @NFT {
104
105
// create a new NFT
106
var newNFT <- create NFT(initID: self.idCount)
107
108
// change the id so that each ID is unique
109
self.idCount = self.idCount + 1
110
111
return <-newNFT
112
}
113
114
init() {
115
self.CollectionStoragePath = /storage/nftTutorialCollection
116
self.CollectionPublicPath = /public/nftTutorialCollection
117
self.MinterStoragePath = /storage/nftTutorialMinter
118
119
// initialize the ID count to one
120
self.idCount = 1
121
122
// store an empty NFT Collection in account storage
123
self.account.save(<-self.createEmptyCollection(), to: self.CollectionStoragePath)
124
125
// publish a reference to the Collection in storage
126
self.account.link<&{NFTReceiver}>(self.CollectionPublicPath, target: self.CollectionStoragePath)
127
}
128
}

This smart contract more closely resembles a contract that a project would actually use in production.

Any user who owns one or more ExampleNFT will have an instance of this @ExampleNFT.Collection resource stored in their account. This collection stores all of their NFTs in a dictionary that maps integer IDs to @NFTs.

Each collection has a deposit and withdraw function. These functions allow users to follow the pattern of moving tokens in and out of their collections through a standard set of functions.

When a user wants to store NFTs in their account, they will create an empty Collection by calling the createEmptyCollection() function in the ExampleNFT smart contract. This returns an empty Collection object that they can store in their account storage.

There are a few new features that we use in this example, so let's walk through them.

The Resource Dictionary

We discussed above that when a dictionary stores a resource, it also becomes a resource!

This means that the collection has to have special rules for how to handle its own resource. You wouldn't want it getting lost by accident!

If the NFT Collection resource is destroyed with the destroy command, it needs to know what to do with the resources it stores in the dictionary. This is why resources that store other resources have to include a destroy function that runs when destroy is called on it. This destroy function has to either explicitly destroy the contained resources or move them somewhere else. In this example, we destroy them.

1
destroy() {
2
destroy self.ownedNFTs
3
}

When the Collection resource is created, the init function is run and must explicitly initialize all member variables. This helps prevent issues in some smart contracts where uninitialized fields can cause bugs. The init function can never run again after this. Here, we initialize the dictionary as a resource type with an empty dictionary.

1
init () {
2
self.ownedNFTs <- {}
3
}

Another feature for dictionaries is the ability to get an array of the keys of the dictionary using the built-in keys function.

1
// getIDs returns an array of the IDs that are in the collection
2
pub fun getIDs(): [UInt64] {
3
return self.ownedNFTs.keys
4
}

This can be used to iterate through the dictionary or just to see a list of what is stored. As you can see, a variable length array type is declared by enclosing the member type within square brackets ([UInt64]).

Resources Owning Resources

This NFT Collection example in ExampleNFT.cdc illustrates an important feature: resources can own other resources.

In the example, a user can transfer one NFT to another user. Additionally, since the Collection explicitly owns the NFTs in it, the owner could transfer all of the NFTs at once by just transferring the single collection.

This is an important feature because it enables numerous additional use cases. In addition to allowing easy batch transfers, this means that if a unique NFT wants to own another unique NFT, like a CryptoKitty owning a hat accessory, the Kitty literally stores the hat in its own storage and effectively owns it. The hat belongs to the CryptoKitty that it is stored in, and the hat can be transferred separately or along with the CryptoKitty that owns it.

Restricting Access to the NFT Collection

In the NFT Collection, all the functions and fields are public, but we do not want everyone in the network to be able to call our withdraw function. This is where Cadence's second layer of access control comes in. Cadence utilizes capability security, which means that for any given object, a user is allowed to access a field or method of that object if they either:

  • Are the owner of the object
  • Have a valid reference to that field or method (note that references can only be created from capabilities, and capabilities can only be created by the owner of the object)

When a user stores their NFT Collection in their account storage, it is by default not available for other users to access. A user's authorized account object (AuthAccount, which gives access to private storage) is only accessible by its owner. To give external accounts access to the deposit function, the getIDs function, and the idExists function, the owner creates an interface that only includes those fields:

1
pub resource interface NFTReceiver {
2
3
pub fun deposit(token: @NFT)
4
5
pub fun getIDs(): [UInt64]
6
7
pub fun idExists(id: UInt64): Bool
8
}

Then, using that interface, they would create a link to the object in storage, specifying that the link only contains the functions in the NFTReceiver interface. This link creates a capability. From there, the owner can then do whatever they want with that capability: they could pass it as a parameter to a function for one-time-use, or they could put in the /public/ domain of their account so that anyone can access it. If a user tried to use this capability to call the withdraw function, it wouldn't work because it doesn't exist in the interface that was used to create the capability.

The creation of the link and capability is seen in the ExampleNFT.cdc contract init() function

1
// publish a reference to the Collection in storage
2
self.account.link<&{NFTReceiver}>(self.CollectionPublicPath, target: self.CollectionStoragePath)

The link function specifies that the capability is typed as &AnyResource{NFTReceiver} to only expose those fields and functions. Then the link is stored in /public/ which is accessible by anyone. The link targets the /storage/NFTCollection (through the self.CollectionStoragePath contract field) that we created earlier.

Now the user has an NFT collection in their account /storage/, along with a capability for it that others can use to see what NFTs they own and to send an NFT to them.

Let's confirm this is true by running a script!

Run a Script


Scripts in Cadence are simple transactions that run without any account permissions and only read information from the blockchain.

Open the script file named Print 0x01 NFTs. Print 0x01 NFTs should contain the following code:

1
import ExampleNFT from 0x01
2
3
// Print the NFTs owned by account 0x01.
4
pub fun main() {
5
// Get the public account object for account 0x01
6
let nftOwner = getAccount(0x01)
7
8
// Find the public Receiver capability for their Collection
9
let capability = nftOwner.getCapability<&{ExampleNFT.NFTReceiver}>(ExampleNFT.CollectionPublicPath)
10
11
// borrow a reference from the capability
12
let receiverRef = capability.borrow()
13
?? panic("Could not borrow receiver reference")
14
15
// Log the NFTs that they own as an array of IDs
16
log("Account 1 NFTs")
17
log(receiverRef.getIDs())
18
}

Execute Print 0x01 NFTs by clicking the Execute button in the top right of the editor box.
This script prints a list of the NFTs that account 0x01 owns.

Because account 0x01 currently doesn't own any in its collection, it will just print an empty array:

1
"Account 1 NFTs"
2
[]
3
Result > "void"

If the script cannot be executed, it probably means that the NFT collection hasn't been stored correctly in account 0x01. If you run into issues, make sure that you deployed the contract in account 0x01 and that you followed the previous steps correctly.

Mint and Distribute Tokens


One way to create NFTs is by having an admin mint new tokens and send them to a user. For the purpose of learning, we are simply implementing minting as a public function here.

Most would implement this by having an NFT Minter resource. This would restrict minting, because the owner of this resource is the only one that can mint tokens.

You can see an example of this in the Marketplace tutorial.

Open the file named Mint NFT. Select account 0x01 as the only signer and send the transaction.
This transaction deposits the minted NFT into the account owner's NFT collection:

1
import ExampleNFT from 0x01
2
3
// This transaction allows the Minter account to mint an NFT
4
// and deposit it into its collection.
5
6
transaction {
7
8
// The reference to the collection that will be receiving the NFT
9
let receiverRef: &{ExampleNFT.NFTReceiver}
10
11
prepare(acct: AuthAccount) {
12
// Get the owner's collection capability and borrow a reference
13
self.receiverRef = acct.getCapability<&{ExampleNFT.NFTReceiver}>(ExampleNFT.CollectionPublicPath)
14
.borrow()
15
?? panic("Could not borrow receiver reference")
16
}
17
18
execute {
19
// Use the minter reference to mint an NFT, which deposits
20
// the NFT into the collection that is sent as a parameter.
21
let newNFT <- ExampleNFT.mintNFT()
22
23
self.receiverRef.deposit(token: <-newNFT)
24
25
log("NFT Minted and deposited to Account 1's Collection")
26
}
27
}

Reopen Print 0x01 NFTs and execute the script. This prints a list of the NFTs that account 0x01 owns.

1
import ExampleNFT from 0x01
2
3
// Print the NFTs owned by account 0x01.
4
pub fun main() {
5
// Get the public account object for account 0x01
6
let nftOwner = getAccount(0x01)
7
8
// Find the public Receiver capability for their Collection
9
let capability = nftOwner.getCapability<&{ExampleNFT.NFTReceiver}>(ExampleNFT.CollectionPublicPath)
10
11
// borrow a reference from the capability
12
let receiverRef = capability.borrow()
13
?? panic("Could not borrow receiver reference")
14
15
// Log the NFTs that they own as an array of IDs
16
log("Account 1 NFTs")
17
log(receiverRef.getIDs())
18
}

You should see that account 0x01 owns the NFT with id = 1

1
"Account 1 NFTs"
2
[1]

Transferring an NFT

Before we are able to transfer an NFT to another account, we need to set up that account with an NFTCollection of their own so they are able to receive NFTs.

Open the file named Setup Account and submit the transaction, using account 0x02 as the only signer.

1
import ExampleNFT from 0x01
2
3
// This transaction configures a user's account
4
// to use the NFT contract by creating a new empty collection,
5
// storing it in their account storage, and publishing a capability
6
transaction {
7
prepare(acct: AuthAccount) {
8
9
// Create a new empty collection
10
let collection <- ExampleNFT.createEmptyCollection()
11
12
// store the empty NFT Collection in account storage
13
acct.save<@ExampleNFT.Collection>(<-collection, to: ExampleNFT.CollectionStoragePath)
14
15
log("Collection created for account 2")
16
17
// create a public capability for the Collection
18
acct.link<&{ExampleNFT.NFTReceiver}>(ExampleNFT.CollectionPublicPath, target: ExampleNFT.CollectionStoragePath)
19
20
log("Capability created")
21
}
22
}

Account 0x02 should now have an empty Collection resource stored in its account storage. It has also created and stored a capability to the collection in its /public/ domain.

Open the file named Transfer, select account 0x01 as the only signer, and send the transaction.
This transaction transfers a token from account 0x01 to account 0x02.

1
import ExampleNFT from 0x01
2
3
// This transaction transfers an NFT from one user's collection
4
// to another user's collection.
5
transaction {
6
7
// The field that will hold the NFT as it is being
8
// transferred to the other account
9
let transferToken: @ExampleNFT.NFT
10
11
prepare(acct: AuthAccount) {
12
13
// Borrow a reference from the stored collection
14
let collectionRef = acct.borrow<&ExampleNFT.Collection>(from: ExampleNFT.CollectionStoragePath)
15
?? panic("Could not borrow a reference to the owner's collection")
16
17
// Call the withdraw function on the sender's Collection
18
// to move the NFT out of the collection
19
self.transferToken <- collectionRef.withdraw(withdrawID: 1)
20
}
21
22
execute {
23
// Get the recipient's public account object
24
let recipient = getAccount(0x02)
25
26
// Get the Collection reference for the receiver
27
// getting the public capability and borrowing a reference from it
28
let receiverRef = recipient.getCapability<&{ExampleNFT.NFTReceiver}>(ExampleNFT.CollectionPublicPath)
29
.borrow()
30
?? panic("Could not borrow receiver reference")
31
32
// Deposit the NFT in the receivers collection
33
receiverRef.deposit(token: <-self.transferToken)
34
35
log("NFT ID 1 transferred from account 1 to account 2")
36
}
37
}

Now we can check both accounts' collections to make sure that account 0x02 owns the token and account 0x01 has nothing.

Execute the script Print all NFTs to see the tokens in each account:

1
import ExampleNFT from 0x01
2
3
// Print the NFTs owned by accounts 0x01 and 0x02.
4
pub fun main() {
5
6
// Get both public account objects
7
let account1 = getAccount(0x01)
8
let account2 = getAccount(0x02)
9
10
// Find the public Receiver capability for their Collections
11
let acct1Capability = account1.getCapability(ExampleNFT.CollectionPublicPath)
12
let acct2Capability = account2.getCapability(ExampleNFT.CollectionPublicPath)
13
14
// borrow references from the capabilities
15
let receiver1Ref = acct1Capability.borrow<&{ExampleNFT.NFTReceiver}>()
16
?? panic("Could not borrow account 1 receiver reference")
17
let receiver2Ref = acct2Capability.borrow<&{ExampleNFT.NFTReceiver}>()
18
?? panic("Could not borrow account 2 receiver reference")
19
20
// Print both collections as arrays of IDs
21
log("Account 1 NFTs")
22
log(receiver1Ref.getIDs())
23
24
log("Account 2 NFTs")
25
log(receiver2Ref.getIDs())
26
}

You should see something like this in the output:

1
"Account 1 NFTs"
2
[]
3
"Account 2 NFTs"
4
[1]

Account 0x02 has one NFT with ID=1 and account 0x01 has none. This shows that the NFT was transferred from account 0x01 to account 0x02.

Congratulations, you now have a working NFT!

Putting It All Together


This was only a basic example how a NFT might work on Flow. Please refer to the Flow NFT Standard repo for information about the official Flow NFT standard and an example implementation of it.

Also check out the Kitty Items Repo for a production ready version!

Fungible Tokens


Now that you have a working NFT, you will probably want to be able to trade it. For that you are going to need to understand how fungible tokens work on Flow, so go ahead and move to the next tutorial!