diff --git a/crates/iota/src/client_commands.rs b/crates/iota/src/client_commands.rs index 0a461867f68..fba4f186d47 100644 --- a/crates/iota/src/client_commands.rs +++ b/crates/iota/src/client_commands.rs @@ -22,10 +22,9 @@ use fastcrypto::{ use iota_execution::verifier::VerifierOverrides; use iota_json::IotaJsonValue; use iota_json_rpc_types::{ - Coin, DynamicFieldPage, IotaCoinMetadata, IotaData, IotaExecutionStatus, IotaObjectData, - IotaObjectDataOptions, IotaObjectResponse, IotaObjectResponseQuery, IotaParsedData, - IotaRawData, IotaTransactionBlockEffectsAPI, IotaTransactionBlockResponse, - IotaTransactionBlockResponseOptions, + Coin, DynamicFieldPage, IotaCoinMetadata, IotaData, IotaObjectData, IotaObjectDataOptions, + IotaObjectResponse, IotaObjectResponseQuery, IotaParsedData, IotaRawData, + IotaTransactionBlockResponse, IotaTransactionBlockResponseOptions, }; use iota_keys::keystore::AccountKeystore; use iota_move::build::resolve_lock_file_path; @@ -85,7 +84,7 @@ mod profiler_tests; #[macro_export] macro_rules! serialize_or_execute { - ($tx_data:expr, $serialize_unsigned:expr, $serialize_signed:expr, $context:expr, $result_variant:ident) => {{ + ($tx_data:expr, $serialize_unsigned:expr, $serialize_signed:expr, $context:expr, $result_variant:ident, $opts:expr) => {{ assert!( !$serialize_unsigned || !$serialize_signed, "Cannot specify both --serialize-unsigned-transaction and --serialize-signed-transaction" @@ -107,15 +106,30 @@ macro_rules! serialize_or_execute { IotaClientCommandResult::SerializedSignedTransaction(sender_signed_data) } else { let transaction = Transaction::new(sender_signed_data); - let response = $context.execute_transaction_may_fail(transaction).await?; - let effects = response.effects.as_ref().ok_or_else(|| { - anyhow!("Effects from IotaTransactionBlockResult should not be empty") - })?; - if matches!(effects.status(), IotaExecutionStatus::Failure { .. }) { - return Err(anyhow!( - "Error executing transaction: {:#?}", - effects.status() - )); + + + let client = $context.get_client().await?; + let response: iota_json_rpc_types::IotaTransactionBlockResponse = + client + .quorum_driver_api() + .execute_transaction_block( + transaction, + $opts.unwrap_or_else(|| + IotaTransactionBlockResponseOptions::new() + .with_effects() + .with_input() + .with_events() + .with_object_changes() + .with_balance_changes(), + ), + Some(iota_types::quorum_driver_types::ExecuteTransactionRequestType::WaitForLocalExecution), + ) + .await?; + + + let errors: &Vec = response.errors.as_ref(); + if !errors.is_empty() { + return Err(anyhow!("Error executing transaction: {:#?}", errors)); } IotaClientCommandResult::$result_variant(response) } @@ -178,7 +192,6 @@ pub enum IotaClientCommands { #[clap(long, num_args(1..))] args: Vec, /// ID of the gas object for gas payment, in 20 bytes Hex string - #[clap(long)] /// If not provided, a gas object with at least gas_budget value will be /// selected #[clap(long)] @@ -208,6 +221,13 @@ pub enum IotaClientCommands { /// `. #[clap(long, required = false)] serialize_signed_transaction: bool, + + /// Select which fields of the response to display. + /// If not provided, all fields are displayed. + /// The fields are: effects, input, events, object_changes, + /// balance_changes. + #[clap(long, required = false, value_delimiter = ',', num_args = 0.., value_parser = parse_emit_opts)] + emit: Vec, }, /// Query the chain identifier from the rpc endpoint. @@ -863,6 +883,48 @@ pub enum IotaClientCommands { }, } +/// Custom parser for emit options. +/*fn parse_emit_opts(s: &str) -> Result { + // Validate the input if necessary + // For instance, if you want to restrict to specific fields, add checks here. + match s { + "effects" | "input" | "events" | "object_changes" | "balance_changes" => Ok(s.to_string()), + _ => Err(format!("Invalid option: {}", s)), // Return an error if invalid + } +}*/ + +#[derive(Clone)] +pub enum EmitOption { + Effects, + Input, + Events, + ObjectChanges, + BalanceChanges, +} + + +/// Converts a string into the corresponding `EmitOption` enum. +/// +/// # Arguments +/// * `s` - A string representing an option. +/// +/// # Returns +/// * `Ok(EmitOption)` if the string is valid. +/// * `Err(String)` if the string is invalid. +/// +/// Valid options: "effects", "input", "events", "object_changes", "balance_changes". +fn parse_emit_opts(s: &str) -> Result { + match s { + "effects" => Ok(EmitOption::Effects), + "input" => Ok(EmitOption::Input), + "events" => Ok(EmitOption::Events), + "object_changes" => Ok(EmitOption::ObjectChanges), + "balance_changes" => Ok(EmitOption::BalanceChanges), + _ => Err(format!("Invalid option: {}", s)), + } +} + + #[derive(serde::Deserialize)] struct FaucetResponse { error: Option, @@ -1099,7 +1161,8 @@ impl IotaClientCommands { serialize_unsigned_transaction, serialize_signed_transaction, context, - Upgrade + Upgrade, + None ) } IotaClientCommands::Publish { @@ -1155,7 +1218,8 @@ impl IotaClientCommands { serialize_unsigned_transaction, serialize_signed_transaction, context, - Publish + Publish, + None ) } @@ -1266,6 +1330,7 @@ impl IotaClientCommands { args, serialize_unsigned_transaction, serialize_signed_transaction, + emit, } => { let tx_data = construct_move_call_transaction( package, &module, &function, type_args, gas, gas_budget, gas_price, args, @@ -1277,7 +1342,8 @@ impl IotaClientCommands { serialize_unsigned_transaction, serialize_signed_transaction, context, - Call + Call, + Some(opts_from_cli(emit)) ) } @@ -1301,7 +1367,8 @@ impl IotaClientCommands { serialize_unsigned_transaction, serialize_signed_transaction, context, - Transfer + Transfer, + None ) } @@ -1325,7 +1392,8 @@ impl IotaClientCommands { serialize_unsigned_transaction, serialize_signed_transaction, context, - TransferIota + TransferIota, + None ) } @@ -1370,7 +1438,8 @@ impl IotaClientCommands { serialize_unsigned_transaction, serialize_signed_transaction, context, - Pay + Pay, + None ) } @@ -1414,7 +1483,8 @@ impl IotaClientCommands { serialize_unsigned_transaction, serialize_signed_transaction, context, - PayIota + PayIota, + None ) } @@ -1442,7 +1512,8 @@ impl IotaClientCommands { serialize_unsigned_transaction, serialize_signed_transaction, context, - PayAllIota + PayAllIota, + None ) } @@ -1582,7 +1653,8 @@ impl IotaClientCommands { serialize_unsigned_transaction, serialize_signed_transaction, context, - SplitCoin + SplitCoin, + None ) } IotaClientCommands::MergeCoin { @@ -1604,7 +1676,8 @@ impl IotaClientCommands { serialize_unsigned_transaction, serialize_signed_transaction, context, - MergeCoin + MergeCoin, + None ) } IotaClientCommands::Switch { address, env } => { @@ -2676,3 +2749,51 @@ fn format_balance( format!("{whole}.{fractional}{suffix}") } + +/*fn opts_from_cli(opts: Vec) -> IotaTransactionBlockResponseOptions { + if opts.is_empty() { + return IotaTransactionBlockResponseOptions::new() + .with_effects() + .with_input() + .with_events() + .with_object_changes() + .with_balance_changes(); + } + + let mut options = IotaTransactionBlockResponseOptions::new(); + for opt in opts { + match opt.as_str() { + "input" => options.show_input = true, + "events" => options.show_events = true, + "object_changes" => options.show_object_changes = true, + "balance_changes" => options.show_balance_changes = true, + "effects" => options.show_effects = true, + _ => {} + } + } + options +}*/ + +fn opts_from_cli(opts: Vec) -> IotaTransactionBlockResponseOptions { + if opts.is_empty() { + return IotaTransactionBlockResponseOptions::new() + .with_effects() + .with_input() + .with_events() + .with_object_changes() + .with_balance_changes(); + } + + let mut options = IotaTransactionBlockResponseOptions::new(); + for opt in opts { + match opt { + EmitOption::Input => options.show_input = true, + EmitOption::Events => options.show_events = true, + EmitOption::ObjectChanges => options.show_object_changes = true, + EmitOption::BalanceChanges => options.show_balance_changes = true, + EmitOption::Effects => options.show_effects = true, + } + } + options +} + diff --git a/crates/iota/src/client_ptb/ptb.rs b/crates/iota/src/client_ptb/ptb.rs index f039a5e6b54..0c9edb7f300 100644 --- a/crates/iota/src/client_ptb/ptb.rs +++ b/crates/iota/src/client_ptb/ptb.rs @@ -168,12 +168,12 @@ impl PTB { ); if program_metadata.serialize_unsigned_set { - serialize_or_execute!(tx_data, true, false, context, PTB).print(true); + serialize_or_execute!(tx_data, true, false, context, PTB, None).print(true); return Ok(()); } if program_metadata.serialize_signed_set { - serialize_or_execute!(tx_data, false, true, context, PTB).print(true); + serialize_or_execute!(tx_data, false, true, context, PTB, None).print(true); return Ok(()); } diff --git a/crates/iota/tests/cli_tests.rs b/crates/iota/tests/cli_tests.rs index c24ab755557..2c1d2500d5e 100644 --- a/crates/iota/tests/cli_tests.rs +++ b/crates/iota/tests/cli_tests.rs @@ -7,13 +7,12 @@ use std::os::unix::fs::FileExt; #[cfg(target_os = "windows")] use std::os::windows::fs::FileExt; use std::{ - collections::BTreeSet, fmt::Write, fs::read_dir, io::Read, path::PathBuf, str, str::FromStr, - thread, time::Duration, + collections::BTreeSet, f32::consts::E, fmt::Write, fs::read_dir, io::Read, path::PathBuf, str::{self, FromStr}, thread, time::Duration }; use expect_test::expect; use iota::{ - client_commands::{IotaClientCommandResult, IotaClientCommands, SwitchResponse}, + client_commands::{EmitOption, IotaClientCommandResult, IotaClientCommands, SwitchResponse}, iota_commands::IotaCommand, key_identity::{get_identity_address, KeyIdentity}, }; @@ -31,6 +30,7 @@ use iota_keys::keystore::AccountKeystore; use iota_macros::sim_test; use iota_move_build::{BuildConfig, IotaPackageHooks}; use iota_sdk::{iota_client_config::IotaClientConfig, wallet_context::WalletContext}; +use iota_simulator::tower::balance; use iota_swarm_config::{ genesis_config::{AccountConfig, GenesisConfig, DEFAULT_NUMBER_OF_AUTHORITIES}, network_config::NetworkConfigLight, @@ -313,6 +313,7 @@ async fn test_ptb_publish_and_complex_arg_resolution() -> Result<(), anyhow::Err args: vec![], serialize_unsigned_transaction: false, serialize_signed_transaction: false, + emit: Vec::new(), } .execute(context) .await?; @@ -365,6 +366,159 @@ async fn test_ptb_publish_and_complex_arg_resolution() -> Result<(), anyhow::Err Ok(()) } +#[sim_test] +async fn test_call_command_emit_args() -> Result<(), anyhow::Error> { + // Publish the package + move_package::package_hooks::register_package_hooks(Box::new(IotaPackageHooks)); + let mut test_cluster = TestClusterBuilder::new().build().await; + let rgp = test_cluster.get_reference_gas_price().await; + let address = test_cluster.get_address_0(); + let context = &mut test_cluster.wallet; + let client = context.get_client().await?; + let object_refs = client + .read_api() + .get_owned_objects( + address, + Some(IotaObjectResponseQuery::new_with_options( + IotaObjectDataOptions::new() + .with_type() + .with_owner() + .with_previous_transaction(), + )), + None, + None, + ) + .await? + .data; + + let gas_obj_id = object_refs.first().unwrap().object().unwrap().object_id; + + // Provide path to well formed package sources + let mut package_path = PathBuf::from(TEST_DATA_DIR); + package_path.push("simple_mod"); + let build_config = BuildConfig::new_for_testing().config; + let resp = IotaClientCommands::Publish { + package_path: package_path.clone(), + build_config, + gas: Some(gas_obj_id), + gas_budget: rgp * TEST_ONLY_GAS_UNIT_FOR_PUBLISH, + skip_dependency_verification: false, + with_unpublished_dependencies: false, + serialize_unsigned_transaction: false, + serialize_signed_transaction: false, + } + .execute(context) + .await?; + + let IotaClientCommandResult::Publish(response) = resp else { + unreachable!("Invalid response"); + }; + + let IotaTransactionBlockEffects::V1(effects) = response.effects.unwrap(); + + assert!(effects.status.is_ok()); + let package = effects + .created() + .iter() + .find(|refe| matches!(refe.owner, Owner::Immutable)) + .unwrap(); + + let start_call_result = IotaClientCommands::Call { + package: package.reference.object_id, + module: "my_mod".to_string(), + function: "simple_add".to_string(), + type_args: vec![], + gas: None, + gas_budget: rgp * TEST_ONLY_GAS_UNIT_FOR_PUBLISH, + gas_price: None, + args: vec![IotaJsonValue::new(json!(1))?, IotaJsonValue::new(json!(2))?], + serialize_unsigned_transaction: false, + serialize_signed_transaction: false, + emit: vec![EmitOption::BalanceChanges], + } + .execute(context) + .await?; + + if let Some(tx_block_response) = start_call_result.tx_block_response() { + // Assert Balance Changes are present in the response + assert!(tx_block_response.balance_changes.is_some()); + + // Assert every other field is not present in the response + assert!(tx_block_response.effects.is_none()); + assert!(tx_block_response.object_changes.is_none()); + assert!(tx_block_response.events.is_none()); + assert!(tx_block_response.transaction.is_none()); + } else { + panic!("Transaction block response is None"); + } + + // Make another call, this time with multiple emit args + let start_call_result = IotaClientCommands::Call { + package: package.reference.object_id, + module: "my_mod".to_string(), + function: "simple_add".to_string(), + type_args: vec![], + gas: None, + gas_budget: rgp * TEST_ONLY_GAS_UNIT_FOR_PUBLISH, + gas_price: None, + args: vec![IotaJsonValue::new(json!(1))?, IotaJsonValue::new(json!(2))?], + serialize_unsigned_transaction: false, + serialize_signed_transaction: false, + emit: vec![ + EmitOption::BalanceChanges, + EmitOption::Effects, + EmitOption::ObjectChanges, + ], + } + .execute(context) + .await?; + + start_call_result.print(true); + + // Assert Balance Changes, effects and object changes are present in the + // response + if let Some(tx_block_response) = start_call_result.tx_block_response() { + assert!(tx_block_response.balance_changes.is_some()); + assert!(tx_block_response.effects.is_some()); + assert!(tx_block_response.object_changes.is_some()); + assert!(tx_block_response.events.is_none()); + assert!(tx_block_response.transaction.is_none()); + } else { + panic!("Transaction block response is None"); + } + + // Make another call, this time with no emit args. This should return the full + // response + let start_call_result = IotaClientCommands::Call { + package: package.reference.object_id, + module: "my_mod".to_string(), + function: "simple_add".to_string(), + type_args: vec![], + gas: None, + gas_budget: rgp * TEST_ONLY_GAS_UNIT_FOR_PUBLISH, + gas_price: None, + args: vec![IotaJsonValue::new(json!(1))?, IotaJsonValue::new(json!(2))?], + serialize_unsigned_transaction: false, + serialize_signed_transaction: false, + emit: vec![], + } + .execute(context) + .await?; + + // Assert all fields are present in the response + if let Some(tx_block_response) = start_call_result.tx_block_response() { + assert!(tx_block_response.balance_changes.is_some()); + assert!(tx_block_response.effects.is_some()); + assert!(tx_block_response.object_changes.is_some()); + assert!(tx_block_response.events.is_some()); + assert!(tx_block_response.transaction.is_some()); + } else { + panic!("Transaction block response is None"); + } + + Ok(()) +} + // fixing issue https://github.com/iotaledger/iota/issues/6546 #[tokio::test] async fn test_regression_6546() -> Result<(), anyhow::Error> { @@ -660,6 +814,7 @@ async fn test_move_call_args_linter_command() -> Result<(), anyhow::Error> { gas_price: None, serialize_unsigned_transaction: false, serialize_signed_transaction: false, + emit: Vec::new(), } .execute(context) .await?; @@ -700,6 +855,7 @@ async fn test_move_call_args_linter_command() -> Result<(), anyhow::Error> { gas_price: None, serialize_unsigned_transaction: false, serialize_signed_transaction: false, + emit: Vec::new(), } .execute(context) .await; @@ -727,6 +883,7 @@ async fn test_move_call_args_linter_command() -> Result<(), anyhow::Error> { gas_price: None, serialize_unsigned_transaction: false, serialize_signed_transaction: false, + emit: Vec::new(), } .execute(context) .await; @@ -751,6 +908,7 @@ async fn test_move_call_args_linter_command() -> Result<(), anyhow::Error> { gas_price: Some(1), serialize_unsigned_transaction: false, serialize_signed_transaction: false, + emit: Vec::new(), } .execute(context) .await; @@ -784,6 +942,7 @@ async fn test_move_call_args_linter_command() -> Result<(), anyhow::Error> { gas_price: None, serialize_unsigned_transaction: false, serialize_signed_transaction: false, + emit: Vec::new(), } .execute(context) .await?; @@ -805,6 +964,7 @@ async fn test_move_call_args_linter_command() -> Result<(), anyhow::Error> { gas_price: Some(12345), serialize_unsigned_transaction: false, serialize_signed_transaction: false, + emit: Vec::new(), } .execute(context) .await?; @@ -962,6 +1122,7 @@ async fn test_delete_shared_object() -> Result<(), anyhow::Error> { args: vec![], serialize_unsigned_transaction: false, serialize_signed_transaction: false, + emit: Vec::new(), } .execute(context) .await?; @@ -985,6 +1146,7 @@ async fn test_delete_shared_object() -> Result<(), anyhow::Error> { args: vec![IotaJsonValue::from_str(&shared_id.to_string()).unwrap()], serialize_unsigned_transaction: false, serialize_signed_transaction: false, + emit: Vec::new(), } .execute(context) .await?; @@ -1071,6 +1233,7 @@ async fn test_receive_argument() -> Result<(), anyhow::Error> { args: vec![], serialize_unsigned_transaction: false, serialize_signed_transaction: false, + emit: Vec::new(), } .execute(context) .await?; @@ -1113,6 +1276,7 @@ async fn test_receive_argument() -> Result<(), anyhow::Error> { ], serialize_unsigned_transaction: false, serialize_signed_transaction: false, + emit: Vec::new(), } .execute(context) .await?; @@ -1199,6 +1363,7 @@ async fn test_receive_argument_by_immut_ref() -> Result<(), anyhow::Error> { args: vec![], serialize_unsigned_transaction: false, serialize_signed_transaction: false, + emit: Vec::new(), } .execute(context) .await?; @@ -1241,6 +1406,7 @@ async fn test_receive_argument_by_immut_ref() -> Result<(), anyhow::Error> { ], serialize_unsigned_transaction: false, serialize_signed_transaction: false, + emit: Vec::new(), } .execute(context) .await?; @@ -1327,6 +1493,7 @@ async fn test_receive_argument_by_mut_ref() -> Result<(), anyhow::Error> { args: vec![], serialize_unsigned_transaction: false, serialize_signed_transaction: false, + emit: Vec::new(), } .execute(context) .await?; @@ -1369,6 +1536,7 @@ async fn test_receive_argument_by_mut_ref() -> Result<(), anyhow::Error> { ], serialize_unsigned_transaction: false, serialize_signed_transaction: false, + emit: Vec::new(), } .execute(context) .await?; diff --git a/crates/iota/tests/data/simple_mod/Move.toml b/crates/iota/tests/data/simple_mod/Move.toml new file mode 100644 index 00000000000..f4bbbe32b93 --- /dev/null +++ b/crates/iota/tests/data/simple_mod/Move.toml @@ -0,0 +1,37 @@ +[package] +name = "simple_mod" +edition = "2024.beta" # edition = "legacy" to use legacy (pre-2024) Move +# license = "" # e.g., "MIT", "GPL", "Apache 2.0" +# authors = ["..."] # e.g., ["Joe Smith (joesmith@noemail.com)", "John Snow (johnsnow@noemail.com)"] + +[dependencies] +Iota = { git = "https://github.com/iotaledger/iota.git", subdir = "crates/iota-framework/packages/iota-framework", rev = "framework/testnet" } + +# For remote import, use the `{ git = "...", subdir = "...", rev = "..." }`. +# Revision can be a branch, a tag, and a commit hash. +# MyRemotePackage = { git = "https://some.remote/host.git", subdir = "remote/path", rev = "main" } + +# For local dependencies use `local = path`. Path is relative to the package root +# Local = { local = "../path/to" } + +# To resolve a version conflict and force a specific version for dependency +# override use `override = true` +# Override = { local = "../conflicting/version", override = true } + +[addresses] +simple_mod = "0x0" + +# Named addresses will be accessible in Move as `@name`. They're also exported: +# for example, `std = "0x1"` is exported by the Standard Library. +# alice = "0xA11CE" + +[dev-dependencies] +# The dev-dependencies section allows overriding dependencies for `--test` and +# `--dev` modes. You can introduce test-only dependencies here. +# Local = { local = "../path/to/dev-build" } + +[dev-addresses] +# The dev-addresses section allows overwriting named addresses for the `--test` +# and `--dev` modes. +# alice = "0xB0B" + diff --git a/crates/iota/tests/data/simple_mod/sources/simple_mod.move b/crates/iota/tests/data/simple_mod/sources/simple_mod.move new file mode 100644 index 00000000000..2b05780b55c --- /dev/null +++ b/crates/iota/tests/data/simple_mod/sources/simple_mod.move @@ -0,0 +1,5 @@ +module simple_mod::my_mod { + public fun simple_add(a: u64, b: u64): u64 { + a + b + } +} \ No newline at end of file