From 0e4cb343dcec38e5287202922ad5e0dd7bcf9cb2 Mon Sep 17 00:00:00 2001 From: Uncle Stretch Date: Sat, 17 May 2025 16:49:14 +0300 Subject: [PATCH] mass payout for all unclaimed eras added Signed-off-by: Uncle Stretch --- src/action.rs | 1 + src/components/validator/history.rs | 20 +++ src/components/validator/mod.rs | 11 +- src/components/validator/payout_all_popup.rs | 145 +++++++++++++++++++ 4 files changed, 176 insertions(+), 1 deletion(-) create mode 100644 src/components/validator/payout_all_popup.rs diff --git a/src/action.rs b/src/action.rs index 6c4eeaa..7c179c7 100644 --- a/src/action.rs +++ b/src/action.rs @@ -36,6 +36,7 @@ pub enum Action { ClosePopup, RotateSessionKeys, PayoutValidatorPopup(u32), + PayoutAllValidatorPopup(Vec), WithdrawValidatorPopup, BalanceRequest([u8; 32], bool), diff --git a/src/components/validator/history.rs b/src/components/validator/history.rs index ab51c9f..2a97ae4 100644 --- a/src/components/validator/history.rs +++ b/src/components/validator/history.rs @@ -65,6 +65,25 @@ impl History { } } + fn payout_all_available(&mut self) { + let unclaimed_keys = self.rewards + .iter() + .filter_map(|(k, v)| (!v.is_claimed).then(|| *k)) + .collect::>(); + + if let Some(action_tx) = &self.action_tx { + let _ = if unclaimed_keys.len() == 0 { + action_tx.send(Action::EventLog( + String::from("no available payouts found for current validator"), + ActionLevel::Warn, + ActionTarget::ValidatorLog)) + } else { + self.pending_payout.extend(&unclaimed_keys); + action_tx.send(Action::PayoutAllValidatorPopup(unclaimed_keys)) + }; + } + } + fn payout_by_era_index(&mut self) { if let Some(index) = self.table_state.selected() { let rev_index = self.rewards.len() @@ -248,6 +267,7 @@ impl Component for History { KeyCode::Down | KeyCode::Char('j') => self.next_row(), KeyCode::Char('g') => self.first_row(), KeyCode::Char('G') => self.last_row(), + KeyCode::Char('H') => self.payout_all_available(), KeyCode::Enter => self.payout_by_era_index(), _ => {}, }; diff --git a/src/components/validator/mod.rs b/src/components/validator/mod.rs index 1cafd43..0ae731c 100644 --- a/src/components/validator/mod.rs +++ b/src/components/validator/mod.rs @@ -23,6 +23,7 @@ mod staking_details; mod reward_details; mod bond_popup; mod payout_popup; +mod payout_all_popup; mod rotate_popup; mod validate_popup; mod chill_popup; @@ -43,6 +44,7 @@ use history::History; use withdrawals::Withdrawals; use bond_popup::BondPopup; use payout_popup::PayoutPopup; +use payout_all_popup::PayoutAllPopup; use rotate_popup::RotatePopup; use validate_popup::ValidatePopup; use chill_popup::ChillPopup; @@ -62,6 +64,7 @@ pub enum CurrentTab { EventLogs, BondPopup, PayoutPopup, + PayoutAllPopup, RotatePopup, ValidatePopup, ChillPopup, @@ -101,6 +104,7 @@ impl Default for Validator { Box::new(EventLogs::default()), Box::new(BondPopup::default()), Box::new(PayoutPopup::default()), + Box::new(PayoutAllPopup::default()), Box::new(RotatePopup::default()), Box::new(ValidatePopup::default()), Box::new(ChillPopup::default()), @@ -171,7 +175,8 @@ impl Component for Validator { CurrentTab::UnbondPopup | CurrentTab::RebondPopup | CurrentTab::WithdrawPopup | - CurrentTab::PayoutPopup => { + CurrentTab::PayoutPopup | + CurrentTab::PayoutAllPopup => { for component in self.components.iter_mut() { component.handle_key_event(key)?; } @@ -244,6 +249,10 @@ impl Component for Validator { self.previous_tab = self.current_tab; self.current_tab = CurrentTab::PayoutPopup; } + Action::PayoutAllValidatorPopup(_) => { + self.previous_tab = self.current_tab; + self.current_tab = CurrentTab::PayoutAllPopup; + } Action::WithdrawValidatorPopup => { self.previous_tab = self.current_tab; self.current_tab = CurrentTab::WithdrawPopup; diff --git a/src/components/validator/payout_all_popup.rs b/src/components/validator/payout_all_popup.rs new file mode 100644 index 0000000..c7e1dbe --- /dev/null +++ b/src/components/validator/payout_all_popup.rs @@ -0,0 +1,145 @@ +use crossterm::event::{KeyCode, KeyEvent, KeyEventKind}; +use color_eyre::Result; +use ratatui::{ + layout::{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, +}; + +#[derive(Debug)] +pub struct PayoutAllPopup { + is_active: bool, + action_tx: Option>, + network_tx: Option>, + secret_seed: [u8; 32], + stash_account_id: [u8; 32], + era_indexes: Vec, + palette: StylePalette +} + +impl Default for PayoutAllPopup { + fn default() -> Self { + Self::new() + } +} + +impl PayoutAllPopup { + pub fn new() -> Self { + Self { + is_active: false, + secret_seed: [0u8; 32], + stash_account_id: [0u8; 32], + era_indexes: Default::default(), + action_tx: None, + network_tx: None, + palette: StylePalette::default(), + } + } + + fn close_popup(&mut self) { + self.is_active = false; + self.era_indexes = Default::default(); + if let Some(action_tx) = &self.action_tx { + let _ = action_tx.send(Action::ClosePopup); + } + } + + fn start_payout(&mut self) { + if let Some(network_tx) = &self.network_tx { + for era_index in &self.era_indexes { + let _ = network_tx.send(Action::PayoutStakers( + self.secret_seed, + self.stash_account_id, + *era_index)); + } + } + self.close_popup(); + } +} + +impl PartialComponent for PayoutAllPopup { + fn set_active(&mut self, current_tab: CurrentTab) { + match current_tab { + CurrentTab::PayoutAllPopup => self.is_active = true, + _ => self.is_active = false, + }; + } +} + +impl Component for PayoutAllPopup { + 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.start_payout(), + KeyCode::Esc => self.close_popup(), + _ => {}, + }; + } + Ok(None) + } + + fn update(&mut self, action: Action) -> Result> { + match action { + Action::PayoutAllValidatorPopup(era_indexes) => self.era_indexes = era_indexes, + Action::SetStashSecret(secret_seed) => self.secret_seed = secret_seed, + Action::SetStashAccount(account_id) => self.stash_account_id = account_id, + _ => {} + }; + 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 popup = Paragraph::new( + format!(" Do payout for {} eras, from {} to {}", + self.era_indexes.len(), + self.era_indexes.first().expect("Length of unclaimed indexes always more then one; qed"), + self.era_indexes.last().expect("Length of unclaimed indexes always more then one; qed"))) + .block(Block::bordered() + .border_style(border_style) + .border_type(border_type) + .title_style(self.palette.create_popup_title_style()) + .title_alignment(Alignment::Right) + .title("Enter to proceed / Esc to close")); + 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(popup, area); + } + Ok(()) + } +}