Skip to main content
Version: 1.0

5.2 Non-Fungible Token Tutorial Part 2

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


tip

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.

info

The playground code that is linked uses Cadence 0.42, but the examples use Cadence 1.0 to show how each contract, transaction and script is implemented in Cadence 1.0. You can access a Cadence 1.0-compatible playground by going to https://v1.play.flow.com/. The project link will still work with the current version of the playground, but when the playground is updated to Cadence 1.0, the link will be replaced with a 1.0-compatible version.

info

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.

_11
// Define a dictionary to store the NFTs in
_11
let myNFTs: @{Int: BasicNFT.NFT} = {}
_11
_11
// Create a new NFT
_11
let newNFT <- BasicNFT.createNFT(id: 1)
_11
_11
// Save the new NFT to the dictionary
_11
myNFTs[newNFT.id] <- newNFT
_11
_11
// Save the NFT to a new storage path
_11
account.storage.save(<-myNFTs, to: /storage/basicNFTDictionary)

Dictionaries​

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


_10
// Keys are `Int`
_10
// Values are `NFT`
_10
access(all) 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 so that there is one NFT for each Int that exists in the dictionary.

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. These types are relatively opaque and don't have much useful functionality on their 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 Contract 1 in account 0x01.

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 specifies three public functions for the collection.
  3. Declares a resource called Collection that acts as a place to intuitively store and manage your NFTs. It implements the NFTReceiver interface
  4. The Collection will declare fields and functions to interact with it, including ownedNFTs, init(), withdraw(), 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 initializer 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.

info

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.

ExampleNFT.cdc

_128
// ExampleNFT.cdc
_128
//
_128
// This is a complete version of the ExampleNFT contract
_128
// that includes withdraw and deposit functionalities, as well as a
_128
// collection resource that can be used to bundle NFTs together.
_128
//
_128
// Learn more about non-fungible tokens in this tutorial: https://developers.flow.com/cadence/tutorial/non-fungible-tokens-1
_128
_128
access(all) contract ExampleNFT {
_128
_128
// Declare Path constants so paths do not have to be hardcoded
_128
// in transactions and scripts
_128
_128
access(all) let CollectionStoragePath: StoragePath
_128
access(all) let CollectionPublicPath: PublicPath
_128
access(all) let MinterStoragePath: StoragePath
_128
_128
// Tracks the unique IDs of the NFT
_128
access(all) var idCount: UInt64
_128
_128
// Declare the NFT resource type
_128
access(all) resource NFT {
_128
// The unique ID that differentiates each NFT
_128
access(all) let id: UInt64
_128
_128
// Initialize both fields in the initializer
_128
init(initID: UInt64) {
_128
self.id = initID
_128
}
_128
}
_128
_128
// We define this interface purely as a way to allow users
_128
// to create public, restricted references to their NFT Collection.
_128
// They would use this to publicly expose only the deposit, getIDs,
_128
// and idExists fields in their Collection
_128
access(all) resource interface NFTReceiver {
_128
_128
access(all) fun deposit(token: @NFT)
_128
_128
access(all) fun getIDs(): [UInt64]
_128
_128
access(all) fun idExists(id: UInt64): Bool
_128
}
_128
_128
access(all) entitlement Withdraw
_128
_128
// The definition of the Collection resource that
_128
// holds the NFTs that a user owns
_128
access(all) resource Collection: NFTReceiver {
_128
// dictionary of NFT conforming tokens
_128
// NFT is a resource type with an `UInt64` ID field
_128
access(all) var ownedNFTs: @{UInt64: NFT}
_128
_128
// Initialize the NFTs field to an empty collection
_128
init () {
_128
self.ownedNFTs <- {}
_128
}
_128
_128
// withdraw
_128
//
_128
// Function that removes an NFT from the collection
_128
// and moves it to the calling context
_128
access(Withdraw) fun withdraw(withdrawID: UInt64): @NFT {
_128
// If the NFT isn't found, the transaction panics and reverts
_128
let token <- self.ownedNFTs.remove(key: withdrawID)
_128
?? panic("Could not withdraw an ExampleNFT.NFT with the specified ID")
_128
_128
return <-token
_128
}
_128
_128
// deposit
_128
//
_128
// Function that takes a NFT as an argument and
_128
// adds it to the collections dictionary
_128
access(all) fun deposit(token: @NFT) {
_128
// add the new token to the dictionary with a force assignment
_128
// if there is already a value at that key, it will fail and revert
_128
self.ownedNFTs[token.id] <-! token
_128
}
_128
_128
// idExists checks to see if a NFT
_128
// with the given ID exists in the collection
_128
access(all) view fun idExists(id: UInt64): Bool {
_128
return self.ownedNFTs[id] != nil
_128
}
_128
_128
// getIDs returns an array of the IDs that are in the collection
_128
access(all) view fun getIDs(): [UInt64] {
_128
return self.ownedNFTs.keys
_128
}
_128
}
_128
_128
// creates a new empty Collection resource and returns it
_128
access(all) fun createEmptyCollection(): @Collection {
_128
return <- create Collection()
_128
}
_128
_128
// mintNFT
_128
//
_128
// Function that mints a new NFT with a new ID
_128
// and returns it to the caller
_128
access(all) fun mintNFT(): @NFT {
_128
_128
// create a new NFT
_128
var newNFT <- create NFT(initID: self.idCount)
_128
_128
// change the id so that each ID is unique
_128
self.idCount = self.idCount + 1
_128
_128
return <-newNFT
_128
}
_128
_128
init() {
_128
self.CollectionStoragePath = /storage/nftTutorialCollection
_128
self.CollectionPublicPath = /public/nftTutorialCollection
_128
self.MinterStoragePath = /storage/nftTutorialMinter
_128
_128
// initialize the ID count to one
_128
self.idCount = 1
_128
_128
// store an empty NFT Collection in account storage
_128
self.account.storage.save(<-self.createEmptyCollection(), to: self.CollectionStoragePath)
_128
_128
// publish a capability to the Collection in storage
_128
let cap = self.account.capabilities.storage.issue<&{NFTReceiver}>(self.CollectionStoragePath)
_128
self.account.capabilities.publish(cap, at: self.CollectionPublicPath)
_128
}
_128
}

