Skip to main content

Create a Non-Fungible Token

On Sui, everything is an object. Moreover, everything is a non-fungible token (NFT) as its objects are unique, non-fungible, and owned.

Creating NFTs on Sui differs from other blockchains that are not object based. Those blockchains require a dedicated standard to handle the properties that define NFTs because they are based on a mapping between smart contracts and the token's ID. For instance, the ERC-721 standard on Ethereum was necessary to pair a globally unique ID with the relevant smart contract address to create a unique token instance on the network.

On Sui, every object already has a unique ID, so whether you're dealing with a million fungible tokens, like coins, or thousands of NFTs with individual characteristics, like SuiFrens, your smart contracts on Sui always interact with individual objects.

Imagine you create an Excitable Chimp NFT collection on Sui and another blockchain that isn't object based. To get an attribute like the Chimp's name on the other blockchain, you would need to interact with the smart contract that created the NFT to get that information (typically from off-chain storage) using the NFT ID. On Sui, the name attribute can be a field on the object that defines the NFT itself. This construct provides a much more straightforward process for accessing metadata for the NFT as the smart contract that wants the information can just return the name from the object itself.

Creating NFTs

The following example creates a basic NFT on Sui. The TestnetNFT struct defines the NFT with an id, name, description, and url fields.

public struct TestnetNFT has key, store {
id: UID,
name: string::String,
description: string::String,
url: Url,
}

In this example, anyone can mint the NFT by calling the mint_to_sender function. As the name suggests, the function creates a new TestnetNFT and transfers it to the address that makes the call.

#[allow(lint(self_transfer))]
public fun mint_to_sender(
name: vector<u8>,
description: vector<u8>,
url: vector<u8>,
ctx: &mut TxContext
) {
let sender = ctx.sender();
let nft = TestnetNFT {
id: object::new(ctx),
name: string::utf8(name),
description: string::utf8(description),
url: url::new_unsafe_from_bytes(url)
};

event::emit(NFTMinted {
object_id: object::id(&nft),
creator: sender,
name: nft.name,
});

transfer::public_transfer(nft, sender);
}

The module includes functions to return NFT metadata, too. Referencing the hypothetical used previously, you can call the name function to get that value. As you can see, the function simply returns the name field value of the NFT itself.

public fun name(nft: &TestnetNFT): &string::String {
&nft.name
}
Click to open

testnet_nft.move

module examples::testnet_nft {
use sui::url::{Self, Url};
use std::string;
use sui::event;

/// An example NFT that can be minted by anybody
public struct TestnetNFT has key, store {
id: UID,
/// Name for the token
name: string::String,
/// Description of the token
description: string::String,
/// URL for the token
url: Url,
// TODO: allow custom attributes
}

// ===== Events =====

public struct NFTMinted has copy, drop {
// The Object ID of the NFT
object_id: ID,
// The creator of the NFT
creator: address,
// The name of the NFT
name: string::String,
}

// ===== Public view functions =====

/// Get the NFT's `name`
public fun name(nft: &TestnetNFT): &string::String {
&nft.name
}

/// Get the NFT's `description`
public fun description(nft: &TestnetNFT): &string::String {
&nft.description
}

/// Get the NFT's `url`
public fun url(nft: &TestnetNFT): &Url {
&nft.url
}

// ===== Entrypoints =====

#[allow(lint(self_transfer))]
/// Create a new devnet_nft
public fun mint_to_sender(
name: vector<u8>,
description: vector<u8>,
url: vector<u8>,
ctx: &mut TxContext
) {
let sender = ctx.sender();
let nft = TestnetNFT {
id: object::new(ctx),
name: string::utf8(name),
description: string::utf8(description),
url: url::new_unsafe_from_bytes(url)
};

event::emit(NFTMinted {
object_id: object::id(&nft),
creator: sender,
name: nft.name,
});

transfer::public_transfer(nft, sender);
}

/// Transfer `nft` to `recipient`
public fun transfer(
nft: TestnetNFT, recipient: address, _: &mut TxContext
) {
transfer::public_transfer(nft, recipient)
}

/// Update the `description` of `nft` to `new_description`
public fun update_description(
nft: &mut TestnetNFT,
new_description: vector<u8>,
_: &mut TxContext
) {
nft.description = string::utf8(new_description)
}

/// Permanently delete `nft`
public fun burn(nft: TestnetNFT, _: &mut TxContext) {
let TestnetNFT { id, name: _, description: _, url: _ } = nft;
id.delete()
}
}

Creating Object Display

If you use the Sui Wallet Chrome extension, checking your Assets tab won't show the visual representation of the NFT you created in the previous section. Instead, you can find its metadata in the Everything Else view.

Everything Else view

To see the NFT in the Visual Assets view, you have to create a display for the object. The following code modifies the Random NFT example to demonstrate the use of Object Display.

To create the Display object, you need to import the package.

use sui::display;

