Skip to main content

Claiming Basic Outputs

An address can own BasicOutput objects that needs to be unlocked. In this case, some off-chain queries can be used to check the unlock conditions of the BasicOutput and assess if it can be unlocked.

    // 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 of a Basic Output

Once a Basic Output can be unlocked the claim of its assets can start

  1. The first step is to fetch the BasicOutput object that needs to be claimed.
    // 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,
)?;
  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 used as bag index. In the case of the native tokens Bag the 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<_>>(),
);
}
  1. Finally, a PTB can be created using the basic_output as input and the Bag keys for iterating the native tokens extracted.
    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()
};