Skip to main content

5.1 Non-Fungible Token Tutorial Part 1

In this tutorial, we're going to deploy, store, and transfer Non-Fungible Tokens (NFTs).


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

https://play.onflow.org/a21087ad-b22c-4981-b49e-17297e916fa6


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.

The NFT is an integral part of blockchain technology. An NFT is a digital asset that represents ownership of a unique asset. NFTs are also indivisible, you can't trade part of an NFT. Possible examples of NFTs include: CryptoKitties, Top Shot Moments, and tickets to a really fun concert.

Instead of being represented in a central ledger, like in most smart contract languages, Cadence represents each NFT as a resource object that users store in their accounts. This allows NFTs to benefit from the resource ownership rules that are enforced by the type system - resources can only have a single owner, they cannot be duplicated, and they cannot be lost due to accidental or malicious programming errors. These protections ensure that owners know that their NFT is safe and can represent an asset that has real value.

NFTs in a real-world context make it possible to trade assets and prove who the owner of an asset is. On Flow, NFTs are interoperable - so the NFTs in an account can be used in different smart contracts and app contexts. All NFTs on Flow implement the NFT Token Standard which defines a basic set of properties for NFTs on Flow. This tutorial, will teach you a basic method of creating an NFT to illustrate important language concepts. After completing the NFT tutorials, readers should visit the NFT standard github repository to learn how full, production-ready NFTs are created.

To get you comfortable using NFTs, this tutorial will teach you to:

  1. Deploy the NFT contract and type definitions.
  2. Create an NFT object and store it in your account storage.
  3. Create an NFT collection object to store multiple NFTs in your account.
  4. Create an NFTMinter and use it to mint an NFT.
  5. Create references to your collection that others can use to send you tokens.
  6. Set up another account the same way.
  7. Transfer an NFT from one account to another.
  8. Use a script to see what NFTs are stored in each account's collection.

It is important to remember that while this tutorial implements a working non-fungible token, it has been simplified for educational purposes and is not what any project should use in production. See the Flow Fungible Token standard for the standard interface and example implementation. Additionally, check out the Kitty Items Repo for a production ready version!

Before proceeding with this tutorial, we highly recommend following the instructions in Getting Started, Hello, World!, Resources, and Capabilities to learn how to use the Playground tools and to learn the fundamentals of Cadence. This tutorial will build on the concepts introduced in those tutorials.

Non-Fungible Tokens on the Flow Emulator


In Cadence, each NFT is represented by a resource with an integer ID. Resources are a perfect type to represent NFTs because resources have important ownership rules that are enforced by the type system. They can only have one owner, cannot be copied, and cannot be accidentally or maliciously lost or duplicated. These protections ensure that owners know that their NFT is safe and can represent an asset that has real value. For more information about resources, see the resources tutorial

An NFT is also usually represented by some sort of metadata like a name or a picture. Historically, most of this metadata has been stored off-chain, and the on-chain token only contains a URL or something similar that points to the off-chain metadata. In Flow, this is possible, but the goal is to make it possible for all the metadata associated with a token to be stored on-chain. This is out of the scope of this tutorial though. This paradigm has been defined by the Flow community and the details are contained in the NFT metadata proposal.

When users on Flow want to transact with each other, they can do so peer-to-peer and without having to interact with a central NFT contract by calling resource-defined methods in each users' account.

Adding an NFT Your Account

We'll start by looking at a basic NFT contract, that adds an NFT to an account. The contract will:

  1. Create a smart contract with the NFT resource type.
  2. Declare an ID field, a metadata field and an init() function in the NFT resource
  3. Create an init() function for the contract that saves an NFT to an account

This contract relies on the account storage API to save NFTs in the AuthAccount object.


First, you'll need to follow this link to open a playground session with the Non-Fungible Token contracts, transactions, and scripts pre-loaded:

https://play.onflow.org/ae2f2a83-6698-4e03-93cf-70d35627e28e

Open Account 0x01 to see BasicNFT.cdc. BasicNFT.cdc should contain the following code:

BasicNFT.cdc

_27
pub contract BasicNFT {
_27
_27
// Declare the NFT resource type
_27
pub resource NFT {
_27
// The unique ID that differentiates each NFT
_27
pub let id: UInt64
_27
_27
// String mapping to hold metadata
_27
pub var metadata: {String: String}
_27
_27
// Initialize both fields in the init function
_27
init(initID: UInt64) {
_27
self.id = initID
_27
self.metadata = {}
_27
}
_27
}
_27
_27
// Function to create a new NFT
_27
pub fun createNFT(id: UInt64): @NFT {
_27
return <-create NFT(initID: id)
_27
}
_27
_27
// Create a single new NFT and save it to account storage
_27
init() {
_27
self.account.save<@NFT>(<-create NFT(initID: 1), to: /storage/BasicNFTPath)
_27
}
_27
}

