Skip to main content

Claiming Basic Outputs

Exchanges and dApp Devs Only

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.

Claiming might not be needed

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.

    // 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.

    // 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.

    // 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.

    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 succesful 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 netive 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()
};