Skip to main content

Claiming Alias 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.

An address can own AliasOutput objects only if it was set as the Alias Governor Address before the migration. In this case, the AliasOutput object is an owned object in the ledger, and its owner is the Governor address. This address can be directly controlled by a user or by another object (either an Alias or Nft object). For the latter use case, check the Claiming an Output Unlockable by an Alias/Nft Address example.

Claim an Alias Output

A Governor address can claim the AliasOutput assets at any time:

1. Fetch the AliasOutput

The first step is to fetch the AliasOutput object that needs to be claimed.

    // Get the AliasOutput object.
const aliasOutputObjectId = "0x354a1864c8af23fde393f7603bc133f755a9405353b30878e41b929eb7e37554";
const aliasOutputObject = await iotaClient.getObject({id: aliasOutputObjectId, options: { showContent: true }});
if (!aliasOutputObject) {
throw new Error("Alias output object not found");
}

// Extract contents of the AliasOutput object.
const moveObject = aliasOutputObject.data?.content as IotaParsedData;
if (moveObject.dataType != "moveObject") {
throw new Error("AliasOutput is not a move object");
}

// Treat fields as key-value object.
const fields = moveObject.fields as Record<string, any>;

2. Check the Native Token Balance

Next, check the native tokens that might be held by this output. A Bag is used for holding these tokens. In this step, we are interested in obtaining the dynamic field keys that are bag indexes. For native tokens, the keys are strings representing the OTW used for the native token declaration.

    const nativeTokensBag = fields['native_tokens'];   // Bag field

// 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 fields owned by 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, create a Programmable Transaction Block (PTB) using the alias_output_object_ref as input along with the native token keys. An AliasOutput differs from an NftOutput or a BasicOutput because it contains the Alias object. The main purpose of claiming is to extract the Alias object from the AliasOutput.

    // Create a PTB to claim the assets related to the alias output.
const tx = new Transaction();
// Type argument for an AliasOutput coming from the IOTA network, i.e., the
// IOTA token or the Gas type tag.
const gasTypeTag = "0x2::iota::IOTA";
// Then pass the AliasOutput object as an input.
const args = [tx.object(aliasOutputObjectId)];
// Finally call the alias_output::extract_assets function.
const extractedAliasOutputAssets = tx.moveCall({
target: `${STARDUST_PACKAGE_ID}::alias_output::extract_assets`,
typeArguments: [gasTypeTag],
arguments: args,
});

// The alias output can always be unlocked by the governor address. So the
// command will be successful and will return a `base_token` (i.e., IOTA)
// balance, a `Bag` of the related native tokens and the related Alias object.
// Extract contents.
const extractedBaseToken = extractedAliasOutputAssets[0];
let extractedNativeTokensBag: any = extractedAliasOutputAssets[1];
const alias = extractedAliasOutputAssets[2];

// Extract the IOTA balance.
const iotaCoin = tx.moveCall({
target: '0x2::coin::from_balance',
typeArguments: [gasTypeTag],
arguments: [extractedBaseToken],
});

// Transfer the IOTA balance to the sender.
tx.transferObjects([iotaCoin], tx.pure.address(sender));