This smart contract more closely resembles a contract that a project would actually use in production, but still does not use the official NFT standard, so it should not be used in any production code.

Any user who owns one or more ExampleNFT should 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!

As we learned in the resource tutorial, you can destroy any resource by explicitly invoking the destroy command.

When the NFT Collection resource is destroyed with the destroy command, all the resources stored in the dictionary are also destroyed.

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


_10
init () {
_10
self.ownedNFTs <- {}
_10
}

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


_10
// getIDs returns an array of the IDs that are in the collection
_10
access(all) view fun getIDs(): [UInt64] {
_10
return self.ownedNFTs.keys
_10
}

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 fields 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.

This also brings up an interesting wrinkle in Cadence in regards to ownership. In other ledger-based languages, ownership is indicated by account addresses. Cadence is a fully object-oriented language, so ownership is indicated by where an object is stored, not just an entry on a ledger.

Resources can own other resources, which means that with some interesting logic, a resource can have more control over the resources it owns than the actual person whose account it is stored in!

You'll encounter more fascinating implications of ownership and interoperability like this as you get deeper into Cadence.

Now, back to the tutorial!

Restricting Access to the NFT Collection​

In the NFT Collection, we will publish a capability to allow anyone to access important functionality for our Collection, like deposit() and getIDs().

This is where an important 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 because it requires access to the authorized account object (auth(Storage) &Account) which is only accessible by a transaction that the owner authorizes and signs.

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:


_10
access(all)
_10
resource interface NFTReceiver {
_10
_10
access(all) fun deposit(token: @NFT)
_10
_10
access(all) fun getIDs(): [UInt64]
_10
_10
access(all) fun idExists(id: UInt64): Bool
_10
}

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.

The creation and publishing of the capability is seen in the ExampleNFT.cdc contract initializer.


_10
// publish a capability to the Collection in storage
_10
let cap = self.account.capabilities.storage.issue<&{NFTReceiver}>(self.CollectionStoragePath)
_10
self.account.capabilities.publish(cap, at: self.CollectionPublicPath)

The issue function specifies that the capability is typed as &AnyResource{NFTReceiver} to only expose those fields and functions. Then the link is published to /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.

info

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


_16
import ExampleNFT from 0x01
_16
_16
// Print the NFTs owned by account 0x01.
_16
access(all) fun main(): [UInt64] {
_16
// Get the public account object for account 0x01
_16
let nftOwner = getAccount(0x01)
_16
_16
// Find the public Receiver capability for their Collection and borrow it
_16
let receiverRef = nftOwner.capabilities
_16
.borrow<&{ExampleNFT.NFTReceiver}>(ExampleNFT.CollectionPublicPath)
_16
?? panic("Could not borrow receiver reference to the ExampleNFT.Collection")
_16
_16
// Log the NFTs that they own as an array of IDs
_16
log("Account 1 NFTs")
_16
return receiverRef.getIDs()
_16
}

info

Execute Print 0x01 NFTs by clicking the Execute button in the top right of the editor box.
This script returns 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:


_10
"Account 1 NFTs"
_10
Result > []

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.

Using Entitlements​

