Skip to main content

Claiming NFT Outputs

Exchanges and dApp Devs Only

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.

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

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.

    // 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');
}
});
}

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.

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

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.

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

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