// Extract the native tokens from the bag.
for (const typeKey of dfTypeKeys) {
const typeArguments = [`0x${typeKey}`];
const args = [extractedNativeTokensBag, tx.pure.address(sender)]

// Extract a 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 alias asset.
tx.transferObjects([alias], tx.pure.address(sender));

Convert an Alias Output into a Custom Object

To convert an Alias into a new entity usable for your project, you need to have a custom package prepared with the necessary logic.

In Stardust, an alias can serve various purposes, such as acting as an NFT collection controller. The following example outlines the process of converting a Stardust Alias into a CollectionControllerCap.

This example extends the one described in the Conversion of an Nft Output into a Custom Nft documentation:

The collection.move module extends the custom_nft package to enable working with NFT collections:

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

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

use iota::event;

use stardust::alias::Alias;

// ===== Errors =====

/// For when someone tries to drop a `Collection` with a wrong capability.
const EWrongCollectionControllerCap: u64 = 0;

// ===== Structures =====

/// A capability allowing the bearer to create or drop an NFT collection.
/// A `stardust::alias::Alias` instance can be converted into `CollectionControllerCap` in this example,
/// since an alias address could be used as a collections controller in Stardust.
///
/// NOTE: To simplify the example, `CollectionControllerCap` is publicly transferable, but to make sure that it can be created,
/// dropped and owned only by the related `stardust::alias::Alias` owner, we can remove the `store` ability and transfer a created
/// capability to the sender in the constructor.
public struct CollectionControllerCap has key, store {
id: UID,
}

/// An NFT collection.
/// Can be created by a `CollectionControllerCap` owner and used to mint collection-related NFTs.
/// Can be dropped only by it's `CollectionControllerCap` owner. Once a collection is dropped,
/// it is impossible to mint new collection-related NFTs.
///
/// NOTE: To simplify the example, `Collection` is publicly transferable, but to make sure that it can be created,
/// dropped and owned only by the related `CollectionControllerCap` owner, we can remove the `store` ability and transfer a created
/// capability to the sender in the constructor.
public struct Collection has key, store {
id: UID,
/// The related `CollectionControllerCap` ID.
cap_id: ID,
/// The collection name.
name: String,
}

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

/// Event marking when a `stardust::alias::Alias` has been converted into `CollectionControllerCap`.
public struct StardustAliasConverted has copy, drop {
/// The `stardust::alias::Alias` ID.
alias_id: ID,
/// The `CollectionControllerCap` ID.
cap_id: ID,
}

/// Event marking when a `CollectionControllerCap` has been dropped.
public struct CollectionControllerCapDropped has copy, drop {
/// The `CollectionControllerCap` ID.
cap_id: ID,
}

/// Event marking when a `Collection` has been created.
public struct CollectionCreated has copy, drop {
/// The collection ID.
collection_id: ID,
}

/// Event marking when a `Collection` has been dropped.
public struct CollectionDropped has copy, drop {
/// The collection ID.
collection_id: ID,
}

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

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

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

/// Convert a `stardust::alias::Alias` into `CollectionControllerCap`.
public fun convert_alias_to_collection_controller_cap(stardust_alias: Alias, ctx: &mut TxContext): CollectionControllerCap {
let cap = CollectionControllerCap {
id: object::new(ctx)
};

event::emit(StardustAliasConverted {
alias_id: object::id(&stardust_alias),
cap_id: object::id(&cap),
});

stardust::alias::destroy(stardust_alias);

cap
}

/// Drop a `CollectionControllerCap` instance.
public fun drop_collection_controller_cap(cap: CollectionControllerCap) {
event::emit(CollectionControllerCapDropped {
cap_id: object::id(&cap),
});

let CollectionControllerCap { id } = cap;

object::delete(id)
}

/// Create a `Collection` instance.
public fun create_collection(cap: &CollectionControllerCap, name: String, ctx: &mut TxContext): Collection {
let collection = Collection {
id: object::new(ctx),
cap_id: object::id(cap),
name,
};

event::emit(CollectionCreated {
collection_id: object::id(&collection),
});

collection
}

/// Drop a `Collection` instance.
public fun drop_collection(cap: &CollectionControllerCap, collection: Collection) {
assert!(object::borrow_id(cap) == &collection.cap_id, EWrongCollectionControllerCap);

event::emit(CollectionDropped {
collection_id: object::id(&collection),
});

let Collection {
id,
cap_id: _,
name: _
} = collection;

object::delete(id)
}
}

Also, the nft.move module was extended with the following function:

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

Once the package is prepared, you can extract and use a Stardust Alias in a single transaction to create a CollectionControllerCap. This capability is then used in later transactions to manage new collections.

    // Create a PTB that extracts the related stardust Alias from the AliasOutput
// and then calls the
// `custom_nft::collection::convert_alias_to_collection_controller_cap` function
// to convert it into an NFT collection controller, create a collection and mint
// a few NFTs.
const tx = new Transaction();
const gasTypeTag = "0x2::iota::IOTA";
const args = [tx.object(aliasOutputObjectId)];

// Call the nft_output::extract_assets function.
const extractedAliasOutputAssets = tx.moveCall({
target: `${STARDUST_PACKAGE_ID}::alias_output::extract_assets`,
typeArguments: [gasTypeTag],
arguments: args,
});

// The alias output can always be unlocked by the governor address. So the
// command will be successful and will return a `base_token` (i.e., IOTA)
// balance, a `Bag` of the related native tokens and the related Alias object.
// Extract contents.
const extractedBaseToken = extractedAliasOutputAssets[0];
let extractedNativeTokensBag: any = extractedAliasOutputAssets[1];
const alias = extractedAliasOutputAssets[2];

// Call the conversion function to create an NFT collection controller from the
// extracted alias.
let nftCollectionController = tx.moveCall({
target: `${customNftPackageId}::collection::convert_alias_to_collection_controller_cap`,
typeArguments: [],
arguments: [alias],
});

// Create an NFT collection.
let nftCollection = tx.moveCall({
target: `${customNftPackageId}::collection::create_collection`,
typeArguments: [],
arguments: [nftCollectionController, tx.pure.string("Collection name")],
});

// Mint a collection-related NFT.
const nftName = tx.pure.string("NFT name");
const nftDescription = tx.pure.string("NFT description");
const nftUrlVal = tx.pure.string("NFT URL");

const nftUrl = tx.moveCall({
target: `0x2::url::new_unsafe`,
typeArguments: [],
arguments: [nftUrlVal],
});

const nft = tx.moveCall({
target: `${customNftPackageId}::nft::mint_collection_related`,
typeArguments: [],
arguments: [nftCollection, nftName, nftDescription, nftUrl],
});

// Transfer the NFT.
tx.transferObjects([nft], tx.pure.address(sender));

// Drop the NFT collection to make impossible to mint new related NFTs.
tx.moveCall({
target: `${customNftPackageId}::collection::drop_collection`,
typeArguments: [],
arguments: [nftCollectionController, nftCollection],
});

// Transfer the NFT collection controller.
tx.transferObjects([nftCollectionController], 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 to the sender.
tx.transferObjects([iotaCoin], tx.pure.address(sender));

// Extract the native tokens from the bag.
for (const typeKey of dfTypeKeys) {
const typeArguments = [`0x${typeKey}`];
const args = [extractedNativeTokensBag, tx.pure.address(sender)]

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