diff --git a/crates/iota-transaction-builder/src/lib.rs b/crates/iota-transaction-builder/src/lib.rs index 55edeb6f1dc..401ebd69933 100644 --- a/crates/iota-transaction-builder/src/lib.rs +++ b/crates/iota-transaction-builder/src/lib.rs @@ -2,47 +2,29 @@ // Modifications Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use std::{collections::BTreeMap, result::Result, str::FromStr, sync::Arc}; +pub mod package; +pub mod stake; +pub mod utils; -use anyhow::{Ok, anyhow, bail, ensure}; +use std::{result::Result, str::FromStr, sync::Arc}; + +use anyhow::{Ok, anyhow, bail}; use async_trait::async_trait; -use futures::future::join_all; -use iota_json::{ - IotaJsonValue, ResolvedCallArg, is_receiving_argument, resolve_move_function_args, -}; +use iota_json::IotaJsonValue; use iota_json_rpc_types::{ - IotaData, IotaObjectDataOptions, IotaObjectResponse, IotaRawData, IotaTypeTag, - RPCTransactionRequestParams, + IotaObjectDataOptions, IotaObjectResponse, IotaTypeTag, RPCTransactionRequestParams, }; -use iota_protocol_config::ProtocolConfig; use iota_types::{ - IOTA_FRAMEWORK_PACKAGE_ID, IOTA_SYSTEM_PACKAGE_ID, - base_types::{IotaAddress, ObjectID, ObjectInfo, ObjectRef, ObjectType}, + IOTA_FRAMEWORK_PACKAGE_ID, + base_types::{IotaAddress, ObjectID, ObjectInfo}, coin, error::UserInputError, fp_ensure, - gas_coin::GasCoin, - governance::{ADD_STAKE_MUL_COIN_FUN_NAME, WITHDRAW_STAKE_FUN_NAME}, - iota_system_state::IOTA_SYSTEM_MODULE_NAME, - move_package::MovePackage, - object::{Object, Owner}, + object::Object, programmable_transaction_builder::ProgrammableTransactionBuilder, - timelock::timelocked_staking::{ - ADD_TIMELOCKED_STAKE_FUN_NAME, TIMELOCKED_STAKING_MODULE_NAME, - WITHDRAW_TIMELOCKED_STAKE_FUN_NAME, - }, - transaction::{ - Argument, CallArg, Command, InputObjectKind, ObjectArg, TransactionData, TransactionKind, - }, -}; -use move_binary_format::{ - CompiledModule, binary_config::BinaryConfig, file_format::SignatureToken, -}; -use move_core_types::{ - ident_str, - identifier::Identifier, - language_storage::{StructTag, TypeTag}, + transaction::{CallArg, Command, InputObjectKind, ObjectArg, TransactionData, TransactionKind}, }; +use move_core_types::{identifier::Identifier, language_storage::StructTag}; #[async_trait] pub trait DataReader { @@ -69,49 +51,6 @@ impl TransactionBuilder { Self(data_reader) } - /// Select a gas coin for the provided gas budget. - async fn select_gas( - &self, - signer: IotaAddress, - input_gas: impl Into>, - gas_budget: u64, - input_objects: Vec, - gas_price: u64, - ) -> Result { - if gas_budget < gas_price { - bail!( - "Gas budget {gas_budget} is less than the reference gas price {gas_price}. The gas budget must be at least the current reference gas price of {gas_price}." - ) - } - if let Some(gas) = input_gas.into() { - self.get_object_ref(gas).await - } else { - let gas_objs = self.0.get_owned_objects(signer, GasCoin::type_()).await?; - - for obj in gas_objs { - let response = self - .0 - .get_object_with_options(obj.object_id, IotaObjectDataOptions::new().with_bcs()) - .await?; - let obj = response.object()?; - let gas: GasCoin = bcs::from_bytes( - &obj.bcs - .as_ref() - .ok_or_else(|| anyhow!("bcs field is unexpectedly empty"))? - .try_as_move() - .ok_or_else(|| anyhow!("Cannot parse move object to gas object"))? - .bcs_bytes, - )?; - if !input_objects.contains(&obj.object_id) && gas.value() >= gas_budget { - return Ok(obj.object_ref()); - } - } - Err(anyhow!( - "Cannot find gas coin for signer address {signer} with amount sufficient for the required gas budget {gas_budget}. If you are using the pay or transfer commands, you can use pay-iota or transfer-iota commands instead, which will use the only object as gas payment." - )) - } - } - /// Construct the transaction data for a dry run pub async fn tx_data_for_dry_run( &self, @@ -319,16 +258,6 @@ impl TransactionBuilder { ) } - /// Get the object references for a list of object IDs - pub async fn input_refs(&self, obj_ids: &[ObjectID]) -> Result, anyhow::Error> { - let handles: Vec<_> = obj_ids.iter().map(|id| self.get_object_ref(*id)).collect(); - let obj_refs = join_all(handles) - .await - .into_iter() - .collect::>>()?; - Ok(obj_refs) - } - /// Construct a transaction kind for the PayIota transaction type. /// /// Use this function together with tx_data_for_dry_run or tx_data @@ -537,277 +466,6 @@ impl TransactionBuilder { Ok(()) } - /// Resolve a provided [`ObjectID`] to the required [`ObjectArg`] for a - /// given move module. - async fn get_object_arg( - &self, - id: ObjectID, - objects: &mut BTreeMap, - is_mutable_ref: bool, - view: &CompiledModule, - arg_type: &SignatureToken, - ) -> Result { - let response = self - .0 - .get_object_with_options(id, IotaObjectDataOptions::bcs_lossless()) - .await?; - - let obj: Object = response.into_object()?.try_into()?; - let obj_ref = obj.compute_object_reference(); - let owner = obj.owner; - objects.insert(id, obj); - if is_receiving_argument(view, arg_type) { - return Ok(ObjectArg::Receiving(obj_ref)); - } - Ok(match owner { - Owner::Shared { - initial_shared_version, - } => ObjectArg::SharedObject { - id, - initial_shared_version, - mutable: is_mutable_ref, - }, - Owner::AddressOwner(_) | Owner::ObjectOwner(_) | Owner::Immutable => { - ObjectArg::ImmOrOwnedObject(obj_ref) - } - }) - } - - /// Convert provided JSON arguments for a move function to their - /// [`Argument`] representation and check their validity. - pub async fn resolve_and_checks_json_args( - &self, - builder: &mut ProgrammableTransactionBuilder, - package_id: ObjectID, - module: &Identifier, - function: &Identifier, - type_args: &[TypeTag], - json_args: Vec, - ) -> Result, anyhow::Error> { - let object = self - .0 - .get_object_with_options(package_id, IotaObjectDataOptions::bcs_lossless()) - .await? - .into_object()?; - let Some(IotaRawData::Package(package)) = object.bcs else { - bail!( - "Bcs field in object [{}] is missing or not a package.", - package_id - ); - }; - let package: MovePackage = MovePackage::new( - package.id, - object.version, - package.module_map, - ProtocolConfig::get_for_min_version().max_move_package_size(), - package.type_origin_table, - package.linkage_table, - )?; - - let json_args_and_tokens = resolve_move_function_args( - &package, - module.clone(), - function.clone(), - type_args, - json_args, - )?; - - let mut args = Vec::new(); - let mut objects = BTreeMap::new(); - let module = package.deserialize_module(module, &BinaryConfig::standard())?; - for (arg, expected_type) in json_args_and_tokens { - args.push(match arg { - ResolvedCallArg::Pure(p) => builder.input(CallArg::Pure(p)), - - ResolvedCallArg::Object(id) => builder.input(CallArg::Object( - self.get_object_arg( - id, - &mut objects, - // Is mutable if passed by mutable reference or by value - matches!(expected_type, SignatureToken::MutableReference(_)) - || !expected_type.is_reference(), - &module, - &expected_type, - ) - .await?, - )), - - ResolvedCallArg::ObjVec(v) => { - let mut object_ids = vec![]; - for id in v { - object_ids.push( - self.get_object_arg( - id, - &mut objects, - // is_mutable_ref - false, - &module, - &expected_type, - ) - .await?, - ) - } - builder.make_obj_vec(object_ids) - } - }?); - } - - Ok(args) - } - - /// Build a [`TransactionKind::ProgrammableTransaction`] that contains - /// [`Command::Publish`] for the provided package. - pub async fn publish_tx_kind( - &self, - sender: IotaAddress, - modules: Vec>, - dep_ids: Vec, - ) -> Result { - let pt = { - let mut builder = ProgrammableTransactionBuilder::new(); - let upgrade_cap = builder.publish_upgradeable(modules, dep_ids); - builder.transfer_arg(sender, upgrade_cap); - builder.finish() - }; - Ok(TransactionKind::programmable(pt)) - } - - /// Publish a new move package. - pub async fn publish( - &self, - sender: IotaAddress, - compiled_modules: Vec>, - dep_ids: Vec, - gas: impl Into>, - gas_budget: u64, - ) -> anyhow::Result { - let gas_price = self.0.get_reference_gas_price().await?; - let gas = self - .select_gas(sender, gas, gas_budget, vec![], gas_price) - .await?; - Ok(TransactionData::new_module( - sender, - gas, - compiled_modules, - dep_ids, - gas_budget, - gas_price, - )) - } - - /// Build a [`TransactionKind::ProgrammableTransaction`] that contains - /// [`Command::Upgrade`] for the provided package. - pub async fn upgrade_tx_kind( - &self, - package_id: ObjectID, - modules: Vec>, - dep_ids: Vec, - upgrade_capability: ObjectID, - upgrade_policy: u8, - digest: Vec, - ) -> Result { - let upgrade_capability = self - .0 - .get_object_with_options( - upgrade_capability, - IotaObjectDataOptions::new().with_owner(), - ) - .await? - .into_object()?; - let capability_owner = upgrade_capability - .owner - .ok_or_else(|| anyhow!("Unable to determine ownership of upgrade capability"))?; - let pt = { - let mut builder = ProgrammableTransactionBuilder::new(); - let capability_arg = match capability_owner { - Owner::AddressOwner(_) => { - ObjectArg::ImmOrOwnedObject(upgrade_capability.object_ref()) - } - Owner::Shared { - initial_shared_version, - } => ObjectArg::SharedObject { - id: upgrade_capability.object_ref().0, - initial_shared_version, - mutable: true, - }, - Owner::Immutable => { - bail!("Upgrade capability is stored immutably and cannot be used for upgrades") - } - // If the capability is owned by an object, then the module defining the owning - // object gets to decide how the upgrade capability should be used. - Owner::ObjectOwner(_) => { - return Err(anyhow::anyhow!("Upgrade capability controlled by object")); - } - }; - builder.obj(capability_arg).unwrap(); - let upgrade_arg = builder.pure(upgrade_policy).unwrap(); - let digest_arg = builder.pure(digest).unwrap(); - let upgrade_ticket = builder.programmable_move_call( - IOTA_FRAMEWORK_PACKAGE_ID, - ident_str!("package").to_owned(), - ident_str!("authorize_upgrade").to_owned(), - vec![], - vec![Argument::Input(0), upgrade_arg, digest_arg], - ); - let upgrade_receipt = builder.upgrade(package_id, upgrade_ticket, dep_ids, modules); - - builder.programmable_move_call( - IOTA_FRAMEWORK_PACKAGE_ID, - ident_str!("package").to_owned(), - ident_str!("commit_upgrade").to_owned(), - vec![], - vec![Argument::Input(0), upgrade_receipt], - ); - - builder.finish() - }; - - Ok(TransactionKind::programmable(pt)) - } - - /// Upgrade an existing move package. - pub async fn upgrade( - &self, - sender: IotaAddress, - package_id: ObjectID, - compiled_modules: Vec>, - dep_ids: Vec, - upgrade_capability: ObjectID, - upgrade_policy: u8, - gas: impl Into>, - gas_budget: u64, - ) -> anyhow::Result { - let gas_price = self.0.get_reference_gas_price().await?; - let gas = self - .select_gas(sender, gas, gas_budget, vec![], gas_price) - .await?; - let upgrade_cap = self - .0 - .get_object_with_options( - upgrade_capability, - IotaObjectDataOptions::new().with_owner(), - ) - .await? - .into_object()?; - let cap_owner = upgrade_cap - .owner - .ok_or_else(|| anyhow!("Unable to determine ownership of upgrade capability"))?; - let digest = - MovePackage::compute_digest_for_modules_and_deps(&compiled_modules, &dep_ids).to_vec(); - TransactionData::new_upgrade( - sender, - gas, - package_id, - compiled_modules, - dep_ids, - (upgrade_cap.object_ref(), cap_owner), - upgrade_policy, - digest, - gas_budget, - gas_price, - ) - } - /// Construct a transaction kind for the SplitCoin transaction type /// It expects that only one of the two: split_amounts or split_count is /// provided If both are provided, it will use split_amounts. @@ -1070,203 +728,4 @@ impl TransactionBuilder { gas_price, )) } - - /// Add stake to a validator's staking pool using multiple IOTA coins. - pub async fn request_add_stake( - &self, - signer: IotaAddress, - mut coins: Vec, - amount: impl Into>, - validator: IotaAddress, - gas: impl Into>, - gas_budget: u64, - ) -> anyhow::Result { - let gas_price = self.0.get_reference_gas_price().await?; - let gas = self - .select_gas(signer, gas, gas_budget, coins.clone(), gas_price) - .await?; - - let mut obj_vec = vec![]; - let coin = coins - .pop() - .ok_or_else(|| anyhow!("Coins input should contain at lease one coin object."))?; - let (oref, coin_type) = self.get_object_ref_and_type(coin).await?; - - let ObjectType::Struct(type_) = &coin_type else { - return Err(anyhow!("Provided object [{coin}] is not a move object.")); - }; - ensure!( - type_.is_coin(), - "Expecting either Coin input coin objects. Received [{type_}]" - ); - - for coin in coins { - let (oref, type_) = self.get_object_ref_and_type(coin).await?; - ensure!( - type_ == coin_type, - "All coins should be the same type, expecting {coin_type}, got {type_}." - ); - obj_vec.push(ObjectArg::ImmOrOwnedObject(oref)) - } - obj_vec.push(ObjectArg::ImmOrOwnedObject(oref)); - - let pt = { - let mut builder = ProgrammableTransactionBuilder::new(); - let arguments = vec![ - builder.input(CallArg::IOTA_SYSTEM_MUT).unwrap(), - builder.make_obj_vec(obj_vec)?, - builder - .input(CallArg::Pure(bcs::to_bytes(&amount.into())?)) - .unwrap(), - builder - .input(CallArg::Pure(bcs::to_bytes(&validator)?)) - .unwrap(), - ]; - builder.command(Command::move_call( - IOTA_SYSTEM_PACKAGE_ID, - IOTA_SYSTEM_MODULE_NAME.to_owned(), - ADD_STAKE_MUL_COIN_FUN_NAME.to_owned(), - vec![], - arguments, - )); - builder.finish() - }; - Ok(TransactionData::new_programmable( - signer, - vec![gas], - pt, - gas_budget, - gas_price, - )) - } - - /// Withdraw stake from a validator's staking pool. - pub async fn request_withdraw_stake( - &self, - signer: IotaAddress, - staked_iota: ObjectID, - gas: impl Into>, - gas_budget: u64, - ) -> anyhow::Result { - let staked_iota = self.get_object_ref(staked_iota).await?; - let gas_price = self.0.get_reference_gas_price().await?; - let gas = self - .select_gas(signer, gas, gas_budget, vec![], gas_price) - .await?; - TransactionData::new_move_call( - signer, - IOTA_SYSTEM_PACKAGE_ID, - IOTA_SYSTEM_MODULE_NAME.to_owned(), - WITHDRAW_STAKE_FUN_NAME.to_owned(), - vec![], - gas, - vec![ - CallArg::IOTA_SYSTEM_MUT, - CallArg::Object(ObjectArg::ImmOrOwnedObject(staked_iota)), - ], - gas_budget, - gas_price, - ) - } - - /// Add stake to a validator's staking pool using a timelocked IOTA coin. - pub async fn request_add_timelocked_stake( - &self, - signer: IotaAddress, - locked_balance: ObjectID, - validator: IotaAddress, - gas: ObjectID, - gas_budget: u64, - ) -> anyhow::Result { - let gas_price = self.0.get_reference_gas_price().await?; - let gas = self - .select_gas(signer, Some(gas), gas_budget, vec![], gas_price) - .await?; - - let (oref, locked_balance_type) = self.get_object_ref_and_type(locked_balance).await?; - - let ObjectType::Struct(type_) = &locked_balance_type else { - anyhow::bail!("Provided object [{locked_balance}] is not a move object."); - }; - ensure!( - type_.is_timelocked_balance(), - "Expecting either TimeLock> input objects. Received [{type_}]" - ); - - let pt = { - let mut builder = ProgrammableTransactionBuilder::new(); - let arguments = vec![ - builder.input(CallArg::IOTA_SYSTEM_MUT)?, - builder.input(CallArg::Object(ObjectArg::ImmOrOwnedObject(oref)))?, - builder.input(CallArg::Pure(bcs::to_bytes(&validator)?))?, - ]; - builder.command(Command::move_call( - IOTA_SYSTEM_PACKAGE_ID, - TIMELOCKED_STAKING_MODULE_NAME.to_owned(), - ADD_TIMELOCKED_STAKE_FUN_NAME.to_owned(), - vec![], - arguments, - )); - builder.finish() - }; - Ok(TransactionData::new_programmable( - signer, - vec![gas], - pt, - gas_budget, - gas_price, - )) - } - - /// Withdraw timelocked stake from a validator's staking pool. - pub async fn request_withdraw_timelocked_stake( - &self, - signer: IotaAddress, - timelocked_staked_iota: ObjectID, - gas: ObjectID, - gas_budget: u64, - ) -> anyhow::Result { - let timelocked_staked_iota = self.get_object_ref(timelocked_staked_iota).await?; - let gas_price = self.0.get_reference_gas_price().await?; - let gas = self - .select_gas(signer, Some(gas), gas_budget, vec![], gas_price) - .await?; - TransactionData::new_move_call( - signer, - IOTA_SYSTEM_PACKAGE_ID, - TIMELOCKED_STAKING_MODULE_NAME.to_owned(), - WITHDRAW_TIMELOCKED_STAKE_FUN_NAME.to_owned(), - vec![], - gas, - vec![ - CallArg::IOTA_SYSTEM_MUT, - CallArg::Object(ObjectArg::ImmOrOwnedObject(timelocked_staked_iota)), - ], - gas_budget, - gas_price, - ) - } - - /// Get the latest object ref for an object. - pub async fn get_object_ref(&self, object_id: ObjectID) -> anyhow::Result { - // TODO: we should add retrial to reduce the transaction building error rate - self.get_object_ref_and_type(object_id) - .await - .map(|(oref, _)| oref) - } - - /// Helper function to get the latest ObjectRef (ObjectID, SequenceNumber, - /// ObjectDigest) and ObjectType for a provided ObjectID. - async fn get_object_ref_and_type( - &self, - object_id: ObjectID, - ) -> anyhow::Result<(ObjectRef, ObjectType)> { - let object = self - .0 - .get_object_with_options(object_id, IotaObjectDataOptions::new().with_type()) - .await? - .into_object()?; - - Ok((object.object_ref(), object.object_type()?)) - } } diff --git a/crates/iota-transaction-builder/src/package.rs b/crates/iota-transaction-builder/src/package.rs new file mode 100644 index 00000000000..062116621f7 --- /dev/null +++ b/crates/iota-transaction-builder/src/package.rs @@ -0,0 +1,174 @@ +// Copyright (c) Mysten Labs, Inc. +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::result::Result; + +use anyhow::{Ok, anyhow, bail}; +use iota_json_rpc_types::IotaObjectDataOptions; +use iota_types::{ + IOTA_FRAMEWORK_PACKAGE_ID, + base_types::{IotaAddress, ObjectID}, + move_package::MovePackage, + object::Owner, + programmable_transaction_builder::ProgrammableTransactionBuilder, + transaction::{Argument, ObjectArg, TransactionData, TransactionKind}, +}; +use move_core_types::ident_str; + +use crate::TransactionBuilder; + +impl TransactionBuilder { + /// Build a [`TransactionKind::ProgrammableTransaction`] that contains + /// [`iota_types::transaction::Command::Publish`] for the provided package. + pub async fn publish_tx_kind( + &self, + sender: IotaAddress, + modules: Vec>, + dep_ids: Vec, + ) -> Result { + let pt = { + let mut builder = ProgrammableTransactionBuilder::new(); + let upgrade_cap = builder.publish_upgradeable(modules, dep_ids); + builder.transfer_arg(sender, upgrade_cap); + builder.finish() + }; + Ok(TransactionKind::programmable(pt)) + } + + /// Publish a new move package. + pub async fn publish( + &self, + sender: IotaAddress, + compiled_modules: Vec>, + dep_ids: Vec, + gas: impl Into>, + gas_budget: u64, + ) -> anyhow::Result { + let gas_price = self.0.get_reference_gas_price().await?; + let gas = self + .select_gas(sender, gas, gas_budget, vec![], gas_price) + .await?; + Ok(TransactionData::new_module( + sender, + gas, + compiled_modules, + dep_ids, + gas_budget, + gas_price, + )) + } + + /// Build a [`TransactionKind::ProgrammableTransaction`] that contains + /// [`iota_types::transaction::Command::Upgrade`] for the provided package. + pub async fn upgrade_tx_kind( + &self, + package_id: ObjectID, + modules: Vec>, + dep_ids: Vec, + upgrade_capability: ObjectID, + upgrade_policy: u8, + digest: Vec, + ) -> Result { + let upgrade_capability = self + .0 + .get_object_with_options( + upgrade_capability, + IotaObjectDataOptions::new().with_owner(), + ) + .await? + .into_object()?; + let capability_owner = upgrade_capability + .owner + .ok_or_else(|| anyhow!("Unable to determine ownership of upgrade capability"))?; + let pt = { + let mut builder = ProgrammableTransactionBuilder::new(); + let capability_arg = match capability_owner { + Owner::AddressOwner(_) => { + ObjectArg::ImmOrOwnedObject(upgrade_capability.object_ref()) + } + Owner::Shared { + initial_shared_version, + } => ObjectArg::SharedObject { + id: upgrade_capability.object_ref().0, + initial_shared_version, + mutable: true, + }, + Owner::Immutable => { + bail!("Upgrade capability is stored immutably and cannot be used for upgrades") + } + // If the capability is owned by an object, then the module defining the owning + // object gets to decide how the upgrade capability should be used. + Owner::ObjectOwner(_) => { + return Err(anyhow::anyhow!("Upgrade capability controlled by object")); + } + }; + builder.obj(capability_arg).unwrap(); + let upgrade_arg = builder.pure(upgrade_policy).unwrap(); + let digest_arg = builder.pure(digest).unwrap(); + let upgrade_ticket = builder.programmable_move_call( + IOTA_FRAMEWORK_PACKAGE_ID, + ident_str!("package").to_owned(), + ident_str!("authorize_upgrade").to_owned(), + vec![], + vec![Argument::Input(0), upgrade_arg, digest_arg], + ); + let upgrade_receipt = builder.upgrade(package_id, upgrade_ticket, dep_ids, modules); + + builder.programmable_move_call( + IOTA_FRAMEWORK_PACKAGE_ID, + ident_str!("package").to_owned(), + ident_str!("commit_upgrade").to_owned(), + vec![], + vec![Argument::Input(0), upgrade_receipt], + ); + + builder.finish() + }; + + Ok(TransactionKind::programmable(pt)) + } + + /// Upgrade an existing move package. + pub async fn upgrade( + &self, + sender: IotaAddress, + package_id: ObjectID, + compiled_modules: Vec>, + dep_ids: Vec, + upgrade_capability: ObjectID, + upgrade_policy: u8, + gas: impl Into>, + gas_budget: u64, + ) -> anyhow::Result { + let gas_price = self.0.get_reference_gas_price().await?; + let gas = self + .select_gas(sender, gas, gas_budget, vec![], gas_price) + .await?; + let upgrade_cap = self + .0 + .get_object_with_options( + upgrade_capability, + IotaObjectDataOptions::new().with_owner(), + ) + .await? + .into_object()?; + let cap_owner = upgrade_cap + .owner + .ok_or_else(|| anyhow!("Unable to determine ownership of upgrade capability"))?; + let digest = + MovePackage::compute_digest_for_modules_and_deps(&compiled_modules, &dep_ids).to_vec(); + TransactionData::new_upgrade( + sender, + gas, + package_id, + compiled_modules, + dep_ids, + (upgrade_cap.object_ref(), cap_owner), + upgrade_policy, + digest, + gas_budget, + gas_price, + ) + } +} diff --git a/crates/iota-transaction-builder/src/stake.rs b/crates/iota-transaction-builder/src/stake.rs new file mode 100644 index 00000000000..b8ab6b41cfd --- /dev/null +++ b/crates/iota-transaction-builder/src/stake.rs @@ -0,0 +1,197 @@ +// Copyright (c) Mysten Labs, Inc. +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use anyhow::{Ok, anyhow, ensure}; +use iota_types::{ + IOTA_SYSTEM_PACKAGE_ID, + base_types::{IotaAddress, ObjectID, ObjectType}, + governance::{ADD_STAKE_MUL_COIN_FUN_NAME, WITHDRAW_STAKE_FUN_NAME}, + iota_system_state::IOTA_SYSTEM_MODULE_NAME, + programmable_transaction_builder::ProgrammableTransactionBuilder, + timelock::timelocked_staking::{ + ADD_TIMELOCKED_STAKE_FUN_NAME, TIMELOCKED_STAKING_MODULE_NAME, + WITHDRAW_TIMELOCKED_STAKE_FUN_NAME, + }, + transaction::{CallArg, Command, ObjectArg, TransactionData}, +}; + +use crate::TransactionBuilder; + +impl TransactionBuilder { + /// Add stake to a validator's staking pool using multiple IOTA coins. + pub async fn request_add_stake( + &self, + signer: IotaAddress, + mut coins: Vec, + amount: impl Into>, + validator: IotaAddress, + gas: impl Into>, + gas_budget: u64, + ) -> anyhow::Result { + let gas_price = self.0.get_reference_gas_price().await?; + let gas = self + .select_gas(signer, gas, gas_budget, coins.clone(), gas_price) + .await?; + + let mut obj_vec = vec![]; + let coin = coins + .pop() + .ok_or_else(|| anyhow!("Coins input should contain at lease one coin object."))?; + let (oref, coin_type) = self.get_object_ref_and_type(coin).await?; + + let ObjectType::Struct(type_) = &coin_type else { + return Err(anyhow!("Provided object [{coin}] is not a move object.")); + }; + ensure!( + type_.is_coin(), + "Expecting either Coin input coin objects. Received [{type_}]" + ); + + for coin in coins { + let (oref, type_) = self.get_object_ref_and_type(coin).await?; + ensure!( + type_ == coin_type, + "All coins should be the same type, expecting {coin_type}, got {type_}." + ); + obj_vec.push(ObjectArg::ImmOrOwnedObject(oref)) + } + obj_vec.push(ObjectArg::ImmOrOwnedObject(oref)); + + let pt = { + let mut builder = ProgrammableTransactionBuilder::new(); + let arguments = vec![ + builder.input(CallArg::IOTA_SYSTEM_MUT).unwrap(), + builder.make_obj_vec(obj_vec)?, + builder + .input(CallArg::Pure(bcs::to_bytes(&amount.into())?)) + .unwrap(), + builder + .input(CallArg::Pure(bcs::to_bytes(&validator)?)) + .unwrap(), + ]; + builder.command(Command::move_call( + IOTA_SYSTEM_PACKAGE_ID, + IOTA_SYSTEM_MODULE_NAME.to_owned(), + ADD_STAKE_MUL_COIN_FUN_NAME.to_owned(), + vec![], + arguments, + )); + builder.finish() + }; + Ok(TransactionData::new_programmable( + signer, + vec![gas], + pt, + gas_budget, + gas_price, + )) + } + + /// Withdraw stake from a validator's staking pool. + pub async fn request_withdraw_stake( + &self, + signer: IotaAddress, + staked_iota: ObjectID, + gas: impl Into>, + gas_budget: u64, + ) -> anyhow::Result { + let staked_iota = self.get_object_ref(staked_iota).await?; + let gas_price = self.0.get_reference_gas_price().await?; + let gas = self + .select_gas(signer, gas, gas_budget, vec![], gas_price) + .await?; + TransactionData::new_move_call( + signer, + IOTA_SYSTEM_PACKAGE_ID, + IOTA_SYSTEM_MODULE_NAME.to_owned(), + WITHDRAW_STAKE_FUN_NAME.to_owned(), + vec![], + gas, + vec![ + CallArg::IOTA_SYSTEM_MUT, + CallArg::Object(ObjectArg::ImmOrOwnedObject(staked_iota)), + ], + gas_budget, + gas_price, + ) + } + + /// Add stake to a validator's staking pool using a timelocked IOTA coin. + pub async fn request_add_timelocked_stake( + &self, + signer: IotaAddress, + locked_balance: ObjectID, + validator: IotaAddress, + gas: ObjectID, + gas_budget: u64, + ) -> anyhow::Result { + let gas_price = self.0.get_reference_gas_price().await?; + let gas = self + .select_gas(signer, Some(gas), gas_budget, vec![], gas_price) + .await?; + + let (oref, locked_balance_type) = self.get_object_ref_and_type(locked_balance).await?; + + let ObjectType::Struct(type_) = &locked_balance_type else { + anyhow::bail!("Provided object [{locked_balance}] is not a move object."); + }; + ensure!( + type_.is_timelocked_balance(), + "Expecting either TimeLock> input objects. Received [{type_}]" + ); + + let pt = { + let mut builder = ProgrammableTransactionBuilder::new(); + let arguments = vec![ + builder.input(CallArg::IOTA_SYSTEM_MUT)?, + builder.input(CallArg::Object(ObjectArg::ImmOrOwnedObject(oref)))?, + builder.input(CallArg::Pure(bcs::to_bytes(&validator)?))?, + ]; + builder.command(Command::move_call( + IOTA_SYSTEM_PACKAGE_ID, + TIMELOCKED_STAKING_MODULE_NAME.to_owned(), + ADD_TIMELOCKED_STAKE_FUN_NAME.to_owned(), + vec![], + arguments, + )); + builder.finish() + }; + Ok(TransactionData::new_programmable( + signer, + vec![gas], + pt, + gas_budget, + gas_price, + )) + } + + /// Withdraw timelocked stake from a validator's staking pool. + pub async fn request_withdraw_timelocked_stake( + &self, + signer: IotaAddress, + timelocked_staked_iota: ObjectID, + gas: ObjectID, + gas_budget: u64, + ) -> anyhow::Result { + let timelocked_staked_iota = self.get_object_ref(timelocked_staked_iota).await?; + let gas_price = self.0.get_reference_gas_price().await?; + let gas = self + .select_gas(signer, Some(gas), gas_budget, vec![], gas_price) + .await?; + TransactionData::new_move_call( + signer, + IOTA_SYSTEM_PACKAGE_ID, + TIMELOCKED_STAKING_MODULE_NAME.to_owned(), + WITHDRAW_TIMELOCKED_STAKE_FUN_NAME.to_owned(), + vec![], + gas, + vec![ + CallArg::IOTA_SYSTEM_MUT, + CallArg::Object(ObjectArg::ImmOrOwnedObject(timelocked_staked_iota)), + ], + gas_budget, + gas_price, + ) + } +} diff --git a/crates/iota-transaction-builder/src/utils.rs b/crates/iota-transaction-builder/src/utils.rs new file mode 100644 index 00000000000..7ec19f44e56 --- /dev/null +++ b/crates/iota-transaction-builder/src/utils.rs @@ -0,0 +1,223 @@ +// Copyright (c) Mysten Labs, Inc. +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::{collections::BTreeMap, result::Result}; + +use anyhow::{Ok, anyhow, bail}; +use futures::future::join_all; +use iota_json::{ + IotaJsonValue, ResolvedCallArg, is_receiving_argument, resolve_move_function_args, +}; +use iota_json_rpc_types::{IotaData, IotaObjectDataOptions, IotaRawData}; +use iota_protocol_config::ProtocolConfig; +use iota_types::{ + base_types::{IotaAddress, ObjectID, ObjectRef, ObjectType}, + gas_coin::GasCoin, + move_package::MovePackage, + object::{Object, Owner}, + programmable_transaction_builder::ProgrammableTransactionBuilder, + transaction::{Argument, CallArg, ObjectArg}, +}; +use move_binary_format::{ + CompiledModule, binary_config::BinaryConfig, file_format::SignatureToken, +}; +use move_core_types::{identifier::Identifier, language_storage::TypeTag}; + +use crate::TransactionBuilder; + +impl TransactionBuilder { + /// Select a gas coin for the provided gas budget. + pub(crate) async fn select_gas( + &self, + signer: IotaAddress, + input_gas: impl Into>, + gas_budget: u64, + input_objects: Vec, + gas_price: u64, + ) -> Result { + if gas_budget < gas_price { + bail!( + "Gas budget {gas_budget} is less than the reference gas price {gas_price}. The gas budget must be at least the current reference gas price of {gas_price}." + ) + } + if let Some(gas) = input_gas.into() { + self.get_object_ref(gas).await + } else { + let gas_objs = self.0.get_owned_objects(signer, GasCoin::type_()).await?; + + for obj in gas_objs { + let response = self + .0 + .get_object_with_options(obj.object_id, IotaObjectDataOptions::new().with_bcs()) + .await?; + let obj = response.object()?; + let gas: GasCoin = bcs::from_bytes( + &obj.bcs + .as_ref() + .ok_or_else(|| anyhow!("bcs field is unexpectedly empty"))? + .try_as_move() + .ok_or_else(|| anyhow!("Cannot parse move object to gas object"))? + .bcs_bytes, + )?; + if !input_objects.contains(&obj.object_id) && gas.value() >= gas_budget { + return Ok(obj.object_ref()); + } + } + Err(anyhow!( + "Cannot find gas coin for signer address {signer} with amount sufficient for the required gas budget {gas_budget}. If you are using the pay or transfer commands, you can use pay-iota or transfer-iota commands instead, which will use the only object as gas payment." + )) + } + } + + /// Get the object references for a list of object IDs + pub async fn input_refs(&self, obj_ids: &[ObjectID]) -> Result, anyhow::Error> { + let handles: Vec<_> = obj_ids.iter().map(|id| self.get_object_ref(*id)).collect(); + let obj_refs = join_all(handles) + .await + .into_iter() + .collect::>>()?; + Ok(obj_refs) + } + + /// Resolve a provided [`ObjectID`] to the required [`ObjectArg`] for a + /// given move module. + async fn get_object_arg( + &self, + id: ObjectID, + objects: &mut BTreeMap, + is_mutable_ref: bool, + view: &CompiledModule, + arg_type: &SignatureToken, + ) -> Result { + let response = self + .0 + .get_object_with_options(id, IotaObjectDataOptions::bcs_lossless()) + .await?; + + let obj: Object = response.into_object()?.try_into()?; + let obj_ref = obj.compute_object_reference(); + let owner = obj.owner; + objects.insert(id, obj); + if is_receiving_argument(view, arg_type) { + return Ok(ObjectArg::Receiving(obj_ref)); + } + Ok(match owner { + Owner::Shared { + initial_shared_version, + } => ObjectArg::SharedObject { + id, + initial_shared_version, + mutable: is_mutable_ref, + }, + Owner::AddressOwner(_) | Owner::ObjectOwner(_) | Owner::Immutable => { + ObjectArg::ImmOrOwnedObject(obj_ref) + } + }) + } + + /// Convert provided JSON arguments for a move function to their + /// [`Argument`] representation and check their validity. + pub async fn resolve_and_checks_json_args( + &self, + builder: &mut ProgrammableTransactionBuilder, + package_id: ObjectID, + module: &Identifier, + function: &Identifier, + type_args: &[TypeTag], + json_args: Vec, + ) -> Result, anyhow::Error> { + let object = self + .0 + .get_object_with_options(package_id, IotaObjectDataOptions::bcs_lossless()) + .await? + .into_object()?; + let Some(IotaRawData::Package(package)) = object.bcs else { + bail!( + "Bcs field in object [{}] is missing or not a package.", + package_id + ); + }; + let package: MovePackage = MovePackage::new( + package.id, + object.version, + package.module_map, + ProtocolConfig::get_for_min_version().max_move_package_size(), + package.type_origin_table, + package.linkage_table, + )?; + + let json_args_and_tokens = resolve_move_function_args( + &package, + module.clone(), + function.clone(), + type_args, + json_args, + )?; + + let mut args = Vec::new(); + let mut objects = BTreeMap::new(); + let module = package.deserialize_module(module, &BinaryConfig::standard())?; + for (arg, expected_type) in json_args_and_tokens { + args.push(match arg { + ResolvedCallArg::Pure(p) => builder.input(CallArg::Pure(p)), + + ResolvedCallArg::Object(id) => builder.input(CallArg::Object( + self.get_object_arg( + id, + &mut objects, + // Is mutable if passed by mutable reference or by value + matches!(expected_type, SignatureToken::MutableReference(_)) + || !expected_type.is_reference(), + &module, + &expected_type, + ) + .await?, + )), + + ResolvedCallArg::ObjVec(v) => { + let mut object_ids = vec![]; + for id in v { + object_ids.push( + self.get_object_arg( + id, + &mut objects, + // is_mutable_ref + false, + &module, + &expected_type, + ) + .await?, + ) + } + builder.make_obj_vec(object_ids) + } + }?); + } + + Ok(args) + } + + /// Get the latest object ref for an object. + pub async fn get_object_ref(&self, object_id: ObjectID) -> anyhow::Result { + // TODO: we should add retrial to reduce the transaction building error rate + self.get_object_ref_and_type(object_id) + .await + .map(|(oref, _)| oref) + } + + /// Helper function to get the latest ObjectRef (ObjectID, SequenceNumber, + /// ObjectDigest) and ObjectType for a provided ObjectID. + pub(crate) async fn get_object_ref_and_type( + &self, + object_id: ObjectID, + ) -> anyhow::Result<(ObjectRef, ObjectType)> { + let object = self + .0 + .get_object_with_options(object_id, IotaObjectDataOptions::new().with_type()) + .await? + .into_object()?; + + Ok((object.object_ref(), object.object_type()?)) + } +}