Claiming Basic 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.
Most basic outputs have no special unlock conditions and don't need to be claimed. They will simply be available as normal Coin<IOTA>
objects for your account with no further action required. Manual claiming is only needed if special unlock conditions like unexpired timelocks apply.
As a user, you may own BasicOutput
objects that need to be unlocked before you can claim them. This guide walks you through the process of evaluating the unlock conditions for a BasicOutput
and the steps to claim the associated assets.
Assessing Unlock Conditions
To begin, you need to determine if your BasicOutput
can be unlocked. You can achieve this by performing specific off-chain queries that will check the unlock conditions for the BasicOutput
.
- TypeScript
- Rust
// This object id was fetched manually. It refers to a Basic Output object that
// contains some Native Tokens.
const basicOutputObjectId = "0xde09139ed46b9f5f876671e4403f312fad867c5ae5d300a252e4b6a6f1fa1fbd";
// Get Basic Output object.
const basicOutputObject = await iotaClient.getObject({id: basicOutputObjectId, options: { showContent: true }});
if (!basicOutputObject) {
throw new Error("Basic output object not found");
}
// Extract contents of the BasicOutput object.
const moveObject = basicOutputObject.data?.content as IotaParsedData;
if (moveObject.dataType != "moveObject") {
throw new Error("BasicOutput is not a move object");
}
// Treat fields as key-value object.
const fields = moveObject.fields as Record<string, any>;
console.log(fields);
const storageDepositReturnUc = fields['storage_deposit_return_uc'];
if (storageDepositReturnUc) {
console.log(`Storage Deposit Return Unlock Condition info: ${storageDepositReturnUc}`);
}
const timeLockUc = fields['time_lock_uc'];
if (timeLockUc) {
console.log(`Timelocked until: ${timeLockUc}`);
}
const expirationUc = fields['expiration_uc'];
if (expirationUc) {
console.log(`Expiration Unlock Condition info: ${expirationUc}`);
}
// This object id was fetched manually. It refers to a Basic Output object that
// contains some Native Tokens.
let basic_output_object_id = ObjectID::from_hex_literal(
"0xde09139ed46b9f5f876671e4403f312fad867c5ae5d300a252e4b6a6f1fa1fbd",
)?;
// Get Basic Output object
let basic_output_object = iota_client
.read_api()
.get_object_with_options(
basic_output_object_id,
IotaObjectDataOptions::new().with_bcs(),
)
.await?
.data
.ok_or(anyhow!("Basic output not found"))?;
// Convert the basic output object into its Rust representation
let basic_output = bcs::from_bytes::<BasicOutput>(
&basic_output_object
.bcs
.expect("should contain bcs")
.try_as_move()
.expect("should convert it to a move object")
.bcs_bytes,
)?;
println!("Basic Output infos: {basic_output:?}");
if let Some(sdruc) = basic_output.storage_deposit_return {
println!("Storage Deposit Return Unlock Condition infos: {sdruc:?}");
}
if let Some(tuc) = basic_output.timelock {
println!("Timelocked until: {}", tuc.unix_time);
}
if let Some(euc) = basic_output.expiration {
println!("Expiration Unlock Condition infos: {euc:?}");
}
Claim a Basic Output
Once you've confirmed that the BasicOutput
can be unlocked, you can start the process of claiming its assets.
1. Retrieve the BasicOutput
The first step is to fetch the BasicOutput
object that you intend to claim.
- TypeScript
- Rust
// This object id was fetched manually. It refers to a Basic Output object that
// contains some Native Tokens.
const basicOutputObjectId = "0xde09139ed46b9f5f876671e4403f312fad867c5ae5d300a252e4b6a6f1fa1fbd";
// Get Basic Output object.
const basicOutputObject = await iotaClient.getObject({id: basicOutputObjectId, options: { showContent: true }});
if (!basicOutputObject) {
throw new Error("Basic output object not found");
}
// Extract contents of the BasicOutput object.
const moveObject = basicOutputObject.data?.content as IotaParsedData;
if (moveObject.dataType != "moveObject") {
throw new Error("BasicOutput is not a move object");
}
// Treat fields as key-value object.
const fields = moveObject.fields as Record<string, any>;
// This object id was fetched manually. It refers to a Basic Output object that
// contains some Native Tokens.
let basic_output_object_id = ObjectID::from_hex_literal(
"0xde09139ed46b9f5f876671e4403f312fad867c5ae5d300a252e4b6a6f1fa1fbd",
)?;
// Get Basic Output object
let basic_output_object = iota_client
.read_api()
.get_object_with_options(
basic_output_object_id,
IotaObjectDataOptions::new().with_bcs(),
)
.await?
.data
.ok_or(anyhow!("Basic output not found"))?;
let basic_output_object_ref = basic_output_object.object_ref();
// Convert the basic output object into its Rust representation
let basic_output = bcs::from_bytes::<BasicOutput>(
&basic_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
After fetching the BasicOutput
, you need to check for any native tokens that might be stored within it.
These tokens are typically stored in a Bag
.
You'll need to obtain the dynamic field keys used as bag indexes to access the native tokens.
For native tokens,
these keys are strings representing the OTW
used for the native token Coin
.
- 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 this is not empty; here 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 = basic_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?;
// should have only one page
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_owned()
})
.collect::<Vec<_>>(),
);
}
3. Construct the PTB
Finally, you can create a Programmable Transaction Block (PTB) using the basic_output
as an input,
along with the Bag
keys to iterate over the extracted native tokens.
- TypeScript
- Rust
// Create a PTB to claim the assets related to the basic output.
const tx = new Transaction();
////// Command #1: extract the base token and native tokens bag.
// Type argument for a Basic Output coming from the IOTA network, i.e., the IOTA
// token or Gas type tag
const gasTypeTag = "0x2::iota::IOTA";
// Then pass the basic output object as input.
const args = [tx.object(basicOutputObjectId)];
// Finally call the basic_output::extract_assets function.
const extractedBasicOutputAssets = tx.moveCall({
target: `${STARDUST_PACKAGE_ID}::basic_output::extract_assets`,
typeArguments: [gasTypeTag],
arguments: args,
});
// If the basic 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.
const extractedBaseToken = extractedBasicOutputAssets[0];
let extractedNativeTokensBag: any = extractedBasicOutputAssets[1];
// Extract the IOTA balance.
const iotaCoin = tx.moveCall({
target: '0x2::coin::from_balance',
typeArguments: [gasTypeTag],
arguments: [extractedBaseToken],
});
// Send back the base token coin to the user.
tx.transferObjects([iotaCoin], tx.pure.address(sender));
////// Extract the native tokens from the Bag and send them to sender.
for (const typeKey of dfTypeKeys) {
// Type argument for a Native Token contained in the basic output bag.
const typeArguments = [`0x${typeKey}`];
// Then pass the the bag and the receiver address as input.
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],
});
let pt = {
// Init the builder
let mut builder = ProgrammableTransactionBuilder::new();
////// Command #1: extract the base token and native tokens bag.
// Type argument for a Basic Output coming from the IOTA network, i.e., the IOTA
// token or Gas type tag
let type_arguments = vec![GAS::type_tag()];
// Then pass the basic output object as input
let arguments = vec![builder.obj(ObjectArg::ImmOrOwnedObject(basic_output_object_ref))?];
// Finally call the basic_output::extract_assets function
if let Argument::Result(extracted_assets) = builder.programmable_move_call(
STARDUST_ADDRESS.into(),
ident_str!("basic_output").to_owned(),
ident_str!("extract_assets").to_owned(),
type_arguments,
arguments,
) {
// If the basic 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
let extracted_base_token = Argument::NestedResult(extracted_assets, 0);
let mut extracted_native_tokens_bag = Argument::NestedResult(extracted_assets, 1);
////// Command #2: extract the native tokens from the Bag and send them to sender.
for type_key in df_type_keys {
// Type argument for a Native Token contained in the basic output bag
let type_arguments = vec![TypeTag::from_str(&format!("0x{type_key}"))?];
// Then pass the the bag and the receiver address as input
let arguments = vec![extracted_native_tokens_bag, builder.pure(sender)?];
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,
);
}
////// Command #3: delete 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,
);
////// Command #4: create a coin from the extracted IOTA balance
// Type argument for the IOTA coin
let type_arguments = vec![GAS::type_tag()];
let arguments = vec![extracted_base_token];
let new_iota_coin = builder.programmable_move_call(
IOTA_FRAMEWORK_ADDRESS.into(),
ident_str!("coin").to_owned(),
ident_str!("from_balance").to_owned(),
type_arguments,
arguments,
);
////// Command #5: send back the base token coin to the user.
builder.transfer_arg(sender, new_iota_coin)
}
builder.finish()
};