Skip to main content

5.4 Creating NFTs on ICP

Advanced
Tutorial

Overview

A non-fungible token, commonly referred to as an NFT, is a type of tokenized asset that is assigned a unique identifier that is used to distinguish one NFT from another. An NFT cannot be replicated or reproduced since it is cryptographically unique. A non-fungible token is a type of token that cannot be exchanged 1:1 with another token of the same type, as the value of the NFT token can vary. In comparison, a fungible token can always be transferred 1:1 for another token of the same type.

For example, 1 USD can always be exchanged for 1 USD. However, one unique painting cannot be exchanged for another, since the value of the two paintings will be different.

On the Internet Computer, ICP is a fungible token that can always be exchanged for ICP of equal value. However, an NFT token deployed on ICP cannot be traded 1:1 with another NFT token deployed on ICP.

NFTs have enabled a wide range of different use cases since the ownership of an NFT can be verified via the blockchain and cannot be spoofed or faked. Just a few use cases for NFTs include:

  • Buy, sell, or trade unique artwork or collectibles.

  • Buy digital assets, such as domain names or virtual worlds.

  • Exclusive memberships to gated content for users who purchase an NFT from a specific collection.

  • Proof of ownership for real-world tangible assets, such as property.

  • Proof of documents, such as an identification card or driver's license.

  • Monetization for content creators.

How do NFTs work?

Similar to fungible tokens, NFTs are created through a process known as minting. Minting is when a token is created on the blockchain by recording the token's data to the chain. When an NFT is minted on ICP, a canister smart contract is used to define the NFT's ownership data and metadata and provide the ability for the NFT to be transferred or sold to another user.

When an NFT is created, it is assigned a unique identifier that is used to distinguish the token from all other NFTs. Each token's data is public, including the ownership information, metadata information, and transaction history.

Some NFTs are '1 of 1' tokens, meaning they are globally unique and not part of a series or collection of NFTs. Other times, NFT tokens are '1 of 100' or '1 of 500', meaning they are part of a larger series that several people can purchase and retain the same benefits. '1 of 1' NFTs are often used in cases of unique art or identifying documents, while '1 of 100' NFTs are often used for providing membership to exclusive content or providing tickets to an event.

Even if 5_000 NFTs of the same exact item are minted, each token will have a unique identifier to define it among the others in the series. In some cases, each NFT in the collection will have a unique image to identify it visually; other times, each NFT will be exactly the same except for the unique token ID.

NFT standards

Similar to token standards, such as the ICRC-1 and ICRC-2 fungible token standards, NFTs are required to use a standard that sets the guidelines for API methods that support necessary NFT functionalities. Some common API methods for NFTs are the ability to mint an NFT, transfer the NFT, and query the NFT's metadata.

Currently on ICP, there are two NFT standards: ICRC-7 and ICRC-37.

ICRC-7

ICRC-7 is a new standard for non-fungible tokens on ICP. Recall that "ICRC" stands for "Internet Computer Request for Comments" and is the standard created by the Internet Computer working group. An ICRC standard can be used for creating anything on ICP, not just fungible tokens such as the ICRC-1 and ICRC-2 token standards.

The ICRC-7 standard is designed to be a minimal standard for allowing an NFT collection to be deployed on ICP. In an NFT collection, each NFT may have unique metadata information. This metadata may include a unique image, traits or tags, or a description describing the NFT.

Read more about the ICRC-7 standard in the specification document.

ICRC-37

ICRC-37 is an extension of the ICRC-7 standard that adds functionality for an approve workflow. The approve workflow can be used to allow other identities to spend or transfer NFTs on your behalf, a workflow that is popular in the Ethereum ecosystem through the ERC-20 token standard.

Read more about the ICRC-37 standard in the specification document.

Previously used standards

The DIP721 NFT standard was designed as the ICP adaptation of the ERC-721 non-fungible token standard. It has since been replaced by the ICRC-7 and ICRC-37 standards, but is still used by some projects.

View the example project for the DIP721 standard, and read the full standard specification document.

Creating and deploying an NFT collection

In this tutorial, you'll create an NFT collection using the ICRC-7, ICRC-37, and ICRC-3 standards. ICRC-3 defines a standard for transaction logs and archives.

This example project uses the Motoko Mops packages for ICRC-3, ICRC-7, and ICRC-37 to provide functionality for each standard. The project uses a single canister, 'icrc7', to deploy the collection and references default configuration options for each standard in the example/initial_state/icrc3.mo, example/initial_state/icrc7.mo, and example/initial_state/icrc37.mo files.

