Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(devx): Add filter flag to Iota CLI #2574

Merged
merged 19 commits into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions crates/iota/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ serde.workspace = true
serde_json.workspace = true
serde_yaml.workspace = true
signature.workspace = true
strum.workspace = true
strum_macros.workspace = true
DaughterOfMars marked this conversation as resolved.
Show resolved Hide resolved
tabled.workspace = true
tap.workspace = true
thiserror.workspace = true
Expand Down
99 changes: 85 additions & 14 deletions crates/iota/src/client_commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ use iota_types::{
transaction::{
SenderSignedData, Transaction, TransactionData, TransactionDataAPI, TransactionKind,
},
quorum_driver_types::ExecuteTransactionRequestType
};
use json_to_table::json_to_table;
use move_binary_format::CompiledModule;
Expand All @@ -83,6 +84,7 @@ use tabled::{
},
};
use tracing::info;
use strum_macros::EnumString;

use crate::{
clever_error_rendering::render_clever_error_opt,
Expand Down Expand Up @@ -649,6 +651,13 @@ pub struct Opts {
/// --signed-tx-bytes <SIGNED_TX_BYTES>`.
#[arg(long, required = false)]
pub 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..)]
pub emit: Vec<EmitOption>,
DaughterOfMars marked this conversation as resolved.
Show resolved Hide resolved
}

/// Global options with gas
Expand All @@ -665,23 +674,37 @@ pub struct OptsWithGas {

impl Opts {
/// Uses the passed gas_budget for the gas budget variable and sets all
/// other flags to false.
/// other flags to false, and emit to an empty vector(defaulting to all emit options).
pub fn for_testing(gas_budget: u64) -> Self {
Self {
gas_budget: Some(gas_budget),
dry_run: false,
serialize_unsigned_transaction: false,
serialize_signed_transaction: false,
emit: vec![],
}
}
/// Uses the passed gas_budget for the gas budget variable, sets dry run to
/// true, and sets all other flags to false.
/// true, and sets all other flags to false, and emit to an empty vector(defaulting to all emit options).
pub fn for_testing_dry_run(gas_budget: u64) -> Self {
Self {
gas_budget: Some(gas_budget),
dry_run: true,
serialize_unsigned_transaction: false,
serialize_signed_transaction: false,
emit: vec![],
}
}

/// Uses the passed gas_budget for the gas budget variable, sets dry run to
/// false, and sets all other flags to false, and emit to the passed emit vector.
pub fn for_testing_emit_options(gas_budget: u64, emit: Vec<EmitOption>) -> Self {
Self {
gas_budget: Some(gas_budget),
dry_run: false,
serialize_unsigned_transaction: false,
serialize_signed_transaction: false,
emit,
}
}
}
Expand All @@ -703,6 +726,30 @@ impl OptsWithGas {
rest: Opts::for_testing_dry_run(gas_budget),
}
}

/// Sets the gas object to gas, and uses the passed gas_budget for the gas
/// budget variable. Dry run is set to false, and emit to the passed emit vector.
/// All other flags are set to false.
pub fn for_testing_emit_options(
gas: Option<ObjectID>,
gas_budget: u64,
emit: Vec<EmitOption>,
) -> Self {
Self {
gas,
rest: Opts::for_testing_emit_options(gas_budget, emit),
}
}
}

#[derive(Clone, Debug, EnumString)]
#[strum(serialize_all = "snake_case")]
pub enum EmitOption {
Effects,
Input,
Events,
ObjectChanges,
BalanceChanges,
}

#[derive(serde::Deserialize)]
Expand Down Expand Up @@ -2930,18 +2977,19 @@ pub(crate) async fn dry_run_or_execute_or_serialize(
))
} else {
let transaction = Transaction::new(sender_signed_data);
let mut response = context.execute_transaction_may_fail(transaction).await?;
if let Some(effects) = response.effects.as_mut() {
prerender_clever_errors(effects, client.read_api()).await;
}
let effects = response.effects.as_ref().ok_or_else(|| {
anyhow!("Effects from IotaTransactionBlockResult should not be empty")
})?;
if let IotaExecutionStatus::Failure { error } = effects.status() {
return Err(anyhow!(
"Error executing transaction '{}': {error}",
response.digest
));
let response =
client
.quorum_driver_api()
.execute_transaction_block(
transaction,
opts_from_cli(opts.emit),
Some(ExecuteTransactionRequestType::WaitForLocalExecution),
)
.await?;

let errors: &Vec<String> = response.errors.as_ref();
if !errors.is_empty() {
return Err(anyhow!("Error executing transaction: {:#?}", errors));
salaheldinsoliman marked this conversation as resolved.
Show resolved Hide resolved
}
Ok(IotaClientCommandResult::TransactionBlock(response))
}
Expand All @@ -2959,3 +3007,26 @@ pub(crate) async fn prerender_clever_errors(
}
}
}

