Non-fungible resources

On this page, you will learn how to create non-fungible resources using the ResourceManager. But first, what does it mean for a resource to be non-fungible? Resources are considered non-fungible when each of its minted tokens is uniquely addressable (it has an ID) and cannot be split into fractions when moving them. Because of these two characteristics, we can attach metadata to individual minted non-fungible tokens which is very useful in many use cases.

Creating a NFT on Radix

The first step of creating a NFT with Scrypto is to define the fields that will be present in the individual metadata of each minted non-fungible token. The process is similar to how you define the variables that are present the on component state when writing a Blueprint. In the following example, we want to create a non-fungible resource that represents tickets to a basketball game. We define the fields that will be present on the ticket metadata by creating a struct outside of our #[blueprint] mod nftblueprint macro like so:

#[derive(ScryptoSbor, NonFungibleData)]
struct GameData {
  team_one: String,
  team_two: String,
  section: String,
  seat_number: u16
}

Each minted game ticket NFT will have metadata attached to it containing these fields. Notice the line we wrote before defining the struct: #[derive(ScryptoSbor, NonFungibleData)]. This macro adds, at compile time, methods to the struct to make it compatible with the Radix Engine and to allow us to use this structure as metadata for our NFTs.

Defining the NFT resource with the ResourceBuilder

The second step for creating our game ticket NFT, is to use the ResourceBuilder to create our resource on the network. This time, instead of calling ResourceBuilder::new_fungible() like we did on the previous page, creating NFTs has a few nuances. Because NFTs have individual unique ID, when we first create an NFT resource, we have to first specify the type of the NonFungibleLocalId. This means there are a few functions offered by the ResourceBuilder to specify the types of NFT we can create:

  • ResourceBuilder::new_string_non_fungible::<GameData>(owner_role)

  • ResourceBuilder::new_integer_non_fungible::<GameData>(owner_role)

  • ResourceBuilder::new_ruid_non_fungible::<GameData>(owner_role)

  • ResourceBuilder::new_bytes_non_fungible::<GameData>(owner_role)

In addition to specifying the non-fungible local ID type, we also need to specify the non-fungible data that we will use for our non-fungible resource. Since we created the custom struct GameData to be our data; we will use that.

For this example, we will want to specify our individual non-fungibles with integer IDs like so: ResourceBuilder::new_integer_non_fungible::<GameData>(OwnerRole::None)

#[derive(ScryptoSbor, NonFungibleData)]
struct GameData {
  team_one: String,
  team_two: String,
  section: String,
  seat_number: u16
}

#[blueprint]
mod nftblueprint {
    struct NftBlueprint {
    }

