Claiming NFT Outputs
The migration documentation describes the processes needed to claim and migrate output types manually; However, for the average user, this knowledge is not needed and is abstracted in the wallet web application (dashboard). The specific migration knowledge described here is unnecessary for people using a regular wallet.
As an owner of NftOutput
objects, you may need to unlock these assets before you can claim them.
This guide will help you understand how to evaluate the unlock conditions for an NftOutput
and proceed with claiming the associated NFT.
Evaluating Unlock Conditions
To start, you must determine whether your NftOutput
can be unlocked.
This involves using off-chain queries to check the unlock conditions,
similar to the process for Basic Outputs
.
Steps to Claim an NFT Output
1. Fetch the NftOutput
The first step in claiming your NFT is to retrieve the NftOutput
object that you intend to claim.
- TypeScript
- Rust
// Get the NFTOutput object.
const nftOutputObjectId = "0xad87a60921c62f84d57301ea127d1706b406cde5ec6fa4d3af2a80f424fab93a";
const nftOutputObject = await iotaClient.getObject({
id: nftOutputObjectId,
options: {
showContent: true,
showBcs: true
}
});
if (!nftOutputObject) {
throw new Error("NFT output object not found");
}
// Extract contents of the NftOutput object.
const moveObject = nftOutputObject.data?.content as IotaParsedData;
if (moveObject.dataType != "moveObject") {
throw new Error("NftOutput is not a move object");
}
// Treat fields as key-value object.
const fields = moveObject.fields as Record<string, any>;
// 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,
)?;
2. Verify the Native Token Balance
After fetching the NftOutput
, you need to check for any native tokens that might be held by this output.
These tokens are stored in a Bag
.
You'll need to obtain the dynamic field keys used as bag indexes to access the native tokens.
For native tokens, these keys are strings
representing the OTW
used for the native token
Coin
.
- TypeScript
- Rust
// Access fields by key
const nativeTokensBag = fields['native_tokens'];
// Extract the keys of the native_tokens bag if it is not empty; the keys
// are the type_arg of each native token, so they can be used later in the PTB.
const dfTypeKeys: string[] = [];
if (nativeTokensBag.fields.size > 0) {
// Get the dynamic fieldss of the native tokens bag.
const dynamicFieldPage = await iotaClient.getDynamicFields({
parentId: nativeTokensBag.fields.id.id
});
// Extract the dynamic fields keys, i.e., the native token type.
dynamicFieldPage.data.forEach(dynamicField => {
if (typeof dynamicField.name.value === 'string') {
dfTypeKeys.push(dynamicField.name.value);
} else {
throw new Error('Dynamic field key is not a string');
}
});
}
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<_>>(),
);
}
3. Create the PTB
Finally,
you can create a Programmable Transaction Block (PTB) using the nft_output
as an input,
along with the Bag
keys to iterate over the extracted native tokens.
The primary goal of this process is to extract the Nft
object from the NftOutput
.
- TypeScript
- Rust
const tx = new Transaction();
// Extract nft assets(base token, native tokens bag, nft asset itself).
const gasTypeTag = "0x2::iota::IOTA";
const args = [tx.object(nftOutputObjectId)];
// Finally call the nft_output::extract_assets function.
const extractedNftOutputAssets = tx.moveCall({
target: `${STARDUST_PACKAGE_ID}::nft_output::extract_assets`,
typeArguments: [gasTypeTag],
arguments: args,
});
// 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.
const extractedBaseToken = extractedNftOutputAssets[0];
let extractedNativeTokensBag: any = extractedNftOutputAssets[1];
const nft = extractedNftOutputAssets[2];
// Extract the IOTA balance.
const iotaCoin = tx.moveCall({
target: '0x2::coin::from_balance',
typeArguments: [gasTypeTag],
arguments: [extractedBaseToken],
});
// Transfer the IOTA balance.
tx.transferObjects([iotaCoin], tx.pure.address(sender));
// Extract the native tokens from the bag.
for (const typeKey of dfTypeKeys) {
const typeArguments = [`0x${typeKey}`];
// Then pass the the bag and the receiver address as input.
const args = [extractedNativeTokensBag, tx.pure.address(sender)]
// Extract native tokens from the bag.
// Extract native token balance.
// Transfer native token balance.
extractedNativeTokensBag = tx.moveCall({
target: `${STARDUST_PACKAGE_ID}::utilities::extract_and_send_to`,
typeArguments: typeArguments,
arguments: args,
});
}
// Cleanup the bag by destroying it.
tx.moveCall({
target: `0x2::bag::destroy_empty`,
typeArguments: [],
arguments: [extractedNativeTokensBag],
});
// Transfer the nft asset.
tx.transferObjects([nft], tx.pure.address(sender));
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 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 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()
};
Converting an Nft Output into a Custom Nft
Once you've claimed the Nft
, you may want to convert it into a custom NFT.
This section outlines the process of transforming a Stardust Nft
into a custom NFT tailored to your specific needs.
Preparing a Custom Nft Package
Before proceeding with the conversion, ensure you have a prepared custom NFT package.
This package will represent the custom NFT.
The following is an example of a simple module for representing, minting, burning,
and converting the custom NFT from the Stardust Nft
.
- Move
// 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)
}
}
Creating the PTB for Conversion
You can then create a PTB that extracts the Stardust Nft
from an NftOutput
and converts it into a custom NFT within your collection.
This method uses the metadata from the Stardust Nft
to mint a new NFT.
- TypeScript
- Rust
// 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.
const tx = new Transaction();
const gasTypeTag = "0x2::iota::IOTA";
const args = [tx.object(nftOutputObjectId)];
// Call the nft_output::extract_assets function.
const extractedNftOutputAssets = tx.moveCall({
target: `${STARDUST_PACKAGE_ID}::nft_output::extract_assets`,
typeArguments: [gasTypeTag],
arguments: args,
});
// 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.
const extractedBaseToken = extractedNftOutputAssets[0];
const extractedNativeTokensBag = extractedNftOutputAssets[1];
const nft = extractedNftOutputAssets[2];
// Call the conversion function to create a custom nft from the stardust nft
// asset.
let customNft = tx.moveCall({
target: `${customNftPackageId}::nft::convert`,
typeArguments: [],
arguments: [nft],
});
// Transfer the converted NFT.
tx.transferObjects([customNft], tx.pure.address(sender));
// Extract the IOTA balance.
const iotaCoin = tx.moveCall({
target: '0x2::coin::from_balance',
typeArguments: [gasTypeTag],
arguments: [extractedBaseToken],
});
// Transfer the IOTA balance.
tx.transferObjects([iotaCoin], tx.pure.address(sender));
// Cleanup the bag by destroying it.
tx.moveCall({
target: `0x2::bag::destroy_empty`,
typeArguments: [],
arguments: [extractedNativeTokensBag],
});
// 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()
};