Claiming Alias 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.
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.
- TypeScript
- Rust
// 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>;
// Get an AliasOutput object.
let alias_output_object_id = ObjectID::from_hex_literal(
"0x354a1864c8af23fde393f7603bc133f755a9405353b30878e41b929eb7e37554",
)?;
let alias_output_object = iota_client
.read_api()
.get_object_with_options(
alias_output_object_id,
IotaObjectDataOptions::new().with_bcs(),
)
.await?
.data
.into_iter()
.next()
.ok_or(anyhow!("alias not found"))?;
let alias_output_object_ref = alias_output_object.object_ref();
// Convert the AliasOutput object into its Rust representation.
let alias_output = bcs::from_bytes::<AliasOutput>(
&alias_output_object
.bcs
.expect("should contain bcs")
.try_as_move()
.expect("should convert it to a move object")
.bcs_bytes,
)?;
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.
- TypeScript
- Rust
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');
}
});
}
// 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.
let mut df_type_keys = vec![];
let native_token_bag = alias_output.native_tokens;
if native_token_bag.size > 0 {
// Get the dynamic fields owned by the native tokens bag.
let dynamic_field_page = iota_client
.read_api()
.get_dynamic_fields(*native_token_bag.id.object_id(), None, None)
.await?;
// Only one page should exist.
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, 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
.
- TypeScript
- Rust
// 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));
// Create a PTB to claim the assets related to the alias output.
let pt = {
// Init a programmable transaction builder.
let mut builder = ProgrammableTransactionBuilder::new();
// Type argument for an AliasOutput coming from the IOTA network, i.e., the
// IOTA token or the Gas type tag.
let type_arguments = vec![GAS::type_tag()];
// Then pass the AliasOutput object as an input.
let arguments = vec![builder.obj(ObjectArg::ImmOrOwnedObject(alias_output_object_ref))?];
// Finally call the alias_output::extract_assets function.
if let Argument::Result(extracted_assets) = builder.programmable_move_call(
STARDUST_ADDRESS.into(),
ident_str!("alias_output").to_owned(),
ident_str!("extract_assets").to_owned(),
type_arguments,
arguments,
) {
// 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.
let extracted_base_token = Argument::NestedResult(extracted_assets, 0);
let mut extracted_native_tokens_bag = Argument::NestedResult(extracted_assets, 1);
let extracted_alias = Argument::NestedResult(extracted_assets, 2);
// Extract the IOTA balance.
let type_arguments = vec![GAS::type_tag()];
let arguments = vec![extracted_base_token];
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 the IOTA balance to the sender.
builder.transfer_arg(sender, iota_coin);
// Extract the native tokens from the bag.
for type_key in df_type_keys {
let type_arguments = vec![TypeTag::from_str(&format!("0x{type_key}"))?];
let arguments = vec![extracted_native_tokens_bag, builder.pure(sender)?];
// Extract a 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,
);
}
// Cleanup the 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,
);
// Transfer the alias asset.
builder.transfer_arg(sender, extracted_alias);
}
builder.finish()
};
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:
- Move
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.
- TypeScript
- Rust
// 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],
});
// 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.
let pt = {
let mut builder = ProgrammableTransactionBuilder::new();
let arguments = vec![builder.obj(ObjectArg::ImmOrOwnedObject(alias_output_object_ref))?];
// Call the nft_output::extract_assets function
if let Argument::Result(extracted_assets) = builder.programmable_move_call(
STARDUST_PACKAGE_ID,
ident_str!("alias_output").to_owned(),
ident_str!("extract_assets").to_owned(),
vec![GAS::type_tag()],
arguments,
) {
// 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.
let extracted_base_token = Argument::NestedResult(extracted_assets, 0);
let mut extracted_native_tokens_bag = Argument::NestedResult(extracted_assets, 1);
let alias_asset = Argument::NestedResult(extracted_assets, 2);
// Call the conversion function to create an NFT collection controller from the
// extracted alias.
let nft_collection_controller = builder.programmable_move_call(
custom_nft_package_id,
ident_str!("collection").to_owned(),
ident_str!("convert_alias_to_collection_controller_cap").to_owned(),
vec![],
vec![alias_asset],
);
// Create an NFT collection
let nft_collection_name = builder
.input(CallArg::Pure(bcs::to_bytes("Collection name").unwrap()))
.unwrap();
let nft_collection = builder.programmable_move_call(
custom_nft_package_id,
ident_str!("collection").to_owned(),
ident_str!("create_collection").to_owned(),
vec![],
vec![nft_collection_controller, nft_collection_name],
);
// Mint a collection-related NFT
let nft_name = builder
.input(CallArg::Pure(bcs::to_bytes("NFT name").unwrap()))
.unwrap();
let nft_description = builder
.input(CallArg::Pure(bcs::to_bytes("NFT description").unwrap()))
.unwrap();
let nft_url_value = builder
.input(CallArg::Pure(bcs::to_bytes("NFT URL").unwrap()))
.unwrap();
let nft_url = builder.programmable_move_call(
IOTA_FRAMEWORK_PACKAGE_ID,
ident_str!("url").to_owned(),
ident_str!("new_unsafe").to_owned(),
vec![],
vec![nft_url_value],
);
let nft = builder.programmable_move_call(
custom_nft_package_id,
ident_str!("nft").to_owned(),
ident_str!("mint_collection_related").to_owned(),
vec![],
vec![nft_collection, nft_name, nft_description, nft_url],
);
// Transfer the NFT
builder.transfer_arg(sender, nft);
// Drop the NFT collection to make impossible to mint new related NFTs
builder.programmable_move_call(
custom_nft_package_id,
ident_str!("collection").to_owned(),
ident_str!("drop_collection").to_owned(),
vec![],
vec![nft_collection_controller, nft_collection],
);
// Transfer the NFT collection controller
builder.transfer_arg(sender, nft_collection_controller);
// Extract IOTA balance
let iota_coin = builder.programmable_move_call(
IOTA_FRAMEWORK_PACKAGE_ID,
ident_str!("coin").to_owned(),
ident_str!("from_balance").to_owned(),
vec![GAS::type_tag()],
vec![extracted_base_token],
);
// Transfer IOTA balance
builder.transfer_arg(sender, iota_coin);
// Extract the native tokens from the bag.
for type_key in df_type_keys {
let type_arguments = vec![TypeTag::from_str(&format!("0x{type_key}"))?];
let arguments = vec![extracted_native_tokens_bag, builder.pure(sender)?];
// Extract a native token balance.
extracted_native_tokens_bag = builder.programmable_move_call(
STARDUST_PACKAGE_ID,
ident_str!("utilities").to_owned(),
ident_str!("extract_and_send_to").to_owned(),
type_arguments,
arguments,
);
}
// Cleanup bag.
builder.programmable_move_call(
IOTA_FRAMEWORK_PACKAGE_ID,
ident_str!("bag").to_owned(),
ident_str!("destroy_empty").to_owned(),
vec![],
vec![extracted_native_tokens_bag],
);
}
builder.finish()
};