This example creates several structs to support the smart contract. The AirDropNFT struct creates an NFT that the reveal function burns in exchange for a metal NFT of either bronze, silver, or gold. The example uses on-chain randomness to determine which type of metal the user receives when turning in their AirDropNFT. For simplicity, this example creates a single airdrop NFT and transfers it to the publisher. The Random NFT example this code is based on shows how to create multiple airdrop NFTs, as well as some additional randomization options.

The MetalNFT struct sets up some basic Display properties.

The MintingCapability struct is needed to authorize minting the metal NFT by the capability owner.

Finally, RANDOM_NFT is a one-time witness needed to create the Publisher object.

public struct AirDropNFT has key, store {
id: UID,
}

public struct MetalNFT has key, store {
id: UID,
name: String,
description: String,
image_url: Url,
metal: u8,
}

public struct MintingCapability has key {
id: UID,
}

public struct RANDOM_NFT has drop {}

This example creates the Display object in the package initializer for ease of use, but you typically create the object using a programmable transaction block. The init function uses variables to define the values vector so you can define them when minting the MetalNFT.

#[allow(unused_function)]
fun init(otw: RANDOM_NFT, ctx: &mut TxContext) {

let publisher = package::claim(otw, ctx);

let keys = vector[
utf8(b"metal"),
utf8(b"name"),
utf8(b"image_url"),
utf8(b"description"),
];

let values = vector[
utf8(b"{metal}"),
utf8(b"{name}"),
utf8(b"{image_url}"),
utf8(b"{description}"),
];

let mut display = display::new_with_fields<MetalNFT>(
&publisher, keys, values, ctx
);

display::update_version(&mut display);

sui::transfer::public_transfer(display, ctx.sender());

transfer::transfer(
MintingCapability { id: object::new(ctx) },
ctx.sender(),
);

transfer::public_transfer(AirDropNFT { id: object::new(ctx) }, ctx.sender());

sui::transfer::public_transfer(publisher, ctx.sender())
}

You can publish the project in the Sui repo to see the results of the code, but you must update the category file to use the online Sui dependency. Your toml file should look like the following.

[package]
name = "nft_display"
edition = "2024.beta"

[dependencies]
Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "framework/testnet" }

[addresses]
metal = "0x0"

Mint metal NFT

Typically, a production dApp includes a frontend that interacts with the smart contract. To limit scope, this example uses the Sui CLI for this interaction. In the root of the project, build and publish the package.

$ sui client publish . --gas-budget 200000000

The terminal or console responds with the results of the publish. Locate the following entries and assign them to variables.

In the Published Objects results, copy the PackageID value and set it to PID.

$ PID=<Package-ID>

Next, in Created Objects, copy the AirDropNFT ObjectID value. Create a variable AIR and give it the copied value.

$ AIR=<Object-ID>

Finally, call the reveal function with the set values. The 0x8 address is the Randomness object.

$ sui client call --function reveal --module random_nft --package $PID --args $AIR 0x8 --gas-budget 20000000
Click to open

reveal function

/// Reveal the metal of the airdrop NFT and convert it to a metal NFT.
/// This function uses arithmetic_is_less_than to determine the metal of the NFT in a way that consumes the same
/// amount of gas regardless of the value of the random number.
entry fun reveal(nft: AirDropNFT, r: &Random, ctx: &mut TxContext) {
destroy_airdrop_nft(nft);

let mut generator = new_generator(r, ctx);
let v = generator.generate_u8_in_range(1, 100);

let is_gold = arithmetic_is_less_than(v, 11, 100); // probability of 10%
let is_silver = arithmetic_is_less_than(v, 41, 100) * (1 - is_gold); // probability of 30%
let is_bronze = (1 - is_gold) * (1 - is_silver); // probability of 60%
let metal = is_gold * GOLD + is_silver * SILVER + is_bronze * BRONZE;
let mut metal_url = BRONZE_URL;
let mut metal_name = b"Bronze".to_string();
let mut metal_description = b"Common metal".to_string();
if (is_gold > 0) {
metal_url = GOLD_URL;
metal_name = b"Gold".to_string();
metal_description = b"Rare metal".to_string();
};
if (is_silver > 0) {
metal_url = SILVER_URL;
metal_name = b"Silver".to_string();
metal_description = b"Uncommon metal".to_string();
};

transfer::public_transfer(
MetalNFT { id: object::new(ctx), image_url: url::new_unsafe_from_bytes({metal_url}), name: {metal_name}, description: {metal_description}, metal, },
ctx.sender()
);
}

After you call reveal, you can check your Sui Wallet to see which NFT you received. The NFT appears in the Assets tab within the Visual Assets view.

Visual Assets

  • NFT Rental: Example that rents NFTs using Kiosk Apps.
  • Asset Tokenization: Example that uses NFTs to tokenize real-world assets.
  • Kiosk: Asset storage on the Sui network.
  • Kiosk Apps: Extend the functionality of the Kiosk standard.
  • Sui Object Display: A template engine that enables on-chain management of off-chain representation (display) for a type.
  • Random NFT example: Example Move code to create AirDrop NFTs that are burned in exchange for a different, randomized NFT.