diff --git a/Cargo.toml b/Cargo.toml index 62f9cb3..09f2ef8 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.47" +version = "0.3.48" 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 9422a8d..dba2947 100644 --- a/src/action.rs +++ b/src/action.rs @@ -51,8 +51,8 @@ pub enum Action { StoreRotatedKeys(String), TransferBalance(String, [u8; 32], u128), - BondValidatorExtraFrom([u8; 32], u128), - BondValidatorFrom([u8; 32], u128), + BondValidatorExtraFrom([u8; 32], u128, ActionTarget), + BondValidatorFrom([u8; 32], u128, ActionTarget), PayoutStakers([u8; 32], [u8; 32], u32), SetSessionKeys([u8; 32], String), ValidateFrom([u8; 32], u32), diff --git a/src/components/validator/bond_popup.rs b/src/components/validator/bond_popup.rs index 6a8cec3..325bb5b 100644 --- a/src/components/validator/bond_popup.rs +++ b/src/components/validator/bond_popup.rs @@ -71,10 +71,11 @@ impl BondPopup { match str_amount.parse::() { Ok(value) => { let amount = (value * 1_000_000_000_000_000_000.0) as u128; + let log_target = ActionTarget::ValidatorLog; let _ = if self.is_bonded { - network_tx.send(Action::BondValidatorExtraFrom(self.stash_secret_seed, amount)) + network_tx.send(Action::BondValidatorExtraFrom(self.stash_secret_seed, amount, log_target)) } else { - network_tx.send(Action::BondValidatorFrom(self.stash_secret_seed, amount)) + network_tx.send(Action::BondValidatorFrom(self.stash_secret_seed, amount, log_target)) }; if let Some(action_tx) = &self.action_tx { let _ = action_tx.send(Action::ClosePopup); diff --git a/src/components/wallet/accounts.rs b/src/components/wallet/accounts.rs index 4dddc54..e5555d9 100644 --- a/src/components/wallet/accounts.rs +++ b/src/components/wallet/accounts.rs @@ -90,7 +90,9 @@ impl Accounts { 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(account_id, used_seed.clone())); + let _ = action_tx.send(Action::UsedAccount( + account_id, + used_seed.clone())); } self.set_sender_nonce(index); } diff --git a/src/components/wallet/bond_popup.rs b/src/components/wallet/bond_popup.rs new file mode 100644 index 0000000..310f80f --- /dev/null +++ b/src/components/wallet/bond_popup.rs @@ -0,0 +1,203 @@ +use crossterm::event::{KeyCode, KeyEvent, KeyEventKind}; +use color_eyre::Result; +use ratatui::{ + layout::{Position, Alignment, Constraint, Flex, Layout, Rect}, + widgets::{Block, Clear, Paragraph}, + Frame +}; +use tokio::sync::mpsc::UnboundedSender; +use std::sync::mpsc::Sender; + +use super::{Component, PartialComponent, CurrentTab}; +use crate::{ + action::Action, + config::Config, + palette::StylePalette, + types::{ActionLevel, ActionTarget}, + widgets::{Input, InputRequest}, +}; + +#[derive(Debug)] +pub struct BondPopup { + is_active: bool, + action_tx: Option>, + network_tx: Option>, + account_secret_seed: [u8; 32], + account_id: [u8; 32], + minimal_bond: u128, + is_bonded: bool, + amount: Input, + palette: StylePalette +} + +impl Default for BondPopup { + fn default() -> Self { + Self::new() + } +} + +impl BondPopup { + pub fn new() -> Self { + Self { + is_active: false, + account_secret_seed: [0u8; 32], + account_id: [0u8; 32], + action_tx: None, + network_tx: None, + minimal_bond: 0u128, + is_bonded: false, + amount: Input::new(String::new()), + palette: StylePalette::default(), + } + } +} + +impl BondPopup { + fn log_event(&mut self, message: String, level: ActionLevel) { + if let Some(action_tx) = &self.action_tx { + let _ = action_tx.send( + Action::EventLog(message, level, ActionTarget::WalletLog)); + } + } + + fn update_used_account(&mut self, account_id: [u8; 32], secret_seed_str: String) { + let secret_seed: [u8; 32] = hex::decode(secret_seed_str) + .expect("stored seed is valid hex string; qed") + .as_slice() + .try_into() + .expect("stored seed is valid length; qed"); + self.account_id = account_id; + self.account_secret_seed = secret_seed; + } + + fn submit_message(&mut self) { + if let Some(network_tx) = &self.network_tx { + let str_amount = self.amount.value(); + let str_amount = if str_amount.starts_with('.') { + &format!("0{}", str_amount)[..] + } else { + str_amount + }; + match str_amount.parse::() { + Ok(value) => { + let amount = (value * 1_000_000_000_000_000_000.0) as u128; + let log_target = ActionTarget::WalletLog; + let _ = if self.is_bonded { + network_tx.send(Action::BondValidatorExtraFrom(self.account_secret_seed, amount, log_target)) + } else { + network_tx.send(Action::BondValidatorFrom(self.account_secret_seed, amount, log_target)) + }; + if let Some(action_tx) = &self.action_tx { + let _ = action_tx.send(Action::ClosePopup); + } + }, + Err(err) => self.log_event( + format!("invalid amount, error: {err}"), ActionLevel::Error), + } + } + } + + fn enter_char(&mut self, new_char: char) { + let is_separator_needed = !self.amount.value().contains('.') && new_char == '.'; + if new_char.is_digit(10) || is_separator_needed { + let _ = self.amount.handle(InputRequest::InsertChar(new_char)); + } + } + + fn delete_char(&mut self) { + let _ = self.amount.handle(InputRequest::DeletePrevChar); + } + + fn move_cursor_right(&mut self) { + let _ = self.amount.handle(InputRequest::GoToNextChar); + } + + fn move_cursor_left(&mut self) { + let _ = self.amount.handle(InputRequest::GoToPrevChar); + } +} + +impl PartialComponent for BondPopup { + fn set_active(&mut self, current_tab: CurrentTab) { + match current_tab { + CurrentTab::BondPopup => self.is_active = true, + _ => { + self.is_active = false; + self.amount = Input::new(String::new()); + } + }; + } +} + +impl Component for BondPopup { + fn register_network_handler(&mut self, tx: Sender) -> Result<()> { + self.network_tx = Some(tx); + Ok(()) + } + + fn register_action_handler(&mut self, tx: UnboundedSender) -> Result<()> { + self.action_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_normal_border_style(style.get("normal_border_style").copied()); + self.palette.with_normal_title_style(style.get("normal_title_style").copied()); + self.palette.with_popup_style(style.get("popup_style").copied()); + self.palette.with_popup_title_style(style.get("popup_title_style").copied()); + } + Ok(()) + } + fn handle_key_event(&mut self, key: KeyEvent) -> Result> { + if self.is_active && key.kind == KeyEventKind::Press { + match key.code { + KeyCode::Enter => self.submit_message(), + KeyCode::Char(to_insert) => self.enter_char(to_insert), + KeyCode::Backspace => self.delete_char(), + KeyCode::Left => self.move_cursor_left(), + KeyCode::Right => self.move_cursor_right(), + KeyCode::Esc => self.is_active = false, + _ => {}, + }; + } + Ok(None) + } + + fn update(&mut self, action: Action) -> Result> { + match action { + Action::SetIsBonded(is_bonded, account_id) if self.account_id == account_id => + self.is_bonded = is_bonded, + Action::UsedAccount(account_id, secret_seed) => self.update_used_account(account_id, secret_seed), + Action::SetMinValidatorBond(minimal_bond) => self.minimal_bond = minimal_bond, + _ => {} + }; + Ok(None) + } + + fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { + if self.is_active { + let (border_style, border_type) = self.palette.create_popup_style(); + let input = Paragraph::new(self.amount.value()) + .block(Block::bordered() + .border_style(border_style) + .border_type(border_type) + .title_style(self.palette.create_popup_title_style()) + .title_alignment(Alignment::Right) + .title(format!("Staking bond amount"))); + let v = Layout::vertical([Constraint::Max(3)]).flex(Flex::Center); + let h = Layout::horizontal([Constraint::Max(50)]).flex(Flex::Center); + let [area] = v.areas(area); + let [area] = h.areas(area); + + frame.render_widget(Clear, area); + frame.render_widget(input, area); + frame.set_cursor_position(Position::new( + area.x + self.amount.cursor() as u16 + 1, + area.y + 1 + )); + } + Ok(()) + } +} diff --git a/src/components/wallet/mod.rs b/src/components/wallet/mod.rs index cf89424..c3ab839 100644 --- a/src/components/wallet/mod.rs +++ b/src/components/wallet/mod.rs @@ -20,6 +20,7 @@ mod add_address_book_record; mod rename_address_book_record; mod details; mod staking_ledger; +mod bond_popup; use balance::Balance; use transfer::Transfer; @@ -33,6 +34,7 @@ use add_address_book_record::AddAddressBookRecord; use rename_address_book_record::RenameAddressBookRecord; use details::AccountDetails; use staking_ledger::StakingLedger; +use bond_popup::BondPopup; use super::Component; use crate::{action::Action, app::Mode, config::Config}; @@ -49,6 +51,7 @@ pub enum CurrentTab { RenameAddressBookRecord, Transfer, AccountDetails, + BondPopup, } pub trait PartialComponent: Component { @@ -81,6 +84,7 @@ impl Default for Wallet { Box::new(RenameAddressBookRecord::default()), Box::new(Transfer::default()), Box::new(AccountDetails::default()), + Box::new(BondPopup::default()), ], } } @@ -137,6 +141,7 @@ impl Component for Wallet { CurrentTab::RenameAddressBookRecord | CurrentTab::Transfer | CurrentTab::AccountDetails | + CurrentTab::BondPopup | CurrentTab::AddAddressBookRecord => match key.code { KeyCode::Esc => { self.current_tab = self.previous_tab; @@ -180,6 +185,13 @@ impl Component for Wallet { component.set_active(self.current_tab.clone()); } }, + KeyCode::Char('B') => { + self.previous_tab = self.current_tab; + self.current_tab = CurrentTab::BondPopup; + for component in self.components.iter_mut() { + component.set_active(self.current_tab.clone()); + } + }, KeyCode::Char('l') | KeyCode::Right => { self.move_right(); for component in self.components.iter_mut() { @@ -213,7 +225,7 @@ impl Component for Wallet { self.previous_tab = self.current_tab; self.current_tab = CurrentTab::Accounts; }, - Action::UpdateAddressBookRecord(_) | Action::NewAddressBookRecord(_, _) | Action::ClosePopup => { + Action::UpdateAddressBookRecord(_) | Action::NewAddressBookRecord(_, _) => { self.previous_tab = self.current_tab; self.current_tab = CurrentTab::AddressBook; }, @@ -233,6 +245,9 @@ impl Component for Wallet { self.previous_tab = self.current_tab; self.current_tab = CurrentTab::AccountDetails; }, + Action::ClosePopup => { + self.current_tab = self.previous_tab; + } _ => {} } for component in self.components.iter_mut() { diff --git a/src/network/mod.rs b/src/network/mod.rs index 2a963b9..bcac64d 100644 --- a/src/network/mod.rs +++ b/src/network/mod.rs @@ -283,7 +283,7 @@ impl Network { } Ok(()) } - Action::BondValidatorExtraFrom(sender, amount) => { + Action::BondValidatorExtraFrom(sender, amount, log_target) => { let sender_str = hex::encode(sender); let maybe_nonce = self.senders.get_mut(&sender_str); @@ -293,16 +293,17 @@ impl Network { &sender, &amount, maybe_nonce, + log_target, ).await { self.transactions_to_watch.push(TxToWatch { tx_progress, sender: sender_str, - target: ActionTarget::ValidatorLog, + target: log_target, }); } Ok(()) } - Action::BondValidatorFrom(sender, amount) => { + Action::BondValidatorFrom(sender, amount, log_target) => { let sender_str = hex::encode(sender); let maybe_nonce = self.senders.get_mut(&sender_str); if let Ok(tx_progress) = predefined_txs::bond( @@ -311,11 +312,12 @@ impl Network { &sender, &amount, maybe_nonce, + log_target, ).await { self.transactions_to_watch.push(TxToWatch { tx_progress, sender: sender_str, - target: ActionTarget::ValidatorLog, + target: log_target, }); } Ok(()) diff --git a/src/network/predefined_txs.rs b/src/network/predefined_txs.rs index 9d1a884..ffb8643 100644 --- a/src/network/predefined_txs.rs +++ b/src/network/predefined_txs.rs @@ -40,6 +40,7 @@ pub async fn bond_extra( sender: &[u8; 32], amount: &u128, maybe_nonce: Option<&mut u32>, + log_target: ActionTarget, ) -> Result>> { let bond_extra_tx = casper_network::tx().staking().bond_extra(*amount); inner_sign_and_submit_then_watch( @@ -49,7 +50,7 @@ pub async fn bond_extra( maybe_nonce, Box::new(bond_extra_tx), "bond extra", - ActionTarget::ValidatorLog, + log_target, ).await } @@ -59,6 +60,7 @@ pub async fn bond( sender: &[u8; 32], amount: &u128, maybe_nonce: Option<&mut u32>, + log_target: ActionTarget, ) -> Result>> { // auto-stake everything by now let reward_destination = casper_network::runtime_types::pallet_staking::RewardDestination::Staked; @@ -70,7 +72,7 @@ pub async fn bond( maybe_nonce, Box::new(bond_tx), "bond", - ActionTarget::ValidatorLog, + log_target, ).await } diff --git a/src/types/log.rs b/src/types/log.rs index 1c2d26e..32cf551 100644 --- a/src/types/log.rs +++ b/src/types/log.rs @@ -9,7 +9,7 @@ pub enum ActionLevel { Error, } -#[derive(Default, Debug, Clone, PartialEq, Eq, Display, Serialize, Deserialize)] +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Display, Serialize, Deserialize)] pub enum ActionTarget { #[default] WalletLog, diff --git a/src/types/mod.rs b/src/types/mod.rs index 64fc7ad..3f7737c 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -16,3 +16,4 @@ pub use peer::PeerInformation; pub use session::SessionKeyInfo; pub use nominator::Nominator; pub use staking::UnlockChunk; +pub use staking::RewardDestination; diff --git a/src/types/staking.rs b/src/types/staking.rs index 5ab423e..167b9ca 100644 --- a/src/types/staking.rs +++ b/src/types/staking.rs @@ -1,4 +1,5 @@ use codec::Decode; +use strum::Display; use serde::{Serialize, Deserialize}; #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, Decode)] @@ -6,3 +7,11 @@ pub struct UnlockChunk { pub value: u128, pub era: u32, } + +#[derive(Default, Debug, Clone, PartialEq, Eq, Display, Serialize, Deserialize)] +pub enum RewardDestination { + #[default] + Staked, + Stash, + Account(String), +}