Skip to main content
This example demonstrates how to create, transfer, and manage NFTs on Sui.

Basic NFT Implementation

From examples/move/nft/sources/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: string::String,
        description: string::String,
        url: Url,
    }

    /// Event emitted when NFT is minted
    public struct NFTMinted has copy, drop {
        object_id: ID,
        creator: address,
        name: string::String,
    }

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

    /// Mint an NFT and transfer to sender
    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 NFT description
    public fun update_description(
        nft: &mut TestnetNFT,
        new_description: vector<u8>,
        _: &mut TxContext,
    ) {
        nft.description = string::utf8(new_description)
    }

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

Key Features

Object Structure

public struct TestnetNFT has key, store {
    id: UID,              // Unique identifier
    name: String,         // NFT name
    description: String,  // Description
    url: Url,            // Image/media URL
}
  • key: Makes it a Sui object
  • store: Allows storage in other objects and transfers

Minting with Events

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

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

    transfer::public_transfer(nft, sender);
}

Deploying and Using

1
Publish the NFT package
2
cd examples/move/nft
sui client publish --gas-budget 100000000
3
Mint an NFT
4
sui client call \
  --package 0xPACKAGE_ID \
  --module testnet_nft \
  --function mint_to_sender \
  --args \
    "My First NFT" \
    "An amazing NFT on Sui" \
    "https://example.com/nft.png" \
  --gas-budget 10000000
5
Transfer NFT
6
sui client call \
  --package 0xPACKAGE_ID \
  --module testnet_nft \
  --function transfer \
  --args 0xNFT_ID 0xRECIPIENT \
  --gas-budget 10000000
7
Update description
8
sui client call \
  --package 0xPACKAGE_ID \
  --module testnet_nft \
  --function update_description \
  --args 0xNFT_ID "Updated description" \
  --gas-budget 10000000
9
Burn NFT
10
sui client call \
  --package 0xPACKAGE_ID \
  --module testnet_nft \
  --function burn \
  --args 0xNFT_ID \
  --gas-budget 10000000

Advanced: Collection with Display

use sui::display;
use sui::package;

public struct TESTNET_NFT has drop {}

fun init(otw: TESTNET_NFT, ctx: &mut TxContext) {
    let keys = vector[
        b"name".to_string(),
        b"description".to_string(),
        b"image_url".to_string(),
        b"project_url".to_string(),
    ];

    let values = vector[
        b"{name}".to_string(),
        b"{description}".to_string(),
        b"{url}".to_string(),
        b"https://your-project.com".to_string(),
    ];

    let publisher = package::claim(otw, ctx);
    let mut display = display::new_with_fields<TestnetNFT>(
        &publisher,
        keys,
        values,
        ctx
    );
    display::update_version(&mut display);

    transfer::public_transfer(publisher, ctx.sender());
    transfer::public_transfer(display, ctx.sender());
}

Best Practices

1. Use accessor functions

public fun name(nft: &TestnetNFT): &String {
    &nft.name
}

2. Emit events

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

3. Implement Display standard

Make NFTs display correctly in wallets.

4. Consider mutability

Decide which fields should be mutable:
// Mutable description
public fun update_description(nft: &mut TestnetNFT, new: vector<u8>) {
    nft.description = string::utf8(new);
}

// Immutable name (no update function)

Next Steps

Build docs developers (and LLMs) love