Skip to main content

Claiming Nft Outputs

An address can own NftOutput objects that needs to be unlocked. In this case, some off-chain queries can be used to check the unlock conditions of the NftOutput and assess if it can be unlocked. Check the example above, as it works the same as the Basic Output.

Claim of an Nft Output

Once an Nft Output can be unlocked the claim of its assets can start

  1. The first step is to fetch the NftOutput object that needs to be claimed.
    // Get an NftOutput object
let nft_output_object_id = ObjectID::from_hex_literal(
"0xad87a60921c62f84d57301ea127d1706b406cde5ec6fa4d3af2a80f424fab93a",
)?;

let nft_output_object = iota_client
.read_api()
.get_object_with_options(
nft_output_object_id,
IotaObjectDataOptions::new().with_bcs(),
)
.await?
.data
.ok_or(anyhow!("Nft not found"))?;

let nft_output_object_ref = nft_output_object.object_ref();

let nft_output = bcs::from_bytes::<NftOutput>(
&nft_output_object
.bcs
.expect("should contain bcs")
.try_as_move()
.expect("should convert it to a move object")
.bcs_bytes,
)?;
  1. Then we check the native tokens that were possibly held by this output. A Bag is used for holding these tokens, so in this step we are interested in obtaining the dynamic field keys that are used as bag index. In the case of the native tokens Bag the keys are strings representing the OTW used for the native token Coin.
    let mut df_type_keys = vec![];
let native_token_bag = nft_output.native_tokens;
if native_token_bag.size > 0 {
// Get the dynamic fieldss of the native tokens bag
let dynamic_field_page = iota_client
.read_api()
.get_dynamic_fields(*native_token_bag.id.object_id(), None, None)
.await?;
// should have only one page
assert!(!dynamic_field_page.has_next_page);

// Extract the dynamic fields keys, i.e., the native token type
df_type_keys.extend(
dynamic_field_page
.data
.into_iter()
.map(|dyi| {
dyi.name
.value
.as_str()
.expect("should be a string")
.to_string()
})
.collect::<Vec<_>>(),
);
}
  1. Finally, a PTB can be created using the nft_output as input and the Bag keys for iterating the native tokens extracted. An Nft Output is different from a Basic Output as it contains the Nft object. In fact, the main purpose of claiming is extracting the Nft object from the NftOutput.
    let pt = {
let mut builder = ProgrammableTransactionBuilder::new();

// Extract nft assets(base token, native tokens bag, nft asset itself).
let type_arguments = vec![GAS::type_tag()];
let arguments = vec![builder.obj(ObjectArg::ImmOrOwnedObject(nft_output_object_ref))?];
// Finally call the nft_output::extract_assets function
if let Argument::Result(extracted_assets) = builder.programmable_move_call(
STARDUST_ADDRESS.into(),
ident_str!("nft_output").to_owned(),
ident_str!("extract_assets").to_owned(),
type_arguments,
arguments,
) {
// If the nft output can be unlocked, the command will be succesful and will
// return a `base_token` (i.e., IOTA) balance and a `Bag` of native tokens and
// related nft object.
let extracted_base_token = Argument::NestedResult(extracted_assets, 0);
let mut extracted_native_tokens_bag = Argument::NestedResult(extracted_assets, 1);
let nft_asset = Argument::NestedResult(extracted_assets, 2);

// Extract IOTA balance
let arguments = vec![extracted_base_token];
let type_arguments = vec![GAS::type_tag()];
let iota_coin = builder.programmable_move_call(
IOTA_FRAMEWORK_ADDRESS.into(),
ident_str!("coin").to_owned(),
ident_str!("from_balance").to_owned(),
type_arguments,
arguments,
);

// Transfer IOTA balance
builder.transfer_arg(sender, iota_coin);

for type_key in df_type_keys {
let type_arguments = vec![TypeTag::from_str(&format!("0x{type_key}"))?];
// Then pass the the bag and the receiver address as input
let arguments = vec![extracted_native_tokens_bag, builder.pure(sender)?];

// Extract native tokens from the bag.
// Extract native token balance
// Transfer native token balance
extracted_native_tokens_bag = builder.programmable_move_call(
STARDUST_ADDRESS.into(),
ident_str!("utilities").to_owned(),
ident_str!("extract_and_send_to").to_owned(),
type_arguments,
arguments,
);
}

// Transferring nft asset
builder.transfer_arg(sender, nft_asset);

// Cleanup bag.
let arguments = vec![extracted_native_tokens_bag];
builder.programmable_move_call(
IOTA_FRAMEWORK_ADDRESS.into(),
ident_str!("bag").to_owned(),
ident_str!("destroy_empty").to_owned(),
vec![],
arguments,
);
}
builder.finish()
};

Conversion of an Nft Output into a custom Nft

This topic outlines the process of converting an stardust Nft into a custom Nft.

