Skip to main content

Creating an NFT collection

Intermediate
NFTs
Tutorial

Overview

An NFT collection is a series of non-fungible tokens that each have a unique identifier value. Each token's metadata may be the same, such as the name, image, and description, or each token's data can be unique and associated with a 'rarity' value, where some metadata values are less common than others. Rarity metadata attributes are common in generative NFT collections, often referring to an NFT collection where the artwork for each image has been randomly generated and some components only appear in a select few of the generated images.

For this basic NFT example, you will create a collection where each token has the same metadata values, with the only difference between each NFT being their unique token identifier value.

To create an NFT collection on ICP, it is recommended to use the ICRC-7 and ICRC-37 standards.

This example combines three standards: ICRC-3, ICRC-7, and ICRC-37:

  • ICRC-3: The standard for transaction logs and archives.

  • ICRC-7: The base NFT standard.

  • ICRC-37: An extension of ICRC-7 that enables an approve workflow. This allows owners of an NFT to allow another user to spend or transfer the NFT on their behalf.

Creating an NFT collection

Prerequisites

  • Download and install the IC SDK.

  • Download and install git.

Step 1: Download the NFT example 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

Step 2: Start the local replica.

Start dfx with the command:

dfx start --clean background

Step 3: Create new local identities.

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)

Step 4: Deploy the icrc7 canister.

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)

Step 5: 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:

import ICRC7 "mo:icrc7-mo";

module{
  public let defaultConfig = func(caller: Principal) : ICRC7.InitArgs{
      ?{
        symbol = ?"NBL";
        name = ?"NASA Nebulas";
        description = ?"A Collection of Nebulas Captured by NASA";
        logo = ?"https://www.nasa.gov/wp-content/themes/nasa/assets/images/nasa-logo.svg";
        supply_cap = null;
        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;
      };
  };
};%

Step 6: Interact with the canister by querying data about 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

("NASA Nebulas")
## Get the collection symbol:
dfx canister call icrc7 icrc7_symbol  --query

("NBL")
## Get the collection description:
dfx canister call icrc7 icrc7_description  --query

(opt "A Collection of Nebulas Captured by NASA")
## Get the collection logo:
dfx canister call icrc7 icrc7_logo  --query

(opt "https://www.nasa.gov/wp-content/themes/nasa/assets/images/nasa-logo.svg")

Step 7: Mint NFTs in the collection.

Execute the following canister call to mint 3 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://images-assets.nasa.gov/image/PIA18249/PIA18249~orig.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://images-assets.nasa.gov/image/GSFC_20171208_Archive_e001465/GSFC_20171208_Archive_e001465~orig.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://images-assets.nasa.gov/image/hubble-sees-the-wings-of-a-butterfly-the-twin-jet-nebula_20283986193_o/hubble-sees-the-wings-of-a-butterfly-the-twin-jet-nebula_20283986193_o~orig.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) };
  },
)

Step 8: 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 3:

(3 : 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})' --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;
    };
  },
)

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 }).

Step 9: Transfer tokens.

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;}})"

Step 10: Use the approve functionality from ICRC-37.

Next, use 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 }).

Step 11: Create a new identity for alice to transfer the token to.

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;}})"

Step 12: Confirm the transfer was successful.

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.

Step 13: Revoke the approval.

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 {}).

Step 14: 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