In the above contract, the NFT is a resource with an integer ID and a field for metadata.

Each NFT resource has a unique ID, so they cannot be combined or duplicated, unless the smart contract allows it.

Another unique feature of this design is that each NFT can contain its own metadata. In this example, we use a simple String-to-String mapping, but you could imagine a much more rich version that can allow the storage of complex file formats and other such data.

An NFT could even own other NFTs! This functionality is shown in the next tutorial.

In the contract's init function, we create a new NFT object and move it into the account storage.


_10
// put it in storage
_10
self.account.save<@NFT>(<-create NFT(initID: 1), to: /storage/BasicNFTPath)

Here we access the AuthAccount object on the account the contract is deployed to and call its save method, specifying @NFT as the type it is being saved as. We also create the NFT in the same line and pass it as the first argument to save. We save it to the /storage domain, where objects are meant to be stored.

Deploy NFTv1 by clicking the Deploy button in the top right of the editor.

You should now have an NFT in your account. Let's run a transaction to check.

Open the NFT Exists transaction, select account 0x01 as the only signer, and send the transaction.
NFT Exists should look like this:

NFTExists.cdc

_13
import BasicNFT from 0x01
_13
_13
// This transaction checks if an NFT exists in the storage of the given account
_13
// by trying to borrow from it. If the borrow succeeds (returns a non-nil value), the token exists!
_13
transaction {
_13
prepare(acct: AuthAccount) {
_13
if acct.borrow<&BasicNFT.NFT>(from: /storage/BasicNFTPath) != nil {
_13
log("The token exists!")
_13
} else {
_13
log("No token found!")
_13
}
_13
}
_13
}

Here, we are trying to directly borrow a reference from the NFT in storage. If the object exists, the borrow will succeed and the reference optional will not be nil, but if the borrow fails, the optional will be nil.

You should see something that says "The token exists!".

Great work! You have your first NFT in your account. Let's move it to another account!

Performing a Basic Transfer

With these powerful assets in your account, you'll probably want to move them around to other accounts. There are many ways to transfer objects in Cadence, but we'll show the simplest one first.

This will also be an opportunity for you to try to write some of your own code!

Open the Basic Transfer transaction.
Basic Transfer should look like this:


_13
import BasicNFT from 0x01
_13
_13
/// Basic transaction for two accounts to authorize
_13
/// to transfer an NFT
_13
_13
transaction {
_13
prepare(signer1: AuthAccount, signer2: AuthAccount) {
_13
_13
// Fill in code here to load the NFT from signer1
_13
// and save it into signer2's storage
_13
_13
}
_13
}

We've provided you with a blank transaction with two signers.

While a transaction is open, you can select one or more accounts to sign a transaction. This is because, in Flow, multiple accounts can sign the same transaction, giving access to their private storage. If multiple accounts are selected as signers, this needs to be reflected in the signature of the transaction to show multiple signers, as is shown in the "Basic Transfer" transaction.

All you need to do is load() the NFT from signer1's storage and save() it into signer2's storage. You have used both of these operations before, so this hopefully shouldn't be too hard to figure out. Feel free to go back to earlier tutorials to see examples of these account methods.

You can also scroll down a bit to see the correct code:










Here is the correct code to load the NFT from one account and save it to another account.


_17
import BasicNFT from 0x01
_17
_17
/// Basic transaction for two accounts to authorize
_17
/// to transfer an NFT
_17
_17
transaction {
_17
prepare(signer1: AuthAccount, signer2: AuthAccount) {
_17
_17
// Load the NFT from signer1's account
_17
let nft <- signer1.load<@BasicNFT.NFT>(from: /storage/BasicNFTPath)
_17
?? panic("Could not load NFT")
_17
_17
// Save the NFT to signer2's account
_17
signer2.save<@BasicNFT.NFT>(<-nft, to: /storage/BasicNFTPath)
_17
_17
}
_17
}

Select both Account 0x01 and Account 0x02 as the signers.
Click the "Send" button to send the transaction.

Now, the NFT should be stored in the storage of Account 0x02! You should be able to run the "NFT Exists" transaction again with 0x02 as the signer to confirm that it is in their account.

Enhancing the NFT Experience

Hopefully by now, you have an idea of how NFTs can be represented by resources in Cadence. You might have noticed by now that if we required users to remember different paths for each NFT and to use a multisig transaction for transfers, we would not have a very friendly developer and user experience.

This is where the true utility of Cadence is shown. Continue on to the next tutorial to find out how we can use capabilities and resources owning other resources to enhance the ease of use and safety of our NFTs.