Claiming an Output Unlockable by an Alias/NFT Address
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 Stardust, outputs had an Address Unlock Condition or, in the case of Alias Outputs,
a Governor Address Unlock Condition.
In the new ledger, this mechanism is represented by an address owning the associated Output object.
Most of the time, the address is directly managed by a user through a keypair.
However, sometimes the address could represent another object.
In this case, that object owns the associated Output object.
After the Stardust migration, only Alias
and Nft
objects can own other Output objects.
Claiming an Output Owned by Another Alias/NFT Object
For this example, we use an AliasOutput
to extract an Alias
object that owns an NftOutput
.
- The first step is to fetch the
AliasOutput
object that is needed to claim theNftOutput
.
- TypeScript
- Rust
// This object id was fetched manually. It refers to an Alias Output object that
// owns a NftOutput.
const aliasOutputObjectId = "0x3b35e67750b8e4ccb45b2fc4a6a26a6d97e74c37a532f17177e6324ab93eaca6";
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
// owns a NftOutput.
let alias_output_object_id = ObjectID::from_hex_literal(
"0x3b35e67750b8e4ccb45b2fc4a6a26a6d97e74c37a532f17177e6324ab93eaca6",
)?;
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 output not found"))?;
let alias_output_object_ref = alias_output_object.object_ref();
- Use the dynamic field function with the "alias" dynamic field key filter to gather 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_address = alias_object.object_ref().0;
- Some objects are owned by the
Alias
object. In this case, filter them by type using theNftOutput
type tag. Apply the filter to getNftOutput
s owned by theAlias
.
- TypeScript
- Rust
// Some objects are owned by the Alias object. In this case we filter them by
// type using the NftOutput type.
const gasTypeTag = "0x2::iota::IOTA";
const nftOutputStructTag = `${STARDUST_PACKAGE_ID}::${NFT_OUTPUT_MODULE_NAME}::${NFT_OUTPUT_STRUCT_NAME}<${gasTypeTag}>`;
const ownedObjects = await iotaClient.getOwnedObjects({
owner: aliasObjectId ? aliasObjectId.toString() : "",
filter: {
StructType: nftOutputStructTag,
},
});
// Get the first NftOutput found
const nftOutputObjectOwnedByAlias = ownedObjects.data?.[0]?.data;
if (!nftOutputObjectOwnedByAlias) {
throw new Error("Owned NftOutput not found");
}
const nftOutputObjectId = nftOutputObjectOwnedByAlias.objectId;
// Some objects are owned by the Alias object. In this case we filter them by
// type using the NftOutput type.
let owned_objects_query_filter =
IotaObjectDataFilter::StructType(NftOutput::tag(GAS::type_tag()));
let owned_objects_query = IotaObjectResponseQuery::new(Some(owned_objects_query_filter), None);
// Get the first NftOutput found
let nft_output_object_owned_by_alias = iota_client
.read_api()
.get_owned_objects(
alias_object_address.into(),
Some(owned_objects_query),
None,
None,
)
.await?
.data
.into_iter()
.next()
.ok_or(anyhow!("Owned nft outputs not found"))?
.data
.ok_or(anyhow!("Nft output data not found"))?;
let nft_output_object_ref = nft_output_object_owned_by_alias.object_ref();
- Create a PTB that first extracts the assets from the
AliasOutput
, and then uses the extractedAlias
to "address unlock" theNftOutput
using the functionunlock_alias_address_owned_nft
.
- TypeScript
- Rust
// Create the ptb.
const tx = new Transaction();
// Extract alias output assets.
const typeArgs = [gasTypeTag];
const args = [tx.object(aliasOutputObjectId)]
const extractedAliasOutputAssets = tx.moveCall({
target: `${STARDUST_PACKAGE_ID}::alias_output::extract_assets`,
typeArguments: typeArgs,
arguments: args,
});
// Extract assets.
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: typeArgs,
arguments: [extractedBaseToken],
});
// Transfer the IOTA balance to the sender.
tx.transferObjects([iotaCoin], tx.pure.address(sender));
// Cleanup the bag by destroying it.
tx.moveCall({
target: '0x2::bag::destroy_empty',
typeArguments: [],
arguments: [extractedNativeTokensBag],
});
// Unlock the nft output.
const aliasArg = alias;
const nftOutputArg = tx.object(nftOutputObjectId);
const nftOutput = tx.moveCall({
target: `${STARDUST_PACKAGE_ID}::address_unlock_condition::unlock_alias_address_owned_nft`,
typeArguments: typeArgs,
arguments: [aliasArg, nftOutputArg],
});
// Transferring alias asset.
tx.transferObjects([alias], tx.pure.address(sender));
// Extract the assets from the NftOutput (base token, native tokens bag, nft asset itself).
const extractedAssets = tx.moveCall({
target: `${STARDUST_PACKAGE_ID}::nft_output::extract_assets`,
typeArguments: typeArgs,
arguments: [nftOutput],
});
// 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 extractedBaseToken2 = extractedAssets[0];
const extractedNativeTokensBag2 = extractedAssets[1];
const nftAsset = extractedAssets[2];
// Extract the IOTA balance.
const iotaCoin2 = tx.moveCall({
target: '0x2::coin::from_balance',
typeArguments: typeArgs,
arguments: [extractedBaseToken2],
});
// Transfer the IOTA balance to the sender.
tx.transferObjects([iotaCoin2], tx.pure.address(sender));
// Cleanup the bag because it is empty.
tx.moveCall({
target: '0x2::bag::destroy_empty',
typeArguments: [],
arguments: [extractedNativeTokensBag2],
});
// Transferring nft asset.
tx.transferObjects([nftAsset], tx.pure.address(sender));
let pt = {
let mut builder = ProgrammableTransactionBuilder::new();
// Extract alias output assets
let type_arguments = vec![GAS::type_tag()];
let arguments = vec![builder.obj(ObjectArg::ImmOrOwnedObject(alias_output_object_ref))?];
if let Argument::Result(extracted_alias_output_assets) = builder.programmable_move_call(
STARDUST_ADDRESS.into(),
ident_str!("alias_output").to_owned(),
ident_str!("extract_assets").to_owned(),
type_arguments,
arguments,
) {
let extracted_base_token = Argument::NestedResult(extracted_alias_output_assets, 0);
let extracted_native_tokens_bag =
Argument::NestedResult(extracted_alias_output_assets, 1);
let alias = Argument::NestedResult(extracted_alias_output_assets, 2);
let type_arguments = vec![GAS::type_tag()];
let arguments = vec![extracted_base_token];
// Extract the IOTA balance.
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);
// 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,
);
// Unlock the nft output.
let type_arguments = vec![GAS::type_tag()];
let arguments = vec![
alias,
builder.obj(ObjectArg::Receiving(nft_output_object_ref))?,
];
let nft_output = builder.programmable_move_call(
STARDUST_ADDRESS.into(),
ident_str!("address_unlock_condition").to_owned(),
ident_str!("unlock_alias_address_owned_nft").to_owned(),
type_arguments,
arguments,
);
// Transferring alias asset
builder.transfer_arg(sender, alias);
// Extract nft assets(base token, native tokens bag, nft asset itself).
let type_arguments = vec![GAS::type_tag()];
let arguments = vec![nft_output];
// Finally call the nft_output::extract_assets function
if let Argument::Result(extracted_assets) = builder.programmable_move_call(
STARDUST_ADDRESS.into(),
ident_str!("nft_output").to_owned(),
ident_str!("extract_assets").to_owned(),
type_arguments,
arguments,
) {
// 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.
let extracted_base_token = Argument::NestedResult(extracted_assets, 0);
let extracted_native_tokens_bag = Argument::NestedResult(extracted_assets, 1);
let nft_asset = Argument::NestedResult(extracted_assets, 2);
let type_arguments = vec![GAS::type_tag()];
let arguments = vec![extracted_base_token];
// Extract the IOTA balance.
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);
// Cleanup the bag because it is empty.
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,
);
// Transferring nft asset
builder.transfer_arg(sender, nft_asset);
}
}
builder.finish()
};