diff --git a/src/action.rs b/src/action.rs index 371a88f..aa0254d 100644 --- a/src/action.rs +++ b/src/action.rs @@ -30,8 +30,10 @@ pub enum Action { NewAccount(String), NewAddressBookRecord(String, String), SetSender(String, Option), + RemoveEraToWatch(u32), ClosePopup, + PayoutValidatorPopup(u32, bool), BalanceRequest([u8; 32], bool), BalanceResponse([u8; 32], Option), @@ -50,6 +52,7 @@ pub enum Action { TransferBalance(String, [u8; 32], u128), BondValidatorExtraFrom([u8; 32], u128), BondValidatorFrom([u8; 32], u128), + PayoutStakers([u8; 32], [u8; 32], u32), EventLog(String, ActionLevel, ActionTarget), NewBestBlock(u32), diff --git a/src/components/validator/history.rs b/src/components/validator/history.rs index df75bfc..efdc507 100644 --- a/src/components/validator/history.rs +++ b/src/components/validator/history.rs @@ -62,6 +62,23 @@ impl History { } } + fn payout_by_era_index(&mut self) { + if let Some(index) = self.table_state.selected() { + let era_index = self.rewards + .keys() + .nth(index) + .expect("BTreeMap of rewards is indexed; qed"); + let is_claimed = self.rewards + .get(era_index) + .map(|x| x.is_claimed) + .expect("BTreeMap of rewards is indexed; qed"); + if let Some(action_tx) = &self.action_tx { + let _ = action_tx.send( + Action::PayoutValidatorPopup(*era_index, is_claimed)); + } + } + } + fn first_row(&mut self) { if self.rewards.len() > 0 { self.table_state.select(Some(0)); @@ -122,7 +139,14 @@ impl History { fn update_claims(&mut self, era_index: u32, is_claimed: bool) { match self.rewards.get_mut(&era_index) { - Some(reward_item) => reward_item.is_claimed = is_claimed, + Some(reward_item) => { + if reward_item.is_claimed == false && is_claimed == true { + if let Some(network_tx) = &self.network_tx { + let _ = network_tx.send(Action::RemoveEraToWatch(era_index)); + } + } + reward_item.is_claimed = is_claimed; + } None => { let _ = self.rewards.insert(era_index, EraStakingInfo { reward: 0u128, @@ -159,8 +183,8 @@ impl PartialComponent for History { CurrentTab::History => self.is_active = true, _ => { self.is_active = false; - self.table_state.select(None); - self.scroll_state = self.scroll_state.position(0); + //self.table_state.select(None); + //self.scroll_state = self.scroll_state.position(0); } } } @@ -208,6 +232,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::Enter => self.payout_by_era_index(), _ => {}, }; } diff --git a/src/components/validator/mod.rs b/src/components/validator/mod.rs index 4554d1a..f04a055 100644 --- a/src/components/validator/mod.rs +++ b/src/components/validator/mod.rs @@ -22,6 +22,7 @@ mod stash_details; mod staking_details; mod reward_details; mod bond_popup; +mod payout_popup; use stash_details::StashDetails; use staking_details::StakingDetails; @@ -34,6 +35,7 @@ use nominators::NominatorsByValidator; use history::History; use withdrawals::Withdrawals; use bond_popup::BondPopup; +use payout_popup::PayoutPopup; #[derive(Debug, Copy, Clone, PartialEq)] pub enum CurrentTab { @@ -46,6 +48,7 @@ pub enum CurrentTab { Peers, EventLogs, BondPopup, + PayoutPopup, } pub trait PartialComponent: Component { @@ -77,6 +80,7 @@ impl Default for Validator { Box::new(ListenAddresses::default()), Box::new(EventLogs::default()), Box::new(BondPopup::default()), + Box::new(PayoutPopup::default()), ], } } @@ -135,7 +139,8 @@ impl Component for Validator { if !self.is_active { return Ok(None) } match self.current_tab { - CurrentTab::BondPopup => match key.code { + CurrentTab::BondPopup | + CurrentTab::PayoutPopup => match key.code { KeyCode::Esc => { self.current_tab = self.previous_tab; for component in self.components.iter_mut() { @@ -192,6 +197,10 @@ impl Component for Validator { self.is_active = true; self.current_tab = CurrentTab::StashInfo; } + Action::PayoutValidatorPopup(_, _) => { + self.previous_tab = self.current_tab; + self.current_tab = CurrentTab::PayoutPopup; + } Action::ClosePopup => { self.current_tab = self.previous_tab; }, diff --git a/src/components/validator/payout_popup.rs b/src/components/validator/payout_popup.rs new file mode 100644 index 0000000..2960618 --- /dev/null +++ b/src/components/validator/payout_popup.rs @@ -0,0 +1,152 @@ +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, + types::{ActionLevel, ActionTarget}, +}; + +#[derive(Debug)] +pub struct PayoutPopup { + is_active: bool, + action_tx: Option>, + network_tx: Option>, + secret_seed: [u8; 32], + stash_account_id: [u8; 32], + era_index: u32, + is_claimed: bool, + palette: StylePalette +} + +impl Default for PayoutPopup { + fn default() -> Self { + Self::new() + } +} + +impl PayoutPopup { + pub fn new() -> Self { + Self { + is_active: false, + secret_seed: [0u8; 32], + stash_account_id: [0u8; 32], + era_index: 0u32, + is_claimed: false, + action_tx: None, + network_tx: None, + palette: StylePalette::default(), + } + } + + fn store_era_to_claim(&mut self, era_index: u32, is_claimed: bool) { + self.is_claimed = is_claimed; + self.era_index = era_index; + } +} + +impl PayoutPopup { + fn start_payout(&mut self) { + if self.is_claimed { + if let Some(action_tx) = &self.action_tx { + let _ = action_tx.send(Action::EventLog( + format!("staking rewards for era index #{} already claimed", self.era_index), + ActionLevel::Warn, + ActionTarget::ValidatorLog)); + } + } else { + if let Some(network_tx) = &self.network_tx { + let _ = network_tx.send(Action::PayoutStakers( + self.secret_seed, + self.stash_account_id, + self.era_index)); + } + } + if let Some(action_tx) = &self.action_tx { + let _ = action_tx.send(Action::ClosePopup); + } + } +} + +impl PartialComponent for PayoutPopup { + fn set_active(&mut self, current_tab: CurrentTab) { + match current_tab { + CurrentTab::PayoutPopup => self.is_active = true, + _ => self.is_active = false, + }; + } +} + +impl Component for PayoutPopup { + 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.is_active = false, + _ => {}, + }; + } + Ok(None) + } + + fn update(&mut self, action: Action) -> Result> { + match action { + Action::PayoutValidatorPopup(era_index, is_claimed) => + self.store_era_to_claim(era_index, is_claimed), + 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 era #{}", self.era_index)) + .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(()) + } +} diff --git a/src/network/mod.rs b/src/network/mod.rs index 31c3cd4..77d3b07 100644 --- a/src/network/mod.rs +++ b/src/network/mod.rs @@ -39,6 +39,7 @@ pub struct Network { stash_to_watch: Option<[u8; 32]>, accounts_to_watch: std::collections::HashSet<[u8; 32]>, transactions_to_watch: Vec, + eras_to_watch: std::collections::HashSet, senders: std::collections::HashMap, } @@ -57,6 +58,7 @@ impl Network { stash_to_watch: None, accounts_to_watch: Default::default(), transactions_to_watch: Default::default(), + eras_to_watch: Default::default(), senders: Default::default(), } } @@ -106,6 +108,10 @@ impl Network { predefined_calls::get_staking_value_ratio(&self.action_tx, &self.online_client_api, &stash_to_watch).await?; predefined_calls::get_is_stash_bonded(&self.action_tx, &self.online_client_api, &stash_to_watch).await?; predefined_calls::get_validators_ledger(&self.action_tx, &self.online_client_api, &stash_to_watch).await?; + + for era_index in self.eras_to_watch.iter() { + predefined_calls::get_validator_staking_result(&self.action_tx, &self.online_client_api, &stash_to_watch, *era_index).await?; + } } for account_id in self.accounts_to_watch.iter() { predefined_calls::get_balance(&self.action_tx, &self.online_client_api, &account_id).await?; @@ -178,6 +184,10 @@ impl Network { self.store_sender_nonce(&seed, maybe_nonce); Ok(()) } + Action::RemoveEraToWatch(era_index) => { + self.eras_to_watch.remove(&era_index); + Ok(()) + } Action::GetValidatorLedger(stash) => { self.store_stash_if_possible(stash); @@ -282,6 +292,26 @@ impl Network { } Ok(()) } + Action::PayoutStakers(sender, stash, era_index) => { + let sender_str = hex::encode(sender); + let maybe_nonce = self.senders.get_mut(&sender_str); + if let Ok(tx_progress) = predefined_txs::payout_stakers( + &self.action_tx, + &self.online_client_api, + &sender, + &stash, + era_index, + maybe_nonce, + ).await { + self.eras_to_watch.insert(era_index); + 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 28c6dc9..e191e9c 100644 --- a/src/network/predefined_calls.rs +++ b/src/network/predefined_calls.rs @@ -401,7 +401,7 @@ async fn get_validator_claims_in_era( if let Some(claimed_rewards) = maybe_claimed_rewards { let already_claimed = claimed_rewards .first() - .map(|x| *x == 1) + .map(|x| *x == 0) .unwrap_or(false); action_tx.send(Action::SetValidatorEraClaimed(era_index, already_claimed))?; } diff --git a/src/network/predefined_txs.rs b/src/network/predefined_txs.rs index a54117f..f6f5204 100644 --- a/src/network/predefined_txs.rs +++ b/src/network/predefined_txs.rs @@ -156,3 +156,50 @@ pub async fn bond( } } } + +pub async fn payout_stakers( + action_tx: &UnboundedSender, + api: &OnlineClient, + sender: &[u8; 32], + stash: &[u8; 32], + era_index: u32, + mut maybe_nonce: Option<&mut u32>, +) -> Result>> { + let stash_id = subxt::utils::AccountId32::from(*stash); + let transfer_tx = casper_network::tx() + .staking() + .payout_stakers(stash_id, era_index); + + 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!("payout stakers {} sent", tx_progress.extrinsic_hash()), + ActionLevel::Info, + ActionTarget::ValidatorLog))?; + Ok(tx_progress) + }, + Err(err) => { + action_tx.send(Action::EventLog( + format!("error during payout stakers: {err}"), + ActionLevel::Error, + ActionTarget::ValidatorLog))?; + Err(err.into()) + } + } +}