fn opts_from_cli(opts: Vec<EmitOption>) -> IotaTransactionBlockResponseOptions {
if opts.is_empty() {
return IotaTransactionBlockResponseOptions::new()
.with_effects()
.with_input()
.with_events()
.with_object_changes()
.with_balance_changes();
Thoralf-M marked this conversation as resolved.
Show resolved Hide resolved
}

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
}
1 change: 1 addition & 0 deletions crates/iota/src/client_ptb/ptb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ impl PTB {
gas_budget: program_metadata.gas_budget.map(|x| x.value),
serialize_unsigned_transaction: program_metadata.serialize_unsigned_set,
serialize_signed_transaction: program_metadata.serialize_signed_set,
emit: Vec::new(),
},
};

Expand Down
158 changes: 157 additions & 1 deletion crates/iota/tests/cli_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use iota::iota_commands::IndexerFeatureArgs;
use iota::{
client_commands::{
IotaClientCommandResult, IotaClientCommands, Opts, OptsWithGas, SwitchResponse,
estimate_gas_budget,
estimate_gas_budget, EmitOption
},
client_ptb::ptb::PTB,
iota_commands::{IotaCommand, parse_host_port},
Expand Down Expand Up @@ -2113,6 +2113,10 @@ async fn test_package_management_on_upgrade_command_conflict() -> Result<(), any
// Purposely add a conflicting `published-at` address to the Move manifest.
lines.insert(idx + 1, "published-at = \"0xbad\"".to_string());
let new = lines.join("\n");

#[cfg(target_os = "windows")]
move_toml.seek_write(new.as_bytes(), 0).unwrap();
#[cfg(not(target_os = "windows"))]
move_toml.write_at(new.as_bytes(), 0).unwrap();

// Create a new build config for the upgrade. Initialize its lock file to the
Expand Down Expand Up @@ -2965,6 +2969,7 @@ async fn test_serialize_tx() -> Result<(), anyhow::Error> {
dry_run: false,
serialize_unsigned_transaction: true,
serialize_signed_transaction: false,
emit: Vec::new(),
},
}
.execute(context)
Expand All @@ -2979,6 +2984,7 @@ async fn test_serialize_tx() -> Result<(), anyhow::Error> {
dry_run: false,
serialize_unsigned_transaction: false,
serialize_signed_transaction: true,
emit: Vec::new(),
},
}
.execute(context)
Expand All @@ -2994,6 +3000,7 @@ async fn test_serialize_tx() -> Result<(), anyhow::Error> {
dry_run: false,
serialize_unsigned_transaction: false,
serialize_signed_transaction: true,
emit: Vec::new(),
},
}
.execute(context)
Expand Down Expand Up @@ -3845,6 +3852,7 @@ async fn test_gas_estimation() -> Result<(), anyhow::Error> {
dry_run: false,
serialize_unsigned_transaction: false,
serialize_signed_transaction: false,
emit: Vec::new(),
},
}
.execute(context)
Expand Down Expand Up @@ -4089,3 +4097,151 @@ async fn test_parse_host_port() {
let input = "127.9.0.1:asb";
assert!(parse_host_port(input.to_string(), 9123).is_err());
}

#[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("dummy_modules_upgrade");
let build_config = BuildConfig::new_for_testing().config;
let resp = IotaClientCommands::Publish {
package_path: package_path.clone(),
build_config,
skip_dependency_verification: false,
with_unpublished_dependencies: false,
opts: OptsWithGas::for_testing(Some(gas_obj_id), rgp * TEST_ONLY_GAS_UNIT_FOR_PUBLISH),
}
.execute(context)
.await?;

let effects = match resp {
IotaClientCommandResult::TransactionBlock(response) => response.effects.unwrap(),
_ => panic!("Expected TransactionBlock response"),
};

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: "trusted_coin".to_string(),
function: "f".to_string(),
type_args: vec![],
gas_price: None,
args: vec![],
opts: OptsWithGas::for_testing_emit_options(
None,
rgp * TEST_ONLY_GAS_UNIT_FOR_PUBLISH,
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: "trusted_coin".to_string(),
function: "f".to_string(),
type_args: vec![],
gas_price: None,
args: vec![],
opts: OptsWithGas::for_testing_emit_options(
None,
rgp * TEST_ONLY_GAS_UNIT_FOR_PUBLISH,
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: "trusted_coin".to_string(),
function: "f".to_string(),
type_args: vec![],
gas_price: None,
args: vec![],
opts: OptsWithGas::for_testing_emit_options(
None,
rgp * TEST_ONLY_GAS_UNIT_FOR_PUBLISH,
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(())
}
Loading