Skip to main content

Claiming Alias Outputs

An address can own AliasOutput objects only if before the migration it was set as the Alias Governor Address. In this case, the AliasOutput object is an owned object in the ledger and its owner is the Governor address. Such 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 of an Alias Output

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

  1. The first step is to fetch an AliasOutput object needed to be claimed.
    // 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,
)?;
  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 bag indexes. In the case of the native tokens, the keys are strings representing the OTW used for the native token declaration.
    // 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<_>>(),
);
}
  1. Finally, a PTB can be created using the alias_output_object_ref as input and the native token keys. An AliasOutput is different from an NftOutput or a BasicOutput as it contains the Alias object. In fact, the main purpose of claiming is extracting the Alias object from the AliasOutput.
    // 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()
};

Conversion of an Alias Output into a custom Object

We need to have a custom package prepared that contains a logic for converting an Alias into a new entity usable for your project.

In Stardust, an alias can be used for different purposes. One of them is acting as an NFT collection controller. In the following, an example of the process of converting a Stardust Alias into a CollectionControllerCap is outlined.

The following 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 make it possible to work 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, we can extract and use a Stardust Alias in a single transaction to create a CollectionControllerCap. This capability is then used in later transactions for managing 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.
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()
};