This collection will contain 5 NFTs that each use a different image stored in their metadata with their own unique NFT identifier.

Prerequisites

Before you start, verify that you have set up your developer environment according to the instructions in 0.3 Developer environment setup.

Creating an NFT collection

Prerequisites

  • Download and install the IC SDK.

  • Download and install git.

Setting up the project

Open a terminal window and run the following commands to download the NFT example project and navigate inside the project's directory:

git clone https://github.com/PanIndustrial-Org/icrc_nft.mo.git
cd icrc_nft.mo

Next, start dfx with the command:

dfx start --clean background

Then, create two new identities to be used with the NFT canister: alice and icrc7_deployer. The alice identity will be used to demonstrate the approve workflow, and icrc7_deployer will be used as the canister's admin controller.

dfx identity new alice
dfx identity use alice
ALICE_PRINCIPAL=$(dfx identity get-principal)

dfx identity new icrc7_deployer
dfx identity use icrc7_deployer
ADMIN_PRINCIPAL=$(dfx identity get-principal)

Deploy the icrc7 canister

To configure the settings of your NFT collection, replace the existing content in example/initial_state/icrc7.mo with your own configuration, such as:

example/initial_state/icrc7.mo
import ICRC7 "mo:icrc7-mo";

module{
  public let defaultConfig = func(caller: Principal) : ICRC7.InitArgs{
      ?{
        symbol = ?"DEVS";
        name = ?"Developer Journey Astronauts";
        description = ?"ICP Developer Journey Astronauts";
        logo = ?"https://internetcomputer.org/img/ethdenver/astronaut.webp";
        supply_cap = ?50;
        allow_transfers = null;
        max_query_batch_size = ?100;
        max_update_batch_size = ?100;
        default_take_value = ?1000;
        max_take_value = ?10000;
        max_memo_size = ?512;
        permitted_drift = null;
        tx_window = null;
        burn_account = null; //burned nfts are deleted
        deployer = caller;
        supported_standards = null;
      };
  };
};

This configuration sets the following parameters for your collection:

  • symbol: The NFT collection's text symbol.

  • name: The NFT collection's full name.

  • description: A description describing the collection.

  • logo: The image used to represent the collection.

  • supply_cap: The total amount of NFTs to be minted from this collection.

  • burn_account: The account identifier for any burned NFTs to be transferred to. If set as null, burned NFTs will be deleted.

  • deployer: The account to be set as the collection's deployer. In this example, it is set as the caller of this method.

Run the dfx deploy command with the following init arguments:

dfx deploy icrc7 --argument 'record {icrc7_args = null; icrc37_args =null; icrc3_args =null;}' --mode reinstall

Then, export the canister's ID as an environment variable:

ICRC7_CANISTER=$(dfx canister id icrc7)

Call the canister's init function to use the canister.

dfx canister call icrc7 init

This command will initialize an NFT collection using the parameters set in the project's example/initial_state/icrc7.mo file.

Interact with the NFT collection

To verify the NFT collection has been initialized correctly, query some information about it, such as the name, symbol, and description:

## Get collection name:
dfx canister call icrc7 icrc7_name  --query

("Developer Journey Astronauts")

## Get the collection symbol:
dfx canister call icrc7 icrc7_symbol  --query

("DEVS")

## Get the collection description:
dfx canister call icrc7 icrc7_description  --query

(opt "ICP Developer Journey Astronauts")

## Get the collection logo:
dfx canister call icrc7 icrc7_logo  --query

(opt "https://internetcomputer.org/img/ethdenver/astronaut.webp")

Mint NFTs in the collection

Execute the following canister call to mint 5 NFTs in the collection:

