Skip to content

Commit 2502447

Browse files
authored
Improve wallet show. (#4642)
## Motivation It's hard to tell from `wallet show` which chain is the admin chain. The table layout uses a lot of horizontal space. ## Proposal Improve the `wallet show` output: ``` WALLET (4 chains in total) Child Chain [DEFAULT] Chain ID: a08244c18c828b7b89289f7164428e0b861fb462042416f6c2cfb1f4b53c8621 Owner: 0x86843df3f162b0a820724d37612ecde13c9a022d0cc96b765be127d34cec2d19 Timestamp: 2025-09-24 12:54:53.051239 Blocks: 0 Epoch: 0 Admin Chain Chain ID: 82fc0ff002b793c66db0c57e657c05a30a98cd49d335f8125d0c3bdc4997ffa1 Owner: No owner key Timestamp: 2025-09-24 12:53:29.841442 Blocks: 0 Epoch: 0 Root Chain 1 Chain ID: 0b67f224ab915776d170eeb7d497e8d1ed831126f22eaf9138de48e3c47c8639 Owner: No owner key Timestamp: 2025-09-24 12:55:04.030389 Blocks: 2 Epoch: 0 Latest Block: f67e7ee881b6f9235f1a4cc6f2515d9ce4afccb1bd799f8780987901a581ac03 Child Chain Chain ID: 87c4ef0bfc8d6581529dc32c98d05ba99be9e9202f8f788f9c0c2a872c02fdf6 Owner: 0x9010b58faf7b4908de3069aa7aa042ba4f2eb0de13724059960aaa5be33aa6a6 Timestamp: 2025-09-24 12:55:04.030389 Blocks: 0 Epoch: 0 ``` ## Test Plan * Discuss the new layout! * CI * I ran the README tests locally. ## Release Plan - These changes _could_ be backported to `testnet_conway`, and - released in a new SDK. ## Links - Original attempt: #4541 - [reviewer checklist](https://github.com/linera-io/linera-protocol/blob/main/CONTRIBUTING.md#reviewer-checklist)
1 parent 4a333bf commit 2502447

File tree

6 files changed

+94
-102
lines changed

6 files changed

+94
-102
lines changed

Cargo.lock

Lines changed: 1 addition & 41 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,6 @@ chrono = { version = "0.4.35", default-features = false }
100100
clap = { version = "4", features = ["cargo", "derive", "env"] }
101101
clap-markdown = "0.1.3"
102102
colored = "2.1.0"
103-
comfy-table = "7.1.0"
104103
console_error_panic_hook = "0.1.7"
105104
convert_case = "0.6.0"
106105
criterion = { version = "0.5.1", default-features = false }

linera-base/src/data_types.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -751,6 +751,14 @@ impl ChainOrigin {
751751
pub fn is_child(&self) -> bool {
752752
matches!(self, ChainOrigin::Child { .. })
753753
}
754+
755+
/// Returns the root chain number, if this is a root chain.
756+
pub fn root(&self) -> Option<u32> {
757+
match self {
758+
ChainOrigin::Root(i) => Some(*i),
759+
ChainOrigin::Child { .. } => None,
760+
}
761+
}
754762
}
755763

756764
/// A number identifying the configuration of the chain (aka the committee).

linera-service/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,6 @@ chrono = { workspace = true, features = ["clock"] }
8080
clap.workspace = true
8181
clap-markdown.workspace = true
8282
colored.workspace = true
83-
comfy-table.workspace = true
8483
convert_case.workspace = true
8584
current_platform = "0.2.0"
8685
custom_debug_derive.workspace = true

linera-service/src/cli/main.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2357,7 +2357,6 @@ async fn run(options: &ClientOptions) -> Result<i32, Error> {
23572357
short,
23582358
owned,
23592359
} => {
2360-
let start_time = Instant::now();
23612360
let wallet = options.wallet()?;
23622361
let chain_ids = if let Some(chain_id) = chain_id {
23632362
ensure!(!owned, "Cannot specify both --owned and a chain ID");
@@ -2374,7 +2373,6 @@ async fn run(options: &ClientOptions) -> Result<i32, Error> {
23742373
} else {
23752374
wallet::pretty_print(&wallet, chain_ids);
23762375
}
2377-
info!("Wallet shown in {} ms", start_time.elapsed().as_millis());
23782376
Ok(0)
23792377
}
23802378

linera-service/src/wallet.rs

Lines changed: 85 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,100 @@
11
// Copyright (c) Zefchain Labs, Inc.
22
// SPDX-License-Identifier: Apache-2.0
33

4-
use comfy_table::{
5-
modifiers::UTF8_ROUND_CORNERS, presets::UTF8_FULL, Attribute, Cell, Color, ContentArrangement,
6-
Table,
4+
use linera_base::{
5+
data_types::{ChainDescription, ChainOrigin},
6+
identifiers::ChainId,
77
};
8-
use linera_base::identifiers::ChainId;
98
pub use linera_client::wallet::*;
109

1110
pub fn pretty_print(wallet: &Wallet, chain_ids: impl IntoIterator<Item = ChainId>) {
12-
let mut table = Table::new();
13-
table
14-
.load_preset(UTF8_FULL)
15-
.apply_modifier(UTF8_ROUND_CORNERS)
16-
.set_content_arrangement(ContentArrangement::Dynamic)
17-
.set_header(vec![
18-
Cell::new("Chain ID").add_attribute(Attribute::Bold),
19-
Cell::new("Latest Block").add_attribute(Attribute::Bold),
20-
]);
11+
let chain_ids: Vec<_> = chain_ids.into_iter().collect();
12+
let total_chains = chain_ids.len();
2113

22-
for chain_id in chain_ids {
14+
if total_chains == 0 {
15+
println!("No chains in wallet.");
16+
return;
17+
}
18+
19+
let plural_s = if total_chains == 1 { "" } else { "s" };
20+
println!("\n\x1b[1mWALLET ({total_chains} chain{plural_s} in total)\x1b[0m",);
21+
22+
let mut chains = chain_ids
23+
.into_iter()
24+
.map(|chain_id| ChainDetails::new(chain_id, wallet))
25+
.collect::<Vec<_>>();
26+
// Print first the default, then the admin chain, then other root chains, and finally the
27+
// child chains.
28+
chains.sort_unstable_by_key(|chain| {
29+
let root_id = chain
30+
.origin
31+
.and_then(|origin| origin.root())
32+
.unwrap_or(u32::MAX);
33+
let chain_id = chain.user_chain.chain_id;
34+
(!chain.is_default, !chain.is_admin, root_id, chain_id)
35+
});
36+
for chain in chains {
37+
println!();
38+
chain.print_paragraph();
39+
}
40+
}
41+
42+
struct ChainDetails<'a> {
43+
is_default: bool,
44+
is_admin: bool,
45+
origin: Option<ChainOrigin>,
46+
user_chain: &'a UserChain,
47+
}
48+
49+
impl<'a> ChainDetails<'a> {
50+
fn new(chain_id: ChainId, wallet: &'a Wallet) -> Self {
2351
let Some(user_chain) = wallet.chains.get(&chain_id) else {
2452
panic!("Chain {} not found.", chain_id);
2553
};
26-
update_table_with_chain(
27-
&mut table,
28-
chain_id,
54+
ChainDetails {
55+
is_default: Some(chain_id) == wallet.default,
56+
is_admin: chain_id == wallet.genesis_admin_chain(),
57+
origin: wallet
58+
.genesis_config()
59+
.chains
60+
.iter()
61+
.find(|description| description.id() == chain_id)
62+
.map(ChainDescription::origin),
2963
user_chain,
30-
Some(chain_id) == wallet.default,
31-
);
64+
}
3265
}
33-
println!("{}", table);
34-
}
3566

36-
fn update_table_with_chain(
37-
table: &mut Table,
38-
chain_id: ChainId,
39-
user_chain: &UserChain,
40-
is_default_chain: bool,
41-
) {
42-
let epoch = user_chain.epoch;
43-
let chain_id_cell = if is_default_chain {
44-
Cell::new(format!("{}", chain_id)).fg(Color::Green)
45-
} else {
46-
Cell::new(format!("{}", chain_id))
47-
};
48-
let epoch_str = match epoch {
49-
None => "-".to_string(),
50-
Some(epoch) => format!("{}", epoch),
51-
};
52-
let account_owner = user_chain.owner;
53-
table.add_row(vec![
54-
chain_id_cell,
55-
Cell::new(format!(
56-
r#"AccountOwner: {}
57-
Block Hash: {}
58-
Timestamp: {}
59-
Next Block Height: {}
60-
Epoch: {}"#,
61-
account_owner
62-
.as_ref()
63-
.map_or_else(|| "-".to_string(), |o| o.to_string()),
64-
user_chain
65-
.block_hash
66-
.map_or_else(|| "-".to_string(), |bh| bh.to_string()),
67-
user_chain.timestamp,
68-
user_chain.next_block_height,
69-
epoch_str
70-
)),
71-
]);
67+
fn print_paragraph(&self) {
68+
let title = if self.is_admin {
69+
"Admin Chain".to_string()
70+
} else {
71+
match self.origin {
72+
Some(ChainOrigin::Root(i)) => format!("Root Chain {i}"),
73+
_ => "Child Chain".to_string(),
74+
}
75+
};
76+
let default_marker = if self.is_default { " [DEFAULT]" } else { "" };
77+
78+
// Print chain header in bold
79+
println!("\x1b[1m{}{}\x1b[0m", title, default_marker);
80+
println!(" Chain ID: {}", self.user_chain.chain_id);
81+
if let Some(owner) = &self.user_chain.owner {
82+
println!(" Owner: {owner}");
83+
} else {
84+
println!(" Owner: No owner key");
85+
}
86+
println!(" Timestamp: {}", self.user_chain.timestamp);
87+
println!(" Blocks: {}", self.user_chain.next_block_height);
88+
if let Some(epoch) = self.user_chain.epoch {
89+
println!(" Epoch: {epoch}");
90+
} else {
91+
println!(" Epoch: -");
92+
}
93+
if let Some(hash) = self.user_chain.block_hash {
94+
println!(" Latest Block: {}", hash);
95+
}
96+
if self.user_chain.pending_proposal.is_some() {
97+
println!(" Status: ⚠ Pending proposal");
98+
}
99+
}
72100
}

0 commit comments

Comments
 (0)