diff --git a/Cargo.lock b/Cargo.lock index 4986908b339..7b562db66da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2276,6 +2276,17 @@ dependencies = [ "memchr", ] +[[package]] +name = "comfy-table" +version = "7.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a65ebfec4fb190b6f90e944a817d60499ee0744e582530e2c9900a22e591d9a" +dependencies = [ + "crossterm", + "unicode-segmentation", + "unicode-width 0.2.1", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -2739,6 +2750,28 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags 2.9.1", + "crossterm_winapi", + "parking_lot", + "rustix 0.38.44", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + [[package]] name = "crowd-funding" version = "0.1.0" @@ -5691,6 +5724,7 @@ dependencies = [ "clap", "clap-markdown", "colored", + "comfy-table", "convert_case", "counter", "counter-no-graphql", @@ -10800,6 +10834,12 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +[[package]] +name = "unicode-width" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" + [[package]] name = "unicode-xid" version = "0.2.6" @@ -11402,7 +11442,7 @@ dependencies = [ "bumpalo", "leb128", "memchr", - "unicode-width", + "unicode-width 0.1.14", "wasm-encoder 0.217.1", ] diff --git a/Cargo.toml b/Cargo.toml index b6f67c41f49..e9c8de8e640 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -100,6 +100,7 @@ chrono = { version = "0.4.35", default-features = false } clap = { version = "4", features = ["cargo", "derive", "env"] } clap-markdown = "0.1.3" colored = "2.1.0" +comfy-table = "7.1.0" console_error_panic_hook = "0.1.7" convert_case = "0.6.0" criterion = { version = "0.5.1", default-features = false } diff --git a/linera-base/src/data_types.rs b/linera-base/src/data_types.rs index cafe440e8cc..c4b6e40d77e 100644 --- a/linera-base/src/data_types.rs +++ b/linera-base/src/data_types.rs @@ -724,14 +724,6 @@ impl ChainOrigin { pub fn is_child(&self) -> bool { matches!(self, ChainOrigin::Child { .. }) } - - /// Returns the root chain number, if this is a root chain. - pub fn root(&self) -> Option { - match self { - ChainOrigin::Root(i) => Some(*i), - ChainOrigin::Child { .. } => None, - } - } } /// A number identifying the configuration of the chain (aka the committee). diff --git a/linera-service/Cargo.toml b/linera-service/Cargo.toml index a0fa0c397e5..8771ae2772c 100644 --- a/linera-service/Cargo.toml +++ b/linera-service/Cargo.toml @@ -80,6 +80,7 @@ chrono = { workspace = true, features = ["clock"] } clap.workspace = true clap-markdown.workspace = true colored.workspace = true +comfy-table.workspace = true convert_case.workspace = true current_platform = "0.2.0" custom_debug_derive.workspace = true diff --git a/linera-service/src/cli/main.rs b/linera-service/src/cli/main.rs index 91ccf055ad4..731e7eb5eaf 100644 --- a/linera-service/src/cli/main.rs +++ b/linera-service/src/cli/main.rs @@ -2559,6 +2559,7 @@ async fn run(options: &ClientOptions) -> Result { short, owned, } => { + let start_time = Instant::now(); let wallet = options.wallet()?; let chain_ids = if let Some(chain_id) = chain_id { ensure!(!owned, "Cannot specify both --owned and a chain ID"); @@ -2575,6 +2576,7 @@ async fn run(options: &ClientOptions) -> Result { } else { wallet::pretty_print(&wallet, chain_ids); } + info!("Wallet shown in {} ms", start_time.elapsed().as_millis()); Ok(0) } diff --git a/linera-service/src/wallet.rs b/linera-service/src/wallet.rs index 970b83e2f23..ba0153f1b06 100644 --- a/linera-service/src/wallet.rs +++ b/linera-service/src/wallet.rs @@ -1,100 +1,89 @@ // Copyright (c) Zefchain Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -use linera_base::{ - data_types::{ChainDescription, ChainOrigin}, - identifiers::ChainId, +use comfy_table::{ + modifiers::UTF8_ROUND_CORNERS, presets::UTF8_FULL, Attribute, Cell, Color, ContentArrangement, + Table, }; +use linera_base::identifiers::ChainId; pub use linera_client::wallet::*; pub fn pretty_print(wallet: &Wallet, chain_ids: impl IntoIterator) { - let chain_ids: Vec<_> = chain_ids.into_iter().collect(); - let total_chains = chain_ids.len(); + let mut table = Table::new(); + table + .load_preset(UTF8_FULL) + .apply_modifier(UTF8_ROUND_CORNERS) + .set_content_arrangement(ContentArrangement::Dynamic) + .set_header(vec![ + Cell::new("Chain ID").add_attribute(Attribute::Bold), + Cell::new("Latest Block").add_attribute(Attribute::Bold), + ]); - if total_chains == 0 { - println!("No chains in wallet."); - return; - } - - let plural_s = if total_chains == 1 { "" } else { "s" }; - println!("\n\x1b[1mWALLET ({total_chains} chain{plural_s} in total)\x1b[0m",); - - let mut chains = chain_ids - .into_iter() - .map(|chain_id| ChainDetails::new(chain_id, wallet)) - .collect::>(); - // Print first the default, then the admin chain, then other root chains, and finally the - // child chains. - chains.sort_unstable_by_key(|chain| { - let root_id = chain - .origin - .and_then(|origin| origin.root()) - .unwrap_or(u32::MAX); - let chain_id = chain.user_chain.chain_id; - (!chain.is_default, !chain.is_admin, root_id, chain_id) - }); - for chain in chains { - println!(); - chain.print_paragraph(); - } -} - -struct ChainDetails<'a> { - is_default: bool, - is_admin: bool, - origin: Option, - user_chain: &'a UserChain, -} - -impl<'a> ChainDetails<'a> { - fn new(chain_id: ChainId, wallet: &'a Wallet) -> Self { + let admin_chain_id = wallet.genesis_admin_chain(); + for chain_id in chain_ids { let Some(user_chain) = wallet.chains.get(&chain_id) else { panic!("Chain {} not found.", chain_id); }; - ChainDetails { - is_default: Some(chain_id) == wallet.default, - is_admin: chain_id == wallet.genesis_admin_chain(), - origin: wallet - .genesis_config() - .chains - .iter() - .find(|description| description.id() == chain_id) - .map(ChainDescription::origin), + update_table_with_chain( + &mut table, + chain_id, user_chain, - } + Some(chain_id) == wallet.default, + chain_id == admin_chain_id, + ); } + println!("{}", table); +} - fn print_paragraph(&self) { - let title = if self.is_admin { - "Admin Chain".to_string() - } else { - match self.origin { - Some(ChainOrigin::Root(i)) => format!("Root Chain {i}"), - _ => "Child Chain".to_string(), - } - }; - let default_marker = if self.is_default { " [DEFAULT]" } else { "" }; +fn update_table_with_chain( + table: &mut Table, + chain_id: ChainId, + user_chain: &UserChain, + is_default_chain: bool, + is_admin_chain: bool, +) { + let epoch = user_chain.epoch; + let mut chain_id_str = format!("{}", chain_id); - // Print chain header in bold - println!("\x1b[1m{}{}\x1b[0m", title, default_marker); - println!(" Chain ID: {}", self.user_chain.chain_id); - if let Some(owner) = &self.user_chain.owner { - println!(" Owner: {owner}"); - } else { - println!(" Owner: No owner key"); - } - println!(" Timestamp: {}", self.user_chain.timestamp); - println!(" Blocks: {}", self.user_chain.next_block_height); - if let Some(epoch) = self.user_chain.epoch { - println!(" Epoch: {epoch}"); - } else { - println!(" Epoch: -"); - } - if let Some(hash) = self.user_chain.block_hash { - println!(" Latest Block: {}", hash); - } - if self.user_chain.pending_proposal.is_some() { - println!(" Status: ⚠ Pending proposal"); - } + // Add labels below the chain ID. + let mut labels = Vec::new(); + if is_default_chain { + labels.push("default"); } + if is_admin_chain { + labels.push("admin"); + } + if !labels.is_empty() { + chain_id_str.push_str(&format!("\n{}", labels.join(", "))); + } + + let chain_id_cell = if is_default_chain { + Cell::new(chain_id_str).fg(Color::Green) + } else { + Cell::new(chain_id_str) + }; + let epoch_str = match epoch { + None => "-".to_string(), + Some(epoch) => format!("{}", epoch), + }; + let account_owner = user_chain.owner; + table.add_row(vec![ + chain_id_cell, + Cell::new(format!( + r#"AccountOwner: {} +Block Hash: {} +Timestamp: {} +Next Block Height: {} +Epoch: {}"#, + account_owner + .as_ref() + .map_or_else(|| "-".to_string(), |o| o.to_string()), + user_chain + .block_hash + .map_or_else(|| "-".to_string(), |bh| bh.to_string()), + user_chain.timestamp, + user_chain.next_block_height, + epoch_str + )), + ]); }