dfx canister call icrc7 icrcX_mint "(
  vec {
    record {
      token_id = 0 : nat;
      owner = opt record { owner = principal \"$ICRC7_CANISTER\"; subaccount = null;};
      metadata = variant {
        Class = vec {
          record {
            value = variant {
              Text = \"https://i.ytimg.com/vi/oBUpJ4CqmN0/maxresdefault.jpg\"
            };
            name = \"icrc7:metadata:uri:image\";
            immutable = true;
          };
        }
      };
      memo = opt blob \"\00\01\";
      override = true;
      created_at_time = null;
    };
    record {
      token_id = 1 : nat;
      owner = opt record { owner = principal \"$ICRC7_CANISTER\"; subaccount = null;};
      metadata = variant {
        Class = vec {
          record {
            value = variant {
              Text = \"https://i.ytimg.com/vi/3WpP8ux1zX0/sddefault.jpg\"
            };
            name = \"icrc7:metadata:uri:image\";
            immutable = true;
          };
        }
      };
      memo = opt blob \"\00\01\";
      override = true;
      created_at_time = null;
    };
    record {
      token_id = 2 : nat;
      owner = opt record { owner = principal \"$ICRC7_CANISTER\"; subaccount = null;};
      metadata = variant {
        Class = vec {
          record {
            value = variant {
              Text = \"https://i.ytimg.com/vi/fDMHUdo7m-k/maxresdefault.jpg\"
            };
            name = \"icrc7:metadata:uri:image\";
            immutable = true;
          };
        }
      };
      memo = opt blob \"\00\01\";
      override = true;
      created_at_time = null;
    };
 record {
      token_id = 3 : nat;
      owner = opt record { owner = principal \"$ICRC7_CANISTER\"; subaccount = null;};
      metadata = variant {
        Class = vec {
          record {
            value = variant {
              Text = \"https://i.ytimg.com/vi/mwbRRk9T5Nw/maxresdefault.jpg\"
            };
            name = \"icrc7:metadata:uri:image\";
            immutable = true;
          };
        }
      };
      memo = opt blob \"\00\01\";
      override = true;
      created_at_time = null;
    };
 record {
      token_id = 4 : nat;
      owner = opt record { owner = principal \"$ICRC7_CANISTER\"; subaccount = null;};
      metadata = variant {
        Class = vec {
          record {
            value = variant {
              Text = \"https://i.ytimg.com/vi/sZxbRAwYYMw/hqdefault.jpg\"
            };
            name = \"icrc7:metadata:uri:image\";
            immutable = true;
          };
        }
      };
      memo = opt blob \"\00\01\";
      override = true;
      created_at_time = null;
    };
  },
)"

If successful, you will receive the output:

(
  vec {
    variant { Ok = opt (1 : nat) };
    variant { Ok = opt (2 : nat) };
    variant { Ok = opt (3 : nat) };
    variant { Ok = opt (4 : nat) };
    variant { Ok = opt (5 : nat) };
  },
)

Query information about the minted tokens

To verify that the NFTs have been minted properly, query some information about the tokens:

## Get the total supply:
dfx canister call icrc7 icrc7_total_supply  --query

This should return the number of tokens that you minted. In this example, that should be 5:

(5 : nat)
## Get supported standards:
dfx canister call icrc7 icrc10_supported_standards  --query

This will return all supported token standards, in this case, ICRC-7 and ICRC-37:

(
  vec {
    record {
      url = "https://github.com/dfinity/ICRC/ICRCs/ICRC-7";
      name = "ICRC-7";
    };
    record {
      url = "https://github.com/dfinity/ICRC/ICRCs/ICRC-37";
      name = "ICRC-37";
    };
  },
)
## List the owner of all tokens:
dfx canister call icrc7 icrc7_owner_of '(vec {0;1;2;3;4})' --query

(
  vec {
    opt record {
      owner = principal "be2us-64aaa-aaaaa-qaabq-cai";
      subaccount = null;
    };
    opt record {
      owner = principal "be2us-64aaa-aaaaa-qaabq-cai";
      subaccount = null;
    };
    opt record {
      owner = principal "be2us-64aaa-aaaaa-qaabq-cai";
      subaccount = null;
    };
    opt record {
      owner = principal "be2us-64aaa-aaaaa-qaabq-cai";
      subaccount = null;
    };
    opt record {
      owner = principal "be2us-64aaa-aaaaa-qaabq-cai";
      subaccount = null;
    };
  },
)

You can see that the canister's principal owns all of the minted tokens. To see if your admin principal is approved to spend tokens, make the following canister call:

dfx canister call icrc7 icrc37_is_approved "(vec{record { spender=record {owner = principal \"$ADMIN_PRINCIPAL\"; subaccount = null;}; from_subaccount=null; token_id=0;}})" --query

This should return (vec { true }).

Transfer NFTs

Transfer ownership of token 0 from the canister's principal to your admin principal using the canister call:

dfx canister call icrc7 icrc37_transfer_from "(vec{record {
  spender = principal \"$ADMIN_PRINCIPAL\";
  from = record { owner = principal \"$ICRC7_CANISTER\"; subaccount = null};
  to = record { owner = principal \"$ADMIN_PRINCIPAL\"; subaccount = null};
  token_id =  0 : nat;
  memo = null;
  created_at_time = null;}})"

Next, use the approve functionality from ICRC-37 by calling the icrc37_approve_tokens canister method to approve the alice identity to spend token 0:

dfx canister call icrc7 icrc37_approve_tokens "(vec {record { token_id=0; approval_info= record {from_subaccount = null; spender = record {owner = principal \"$ALICE_PRINCIPAL\"; subaccount = null}; memo = null; expires_at = null; created_at_time = null }}})"

Then, confirm that it was set correctly by calling the icrc37_is_approved method:

dfx canister call icrc7 icrc37_is_approved "(vec { record {spender= record { owner = principal \"$ALICE_PRINCIPAL\"; subaccount = null;}; from_subaccount=null; token_id=0}})" --query

This should return (vec { true }).

Next, create a bob identity for alice to transfer token 0 to:

dfx identity new bob
dfx identity use bob
BOB_PRINCIPAL=$(dfx identity get-principal)

Then, switch back to alice and transfer the token to bob:

dfx identity use alice
dfx canister call icrc7 icrc37_transfer_from "(vec {record {
  spender = principal \"$ALICE_PRINCIPAL\";
  from = record { owner = principal \"$ADMIN_PRINCIPAL\"; subaccount = null};
  to = record { owner = principal \"$BOB_PRINCIPAL\"; subaccount = null};
  token_id = 0 : nat;
  memo = null;
  created_at_time = null;}})"

Confirm that the token transfer was successful by echoing bob's principal, then viewing the principal that owns token 0:

echo $BOB_PRINCIPAL
dfx canister call icrc7 icrc7_owner_of '(vec {0})' --query

The two principal IDs should match.

You can revoke the approval with the command:

dfx canister call icrc7 icrc37_revoke_collection_approvals "(vec {record {
  from_subaccount = null;
  spender = null;
  memo = null;
  created_at_time = null;
}})"

