diff --git a/src/action.rs b/src/action.rs index d2f5aaa..fe07df4 100644 --- a/src/action.rs +++ b/src/action.rs @@ -58,6 +58,7 @@ pub enum Action { SetSessionKeys([u8; 32], String), ValidateFrom([u8; 32], u32), ChillFrom([u8; 32]), + UnbondFrom([u8; 32], u128), EventLog(String, ActionLevel, ActionTarget), NewBestBlock(u32), diff --git a/src/components/validator/mod.rs b/src/components/validator/mod.rs index 8f1139c..05a2937 100644 --- a/src/components/validator/mod.rs +++ b/src/components/validator/mod.rs @@ -26,6 +26,7 @@ mod payout_popup; mod rotate_popup; mod validate_popup; mod chill_popup; +mod unbond_popup; use stash_details::StashDetails; use staking_details::StakingDetails; @@ -42,6 +43,7 @@ use payout_popup::PayoutPopup; use rotate_popup::RotatePopup; use validate_popup::ValidatePopup; use chill_popup::ChillPopup; +use unbond_popup::UnbondPopup; #[derive(Debug, Copy, Clone, PartialEq)] pub enum CurrentTab { @@ -58,6 +60,7 @@ pub enum CurrentTab { RotatePopup, ValidatePopup, ChillPopup, + UnbondPopup, } pub trait PartialComponent: Component { @@ -93,6 +96,7 @@ impl Default for Validator { Box::new(RotatePopup::default()), Box::new(ValidatePopup::default()), Box::new(ChillPopup::default()), + Box::new(UnbondPopup::default()), ], } } @@ -155,6 +159,7 @@ impl Component for Validator { CurrentTab::RotatePopup | CurrentTab::ValidatePopup | CurrentTab::ChillPopup | + CurrentTab::UnbondPopup | CurrentTab::PayoutPopup => match key.code { KeyCode::Esc => { self.current_tab = self.previous_tab; @@ -203,6 +208,13 @@ impl Component for Validator { component.set_active(self.current_tab.clone()); } }, + KeyCode::Char('U') => { + self.previous_tab = self.current_tab; + self.current_tab = CurrentTab::UnbondPopup; + for component in self.components.iter_mut() { + component.set_active(self.current_tab.clone()); + } + }, KeyCode::Char('C') => { self.previous_tab = self.current_tab; self.current_tab = CurrentTab::ChillPopup; diff --git a/src/components/validator/unbond_popup.rs b/src/components/validator/unbond_popup.rs new file mode 100644 index 0000000..9bd691d --- /dev/null +++ b/src/components/validator/unbond_popup.rs @@ -0,0 +1,181 @@ +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 UnbondPopup { + is_active: bool, + action_tx: Option>, + network_tx: Option>, + secret_seed: [u8; 32], + is_bonded: bool, + amount: Input, + palette: StylePalette +} + +impl Default for UnbondPopup { + fn default() -> Self { + Self::new() + } +} + +impl UnbondPopup { + pub fn new() -> Self { + Self { + is_active: false, + secret_seed: [0u8; 32], + action_tx: None, + network_tx: None, + is_bonded: false, + amount: Input::new(String::new()), + palette: StylePalette::default(), + } + } + + 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) => { + 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)); + } else { + self.log_event( + format!("current stash doesn't have bond yet"), + ActionLevel::Warn); + } + 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 UnbondPopup { + fn set_active(&mut self, current_tab: CurrentTab) { + match current_tab { + CurrentTab::UnbondPopup => self.is_active = true, + _ => { + self.is_active = false; + self.amount = Input::new(String::new()); + } + }; + } +} + +impl Component for UnbondPopup { + 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::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!("Unbond 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/withdrawals.rs b/src/components/validator/withdrawals.rs index 420838e..3dade8f 100644 --- a/src/components/validator/withdrawals.rs +++ b/src/components/validator/withdrawals.rs @@ -178,25 +178,21 @@ impl Component for Withdrawals { self.unlockings .iter() .map(|(key, value)| { - let mut era_index_text = Text::from(key.to_string()).alignment(Alignment::Left); let mut est_era_text = Text::from(self.estimate_time(*key)).alignment(Alignment::Center); let mut value_text = Text::from(self.prepare_u128(*value)).alignment(Alignment::Right); if *key > self.current_era { - era_index_text = era_index_text.add_modifier(Modifier::CROSSED_OUT); est_era_text = est_era_text.add_modifier(Modifier::CROSSED_OUT); value_text = value_text.add_modifier(Modifier::CROSSED_OUT); } Row::new(vec![ - Cell::from(era_index_text), Cell::from(est_era_text), Cell::from(value_text), ]) }), [ - Constraint::Length(12), - Constraint::Length(13), + Constraint::Length(7), Constraint::Min(0), ], ) diff --git a/src/network/mod.rs b/src/network/mod.rs index 17fb2ee..f0d3d14 100644 --- a/src/network/mod.rs +++ b/src/network/mod.rs @@ -366,6 +366,24 @@ impl Network { } Ok(()) } + Action::UnbondFrom(sender, amount) => { + let sender_str = hex::encode(sender); + let maybe_nonce = self.senders.get_mut(&sender_str); + if let Ok(tx_progress) = predefined_txs::unbond( + &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_txs.rs b/src/network/predefined_txs.rs index d1009c5..6a84e6b 100644 --- a/src/network/predefined_txs.rs +++ b/src/network/predefined_txs.rs @@ -362,3 +362,48 @@ pub async fn chill( } } } + +pub async fn unbond( + action_tx: &UnboundedSender, + api: &OnlineClient, + sender: &[u8; 32], + amount: &u128, + mut maybe_nonce: Option<&mut u32>, +) -> Result>> { + let transfer_tx = casper_network::tx() + .staking() + .unbond(*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!("unbond {} sent", tx_progress.extrinsic_hash()), + ActionLevel::Info, + ActionTarget::ValidatorLog))?; + Ok(tx_progress) + }, + Err(err) => { + action_tx.send(Action::EventLog( + format!("error during unbond: {err}"), + ActionLevel::Error, + ActionTarget::ValidatorLog))?; + Err(err.into()) + } + } +}