diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index 95adc79527..c41a80a0b6 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -19,6 +19,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Security --> +## 1.2.0 - 2023-xx-xx + +### Added + +- `outputs` and `unspent_outputs` print the booked milestone timestamps and sort by them; +- `outputs` and `unspent_outputs` include spent/unspent information; +- `UTC` suffix to the formatted date of `transactions`; + +### Changed + +- `AccountCommand::Output` accepts either a list index or an `OutputId`; + +### Fixed + +- `transaction` and `transactions` indexed transactions in opposite order; + ## 1.1.0 - 2023-09-29 ### Added diff --git a/cli/src/account.rs b/cli/src/account.rs index 83f9d32644..0bd413a133 100644 --- a/cli/src/account.rs +++ b/cli/src/account.rs @@ -164,7 +164,7 @@ pub async fn account_prompt_internal( } AccountCommand::NewAddress => new_address_command(account).await, AccountCommand::NodeInfo => node_info_command(account).await, - AccountCommand::Output { output_id } => output_command(account, output_id).await, + AccountCommand::Output { selector } => output_command(account, selector).await, AccountCommand::Outputs => outputs_command(account).await, AccountCommand::Send { address, diff --git a/cli/src/command/account.rs b/cli/src/command/account.rs index f5754104f6..337a660537 100644 --- a/cli/src/command/account.rs +++ b/cli/src/command/account.rs @@ -20,7 +20,7 @@ use iota_sdk::{ }, wallet::{ account::{ - types::{AccountAddress, AccountIdentifier}, + types::{AccountAddress, AccountIdentifier, OutputData, Transaction}, Account, ConsolidationParams, OutputsToClaim, SyncOptions, TransactionOptions, }, CreateNativeTokenParams, MintNftParams, SendNativeTokensParams, SendNftParams, SendParams, @@ -157,8 +157,9 @@ pub enum AccountCommand { NodeInfo, /// Display an output. Output { - /// Output ID to be displayed. - output_id: String, + /// Selector for output. + /// Either by ID (e.g. 0xbce525324af12eda02bf7927e92cea3a8e8322d0f41966271443e6c3b245a4400000) or index. + selector: OutputSelector, }, /// List all outputs. Outputs, @@ -281,6 +282,25 @@ impl FromStr for TransactionSelector { } } +/// Select by output ID or list index +#[derive(Debug, Copy, Clone)] +pub enum OutputSelector { + Id(OutputId), + Index(usize), +} + +impl FromStr for OutputSelector { + type Err = Error; + + fn from_str(s: &str) -> Result { + Ok(if let Ok(index) = s.parse() { + Self::Index(index) + } else { + Self::Id(s.parse()?) + }) + } +} + /// `addresses` command pub async fn addresses_command(account: &Account) -> Result<(), Error> { let addresses = account.addresses().await?; @@ -644,8 +664,15 @@ pub async fn node_info_command(account: &Account) -> Result<(), Error> { } /// `output` command -pub async fn output_command(account: &Account, output_id: String) -> Result<(), Error> { - let output = account.get_output(&OutputId::from_str(&output_id)?).await; +pub async fn output_command(account: &Account, selector: OutputSelector) -> Result<(), Error> { + let output = match selector { + OutputSelector::Id(id) => account.get_output(&id).await, + OutputSelector::Index(index) => { + let mut outputs = account.outputs(None).await?; + outputs.sort_unstable_by(outputs_ordering); + outputs.into_iter().nth(index) + } + }; if let Some(output) = output { println_log_info!("{output:#?}"); @@ -658,17 +685,7 @@ pub async fn output_command(account: &Account, output_id: String) -> Result<(), /// `outputs` command pub async fn outputs_command(account: &Account) -> Result<(), Error> { - let outputs = account.outputs(None).await?; - - if outputs.is_empty() { - println_log_info!("No outputs found"); - } else { - println_log_info!("Outputs:"); - for (i, output_data) in outputs.into_iter().enumerate() { - println_log_info!("{}\t{}\t{}", i, &output_data.output_id, output_data.output.kind_str()); - } - } - Ok(()) + print_outputs(account.outputs(None).await?, "Outputs:").await } // `send` command @@ -786,7 +803,7 @@ pub async fn transaction_command(account: &Account, selector: TransactionSelecto let transaction = match selector { TransactionSelector::Id(id) => transactions.into_iter().find(|tx| tx.transaction_id == id), TransactionSelector::Index(index) => { - transactions.sort_by(|a, b| a.timestamp.cmp(&b.timestamp)); + transactions.sort_unstable_by(transactions_ordering); transactions.into_iter().nth(index) } }; @@ -803,17 +820,17 @@ pub async fn transaction_command(account: &Account, selector: TransactionSelecto /// `transactions` command pub async fn transactions_command(account: &Account, show_details: bool) -> Result<(), Error> { let mut transactions = account.transactions().await; - transactions.sort_by(|a, b| a.timestamp.cmp(&b.timestamp)); + transactions.sort_unstable_by(transactions_ordering); if transactions.is_empty() { println_log_info!("No transactions found"); } else { - for (i, tx) in transactions.into_iter().rev().enumerate() { + for (i, tx) in transactions.into_iter().enumerate() { if show_details { println_log_info!("{:#?}", tx); } else { let transaction_time = to_utc_date_time(tx.timestamp)?; - let formatted_time = transaction_time.format("%Y-%m-%d %H:%M:%S").to_string(); + let formatted_time = transaction_time.format("%Y-%m-%d %H:%M:%S UTC").to_string(); println_log_info!("{:<5}{}\t{}", i, tx.transaction_id, formatted_time); } @@ -825,18 +842,7 @@ pub async fn transactions_command(account: &Account, show_details: bool) -> Resu /// `unspent-outputs` command pub async fn unspent_outputs_command(account: &Account) -> Result<(), Error> { - let outputs = account.unspent_outputs(None).await?; - - if outputs.is_empty() { - println_log_info!("No outputs found"); - } else { - println_log_info!("Unspent outputs:"); - for (i, output_data) in outputs.into_iter().enumerate() { - println_log_info!("{}\t{}\t{}", i, &output_data.output_id, output_data.output.kind_str()); - } - } - - Ok(()) + print_outputs(account.unspent_outputs(None).await?, "Unspent outputs:").await } pub async fn vote_command(account: &Account, event_id: ParticipationEventId, answers: Vec) -> Result<(), Error> { @@ -992,3 +998,36 @@ async fn print_address(account: &Account, address: &AccountAddress) -> Result<() Ok(()) } + +async fn print_outputs(mut outputs: Vec, title: &str) -> Result<(), Error> { + if outputs.is_empty() { + println_log_info!("No outputs found"); + } else { + println_log_info!("{title}"); + outputs.sort_unstable_by(outputs_ordering); + + for (i, output_data) in outputs.into_iter().enumerate() { + let booked_time = to_utc_date_time(output_data.metadata.milestone_timestamp_booked() as u128 * 1000)?; + let formatted_time = booked_time.format("%Y-%m-%d %H:%M:%S UTC").to_string(); + + println_log_info!( + "{:<5}{}\t{}\t{}\t{}", + i, + &output_data.output_id, + output_data.output.kind_str(), + formatted_time, + if output_data.is_spent { "Spent" } else { "Unspent" }, + ); + } + } + + Ok(()) +} + +fn outputs_ordering(a: &OutputData, b: &OutputData) -> std::cmp::Ordering { + (b.metadata.milestone_timestamp_booked(), a.output_id).cmp(&(a.metadata.milestone_timestamp_booked(), b.output_id)) +} + +fn transactions_ordering(a: &Transaction, b: &Transaction) -> std::cmp::Ordering { + b.timestamp.cmp(&a.timestamp) +}