Files
grin-wallet/controller/src/display.rs
T
wiescheandardocrat 2daa36f748 handle unavailable terminal colors (#797)
Co-authored-by: ardocrat <ardocrat@gri.mw>
(cherry picked from commit 99cca5b8bf)
2026-10-11 00:45:56 +03:00

650 lines
16 KiB
Rust

// Copyright 2021 The Grin Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use crate::core::core::amount_to_hr_string;
use crate::core::core::FeeFields;
use crate::core::global;
use crate::libwallet::{
address, AcctPathMapping, Error, OutputCommitMapping, OutputStatus, SlatepackAddress,
TxLogEntry, ViewWallet, WalletInfo,
};
use crate::util::ToHex;
use prettytable;
use prettytable::format::{FormatBuilder, LinePosition, LineSeparator};
use std::io::prelude::Write;
use term;
/// Display outputs in a pretty way
pub fn outputs(
account: &str,
cur_height: u64,
validated: bool,
outputs: Vec<OutputCommitMapping>,
dark_background_color_scheme: bool,
) -> Result<(), Error> {
let title = format!(
"Wallet Outputs - Account '{}' - Block Height: {}",
account, cur_height
);
println!();
if term::stdout().is_none() {
println!("Could not open terminal");
return Ok(());
}
let mut t = term::stdout().unwrap();
let _ = t.fg(term::color::MAGENTA);
writeln!(t, "{}", title)?;
let _ = t.reset();
let mut table = table!();
table.set_titles(row![
bMG->"Output Commitment",
bMG->"MMR Index",
bMG->"Block Height",
bMG->"Locked Until",
bMG->"Status",
bMG->"Coinbase?",
bMG->"# Confirms",
bMG->"Value",
bMG->"Tx"
]);
for m in outputs {
let commit = format!("{}", m.commit.as_ref().to_hex());
let index = match m.output.mmr_index {
None => "None".to_owned(),
Some(t) => t.to_string(),
};
let height = format!("{}", m.output.height);
let lock_height = format!("{}", m.output.lock_height);
let is_coinbase = format!("{}", m.output.is_coinbase);
// Mark unconfirmed coinbase outputs as "Mining" instead of "Unconfirmed"
let status = match m.output.status {
OutputStatus::Unconfirmed if m.output.is_coinbase => "Mining".to_string(),
_ => format!("{}", m.output.status),
};
let num_confirmations = format!("{}", m.output.num_confirmations(cur_height));
let value = format!("{}", amount_to_hr_string(m.output.value, false));
let tx = match m.output.tx_log_entry {
None => "".to_owned(),
Some(t) => t.to_string(),
};
if dark_background_color_scheme {
table.add_row(row![
bFC->commit,
bFB->index,
bFB->height,
bFB->lock_height,
bFR->status,
bFY->is_coinbase,
bFB->num_confirmations,
bFG->value,
bFC->tx,
]);
} else {
table.add_row(row![
bFD->commit,
bFB->index,
bFB->height,
bFB->lock_height,
bFR->status,
bFD->is_coinbase,
bFB->num_confirmations,
bFG->value,
bFD->tx,
]);
}
}
table.set_format(*prettytable::format::consts::FORMAT_NO_COLSEP);
table.printstd();
println!();
if !validated {
println!(
"\nWARNING: Wallet failed to verify data. \
The above is from local cache and possibly invalid! \
(is your `grin server` offline or broken?)"
);
}
Ok(())
}
/// Display transaction log in a pretty way
pub fn txs(
account: &str,
cur_height: u64,
validated: bool,
txs: &[TxLogEntry],
include_status: bool,
dark_background_color_scheme: bool,
) -> Result<(), Error> {
let title = format!(
"Transaction Log - Account '{}' - Block Height: {}",
account, cur_height
);
println!();
if term::stdout().is_none() {
println!("Could not open terminal");
return Ok(());
}
let mut t = term::stdout().unwrap();
let _ = t.fg(term::color::MAGENTA);
writeln!(t, "{}", title)?;
let _ = t.reset();
let mut table = table!();
table.set_titles(row![
bMG->"Id",
bMG->"Type",
bMG->"State",
bMG->"Shared Transaction Id",
bMG->"Creation Time",
bMG->"TTL Cutoff Height",
bMG->"Confirmed?",
bMG->"Confirmation Time",
bMG->"Num. \nInputs",
bMG->"Num. \nOutputs",
bMG->"Amount \nCredited",
bMG->"Amount \nDebited",
bMG->"Fee",
bMG->"Net \nDifference",
bMG->"Payment \nProof",
bMG->"Kernel",
bMG->"Tx \nData",
]);
for t in txs {
let id = format!("{}", t.id);
let slate_id = match t.tx_slate_id {
Some(m) => format!("{}", m),
None => "None".to_owned(),
};
let slate_state = match t.tx_slate_state.as_ref() {
Some(m) => format!("{}", m),
None => "None".to_owned(),
};
let entry_type = format!("{}", t.tx_type);
let creation_ts = format!("{}", t.creation_ts.format("%Y-%m-%d %H:%M:%S"));
let ttl_cutoff_height = match t.ttl_cutoff_height {
Some(b) => format!("{}", b),
None => "None".to_owned(),
};
let confirmation_ts = match t.confirmation_ts {
Some(m) => format!("{}", m.format("%Y-%m-%d %H:%M:%S")),
None => "None".to_owned(),
};
let confirmed = format!("{}", t.confirmed);
let num_inputs = format!("{}", t.num_inputs);
let num_outputs = format!("{}", t.num_outputs);
let amount_debited_str = amount_to_hr_string(t.amount_debited, true);
let amount_credited_str = amount_to_hr_string(t.amount_credited, true);
let fee = match t.fee {
Some(f) => format!("{}", amount_to_hr_string(f.fee(), true)),
None => "None".to_owned(),
};
let net_diff = if t.amount_credited >= t.amount_debited {
amount_to_hr_string(t.amount_credited - t.amount_debited, true)
} else {
format!(
"-{}",
amount_to_hr_string(t.amount_debited - t.amount_credited, true)
)
};
let tx_data = match t.stored_tx {
Some(_) => "Yes".to_owned(),
None => "None".to_owned(),
};
let kernel_excess = match t.kernel_excess {
Some(e) => {
let excess: &[u8] = e.0.as_ref();
excess.to_hex()
}
None => "None".to_owned(),
};
let payment_proof = match t.payment_proof {
Some(_) => "Yes".to_owned(),
None => "None".to_owned(),
};
if dark_background_color_scheme {
table.add_row(row![
bFC->id,
bFC->entry_type,
bFC->slate_state,
bFC->slate_id,
bFB->creation_ts,
bFB->ttl_cutoff_height,
bFC->confirmed,
bFB->confirmation_ts,
bFC->num_inputs,
bFC->num_outputs,
bFG->amount_credited_str,
bFR->amount_debited_str,
bFR->fee,
bFY->net_diff,
bfG->payment_proof,
bFB->kernel_excess,
bFb->tx_data,
]);
} else {
if t.confirmed {
table.add_row(row![
bFD->id,
bFb->entry_type,
bFD->slate_id,
bFB->creation_ts,
bFg->confirmed,
bFB->confirmation_ts,
bFD->num_inputs,
bFD->num_outputs,
bFG->amount_credited_str,
bFD->amount_debited_str,
bFD->fee,
bFG->net_diff,
bfG->payment_proof,
bFB->kernel_excess,
bFB->tx_data,
]);
} else {
table.add_row(row![
bFD->id,
bFb->entry_type,
bFD->slate_id,
bFB->creation_ts,
bFR->confirmed,
bFB->confirmation_ts,
bFD->num_inputs,
bFD->num_outputs,
bFG->amount_credited_str,
bFD->amount_debited_str,
bFD->fee,
bFG->net_diff,
bfG->payment_proof,
bFB->kernel_excess,
bFB->tx_data,
]);
}
}
}
table.set_format(*prettytable::format::consts::FORMAT_NO_COLSEP);
table.printstd();
println!();
if !validated && include_status {
println!(
"\nWARNING: Wallet failed to verify data. \
The above is from local cache and possibly invalid! \
(is your `grin server` offline or broken?)"
);
}
Ok(())
}
pub fn view_wallet_balance(w: ViewWallet, cur_height: u64, dark_background_color_scheme: bool) {
println!(
"\n____ View Wallet Summary Info - Block Height: {} ____\n Rewind Hash - {}\n",
cur_height, w.rewind_hash
);
let mut table = table!();
if dark_background_color_scheme {
table.add_row(row![
bFG->"Total Balance",
FG->amount_to_hr_string(w.total_balance, false)
]);
} else {
table.add_row(row![
bFG->"Total Balance",
FG->amount_to_hr_string(w.total_balance, false)
]);
};
table.set_format(*prettytable::format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
table.printstd();
println!();
}
pub fn view_wallet_output(
view_wallet: ViewWallet,
cur_height: u64,
dark_background_color_scheme: bool,
) -> Result<(), Error> {
println!();
let title = format!("View Wallet Outputs - Block Height: {}", cur_height);
if term::stdout().is_none() {
println!("Could not open terminal");
return Ok(());
}
let mut t = term::stdout().unwrap();
let _ = t.fg(term::color::MAGENTA);
writeln!(t, "{}", title)?;
let _ = t.reset();
let mut table = table!();
table.set_titles(row![
bMG->"Output Commitment",
bMG->"MMR Index",
bMG->"Block Height",
bMG->"Locked Until",
bMG->"Coinbase?",
bMG->"# Confirms",
bMG->"Value",
]);
for m in view_wallet.output_result {
let commit = format!("{}", m.commit);
let index = m.mmr_index;
let height = format!("{}", m.height);
let lock_height = format!("{}", m.lock_height);
let is_coinbase = format!("{}", m.is_coinbase);
let num_confirmations = format!("{}", m.num_confirmations(cur_height));
let value = format!("{}", amount_to_hr_string(m.value, false));
if dark_background_color_scheme {
table.add_row(row![
bFC->commit,
bFB->index,
bFB->height,
bFB->lock_height,
bFY->is_coinbase,
bFB->num_confirmations,
bFG->value,
]);
} else {
table.add_row(row![
bFD->commit,
bFB->index,
bFB->height,
bFB->lock_height,
bFD->is_coinbase,
bFB->num_confirmations,
bFG->value,
]);
}
}
table.set_format(*prettytable::format::consts::FORMAT_NO_COLSEP);
table.printstd();
println!();
Ok(())
}
/// Display summary info in a pretty way
pub fn info(
account: &str,
wallet_info: &WalletInfo,
validated: bool,
dark_background_color_scheme: bool,
) {
println!(
"\n____ Wallet Summary Info - Account '{}' as of height {} ____\n",
account, wallet_info.last_confirmed_height,
);
let mut table = table!();
if dark_background_color_scheme {
table.add_row(row![
bFG->"Confirmed Total",
FG->amount_to_hr_string(wallet_info.total, false)
]);
if wallet_info.amount_reverted > 0 {
table.add_row(row![
Fr->"Reverted",
Fr->amount_to_hr_string(wallet_info.amount_reverted, false)
]);
}
// Only display "Immature Coinbase" if we have related outputs in the wallet.
// This row just introduces confusion if the wallet does not receive coinbase rewards.
if wallet_info.amount_immature > 0 {
table.add_row(row![
bFY->format!("Immature Coinbase (< {})", global::coinbase_maturity()),
FY->amount_to_hr_string(wallet_info.amount_immature, false)
]);
}
table.add_row(row![
bFY->format!("Awaiting Confirmation (< {})", wallet_info.minimum_confirmations),
FY->amount_to_hr_string(wallet_info.amount_awaiting_confirmation, false)
]);
table.add_row(row![
bFB->"Awaiting Finalization",
FB->amount_to_hr_string(wallet_info.amount_awaiting_finalization, false)
]);
table.add_row(row![
Fr->"Locked by previous transaction",
Fr->amount_to_hr_string(wallet_info.amount_locked, false)
]);
table.add_row(row![
Fw->"--------------------------------",
Fw->"-------------"
]);
table.add_row(row![
bFG->"Currently Spendable",
FG->amount_to_hr_string(wallet_info.amount_currently_spendable, false)
]);
} else {
table.add_row(row![
bFG->"Total",
FG->amount_to_hr_string(wallet_info.total, false)
]);
if wallet_info.amount_reverted > 0 {
table.add_row(row![
Fr->"Reverted",
Fr->amount_to_hr_string(wallet_info.amount_reverted, false)
]);
}
// Only display "Immature Coinbase" if we have related outputs in the wallet.
// This row just introduces confusion if the wallet does not receive coinbase rewards.
if wallet_info.amount_immature > 0 {
table.add_row(row![
bFB->format!("Immature Coinbase (< {})", global::coinbase_maturity()),
FB->amount_to_hr_string(wallet_info.amount_immature, false)
]);
}
table.add_row(row![
bFB->format!("Awaiting Confirmation (< {})", wallet_info.minimum_confirmations),
FB->amount_to_hr_string(wallet_info.amount_awaiting_confirmation, false)
]);
table.add_row(row![
Fr->"Locked by previous transaction",
Fr->amount_to_hr_string(wallet_info.amount_locked, false)
]);
table.add_row(row![
Fw->"--------------------------------",
Fw->"-------------"
]);
table.add_row(row![
bFG->"Currently Spendable",
FG->amount_to_hr_string(wallet_info.amount_currently_spendable, false)
]);
};
table.set_format(*prettytable::format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
table.printstd();
println!();
if !validated {
println!(
"\nWARNING: Wallet failed to verify data against a live chain. \
The above is from local cache and only valid up to the given height! \
(is your `grin server` offline or broken?)"
);
}
}
/// Display summary info in a pretty way
pub fn estimate(
amount: u64,
strategies: Vec<(
&str, // strategy
u64, // total amount to be locked
FeeFields, // fee
)>,
dark_background_color_scheme: bool,
) {
println!(
"\nEstimation for sending {}:\n",
amount_to_hr_string(amount, false)
);
let mut table = table!();
table.set_titles(row![
bMG->"Selection strategy",
bMG->"Fee",
bMG->"Will be locked",
]);
for (strategy, total, fee_fields) in strategies {
if dark_background_color_scheme {
table.add_row(row![
bFC->strategy,
FR->amount_to_hr_string(fee_fields.fee(), false), // apply fee mask past HF4
FY->amount_to_hr_string(total, false),
]);
} else {
table.add_row(row![
bFD->strategy,
FR->amount_to_hr_string(fee_fields.fee(), false), // apply fee mask past HF4
FY->amount_to_hr_string(total, false),
]);
}
}
table.printstd();
println!();
}
/// Display list of wallet accounts in a pretty way
pub fn accounts(acct_mappings: Vec<AcctPathMapping>) {
println!("\n____ Wallet Accounts ____\n");
let mut table = table!();
table.set_titles(row![
mMG->"Name",
bMG->"Parent Output Key",
bMG->"Slatepack Address (Index 0)",
]);
for m in acct_mappings {
let slatepack_path = address::address_derivation_path(&m.path, 0).to_bip_32_string();
table.add_row(row![
bFC->m.label,
bGC->m.path.to_bip_32_string(),
bGC->slatepack_path,
]);
}
table.set_format(
FormatBuilder::new()
.column_separator('|')
.separators(
&[LinePosition::Top, LinePosition::Title],
LineSeparator::new('-', '+', '+', '+'),
)
.padding(1, 1)
.build(),
);
let width = table.to_string().find('\n').unwrap_or(0);
println!("{:^1$}", "BIP-32 Derivation Path", width);
table.printstd();
println!();
}
/// Display individual Payment Proof
pub fn payment_proof(tx: &TxLogEntry) -> Result<(), Error> {
let title = format!("Payment Proof - Transaction '{}'", tx.id);
println!();
if term::stdout().is_none() {
println!("Could not open terminal");
return Ok(());
}
let mut t = term::stdout().unwrap();
let _ = t.fg(term::color::MAGENTA);
writeln!(t, "{}", title)?;
let _ = t.reset();
let pp = match &tx.payment_proof {
None => {
writeln!(t, "None")?;
let _ = t.reset();
return Ok(());
}
Some(p) => p.clone(),
};
let _ = t.fg(term::color::WHITE);
writeln!(t)?;
let receiver_signature = match pp.receiver_signature {
Some(s) => {
let sig_bytes = s.to_bytes();
let sig_ref: &[u8] = sig_bytes.as_ref();
sig_ref.to_hex()
}
None => "None".to_owned(),
};
let fee = match tx.fee {
Some(f) => f.fee(), // apply fee mask past HF4
None => 0,
};
let amount = if tx.amount_credited >= tx.amount_debited {
amount_to_hr_string(tx.amount_credited - tx.amount_debited, true)
} else {
format!(
"{}",
amount_to_hr_string(tx.amount_debited - tx.amount_credited - fee, true)
)
};
let sender_signature = match pp.sender_signature {
Some(s) => {
let sig_bytes = s.to_bytes();
let sig_ref: &[u8] = sig_bytes.as_ref();
sig_ref.to_hex()
}
None => "None".to_owned(),
};
let kernel_excess = match tx.kernel_excess {
Some(e) => {
let excess: &[u8] = e.0.as_ref();
excess.to_hex()
}
None => "None".to_owned(),
};
writeln!(
t,
"Receiver Address: {}",
SlatepackAddress::new(&pp.receiver_address)
)?;
writeln!(t, "Receiver Signature: {}", receiver_signature)?;
writeln!(t, "Amount: {}", amount)?;
writeln!(t, "Kernel Excess: {}", kernel_excess)?;
writeln!(
t,
"Sender Address: {}",
SlatepackAddress::new(&pp.sender_address)
)?;
writeln!(t, "Sender Signature: {}", sender_signature)?;
let _ = t.reset();
println!();
Ok(())
}