diff --git a/Cargo.toml b/Cargo.toml index 76ec998..b4ed0c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "ghost-eye" authors = ["str3tch "] description = "Application for interacting with Casper/Ghost nodes that are exposing RPC only to the localhost" -version = "0.3.43" +version = "0.3.44" edition = "2021" homepage = "https://git.ghostchain.io/ghostchain" repository = "https://git.ghostchain.io/ghostchain/ghost-eye" diff --git a/src/action.rs b/src/action.rs index 879c303..7d886f3 100644 --- a/src/action.rs +++ b/src/action.rs @@ -26,7 +26,7 @@ pub enum Action { SetActiveScreen(crate::app::Mode), UsedExplorerBlock(Option), UsedExplorerLog(Option), - UsedAccount(String), + UsedAccount([u8; 32], String), NewAccount(String), NewAddressBookRecord(String, String), SetSender(String, Option), @@ -138,8 +138,8 @@ pub enum Action { SetValidatorEraUnlocking(u32, u128, [u8; 32]), SetValidatorLatestClaim(u32, [u8; 32]), SetValidatorInSession(bool, [u8; 32]), - SetIsBonded(bool), - SetStakedAmountRatio(u128, u128, [u8; 32]), + SetIsBonded(bool, [u8; 32]), + SetStakedAmountRatio(Option, Option, [u8; 32]), SetStakedRatio(u128, u128, [u8; 32]), SetValidatorPrefs(Option, bool, [u8; 32]), SetCurrentValidatorEraRewards(u32, u32, Vec), diff --git a/src/components/nominator/current_validator_details.rs b/src/components/nominator/current_validator_details.rs index 9989358..f5ed658 100644 --- a/src/components/nominator/current_validator_details.rs +++ b/src/components/nominator/current_validator_details.rs @@ -141,7 +141,7 @@ impl Component for CurrentValidatorDetails { Action::SetChoosenValidator(account_id, individual, total) => self.update_choosen_validator(account_id, individual, total), Action::SetValidatorPrefs(commission, is_disabled, account_id) if self.choosen == account_id => self.update_commission(commission, is_disabled), Action::SetNominatorsByValidator(noms, account_id) if self.choosen == account_id => self.others_len = noms.len(), - Action::SetStakedAmountRatio(_, active_stake, account_id) if self.choosen == account_id => self.active_stake = active_stake, + Action::SetStakedAmountRatio(_, active_stake, account_id) if self.choosen == account_id => self.active_stake = active_stake.unwrap_or_default(), Action::SetValidatorLatestClaim(era_index, account_id) if self.choosen == account_id => self.latest_era_claim = era_index, Action::SetStakedRatio(total, own, account_id) if self.choosen == account_id => { self.total_balance = total; diff --git a/src/components/validator/bond_popup.rs b/src/components/validator/bond_popup.rs index a1c0550..6a8cec3 100644 --- a/src/components/validator/bond_popup.rs +++ b/src/components/validator/bond_popup.rs @@ -22,7 +22,8 @@ pub struct BondPopup { is_active: bool, action_tx: Option>, network_tx: Option>, - secret_seed: [u8; 32], + stash_secret_seed: [u8; 32], + stash_account_id: [u8; 32], minimal_bond: u128, is_bonded: bool, amount: Input, @@ -39,7 +40,8 @@ impl BondPopup { pub fn new() -> Self { Self { is_active: false, - secret_seed: [0u8; 32], + stash_secret_seed: [0u8; 32], + stash_account_id: [0u8; 32], action_tx: None, network_tx: None, minimal_bond: 0u128, @@ -70,9 +72,9 @@ impl BondPopup { Ok(value) => { let amount = (value * 1_000_000_000_000_000_000.0) as u128; let _ = if self.is_bonded { - network_tx.send(Action::BondValidatorExtraFrom(self.secret_seed, amount)) + network_tx.send(Action::BondValidatorExtraFrom(self.stash_secret_seed, amount)) } else { - network_tx.send(Action::BondValidatorFrom(self.secret_seed, amount)) + network_tx.send(Action::BondValidatorFrom(self.stash_secret_seed, amount)) }; if let Some(action_tx) = &self.action_tx { let _ = action_tx.send(Action::ClosePopup); @@ -154,9 +156,11 @@ impl Component for BondPopup { fn update(&mut self, action: Action) -> Result> { match action { - Action::SetIsBonded(is_bonded) => self.is_bonded = is_bonded, + Action::SetIsBonded(is_bonded, account_id) if self.stash_account_id == account_id => + self.is_bonded = is_bonded, Action::SetMinValidatorBond(minimal_bond) => self.minimal_bond = minimal_bond, - Action::SetStashSecret(secret_seed) => self.secret_seed = secret_seed, + Action::SetStashSecret(secret_seed) => self.stash_secret_seed = secret_seed, + Action::SetStashAccount(account_id) => self.stash_account_id = account_id, _ => {} }; Ok(None) diff --git a/src/components/validator/stash_details.rs b/src/components/validator/stash_details.rs index 979debf..92aee81 100644 --- a/src/components/validator/stash_details.rs +++ b/src/components/validator/stash_details.rs @@ -98,10 +98,11 @@ impl Component for StashDetails { match action { Action::SetStashSecret(secret) => self.stash_secret = secret, Action::SetStashAccount(account_id) => self.stash_account_id = account_id, - Action::SetIsBonded(is_bonded) => self.is_bonded = is_bonded, + Action::SetIsBonded(is_bonded, account_id) if self.stash_account_id == account_id => + self.is_bonded = is_bonded, Action::SetStakedAmountRatio(total, active, account_id) if self.stash_account_id == account_id => { - self.staked_total = Some(total); - self.staked_active = Some(active); + self.staked_total = total; + self.staked_active = active; }, Action::BalanceResponse(account_id, maybe_balance) if account_id == self.stash_account_id => { if let Some(network_tx) = &self.network_tx { diff --git a/src/components/validator/unbond_popup.rs b/src/components/validator/unbond_popup.rs index 22d8449..e9bed00 100644 --- a/src/components/validator/unbond_popup.rs +++ b/src/components/validator/unbond_popup.rs @@ -22,7 +22,8 @@ pub struct UnbondPopup { is_active: bool, action_tx: Option>, network_tx: Option>, - secret_seed: [u8; 32], + stash_secret_seed: [u8; 32], + stash_account_id: [u8; 32], is_bonded: bool, amount: Input, palette: StylePalette @@ -38,7 +39,8 @@ impl UnbondPopup { pub fn new() -> Self { Self { is_active: false, - secret_seed: [0u8; 32], + stash_secret_seed: [0u8; 32], + stash_account_id: [0u8; 32], action_tx: None, network_tx: None, is_bonded: false, @@ -67,7 +69,7 @@ impl UnbondPopup { if self.is_bonded { let amount = (value * 1_000_000_000_000_000_000.0) as u128; let _ = network_tx.send(Action::UnbondFrom( - self.secret_seed, amount)); + self.stash_secret_seed, amount)); } else { self.log_event( format!("current stash doesn't have bond yet"), @@ -153,8 +155,10 @@ impl Component for UnbondPopup { fn update(&mut self, action: Action) -> Result> { match action { - Action::SetIsBonded(is_bonded) => self.is_bonded = is_bonded, - Action::SetStashSecret(secret_seed) => self.secret_seed = secret_seed, + Action::SetIsBonded(is_bonded, account_id) if self.stash_account_id == account_id => + self.is_bonded = is_bonded, + Action::SetStashSecret(secret_seed) => self.stash_secret_seed = secret_seed, + Action::SetStashAccount(account_id) => self.stash_account_id = account_id, _ => {} }; Ok(None) diff --git a/src/components/validator/withdraw_popup.rs b/src/components/validator/withdraw_popup.rs index 84eddd5..40dab5c 100644 --- a/src/components/validator/withdraw_popup.rs +++ b/src/components/validator/withdraw_popup.rs @@ -126,7 +126,7 @@ impl Component for WithdrawPopup { Action::SetSlashingSpansLength(length, account_id) if self.stash_account == account_id => self.slashing_spans_length = length as u32, Action::SetStakedAmountRatio(_, active_balance, account_id) if self.stash_account == account_id => - self.active_balance = active_balance, + self.active_balance = active_balance.unwrap_or_default(), Action::SetUnlockingIsEmpty(is_empty, account_id) if self.stash_account == account_id => self.unlocking_is_empty = is_empty, _ => {} diff --git a/src/components/wallet/accounts.rs b/src/components/wallet/accounts.rs index 3575ee7..8ec303c 100644 --- a/src/components/wallet/accounts.rs +++ b/src/components/wallet/accounts.rs @@ -88,8 +88,9 @@ impl Accounts { fn set_used_account(&mut self, index: usize) { let used_seed = self.wallet_keys[index].seed.clone(); + let account_id = self.wallet_keys[index].account_id; if let Some(action_tx) = &self.action_tx { - let _ = action_tx.send(Action::UsedAccount(used_seed.clone())); + let _ = action_tx.send(Action::UsedAccount(account_id, used_seed.clone())); } self.set_sender_nonce(index); } @@ -526,7 +527,7 @@ impl Component for Accounts { } fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { - let [_, place, _] = super::account_layout(area); + let [_, place, _, _] = super::account_layout(area); let (border_style, border_type) = self.palette.create_border_style(self.is_active); let table = Table::new( diff --git a/src/components/wallet/balance.rs b/src/components/wallet/balance.rs index 2cb56ff..0550b67 100644 --- a/src/components/wallet/balance.rs +++ b/src/components/wallet/balance.rs @@ -111,7 +111,7 @@ impl Component for Balance { } fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { - let [_, _, place] = super::account_layout(area); + let [_, _, place, _] = super::account_layout(area); let (border_style, border_type) = self.palette .create_border_style(self.is_active); diff --git a/src/components/wallet/mod.rs b/src/components/wallet/mod.rs index 2c5134f..cf89424 100644 --- a/src/components/wallet/mod.rs +++ b/src/components/wallet/mod.rs @@ -19,6 +19,7 @@ mod overview; mod add_address_book_record; mod rename_address_book_record; mod details; +mod staking_ledger; use balance::Balance; use transfer::Transfer; @@ -31,6 +32,7 @@ use overview::Overview; use add_address_book_record::AddAddressBookRecord; use rename_address_book_record::RenameAddressBookRecord; use details::AccountDetails; +use staking_ledger::StakingLedger; use super::Component; use crate::{action::Action, app::Mode, config::Config}; @@ -70,6 +72,7 @@ impl Default for Wallet { Box::new(Overview::default()), Box::new(Accounts::default()), Box::new(Balance::default()), + Box::new(StakingLedger::default()), Box::new(AddressBook::default()), Box::new(EventLogs::default()), Box::new(AddAccount::default()), @@ -263,11 +266,12 @@ pub fn bars_layout(area: Rect) -> [Rect; 2] { ]).areas(place) } -pub fn account_layout(area: Rect) -> [Rect; 3] { +pub fn account_layout(area: Rect) -> [Rect; 4] { let [place, _] = bars_layout(area); Layout::vertical([ Constraint::Max(4), Constraint::Min(0), Constraint::Max(7), + Constraint::Max(5), ]).areas(place) } diff --git a/src/components/wallet/overview.rs b/src/components/wallet/overview.rs index 59d7932..935931b 100644 --- a/src/components/wallet/overview.rs +++ b/src/components/wallet/overview.rs @@ -84,7 +84,7 @@ impl Component for Overview { } fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { - let [place, _, _] = super::account_layout(area); + let [place, _, _, _] = super::account_layout(area); let (border_style, border_type) = self.palette .create_border_style(self.is_active); diff --git a/src/components/wallet/staking_ledger.rs b/src/components/wallet/staking_ledger.rs new file mode 100644 index 0000000..7e0fe48 --- /dev/null +++ b/src/components/wallet/staking_ledger.rs @@ -0,0 +1,154 @@ +use color_eyre::Result; +use ratatui::{ + text::Text, + layout::{Alignment, Constraint, Rect}, + widgets::{Block, Cell, Row, Table}, + Frame +}; +use std::sync::mpsc::Sender; + +use super::{Component, PartialComponent, CurrentTab}; +use crate::{ + widgets::DotSpinner, + action::Action, + config::Config, + palette::StylePalette, +}; + +#[derive(Debug)] +pub struct StakingLedger { + is_active: bool, + is_bonded: bool, + account_id: [u8; 32], + network_tx: Option>, + total_staked: Option, + active_staked: Option, + palette: StylePalette +} + +impl Default for StakingLedger { + fn default() -> Self { + Self::new() + } +} + +impl StakingLedger { + const TICKER: &str = " CSPR"; + const DECIMALS: usize = 6; + + pub fn new() -> Self { + Self { + is_active: false, + is_bonded: false, + account_id: [0u8; 32], + network_tx: None, + total_staked: None, + active_staked: None, + palette: StylePalette::default(), + } + } + + fn set_used_account_id(&mut self, account_id: [u8; 32]) { + self.account_id = account_id; + if let Some(network_tx) = &self.network_tx { + let _ = network_tx.send(Action::GetValidatorLedger(account_id, false)); + let _ = network_tx.send(Action::GetIsStashBonded(account_id, false)); + } + } + + fn prepare_u128(&self, maybe_value: Option) -> String { + match maybe_value { + Some(value) => { + let value = value as f64 / 10f64.powi(18); + let after = Self::DECIMALS; + format!("{:.after$}{}", value, Self::TICKER) + }, + None => format!("{}{}", DotSpinner::default().to_string(), Self::TICKER) + } + } + + fn is_bonded_to_string(&self) -> String { + if self.is_bonded { + "bonded".to_string() + } else { + "no bond".to_string() + } + } +} + +impl PartialComponent for StakingLedger { + fn set_active(&mut self, current_tab: CurrentTab) { + match current_tab { + CurrentTab::Accounts => self.is_active = true, + _ => self.is_active = false, + } + } +} + +impl Component for StakingLedger { + fn register_network_handler(&mut self, tx: Sender) -> Result<()> { + self.network_tx = Some(tx); + Ok(()) + } + + fn register_config_handler(&mut self, config: Config) -> Result<()> { + if let Some(style) = config.styles.get(&crate::app::Mode::Wallet) { + self.palette.with_normal_style(style.get("normal_style").copied()); + self.palette.with_hover_style(style.get("hover_style").copied()); + self.palette.with_normal_border_style(style.get("normal_border_style").copied()); + self.palette.with_hover_border_style(style.get("hover_border_style").copied()); + self.palette.with_normal_title_style(style.get("normal_title_style").copied()); + self.palette.with_hover_title_style(style.get("hover_title_style").copied()); + } + Ok(()) + } + + fn update(&mut self, action: Action) -> Result> { + match action { + Action::UsedAccount(account_id, _) => self.set_used_account_id(account_id), + Action::SetIsBonded(is_bonded, account_id) if self.account_id == account_id => self.is_bonded = is_bonded, + Action::SetStakedAmountRatio(total, active, account_id) if self.account_id == account_id => { + self.total_staked = total; + self.active_staked = active; + }, + _ => {} + }; + Ok(None) + } + + fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { + let [_, _, _, place] = super::account_layout(area); + let (border_style, border_type) = self.palette + .create_border_style(self.is_active); + + let table = Table::new( + [ + Row::new(vec![ + Cell::from(Text::from("Bond ready: ".to_string()).alignment(Alignment::Left)), + Cell::from(Text::from(self.is_bonded_to_string()).alignment(Alignment::Right)), + ]), + Row::new(vec![ + Cell::from(Text::from("total: ".to_string()).alignment(Alignment::Left)), + Cell::from(Text::from(self.prepare_u128(self.total_staked)).alignment(Alignment::Right)) + ]), + Row::new(vec![ + Cell::from(Text::from("active: ".to_string()).alignment(Alignment::Left)), + Cell::from(Text::from(self.prepare_u128(self.active_staked)).alignment(Alignment::Right)), + ]), + ], + [ + Constraint::Max(10), + Constraint::Min(14), + ] + ) + .block(Block::bordered() + .border_style(border_style) + .border_type(border_type) + .title_alignment(Alignment::Right) + .title_style(self.palette.create_title_style(false)) + .title("Nomination stake")); + + frame.render_widget(table, place); + Ok(()) + } +} diff --git a/src/components/wallet/transfer.rs b/src/components/wallet/transfer.rs index 8b06312..60f5b82 100644 --- a/src/components/wallet/transfer.rs +++ b/src/components/wallet/transfer.rs @@ -215,7 +215,7 @@ impl Component for Transfer { fn update(&mut self, action: Action) -> Result> { match action { - Action::UsedAccount(seed) => self.sender = seed, + Action::UsedAccount(_, seed) => self.sender = seed, Action::TransferTo(who) => { self.receiver = Input::new(who); self.receiver_or_amount = ReceiverOrAmount::Amount; diff --git a/src/network/mod.rs b/src/network/mod.rs index 6ea17d0..349ea0b 100644 --- a/src/network/mod.rs +++ b/src/network/mod.rs @@ -129,6 +129,8 @@ impl Network { predefined_calls::get_validator_prefs(&self.action_tx, &self.online_client_api, &validator_details_to_watch).await?; predefined_calls::get_staking_value_ratio(&self.action_tx, &self.online_client_api, &validator_details_to_watch).await?; predefined_calls::get_validator_latest_claim(&self.action_tx, &self.online_client_api, &validator_details_to_watch).await?; + predefined_calls::get_validators_ledger(&self.action_tx, &self.online_client_api, &validator_details_to_watch).await?; + predefined_calls::get_is_stash_bonded(&self.action_tx, &self.online_client_api, &validator_details_to_watch).await?; } for account_id in self.accounts_to_watch.iter() { predefined_calls::get_balance(&self.action_tx, &self.online_client_api, &account_id).await?; diff --git a/src/network/predefined_calls.rs b/src/network/predefined_calls.rs index 43dddd6..b5f11b4 100644 --- a/src/network/predefined_calls.rs +++ b/src/network/predefined_calls.rs @@ -430,11 +430,17 @@ pub async fn get_validators_ledger( let maybe_ledger = super::raw_calls::staking::ledger(api, None, account_id) .await?; - if let Some(ledger) = maybe_ledger { - action_tx.send(Action::SetStakedAmountRatio(ledger.total, ledger.active, *account_id))?; - action_tx.send(Action::SetUnlockingIsEmpty(ledger.unlocking.0.is_empty(), *account_id))?; - for chunk in ledger.unlocking.0.iter() { - action_tx.send(Action::SetValidatorEraUnlocking(chunk.era, chunk.value, *account_id))?; + match maybe_ledger { + Some(ledger) => { + action_tx.send(Action::SetStakedAmountRatio(Some(ledger.total), Some(ledger.active), *account_id))?; + action_tx.send(Action::SetUnlockingIsEmpty(ledger.unlocking.0.is_empty(), *account_id))?; + for chunk in ledger.unlocking.0.iter() { + action_tx.send(Action::SetValidatorEraUnlocking(chunk.era, chunk.value, *account_id))?; + } + }, + None => { + action_tx.send(Action::SetStakedAmountRatio(None, None, *account_id))?; + action_tx.send(Action::SetUnlockingIsEmpty(true, *account_id))?; } } @@ -481,7 +487,7 @@ pub async fn get_is_stash_bonded( let is_bonded = super::raw_calls::staking::bonded(api, None, account_id) .await? .is_some(); - action_tx.send(Action::SetIsBonded(is_bonded))?; + action_tx.send(Action::SetIsBonded(is_bonded, *account_id))?; Ok(()) }