You need to have a prepared custom Nft package that you want to convert the Stardust Nft You need to have a prepared custom Nft package that you want to convert the Stardust Nft into. The following is an example of a simple module for representing a custom NFT, minting it, burning it and converting it from a Stardust Nft:

// Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

module custom_nft::nft {
use std::string::String;

use iota::event;
use iota::url::Url;

use custom_nft::collection::Collection;

/// An example NFT that can be minted by anybody.
public struct Nft has key, store {
id: UID,
/// The token name.
name: String,
/// The token description.
description: Option<String>,
/// The token URL.
url: Url,
/// The related collection name.
collection_name: Option<String>

// Allow custom attributes.
}

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

/// Event marking when an `Nft` has been minted.
public struct NftMinted has copy, drop {
/// The NFT id.
object_id: ID,
/// The NFT creator.
creator: address,
/// The NFT name.
name: String,
}

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

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

/// Get the NFT's `description`.
public fun description(nft: &Nft): &Option<String> {
&nft.description
}

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

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

/// Convert a `stardust::nft::Nft` into `Nft`.
///
/// The developer of the `custom_nft` package could tie minting to several conditions, for example:
/// - Only accept Stardust NFTs from a certain issuer, with a certain name/collection name, `NftId` even.
/// - Only the `immutable_issuer` and `id` fields count as proof for an NFT belonging to the original collection.
///
/// The developer could technically mint the same NFT on the running Stardust network before the mainnet switch
/// and fake the name and metadata.
public fun convert(stardust_nft: stardust::nft::Nft, ctx: &mut TxContext): Nft {
let nft_metadata = stardust_nft.immutable_metadata();

let nft = mint(
*nft_metadata.name(),
*nft_metadata.description(),
*nft_metadata.uri(),
*nft_metadata.collection_name(),
ctx
);

stardust::nft::destroy(stardust_nft);

nft
}

/// Mint a collection-related NFT.
public fun mint_collection_related(
collection: &Collection,
name: String,
description: String,
url: Url,
ctx: &mut TxContext
): Nft {
mint(
name,
option::some(description),
url,
option::some(*collection.name()),
ctx
)
}

/// Create a new `Nft` instance.
fun mint(
name: String,
description: Option<String>,
url: Url,
collection_name: Option<String>,
ctx: &mut TxContext
): Nft {
let nft = Nft {
id: object::new(ctx),
name,
description,
url,
collection_name
};

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

nft
}

/// Permanently delete the `Nft` instance.
public fun burn(nft: Nft) {
let Nft {
id,
name: _,
description: _,
url: _,
collection_name: _
} = nft;

object::delete(id)
}
}

Create a PTB that extracts the Stardust Nft from an NftOutput and then converts it into a custom NFT of the collection just published. This conversion method extracts the Stardust Nft metadata and uses it for minting a new NFT.

    // Create a PTB that extracts the stardust NFT from an NFTOutput and then calls
// the `custom_nft::nft::convert` function for converting it into a custom NFT
// of the just published package.
let pt = {
let mut builder = ProgrammableTransactionBuilder::new();
let type_arguments = vec![GAS::type_tag()];
let arguments = vec![builder.obj(ObjectArg::ImmOrOwnedObject(nft_output_object_ref))?];
// Call the nft_output::extract_assets function
if let Argument::Result(extracted_assets) = builder.programmable_move_call(
STARDUST_ADDRESS.into(),
ident_str!("nft_output").to_owned(),
ident_str!("extract_assets").to_owned(),
type_arguments,
arguments,
) {
// If the nft output can be unlocked, the command will be successful
// and will return a `base_token` (i.e., IOTA) balance and a
// `Bag` of native tokens and related nft object.
let extracted_base_token = Argument::NestedResult(extracted_assets, 0);
let extracted_native_tokens_bag = Argument::NestedResult(extracted_assets, 1);
let nft_asset = Argument::NestedResult(extracted_assets, 2);

// Call the conversion function to create a custom nft from the stardust nft
// asset.
let custom_nft = builder.programmable_move_call(
custom_nft_package_id,
ident_str!("nft").to_owned(),
ident_str!("convert").to_owned(),
vec![],
vec![nft_asset],
);

// Transfer the converted NFT
builder.transfer_arg(sender, custom_nft);

// Extract IOTA balance
let arguments = vec![extracted_base_token];
let type_arguments = vec![GAS::type_tag()];
let iota_coin = builder.programmable_move_call(
IOTA_FRAMEWORK_ADDRESS.into(),
ident_str!("coin").to_owned(),
ident_str!("from_balance").to_owned(),
type_arguments,
arguments,
);

// Transfer IOTA balance
builder.transfer_arg(sender, iota_coin);

// Cleanup bag.
let arguments = vec![extracted_native_tokens_bag];
builder.programmable_move_call(
IOTA_FRAMEWORK_ADDRESS.into(),
ident_str!("bag").to_owned(),
ident_str!("destroy_empty").to_owned(),
vec![],
arguments,
);
}
builder.finish()
};