Then confirm that it was revoked properly:

dfx canister call icrc7 icrc37_get_token_approvals "(vec { 0;},null,null)" --query

This should return (vec {}).

Get the full transaction log

To view all transactions with the NFT collection, run the command:

dfx canister call icrc7 icrc3_get_blocks "(vec {record {start =0; length = 1000}})" --query

This will return long output, such as:

(
  record {
    log_length = 8 : nat;
    blocks = vec {
      record {
        id = 0 : nat;
        block = variant {
          Map = vec {
            record {
              "tx";
              variant {
                Map = vec {
                  record { "op"; variant { Text = "37approve_coll" } };
                  record {
                    "from";
                    variant {
                      Array = vec { variant { Blob = blob "\80\00\00\00\00\10\00\03\01\01" };}
                    };
                  };
                  record {
                    "spender";
                    variant {
                      Array = vec { variant { Blob = blob "\f6\90\e7\6a\1f\f0\9b\f0\67\11\5e\47\f9\fc\c2\7d\d5\3f\cc\64\e4\bb\5e\7a\be\fc\7f\03\02" };}
                    };
                  };
                }
              };
            };
            record { "btype"; variant { Text = "37approve_coll" } };
            record { "ts"; variant { Nat = 1_719_262_528_485_678_000 : nat } };
          }
        };
      };
      record {
        id = 1 : nat;
        block = variant {
          Map = vec {
            record {
              "phash";
              variant {
                Blob = blob "\6c\c9\c8\d4\a2\f5\e9\f1\09\25\0b\af\0f\fc\d4\01\a8\47\57\57\a8\01\e1\1e\ca\01\ba\e7\64\1c\52\2b"
              };
            };
            record {
              "tx";
              variant {
                Map = vec {
                  record { "memo"; variant { Blob = blob "\00\01" } };
                  record { "tid"; variant { Nat = 0 : nat } };
                  record { "op"; variant { Text = "mint" } };
                  record {
                    "meta";
                    variant {
                      Map = vec { record { "icrc7:token_metadata"; variant { Map = vec { record { "icrc7:metadata:uri:image"; variant { Text = "https://images-assets.nasa.gov/image/PIA18249/PIA18249~orig.jpg" };};} };};}
                    };
                  };
                  record {
                    "to";
                    variant {
                      Array = vec { variant { Blob = blob "\80\00\00\00\00\10\00\03\01\01" };}
                    };
                ...

Resources

Need help?

Did you get stuck somewhere in this tutorial, or feel like you need additional help understanding some of the concepts? The ICP community has several resources available for developers, like working groups and bootcamps, along with our Discord community, forum, and events such as hackathons. Here are a few to check out:

Next steps