We do not want everyone in the network to be able to call our withdraw function though. In Cadence, any reference can be freely up-casted or down-casted to any subtype or supertype that the reference conforms to. This means that if I had a reference of the type &{ExampleNFT.NFTReceiver}, I could cast it to &ExampleNFT.Collection, which exposes all the access(all) functions on the Collection.

This is a powerful feature that is very useful, but developers need to understand that this means that if there is any privileged functionality on an resource that has a public capability, then this functionality cannot be access(all). It needs to use Entitlements.

Entitlements are a way for developers to explicitly declare which functions are restricted and to who they are restricted to by adding an entitlement to them. As you can see in our NFT contract, we've added an entitlement:


_10
access(all) entitlement Withdraw

We also added this entitlement to the withdraw() method:


_10
access(Withdraw) fun withdraw(withdrawID: UInt64): @NFT {

A function with entitled access means that that function is callable by someone with a concrete object of the containing type as if it were access(all), but it is not callable from a regular reference to that object. So if I borrowed a public reference to the Collection above of the type &ExampleNFT.Collection, I could call every function and access every field on it except the withdraw() function.


_10
// Get the public capability and borrow a reference from it
_10
let collectionRef = recipient.capabilities
_10
.borrow<&ExampleNFT.Collection>(ExampleNFT.CollectionPublicPath)
_10
?? panic("Could not borrow a reference to the ExampleNFT.Collection")
_10
_10
// Try to withdraw an NFT from their collection
_10
// ERROR: The reference is not entitled, so this call is not possible and will FAIL
_10
let stolenNFT <- collectionRef.withdraw(withdrawID: 1)

In order to access an entitled field or function through a reference, the reference needs to also be entitled. This means that when the reference or capability is created, the owner of that object has to explicitly specify that is has that entitlement.

The owner of an object is the only one who can create an entitled capability or reference. In the above example, if you wanted to make your withdraw function publicly accessible, you would issue the capability as an entitled capability by specifying all the entitlements in the capability's type specification using the auth keyword:


_10
// publish an entitled capability to the Collection in storage
_10
// This capability is issued with the `auth(ExampleNFT.Withdraw)` entitlement
_10
// This gives access to the withdraw function
_10
let cap = self.account.capabilities.storage.issue<auth(ExampleNFT.Withdraw) &{NFTReceiver}>(self.CollectionStoragePath)
_10
self.account.capabilities.publish(cap, at: self.CollectionPublicPath)

Now, anyone could borrow that capability as the entitled version it was issued as:


_10
// Get the public entitled capability and borrow a reference from it
_10
let entitledCollectionRef = recipient.capabilities
_10
.borrow<auth(ExampleNFT.Withdraw) &ExampleNFT.Collection>(ExampleNFT.CollectionPublicPath)
_10
?? panic("Could not borrow a reference to the ExampleNFT.Collection")
_10
_10
// Try to withdraw an NFT from their collection
_10
// This will succeed because the reference is entitled
_10
let stolenNFT <- entitledCollectionRef.withdraw(withdrawID: 1)

Obviously, you would not want to create a public entitled reference like this because you don't want anyone accessing your withdraw function. Entitlements are primarily meant for sharing private capabilities with small subsets of trusted users or smart contracts and should never be used for public capabilities.

The most important thing to remember is, if you don't want everyone in the network to be able to access a function on a resource, you should default put an entitlement on that function. Better to be safe than sorry.

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. Normally, most would implement restricted minting 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.

info

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:

mint_nft.cdc

_27
import ExampleNFT from 0x01
_27
_27
// This transaction allows the Minter account to mint an NFT
_27
// and deposit it into its own collection.
_27
_27
transaction {
_27
_27
// The reference to the collection that will be receiving the NFT
_27
let receiverRef: &{ExampleNFT.NFTReceiver}
_27
_27
prepare(acct: AuthAccount) {
_27
// Get the owner's collection capability and borrow a reference
_27
self.receiverRef = acct.capabilities
_27
.borrow<&{ExampleNFT.NFTReceiver}>(ExampleNFT.CollectionPublicPath)
_27
?? panic("Could not borrow receiver reference to ExampleNFT.Collection")
_27
}
_27
_27
execute {
_27
// Use the minter reference to mint an NFT, which deposits
_27
// the NFT into the collection that is sent as a parameter.
_27
let newNFT <- ExampleNFT.mintNFT()
_27
_27
self.receiverRef.deposit(token: <-newNFT)
_27
_27
log("NFT Minted and deposited to Account 1's Collection")
_27
}
_27
}

info

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

print_01_nfts.cdc

_18
import ExampleNFT from 0x01
_18
_18
// Print the NFTs owned by account 0x01.
_18
access(all) fun main(): [UInt64] {
_18
// Get the public account object for account 0x01
_18
let nftOwner = getAccount(0x01)
_18
_18
// Find the public Receiver capability for their Collection
_18
let capability = nftOwner.capabilities.get<&{ExampleNFT.NFTReceiver}>(ExampleNFT.CollectionPublicPath)
_18
_18
// borrow a reference from the capability
_18
let receiverRef = capability.borrow()
_18
?? panic("Could not borrow receiver reference to 0x01's ExampleNFT.Collection")
_18
_18
// Log the NFTs that they own as an array of IDs
_18
log("Account 1 NFTs")
_18
return receiverRef.getIDs()
_18
}

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


_10
"Account 1 NFTs"
_10
[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.

info

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

SetupAccount.cdc

_23
import ExampleNFT from 0x01
_23
_23
// This transaction configures a user's account
_23
// to use the NFT contract by creating a new empty collection,
_23
// storing it in their account storage, and publishing a capability
_23
transaction {
_23
prepare(acct: auth(SaveValue, StorageCapabilities) &Account) {
_23
_23
// Create a new empty collection
_23
let collection <- ExampleNFT.createEmptyCollection()
_23
_23
// store the empty NFT Collection in account storage
_23
acct.storage.save(<-collection, to: ExampleNFT.CollectionStoragePath)
_23
_23
log("Collection created for account 2")
_23
_23
// create a public capability for the Collection
_23
let cap = acct.capabilities.storage.issue<&{ExampleNFT.NFTReceiver}>(ExampleNFT.CollectionStoragePath)
_23
acct.capabilities.publish(cap, at: ExampleNFT.CollectionPublicPath)
_23
_23
log("Capability created")
_23
}
_23
}

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.

info

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.

transfer_nft.cdc

_38
import ExampleNFT from 0x01
_38
_38
// This transaction transfers an NFT from one user's collection
_38
// to another user's collection.
_38
transaction {
_38
_38
// The field that will hold the NFT as it is being
_38
// transferred to the other account
_38
let transferToken: @ExampleNFT.NFT
_38
_38
prepare(acct: auth(BorrowValue) &Account) {
_38
_38
// Borrow a reference from the stored collection
_38
let collectionRef = acct.storage
_38
.borrow<&ExampleNFT.Collection>(from: ExampleNFT.CollectionStoragePath)
_38
?? panic("Could not borrow a reference to the owner's collection")
_38
_38
// Call the withdraw function on the sender's Collection
_38
// to move the NFT out of the collection
_38
self.transferToken <- collectionRef.withdraw(withdrawID: 1)
_38
}
_38
_38
execute {
_38
// Get the recipient's public account object
_38
let recipient = getAccount(0x02)
_38
_38
// Get the Collection reference for the receiver
_38
// getting the public capability and borrowing a reference from it
_38
let receiverRef = recipient.capabilities
_38
.borrow<&{ExampleNFT.NFTReceiver}>(ExampleNFT.CollectionPublicPath)
_38
?? panic("Could not borrow receiver reference")
_38
_38
// Deposit the NFT in the receivers collection
_38
receiverRef.deposit(token: <-self.transferToken)
_38
_38
log("NFT ID 1 transferred from account 1 to account 2")
_38
}
_38
}

See, with the use of Collections and capabilities, now the only account that needs to sign a transaction to transfer a token is the one who is sending the token.

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

info

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

print_all_owned_nfts.cdc

_26
import ExampleNFT from 0x01
_26
_26
// Print the NFTs owned by accounts 0x01 and 0x02.
_26
access(all) fun main() {
_26
_26
// Get both public account objects
_26
let account1 = getAccount(0x01)
_26
let account2 = getAccount(0x02)
_26
_26
// Find the public Receiver capability for their Collections
_26
let acct1Capability = account1.capabilities.get(ExampleNFT.CollectionPublicPath)
_26
let acct2Capability = account2.capabilities.get(ExampleNFT.CollectionPublicPath)
_26
_26
// borrow references from the capabilities
_26
let receiver1Ref = acct1Capability.borrow<&{ExampleNFT.NFTReceiver}>()
_26
?? panic("Could not borrow account 1 receiver reference")
_26
let receiver2Ref = acct2Capability.borrow<&{ExampleNFT.NFTReceiver}>()
_26
?? panic("Could not borrow account 2 receiver reference")
_26
_26
// Print both collections as arrays of IDs
_26
log("Account 1 NFTs")
_26
log(receiver1Ref.getIDs())
_26
_26
log("Account 2 NFTs")
_26
log(receiver2Ref.getIDs())
_26
}

You should see something like this in the output:


_10
"Account 1 NFTs"
_10
[]
_10
"Account 2 NFTs"
_10
[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 and the NFT Developer Guide for information about the official Flow NFT standard and how to implement a real version of an NFT smart contract.

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!