Basic NFT
In this tutorial, we're going to deploy, store, and transfer Non-Fungible Tokens (NFTs). The NFT is an integral part of blockchain technology. An NFT is a digital asset that represents ownership of something unique and indivisible. Unlike fungible tokens, which operate more like money, you can't divide an NFT, and the owner is likely to be upset if you were to swap one for another without their consent. Examples of NFTs include: CryptoKitties, Top Shot Moments, tickets to a really fun concert, or even real property such as a horse or a house!
Production-quality NFTs on Flow implement the Flow NFT Standard, which defines a basic set of properties for NFTs on Flow.
This tutorial teaches you a basic method of creating simple NFTs to illustrate important language concepts, but will not use the full NFT Standard for the sake of simplicity.
If you're already comfortable with Cadence and have found this page looking for information on how to build production-ready NFTs, check out the NFT Guide and Flow NFT Standard repository.
Objectives
After completing this tutorial, you'll be able to:
- Deploy a basic NFT contract and type definitions.
- Create an NFT object and store it in a user's account storage.
- Write a transaction to mint an NFT and create a capability so others can view it.
- Transfer an NFT from one account to another.
- Use a script to see if an NFT is stored in an account.
- Implement and utilize a dictionary in Cadence.
NFTs on Cadence
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 strategy is a response to the lessons learned by the Flow team (the Chief Architect of Flow is the original proposer and co-author of the ERC-721 NFT standard).
It 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, and helps prevent developers from breaking this trust with easy-to-make programming mistakes.
When users on Flow want to transact with each other, they can do so peer-to-peer, without having to interact with a central NFT contract, by calling resource-defined methods in both users' accounts.
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: they can be used in different smart contracts and app contexts in an account.
The simplest possible NFT
Open the starter code for this tutorial in the Flow Playground: play.flow.com/ea3aadb6-1ce6-4734-9792-e8fd334af7dc.
At their core, NFTs are simply a way to create true ownership of unique digital property. The simplest possible implementation is a resource with a unique id number.
Implement a simple NFT by creating a resource with a constant id
that is assigned in init
. The id
should be public:
_10access(all) resource NFT {_10_10 access(all) let id: UInt64_10_10 init(initID: UInt64) {_10 self.id = initID_10 }_10}
Adding basic metadata
An NFT is also usually expected to include some metadata like a name, description, traits, 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.
This practice was necessary due to the original costs of doing anything onchain, but it created the illusion that the actual content of an NFT was permanent and onchain. Unfortunately, the metadata and images for many older NFT collections can vanish (and sadly, sometimes have vanished) at any time.
In Flow, storing this data offchain is possible, but you can—and normally should—store all the metadata associated with a token directly on-chain. Unlike many other blockchain networks, you do not need to consider string storage or manipulation as particularly expensive.
This tutorial describes a simplified implementation. Check out the the NFT metadata guide if you want to learn how to do this in production.
Add a public metadata
variable to your NFT. For now, it can be a simple String
-to-String
dictionary. Update the init
to also initialize a description
in your metadata. It should now look similar to:
_10access(all) resource NFT {_10 access(all) let id: UInt64_10 access(all) var metadata: {String: String}_10_10 init(initID: UInt64, initDescription: String) {_10 self.id = initID_10 self.metadata = {"description": initDescription}_10 }_10}
Creating the NFT
As with any complex type in any language, now that you've created the definition for the type, you need to add a way to instantiate new instances of that type, since these instances are the NFTs. This simple NFT type must be initialized with an id number and a String
description.
Traditionally, NFTs are provided with id numbers that indicate the order in which they were minted. To handle this, you can use a simple counter:
- Add a public, contract-level field to keep track of the last assigned id number.
_10access(contract) var counter: UInt64
- You're going to immediately get an error in the editor with
counter
. - Contract-level fields must be initialized in the
init
function.
- You're going to immediately get an error in the editor with
- Add an
init
function to theBasicNFT
contract and initializecounter
to zero:_10init() {_10self.counter = 0_10} - Add a public function to increment the counter and
create
andreturn
anNFT
with a provided description.
We're creating a public function that allows anyone to provide any string. Take care when building real apps that will be exposed to humanity.
_10access(all) fun mintNFT(description: String): @NFT {_10 self.counter = self.counter + 1_10 return <- create NFT(initID: self.counter, initDescription: description)_10}
Remember, when you work with a resource, you must use the move operator (<-
) to move it from one location to another.
Adding an NFT to your account
You've gone through the trouble of creating this NFT contract — you deserve the first NFT!
Protect yourself from snipers by updating the init
function to give yourself the first NFT
. You'll need to mint it and save it to your account storage:
_10self_10 .account_10 .storage_10 .save(<-self.mintNFT(description: "First one for me!"), to: /storage/BasicNFTPath)
NFT capability
Saving the NFT to your account will give you one, but it will be locked away where no apps can see or access it. Since you've just learned how to create capabilities in the previous tutorial, you can use the same techniques here to create a capability to give others the ability to access the NFT.
In Cadence, users own and control their data. A user can destroy a capability such as this whenever they choose. If you want complete control over NFTs or other data, you'd need to store it directly in the contract.
Most of the time, you probably won't want to do this because it will limit what your users can do with their own property without your permission. You don't want to end up in a situation where your users would buy more of your umbrellas to use for shade on sunny days, except you've made it so that they only open when it's raining.
Cadence contracts are deployed to the account of the deployer. As a result, the contract is in the deployer's storage, and the contract itself has read and write access to the storage of the account that they are deployed to by using the built-in self.account
field. This is an account reference (&Account
), authorized and entitled to access and manage all aspects of the account, such as account storage, capabilities, keys, and contracts.
You can access any of the account functions with self.account
by updating the init
function to create and publish a capability allowing public access to the NFT:
_10let capability = self_10 .account_10 .capabilities_10 .storage_10 .issue<&NFT>(/storage/BasicNFTPath)_10_10self_10 .account_10 .capabilities_10 .publish(capability, at: /public/BasicNFTPath)
The capability you are creating gives everyone full access to all properties of the resource. It does not allow other users or developers to move or destroy the resource and is thus harmless.
However, if the resource contained functions to mutate data within the token, this capability would allow anyone to call it and mutate the data!
You might be tempted to add this code to mintNFT
so that you can reuse it for anyone who wants to mint the NFT and automatically create the related capability.
The code will work, but it will not function the way you're probably expecting it to. In the context of being called from a function inside a contract, self.account
refers to the account of the contract deployer, not the caller of the function. That's you!
Adding self.account.save
or self.account.publish
to mintNFT
allows anyone to attempt to mint and publish capabilities to your account, so don't do it!
Passing a fully authorized account reference as a function parameter is a dangerous anti-pattern.
Deploying and testing
Deploy the contract and check the storage for account 0x06
.
You'll be able to find your NFT in the storage for 0x06
:
_40"value": {_40 "value": {_40 "id": "A.0000000000000006.BasicNFT.NFT",_40 "fields": [_40 {_40 "value": {_40 "value": "41781441855488",_40 "type": "UInt64"_40 },_40 "name": "uuid"_40 },_40 {_40 "value": {_40 "value": "1",_40 "type": "UInt64"_40 },_40 "name": "id"_40 },_40 {_40 "value": {_40 "value": [_40 {_40 "key": {_40 "value": "description",_40 "type": "String"_40 },_40 "value": {_40 "value": "First one for me!",_40 "type": "String"_40 }_40 }_40 ],_40 "type": "Dictionary"_40 },_40 "name": "metadata"_40 }_40 ]_40 },_40 "type": "Resource"_40}
Getting the number of an NFT owned by a user
We can see the NFT from the storage view for each account, but it's much more useful to write a script or transaction that can do that for any account. You can follow a similar technique as the last tutorial and create a script to use the capability.
Add a script called GetNFTNumber
that returns the id number of the NFT owned by an address. It should accept the Address
of the account you wish to check as an argument.
Try to do this on your own. You should end up with something similar to:
_12import BasicNFT from 0x06_12_12access(all) fun main(address: Address): UInt64 {_12 let account = getAccount(address)_12_12 let nftReference = account_12 .capabilities_12 .borrow<&BasicNFT.NFT>(/public/BasicNFTPath)_12 ?? panic("Could not borrow a reference\n")_12_12 return nftReference.id_12}
Minting with a transaction
You usually don't want a contract with just one NFT given to the account holder. One strategy is to allow anyone who wants to mint an NFT. To do this, you can simply create a transaction that calls the mintNFT
function you added to your contract, and adds the capability for others to view the NFT:
- Create a transaction called
MintNFT.cdc
that mints an NFT for the caller with thedescription
they provide. You'll need entitlements to borrow values, save values, and issue and publish capabilities. - Verify that the NFT isn't already stored in the location used by the contract:
- Use the
mintNFT
function to create an NFT, and then save that NFT in the user's account storage:_10account.storage_10.save(<-BasicNFT.mintNFT(description: "Hi there!"), to: /storage/BasicNFTPath) - Create and publish a capability to access the NFT:
_10let capability = account_10.capabilities_10.storage_10.issue<&BasicNFT.NFT>(/storage/BasicNFTPath)_10_10account_10.capabilities_10.publish(capability, at: /public/BasicNFTPath)
- Call the
MintNFT
transaction from account0x06
.- It will fail because you minted an NFT with
0x06
when you deployed the contract.
- It will fail because you minted an NFT with
- Call
MintNFT
from account0x07
. Then,Execute
theGetNFTNumber
script for account0x07
.
You'll see the NFT number 2
returned in the log.
Performing a basic transfer
Users, independently or with the help of other developers, have the inherent ability to delete or transfer any resources in their accounts, including those created by your contracts. To perform a basic transfer:
- Open the
Basic Transfer
transaction. We've stubbed out the beginnings of a transfer transaction for you. Note that we're preparing account references for not one, but two accounts: the sender and the recipient.- 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.
- Write a transaction to execute the transfer. You'll need to
load()
the NFT fromsigner1
's storage andsave()
it intosigner2
's storage:_14import BasicNFT from 0x06_14_14transaction {_14prepare(_14signer1: auth(LoadValue) &Account,_14signer2: auth(SaveValue) &Account_14) {_14let nft <- signer1.storage.load<@BasicNFT.NFT>(from: /storage/BasicNFTPath)_14?? panic("Could not load NFT from the first signer's storage")_14_14// WARNING: Incomplete code, see below_14signer2.storage.save(<-nft, to: /storage/BasicNFTPath)_14}_14} - Select both account
0x06
and account0x08
as the signers. Make sure account0x06
is the first signer. - Click the
Send
button to send the transaction. - Verify the NFT is in account storage for account
0x08
. - Run the
GetNFTNumber
script to check account0x08
to see if a user has an NFT.- You'll get an error here. The reason is that you haven't created or published the capability on account
0x08
to access and return the id number of the NFT owned by that account. You can do this as a part of your transaction, but remember that it isn't required. Another dev, or sophisticated user, could do the transfer without publishing a capability.
- You'll get an error here. The reason is that you haven't created or published the capability on account
- On your own, refactor your transaction to publish a capability in the new owner's account.
- You're also not making sure that the recipient doesn't already have an NFT in the storage location, so go ahead and add that check as well.
You should end up with something similar to:
_29import BasicNFT from 0x06_29_29transaction {_29 prepare(_29 signer1: auth(LoadValue) &Account,_29 signer2: auth(_29 SaveValue,_29 IssueStorageCapabilityController,_29 PublishCapability) &Account_29 ) {_29 let nft <- signer1.storage.load<@BasicNFT.NFT>(from: /storage/BasicNFTPath)_29 ?? panic("Could not load NFT from the first signer's storage")_29_29 if signer2.storage.check<&BasicNFT.NFT>(from: /storage/BasicNFTPath) {_29 panic("The recipient already has an NFT")_29 }_29_29 signer2.storage.save(<-nft, to: /storage/BasicNFTPath)_29_29 let capability = signer2_29 .capabilities_29 .storage_29 .issue<&BasicNFT.NFT>(/storage/BasicNFTPath)_29_29 signer2_29 .capabilities_29 .publish(capability, at: /public/BasicNFTPath)_29 }_29}
Capabilities referencing moved objects
What about the capability you published for account 0x06
to access the NFT? What happens to that?
Run GetNFTNumber
for account 0x06
to find out.
You'll get an error here as well, but this is expected. Capabilities that reference an object in storage return nil
if that storage path is empty.
The capability itself is not deleted. If you move an object of the same type back to the storage location reference by the capability, the capability will function again.
Reviewing Basic NFTs
In this tutorial, you learned how to create a basic NFT with minimal functionality. Your NFT can be held, viewed, and transferred, though it does not adhere to the official standard, doesn't allow anyone to own more than one, and is missing other features.
Now that you have completed the tutorial, you should be able to:
- Deploy a basic NFT contract and type definitions.
- Create an NFT object and store it in a user's account storage.
- Write a transaction to mint an NFT and create a capability so others can view it.
- Transfer an NFT from one account to another.
- Use a script to see if an NFT is stored in an account.
In the next tutorial, you'll learn how to make more complete NFTs that allow each user to possess many NFTs from your collection.
Reference Solution
You are not saving time by skipping the reference implementation. You'll learn much faster by doing the tutorials as presented!
Reference solutions are functional, but may not be optimal.