Skip to main content

Create a Non-Fungible Token

On Sui, every object is unique and has an owner, properties that other blockchains like Ethereum require a dedicated standard such as ERC-721 to achieve.

Imagine you create a Rare Reef Shark NFT collection on Sui and another blockchain that is not object based. To get an attribute like the shark'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 offchain storage) using the NFT ID. On Sui, the name attribute is a field on the object that defines the NFT itself. This construct provides a more direct process for accessing NFT metadata because the smart contract that needs the information can return the name directly from the object.

Use cases​

NFTs on Sui are useful in a wide range of applications:

  • Digital collectibles: Unique items like art, trading cards, or in-game characters with distinct attributes and provenance. SuiFrens is an example of a collectible NFT collection on Sui.
  • Gaming assets: In-game items such as weapons, skins, or land parcels, where each asset has unique properties and can be owned, traded, or used across compatible games.
  • Event tickets and access passes: Tickets or credentials that grant the holder access to an event, community, or gated experience. Uniqueness and ownership are verifiable on-chain.
  • Identity and credentials: Soulbound NFTs that represent non-transferable credentials such as certifications, memberships, or reputation scores.
  • NFT rentals: NFTs can be rented to other users for a defined period using Kiosk extensions. This enables use cases where a holder wants to monetize an asset without selling it.
  • Encrypted media: NFTs can wrap encrypted content so that only the owner can access the underlying asset, which enables freemium and try-before-you-buy models.

NFTs vs tokenized assets​

On Sui, both NFTs and tokenized assets are objects. The protocol does not distinguish between them. The difference is in how you design your smart contract.

An NFT is typically an object with a supply of 1 and metadata specific to that instance, such as a name, description, or image URL. A tokenized asset is designed for fractional ownership, with a supply greater than 1 and metadata shared across all fractions. Tokenized assets can also be split or merged, which NFTs are not designed for.

The following table summarizes the key design differences:

PropertyNFTTokenized asset
Supply1Greater than 1
MetadataUnique per tokenShared across all fractions
DivisibilityNot divisibleDivisible (split or merge)
Use caseUnique ownership of a single itemFractional ownership of an asset
ExampleA unique piece of digital artFractional shares in real estate

Example​

The following example creates a basic NFT on Sui. The TestnetNFT struct defines the NFT with 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. 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. You can call the name function to get that value. The function returns the name field value directly from the NFT object.

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

testnet_nft.move

module examples::testnet_nft;

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

/// 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()
}