    impl NftBlueprint {
        pub fn create_game_nfts() {
            ResourceBuilder::new_integer_non_fungible::<GameData>(OwnerRole::None)
                .metadata(metadata! {
                     init {
                       "name" => "Mavs vs Lakers - 12/25/2023", locked;
                       "description" => "Tickets to the 2023 season of the Dallas Mavericks", locked;
                     }
                 )
                .create_with_no_initial_supply();
        }
    }
}

Unlike the new_fungible(OwnerRole) function, new_integer_non_fungible(OwnerRole) defines the ID type of our NFT as an integer and its data. Therefore, every minted token of this resource, will have a unique identifier with an integer type and data from GameData.

Non Fungible ID types

Listed in the following table are the available types we can assign to NonFungibleLocalId which you can specify:

Type

Description

NonFungibleLocalIdType::String

Represent the ID of the tokens using a String

NonFungibleLocalIdType::Integer

Represent the ID of the tokens using unsigned integers up to u64.

NonFungibleIdLocalType::RUID

Represent the ID of the tokens using a u128 number

NonFungibleLocalIdType::Bytes

Represent the ID of the tokens with a vector of bytes.

Minting an initial supply of ticket NFTs

Creating a non-fungible resource with an initial supply is a bit different from the way you would do with a fungible resource as instead of only specifying a number of tokens, we have to provide an ID and the data for each individual token. In the case of non-fungible resources, the mint_initial_supply method takes a vector of tuples where each tuple contains the ID and the data. Here is an example:

pub fn create_game_nfts() -> Bucket {
    ResourceBuilder::new_integer_non_fungible::<GameData>()
         .metadata(metadata! {
              init {
                 "name" => "Mavs vs Lakers - 12/25/2023", locked;
                 "description" => "Tickets to the 2023 season of the Dallas Mavericks", locked;
            }
         )
        .mint_initial_supply(vec![
            (NonFungibleLocalId::Integer(1), GameData{ team_one: "Mavs".to_string(), team_two: "Lakers".to_string(), section: "A".to_string(), seat_number: 1 }),
            (NonFungibleLocalId::Integer(2), GameData{ team_one: "Mavs".to_string(), team_two: "Lakers".to_string(), section: "A".to_string(), seat_number: 2 }),
            (NonFungibleLocalId::Integer(3), GameData{ team_one: "Mavs".to_string(), team_two: "Lakers".to_string(), section: "A".to_string(), seat_number: 3 })
        ]
    );
}

Here, we are initializing our resource with three tickets. Notice that the mint_initial_supply method returns a Bucket and we are returning it back to the caller for them to decide what to do with it.

The only exception to this is the NonFungibleLocalId::RUID type. With RUID types, we do not need to manually specify the NonFungibleLocalId. The engine will randomly generate a NonFungibleLocalId for us to prevent us from accidentally creating multiple NonFungibleLocalId of the same value. Remember that each non-fungibles need to be unique!

Mutable Fields

By default, all the fields you specify in the NFT data struct are not mutable. Once we attach metadata to our ticket NFTs, we cannot change it. We will go through an example showing you how to define mutable fields and how to update them after a NFT have been minted.

Let’s add a boolean used field to our GameData structure that can be mutated. When the tickets are first minted, its value will be set to false and once the ticket has been presented to enter the game, we will update it to true.

#[derive(ScryptoSbor, NonFungibleData)]
struct GameData {
  team_one: String,
  team_two: String,
  section: String,
  seat_number: u16,

  #[mutable]
  used: bool
}

As you can see, to mark a field as mutable you just have to prepend the #[mutable] line in front of it.

The next step is to define the access rule to authorize an actor to update our resource fields:

pub fn create_game_nfts() -> Bucket {
    ResourceBuilder::new_integer_non_fungible::<GameData>()
         .metadata(metadata! {
              init {
                 "name" => "Mavs vs Lakers - 12/25/2023", locked;
                 "description" => "Tickets to the 2023 season of the Dallas Mavericks", locked;
            }
         )
        // Here we are allowing anyone (AllowAll) to update the NFT metadata.
        // The second parameter (DenyAll) specifies that no one can update this rule.
        .non_fungible_data_roles(non_fungible_data_roles!(
              non_fungible_data_updater => AccessRule::AllowAll
              non_fungible_data_updaer_updater => AccessRule::DenyAll
        ))
        .mint_initial_supply(vec![
            (NonFungibleLocalId::Integer(1), GameData{ team_one: "Mavs".to_string(), team_two: "Lakers".to_string(), section: "A".to_string(), seat_number: 1 }),
            (NonFungibleLocalId::Integer(2), GameData{ team_one: "Mavs".to_string(), team_two: "Lakers".to_string(), section: "A".to_string(), seat_number: 2 }),
            (NonFungibleLocalId::Integer(3), GameData{ team_one: "Mavs".to_string(), team_two: "Lakers".to_string(), section: "A".to_string(), seat_number: 3 })
        ]
    );
}

To keep this example simple, we are allowing anyone (AccessRule::AllowAll) to update the NFT metadata. To learn how to work with more complex authorization rules, read this page on authorization.

Using the ResourceManager of our game ticket resource, we can then update the used field on minted NFTs like so:

#[derive(ScryptoSbor, NonFungibleData)]
struct GameData {
  team_one: String,
  team_two: String,
  section: String,
  seat_number: u16,

  #[mutable]
  used: bool
}

#[blueprint]
mod nftblueprint {
    struct NftBlueprint {
        ticket_resource_manager: ResourceManager
    }

    impl NftBlueprint {
        pub fn instantiate() -> (ComponentAddress, Bucket) {
            let tickets = ResourceBuilder::new_integer_non_fungible::<GameData>(OwnerRole::None)
                 .metadata(metadata! {
                    init {
                      "name" => "Mavs vs Lakers - 12/25/2023", locked;
                      "description" => "Tickets to the 2023 season of the Dallas Mavericks", locked;
                   }
                )
                .non_fungible_data_roles(non_fungible_data_roles!(
                    non_fungible_data_updater => AccessRule::AllowAll
                    non_fungible_data_updaer_updater => AccessRule::DenyAll
                 ))
                .mint_initial_supply(vec![
                    (NonFungibleLocalId::Integer(1), GameData{ team_one: "Mavs".to_string(), team_two: "Lakers".to_string(), section: "A".to_string(), seat_number: 1, used: false}),
                    (NonFungibleLocalId::Integer(2), GameData{ team_one: "Mavs".to_string(), team_two: "Lakers".to_string(), section: "A".to_string(), seat_number: 2, used: false}),
                    (NonFungibleLocalId::Integer(3), GameData{ team_one: "Mavs".to_string(), team_two: "Lakers".to_string(), section: "A".to_string(), seat_number: 3, used: false })
                ]
            );

            // Create a component and store the ticket resource address
            // on it. We will need this in the `set_used_to_true` method
            let component = Self {
                ticket_resource_manager: tickets
            }
            .instantiate()
            .prepare_to_globalize(OwnerRole::None)
            .globalize();

            (component, tickets)
        }

        pub fn set_used_to_true(&self, id: NonFungibleLocalId) {
            let resource_manager = borrow_resource_manager!(self.ticket_resource_address);
            let mut game_data: GameData = resource_manager.get_non_fungible_data(&id);

            // Update the data on the network
            resource_manager.update_non_fungible_data(&id, "used", true);
        }
    }
}

We’ve updated our game ticket example so that we instantiate a component to keep track of the ticket non-fungible resource address and to offer a set_used_to_true(non_fungible_local_id) method. To know more about the ResourceManager and the methods it offers, read this page.

ResourceBuilder methods for non-fungibles

Listed in the following tables are the methods you can use when creating non-fungible resources.

Method

Description

.metadata(metadata!)

Specify metadata on the resource. This is where you define data that will be displayed on the wallets and explorers, for example. You can find a standard for the metadata keys here.

Note: You can include multiple metadata by calling this method multiple times.

.mint_roles(mint_roles!)

Specify the AccessRule for MintRoles to provide permission to mint tokens.

.burn_roles(burn_roles!)

Specify the AccessRule for BurnRoles to provide permission to burn tokens.

.withdraw_roles(withdraw_roles!)

Specify the AccessRule for WithdrawRoles to provide permission to withdraw tokens from a Vault.

.deposit_roles(deposit_roles!)

Specify the AccessRule for DepositRoles to provide permission to deposit tokens to a Vault.

.non_fungible_data_roles(non_fungible_data_roles!)

Specify the AccessRule for NonFungibleDataUpdateRoles to provide permission to update NonFungibleData.

.recall_roles(recall_roles!)

Specify the AccessRule for RecallRoles to provide permission to freeze tokens.

.freezer_roles(freezer_roles!)

Specify the AccessRule for FreezeRoles to provide permission to freeze tokens.