diff --git a/src/action.rs b/src/action.rs index ffd93ef..371a88f 100644 --- a/src/action.rs +++ b/src/action.rs @@ -48,6 +48,8 @@ pub enum Action { AccountDetailsOf(String, Option), TransferBalance(String, [u8; 32], u128), + BondValidatorExtraFrom([u8; 32], u128), + BondValidatorFrom([u8; 32], u128), EventLog(String, ActionLevel, ActionTarget), NewBestBlock(u32), @@ -98,6 +100,7 @@ pub enum Action { SetChainName(Option), SetChainVersion(Option), SetStashAccount([u8; 32]), + SetStashSecret([u8; 32]), BestBlockInformation(H256, u32), FinalizedBlockInformation(H256, u32), @@ -115,7 +118,7 @@ pub enum Action { SetValidatorEraClaimed(u32, bool), SetValidatorEraSlash(u32, u128), SetValidatorEraUnlocking(u32, u128), - SetBondedAmount(bool), + SetIsBonded(bool), SetStakedAmountRatio(u128, u128), SetStakedRatio(u128, u128), SetValidatorPrefs(u32, bool), @@ -123,7 +126,9 @@ pub enum Action { GetTotalIssuance, GetExistentialDeposit, + GetMinValidatorBond, SetExistentialDeposit(u128), + SetMinValidatorBond(u128), SetTotalIssuance(Option), } diff --git a/src/components/validator/bond_popup.rs b/src/components/validator/bond_popup.rs new file mode 100644 index 0000000..7ac320c --- /dev/null +++ b/src/components/validator/bond_popup.rs @@ -0,0 +1,179 @@ +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>, + secret_seed: [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, + secret_seed: [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::ValidatorLog)); + } + } + + fn submit_message(&mut self) { + if let Some(network_tx) = &self.network_tx { + match self.amount.value().parse::() { + 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)) + } else { + network_tx.send(Action::BondValidatorFrom(self.secret_seed, amount)) + }; + 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) => self.is_bonded = is_bonded, + Action::SetMinValidatorBond(minimal_bond) => self.minimal_bond = minimal_bond, + Action::SetStashSecret(secret_seed) => self.secret_seed = secret_seed, + _ => {} + }; + 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/validator/mod.rs b/src/components/validator/mod.rs index 3cc84e9..4554d1a 100644 --- a/src/components/validator/mod.rs +++ b/src/components/validator/mod.rs @@ -21,6 +21,7 @@ mod withdrawals; mod stash_details; mod staking_details; mod reward_details; +mod bond_popup; use stash_details::StashDetails; use staking_details::StakingDetails; @@ -32,8 +33,9 @@ use listen_addresses::ListenAddresses; use nominators::NominatorsByValidator; use history::History; use withdrawals::Withdrawals; +use bond_popup::BondPopup; -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Copy, Clone, PartialEq)] pub enum CurrentTab { Nothing, StashInfo, @@ -43,6 +45,7 @@ pub enum CurrentTab { Withdrawals, Peers, EventLogs, + BondPopup, } pub trait PartialComponent: Component { @@ -52,6 +55,7 @@ pub trait PartialComponent: Component { pub struct Validator { is_active: bool, current_tab: CurrentTab, + previous_tab: CurrentTab, components: Vec>, } @@ -60,6 +64,7 @@ impl Default for Validator { Self { is_active: false, current_tab: CurrentTab::Nothing, + previous_tab: CurrentTab::Nothing, components: vec![ Box::new(StashInfo::default()), Box::new(NominatorsByValidator::default()), @@ -71,6 +76,7 @@ impl Default for Validator { Box::new(Peers::default()), Box::new(ListenAddresses::default()), Box::new(EventLogs::default()), + Box::new(BondPopup::default()), ], } } @@ -128,30 +134,52 @@ impl Component for Validator { fn handle_key_event(&mut self, key: KeyEvent) -> Result> { if !self.is_active { return Ok(None) } - match key.code { - KeyCode::Esc => { - self.is_active = false; - self.current_tab = CurrentTab::Nothing; - for component in self.components.iter_mut() { - component.set_active(self.current_tab.clone()); - } - return Ok(Some(Action::SetActiveScreen(Mode::Menu))); - }, - KeyCode::Char('l') | KeyCode::Right => { - self.move_right(); - for component in self.components.iter_mut() { - component.set_active(self.current_tab.clone()); + match self.current_tab { + CurrentTab::BondPopup => match key.code { + KeyCode::Esc => { + self.current_tab = self.previous_tab; + for component in self.components.iter_mut() { + component.set_active(self.current_tab.clone()); + } + }, + _ => { + for component in self.components.iter_mut() { + component.handle_key_event(key)?; + } } }, - KeyCode::Char('h') | KeyCode::Left => { - self.move_left(); - for component in self.components.iter_mut() { - component.set_active(self.current_tab.clone()); - } - }, - _ => { - for component in self.components.iter_mut() { - component.handle_key_event(key)?; + _ => match key.code { + KeyCode::Esc => { + self.is_active = false; + self.current_tab = CurrentTab::Nothing; + for component in self.components.iter_mut() { + component.set_active(self.current_tab.clone()); + } + return Ok(Some(Action::SetActiveScreen(Mode::Menu))); + }, + KeyCode::Char('l') | KeyCode::Right => { + self.move_right(); + for component in self.components.iter_mut() { + component.set_active(self.current_tab.clone()); + } + }, + KeyCode::Char('h') | KeyCode::Left => { + self.move_left(); + for component in self.components.iter_mut() { + 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()); + } + }, + _ => { + for component in self.components.iter_mut() { + component.handle_key_event(key)?; + } } } } @@ -163,6 +191,9 @@ impl Component for Validator { Action::SetActiveScreen(Mode::Validator) => { self.is_active = true; self.current_tab = CurrentTab::StashInfo; + } + Action::ClosePopup => { + self.current_tab = self.previous_tab; }, _ => {}, } diff --git a/src/components/validator/stash_details.rs b/src/components/validator/stash_details.rs index 5031d57..238fd2c 100644 --- a/src/components/validator/stash_details.rs +++ b/src/components/validator/stash_details.rs @@ -6,6 +6,7 @@ use ratatui::{ widgets::{Block, Cell, Row, Table}, Frame }; +use std::sync::mpsc::Sender; use super::{PartialComponent, Component, CurrentTab}; use crate::widgets::DotSpinner; @@ -17,6 +18,7 @@ use crate::{ pub struct StashDetails { palette: StylePalette, + network_tx: Option>, is_bonded: bool, free_balance: Option, staked_total: Option, @@ -37,6 +39,7 @@ impl StashDetails { pub fn new() -> Self { Self { palette: StylePalette::default(), + network_tx: None, is_bonded: false, free_balance: None, staked_total: None, @@ -70,6 +73,11 @@ impl PartialComponent for StashDetails { } impl Component for StashDetails { + 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::Validator) { self.palette.with_normal_style(style.get("normal_style").copied()); @@ -87,12 +95,17 @@ impl Component for StashDetails { fn update(&mut self, action: Action) -> Result> { match action { Action::SetStashAccount(account_id) => self.stash_account_id = account_id, - Action::SetBondedAmount(is_bonded) => self.is_bonded = is_bonded, + Action::SetIsBonded(is_bonded) => self.is_bonded = is_bonded, Action::SetStakedAmountRatio(total, active) => { self.staked_total = Some(total); self.staked_active = Some(active); }, Action::BalanceResponse(account_id, maybe_balance) if account_id == self.stash_account_id => { + if let Some(network_tx) = &self.network_tx { + let _ = network_tx.send(Action::SetSender( + hex::encode(self.stash_account_id), + maybe_balance.clone().map(|b| b.nonce))); + } self.free_balance = maybe_balance.map(|balance| balance.free .saturating_sub(balance.frozen) .saturating_sub(balance.reserved)); diff --git a/src/components/validator/stash_info.rs b/src/components/validator/stash_info.rs index 942a37d..dc42484 100644 --- a/src/components/validator/stash_info.rs +++ b/src/components/validator/stash_info.rs @@ -146,7 +146,7 @@ impl StashInfo { .to_ss58check_with_version(Ss58AddressFormat::custom(1996)); let pair_signer = PairSigner::::new(pair); - self.initiate_stash_info(account_id); + self.initiate_stash_info(account_id, seed); self.log_event( format!("stash key {address} read from disk"), ActionLevel::Info); @@ -184,7 +184,7 @@ impl StashInfo { let mut new_file = File::create(&self.file_path)?; writeln!(new_file, "0x{}", &secret_seed)?; - self.initiate_stash_info(account_id); + self.initiate_stash_info(account_id, seed); self.log_event( format!("new stash key {} created and stored at {:?}", &address, self.file_path), ActionLevel::Info); @@ -195,9 +195,10 @@ impl StashInfo { Ok(()) } - fn initiate_stash_info(&self, account_id: [u8; 32]) { + fn initiate_stash_info(&self, account_id: [u8; 32], secret_seed: [u8; 32]) { if let Some(action_tx) = &self.action_tx { let _ = action_tx.send(Action::SetStashAccount(account_id)); + let _ = action_tx.send(Action::SetStashSecret(secret_seed)); } if let Some(network_tx) = &self.network_tx { @@ -245,6 +246,7 @@ impl Component for StashInfo { self.network_tx = Some(tx); Ok(()) } + fn register_action_handler(&mut self, tx: UnboundedSender) -> Result<()> { self.action_tx = Some(tx); Ok(()) diff --git a/src/network/mod.rs b/src/network/mod.rs index 215ca25..31c3cd4 100644 --- a/src/network/mod.rs +++ b/src/network/mod.rs @@ -165,6 +165,7 @@ impl Network { Action::GetActiveEra => predefined_calls::get_active_era(&self.action_tx, &self.online_client_api).await, Action::GetCurrentEra => predefined_calls::get_current_era(&self.action_tx, &self.online_client_api).await, Action::GetEpochProgress => predefined_calls::get_epoch_progress(&self.action_tx, &self.online_client_api).await, + Action::GetMinValidatorBond => predefined_calls::get_minimal_validator_bond(&self.action_tx, &self.online_client_api).await, Action::GetExistentialDeposit => predefined_calls::get_existential_deposit(&self.action_tx, &self.online_client_api).await, Action::GetTotalIssuance => predefined_calls::get_total_issuance(&self.action_tx, &self.online_client_api).await, @@ -245,6 +246,42 @@ impl Network { } Ok(()) } + Action::BondValidatorExtraFrom(sender, amount) => { + let sender_str = hex::encode(sender); + let maybe_nonce = self.senders.get_mut(&sender_str); + if let Ok(tx_progress) = predefined_txs::bond_extra( + &self.action_tx, + &self.online_client_api, + &sender, + &amount, + maybe_nonce, + ).await { + self.transactions_to_watch.push(TxToWatch { + tx_progress, + sender: sender_str, + target: ActionTarget::ValidatorLog, + }); + } + Ok(()) + } + Action::BondValidatorFrom(sender, amount) => { + let sender_str = hex::encode(sender); + let maybe_nonce = self.senders.get_mut(&sender_str); + if let Ok(tx_progress) = predefined_txs::bond( + &self.action_tx, + &self.online_client_api, + &sender, + &amount, + maybe_nonce, + ).await { + self.transactions_to_watch.push(TxToWatch { + tx_progress, + sender: sender_str, + target: ActionTarget::ValidatorLog, + }); + } + Ok(()) + } _ => Ok(()) } } diff --git a/src/network/predefined_calls.rs b/src/network/predefined_calls.rs index 45f56d6..28c6dc9 100644 --- a/src/network/predefined_calls.rs +++ b/src/network/predefined_calls.rs @@ -483,7 +483,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::SetBondedAmount(is_bonded))?; + action_tx.send(Action::SetIsBonded(is_bonded))?; Ok(()) } @@ -521,3 +521,14 @@ pub async fn get_validator_prefs( action_tx.send(Action::SetValidatorPrefs(comission, blocked))?; Ok(()) } + +pub async fn get_minimal_validator_bond( + action_tx: &UnboundedSender, + api: &OnlineClient, +) -> Result<()> { + let min_validator_bond = super::raw_calls::staking::min_validator_bond(api, None) + .await? + .unwrap_or_default(); + action_tx.send(Action::SetMinValidatorBond(min_validator_bond))?; + Ok(()) +} diff --git a/src/network/predefined_txs.rs b/src/network/predefined_txs.rs index 0a23d17..a54117f 100644 --- a/src/network/predefined_txs.rs +++ b/src/network/predefined_txs.rs @@ -62,3 +62,97 @@ pub async fn transfer_balance( } } } + +pub async fn bond_extra( + action_tx: &UnboundedSender, + api: &OnlineClient, + sender: &[u8; 32], + amount: &u128, + mut maybe_nonce: Option<&mut u32>, +) -> Result>> { + let transfer_tx = casper_network::tx() + .staking() + .bond_extra(*amount); + + let tx_params = match maybe_nonce { + Some(ref mut nonce) => { + **nonce = nonce.saturating_add(1); + CasperExtrinsicParamsBuilder::new() + .nonce(nonce.saturating_sub(1) as u64) + .build() + }, + None => CasperExtrinsicParamsBuilder::new().build(), + }; + + let pair = Pair::from_seed(sender); + let signer = PairSigner::::new(pair); + + match api + .tx() + .sign_and_submit_then_watch(&transfer_tx, &signer, tx_params) + .await { + Ok(tx_progress) => { + action_tx.send(Action::EventLog( + format!("bond extra transaction {} sent", tx_progress.extrinsic_hash()), + ActionLevel::Info, + ActionTarget::ValidatorLog))?; + Ok(tx_progress) + }, + Err(err) => { + action_tx.send(Action::EventLog( + format!("error during bond extra: {err}"), + ActionLevel::Error, + ActionTarget::ValidatorLog))?; + Err(err.into()) + } + } +} + +pub async fn bond( + action_tx: &UnboundedSender, + api: &OnlineClient, + sender: &[u8; 32], + amount: &u128, + mut maybe_nonce: Option<&mut u32>, +) -> Result>> { + // auto-stake everything by now + let reward_destination = + casper_network::runtime_types::pallet_staking::RewardDestination::Staked; + + let transfer_tx = casper_network::tx() + .staking() + .bond(*amount, reward_destination); + + let tx_params = match maybe_nonce { + Some(ref mut nonce) => { + **nonce = nonce.saturating_add(1); + CasperExtrinsicParamsBuilder::new() + .nonce(nonce.saturating_sub(1) as u64) + .build() + }, + None => CasperExtrinsicParamsBuilder::new().build(), + }; + + let pair = Pair::from_seed(sender); + let signer = PairSigner::::new(pair); + + match api + .tx() + .sign_and_submit_then_watch(&transfer_tx, &signer, tx_params) + .await { + Ok(tx_progress) => { + action_tx.send(Action::EventLog( + format!("bond transaction {} sent", tx_progress.extrinsic_hash()), + ActionLevel::Info, + ActionTarget::ValidatorLog))?; + Ok(tx_progress) + }, + Err(err) => { + action_tx.send(Action::EventLog( + format!("error during bond: {err}"), + ActionLevel::Error, + ActionTarget::ValidatorLog))?; + Err(err.into()) + } + } +} diff --git a/src/network/raw_calls/staking.rs b/src/network/raw_calls/staking.rs index 6626de4..3eba416 100644 --- a/src/network/raw_calls/staking.rs +++ b/src/network/raw_calls/staking.rs @@ -171,3 +171,12 @@ pub async fn disabled_validators( let maybe_disabled_validators = super::do_storage_call(online_client, &storage_key, at_hash).await?; Ok(maybe_disabled_validators) } + +pub async fn min_validator_bond( + online_client: &OnlineClient, + at_hash: Option<&H256>, +) -> Result> { + let storage_key = casper_network::storage().staking().min_validator_bond(); + let maybe_min_validator_bond = super::do_storage_call(online_client, &storage_key, at_hash).await?; + Ok(maybe_min_validator_bond) +} diff --git a/src/network/subscriptions.rs b/src/network/subscriptions.rs index 8123cc2..3fe77f8 100644 --- a/src/network/subscriptions.rs +++ b/src/network/subscriptions.rs @@ -119,6 +119,7 @@ impl BestSubscription { self.network_tx.send(Action::GetNominatorsNumber)?; self.network_tx.send(Action::GetInflation)?; self.network_tx.send(Action::GetCurrentValidatorEraRewards)?; + self.network_tx.send(Action::GetMinValidatorBond)?; } Ok(()) }