Claiming Foundry 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.
In the Move-based IOTA ledger, Foundry Outputs don't have a direct equivalent representation.
Instead, claiming a Foundry Output involves extracting a CoinManagerTreasuryCap
from the AliasOutput
that originally controlled the Foundry in Stardust. This capability allows you to manage the supply of the Coin
created during migration to represent the native token controlled by the Foundry.
Steps to Claim a Foundry Output
1. Fetch the AliasOutput
The first step is to retrieve the AliasOutput
object that you intend to claim.
- TypeScript
- Rust
// This object id was fetched manually. It refers to an Alias Output object that
// contains a CoinManagerTreasuryCap (i.e., a Foundry representation).
const aliasOutputObjectId = "0xa58e9b6b85863e2fa50710c4594f701b2f5e2c6ff5e3c2b10cf09e6b18d740da";
const aliasOutputObject = await iotaClient.getObject({id: aliasOutputObjectId});
if (!aliasOutputObject) {
throw new Error("Alias output object not found");
}
// This object id was fetched manually. It refers to an Alias Output object that
// contains a CoinManagerTreasuryCap (i.e., a Foundry representation).
let alias_output_object_id = ObjectID::from_hex_literal(
"0xa58e9b6b85863e2fa50710c4594f701b2f5e2c6ff5e3c2b10cf09e6b18d740da",
)?;
let alias_output_object = iota_client
.read_api()
.get_object_with_options(
alias_output_object_id,
IotaObjectDataOptions::new().with_bcs(),
)
.await?
.data
.ok_or(anyhow!("alias output not found"))?;
let alias_output_object_ref = alias_output_object.object_ref();
2. Gather the Alias
Object
Next, use the dynamic field function with the "alias" dynamic field key filter to obtain the Alias
object itself.
- TypeScript
- Rust
// Get the dynamic field owned by the Alias Output, i.e., only the Alias
// object.
// The dynamic field name for the Alias object is "alias", of type vector<u8>.
const dfName = {
type: bcs.TypeTag.serialize({
vector: {
u8: true,
},
}).parse(),
value: "alias"
};
const aliasObject = await iotaClient.getDynamicFieldObject({ parentId: aliasOutputObjectId, name: dfName});
if (!aliasObject) {
throw new Error("Alias object not found");
}
// Get the object id of the Alias object.
const aliasObjectId = aliasObject.data?.objectId;
// Get the dynamic field owned by the Alias Output, i.e., only the Alias
// object.
// The dynamic field name for the Alias object is "alias", of type vector<u8>
let df_name = DynamicFieldName {
type_: TypeTag::Vector(Box::new(TypeTag::U8)),
value: serde_json::Value::String("alias".to_string()),
};
let alias_object = iota_client
.read_api()
.get_dynamic_field_object(alias_output_object_id, df_name)
.await?
.data
.ok_or(anyhow!("alias not found"))?;
let alias_object_ref = alias_object.object_ref();
3. Filter Owned Objects by Type
The Alias
object may own various other objects (for more details, refer to the Output Unlockable by an Alias/Nft Address page). In this step, you filter these objects by type, specifically looking for the CoinManagerTreasuryCap
type.
- TypeScript
- Rust
// Get the objects owned by the alias object and filter in the ones with
// CoinManagerTreasuryCap as type.
const aliasOwnedObjects = await iotaClient.getOwnedObjects({
owner: aliasObjectId ? aliasObjectId.toString() : "",
options: {
showBcs: true,
showType: true
},
});
// Only one page should exist.
assert.ok(!aliasOwnedObjects.hasNextPage, "Only one page should exist");
// Get the CoinManagerTreasuryCaps from the query.
const ownedCoinManagerTreasuryCaps = aliasOwnedObjects.data
.filter(object => {
return isCoinManagerTreasuryCap(object.data as IotaObjectData);
});
// Get only the first coin manager treasury cap.
const coinManagerTreasuryCap = ownedCoinManagerTreasuryCaps[0]?.data;
if (!coinManagerTreasuryCap) {
throw new Error("CoinManagerTreasuryCap not found");
}
const coinManagerTreasuryCapId = coinManagerTreasuryCap.objectId;
// Get the objects owned by the alias object and filter in the ones with
// CoinManagerTreasuryCap as type.
let alias_owned_objects_page = iota_client
.read_api()
.get_owned_objects(
alias_object_ref.0.into(),
Some(IotaObjectResponseQuery::new_with_options(
IotaObjectDataOptions::new().with_bcs().with_type(),
)),
None,
None,
)
.await?;
// Only one page should exist.
assert!(!alias_owned_objects_page.has_next_page);
// Get the CoinManagerTreasuryCaps from the query
let owned_coin_manager_treasury_caps = alias_owned_objects_page
.data
.into_iter()
.filter(|object| {
CoinManagerTreasuryCap::is_coin_manager_treasury_cap(
&object
.data
.as_ref()
.expect("the query should request the data")
.object_type()
.expect("should contain the type")
.try_into()
.expect("should convert into a struct tag"),
)
})
.collect::<Vec<_>>();
// Get only the first coin manager treasury cap
let coin_manager_treasury_cap_object = owned_coin_manager_treasury_caps
.into_iter()
.next()
.ok_or(anyhow!("no coin manager treasury caps found"))?
.data
.ok_or(anyhow!("coin manager treasury cap data not found"))?;
let coin_manager_treasury_cap_object_ref = coin_manager_treasury_cap_object.object_ref();
4. Extract the One-Time Witness (OTW)
Since each native token is tied to its own package, a Foundry's native token has a specific OTW
. Here, you will extract the OTW
from the CoinManagerTreasuryCap
object.
- TypeScript
- Rust
// Extract the foundry token type from the type parameters of the coin manager
// treasury cap object.
const foundryTokenTypeStructTag = coinManagerTreasuryCap.type
const foundryTokenType = foundryTokenTypeStructTag?.split("<")[1].split(">")[0] || "";
// Extract the foundry token type from the type parameters of the coin manager
// treasury cap object
let foundry_token_type_struct_tag: StructTag = coin_manager_treasury_cap_object
.object_type()
.expect("should contain the type")
.try_into()?;
let foundry_token_type = foundry_token_type_struct_tag
.type_params
.first()
.expect("should contain the type param");
5. Create the PTB to Claim the CoinManagerTreasuryCap
Finally, you should create a Programmable Transaction Block (PTB) that claims the CoinManagerTreasuryCap
associated with the Foundry Output from the AliasOutput
using the unlock_alias_address_owned_coinmanager_treasury
function.
- TypeScript
- Rust
// Create a PTB to claim the CoinManagerTreasuryCap related to the foundry
// output from 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.
const extractedBaseToken = extractedAliasOutputAssets[0];
const extractedNativeTokensBag = 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));
// In this example the native tokens bag is empty, so it can be destroyed.
tx.moveCall({
target: '0x2::bag::destroy_empty',
typeArguments: [],
arguments: [extractedNativeTokensBag],
});
// Extract the CoinManagerTreasuryCap.
const coinManagerTreasuryCapObject = tx.moveCall({
target: `${STARDUST_PACKAGE_ID}::address_unlock_condition::unlock_alias_address_owned_coinmanager_treasury`,
typeArguments: [foundryTokenType],
arguments: [alias, tx.object(coinManagerTreasuryCapId)],
});
// Transfer the coin manager treasury cap.
tx.transferObjects([coinManagerTreasuryCapObject], tx.pure.address(sender));
// Transfer the alias asset.
tx.transferObjects([alias], tx.pure.address(sender));
// Create a PTB to claim the CoinManagerTreasuryCap related to the foundry
// output from 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 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);
// In this example the native tokens bag is empty, so it can be destroyed.
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,
);
// Extract the CoinManagerTreasuryCap
let type_arguments = vec![foundry_token_type.clone()];
let arguments = vec![
extracted_alias,
builder.obj(ObjectArg::Receiving(coin_manager_treasury_cap_object_ref))?,
];
let coin_manager_treasury_cap = builder.programmable_move_call(
STARDUST_ADDRESS.into(),
ident_str!("address_unlock_condition").to_owned(),
ident_str!("unlock_alias_address_owned_coinmanager_treasury").to_owned(),
type_arguments,
arguments,
);
// Transfer the coin manager treasury cap.
builder.transfer_arg(sender, coin_manager_treasury_cap);
// Transfer the alias asset.
builder.transfer_arg(sender, extracted_alias);
}
builder.finish()
};