use std::collections::BTreeMap; use std::sync::mpsc::Sender; use color_eyre::Result; use crossterm::event::{KeyCode, KeyEvent}; use ratatui::layout::{Constraint, Margin}; use ratatui::style::Modifier; use ratatui::{ prelude::Stylize, text::Text, layout::{Alignment, Rect}, widgets::{ Block, Cell, Row, Table, TableState, Scrollbar, Padding, ScrollbarOrientation, ScrollbarState, }, Frame }; use tokio::sync::mpsc::UnboundedSender; use super::{PartialComponent, Component, CurrentTab}; use crate::{ action::Action, config::Config, palette::StylePalette, }; struct EraStakingInfo { reward: u128, slash: u128, is_claimed: bool, } pub struct History { is_active: bool, network_tx: Option>, action_tx: Option>, palette: StylePalette, scroll_state: ScrollbarState, table_state: TableState, rewards: BTreeMap, } impl Default for History { fn default() -> Self { Self::new() } } impl History { const TICKER: &str = " CSPR"; const DECIMALS: usize = 5; pub fn new() -> Self { Self { is_active: false, network_tx: None, action_tx: None, scroll_state: ScrollbarState::new(0), table_state: TableState::new(), rewards: BTreeMap::new(), palette: StylePalette::default(), } } 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)); self.scroll_state = self.scroll_state.position(0); } } fn next_row(&mut self) { let i = match self.table_state.selected() { Some(i) => { if i >= self.rewards.len() - 1 { i } else { i + 1 } }, None => 0, }; self.table_state.select(Some(i)); self.scroll_state = self.scroll_state.position(i); } fn last_row(&mut self) { if self.rewards.len() > 0 { let last = self.rewards.len() - 1; self.table_state.select(Some(last)); self.scroll_state = self.scroll_state.position(last); } } fn previous_row(&mut self) { let i = match self.table_state.selected() { Some(i) => { if i == 0 { 0 } else { i - 1 } }, None => 0 }; self.table_state.select(Some(i)); self.scroll_state = self.scroll_state.position(i); } fn update_rewards(&mut self, era_index: u32, reward: u128) { match self.rewards.get_mut(&era_index) { Some(reward_item) => reward_item.reward = reward, None => { let _ = self.rewards.insert(era_index, EraStakingInfo { reward, slash: 0u128, is_claimed: false, }); } } } fn update_claims(&mut self, era_index: u32, is_claimed: bool) { match self.rewards.get_mut(&era_index) { 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, slash: 0u128, is_claimed, }); } } } fn update_slashes(&mut self, era_index: u32, slash: u128) { match self.rewards.get_mut(&era_index) { Some(reward_item) => reward_item.slash = slash, None => { let _ = self.rewards.insert(era_index, EraStakingInfo { reward: 0u128, slash, is_claimed: false, }); } } } fn prepare_u128(&self, value: u128) -> String { let value = value as f64 / 10f64.powi(18); let after = Self::DECIMALS; format!("{:.after$}{}", value, Self::TICKER) } } impl PartialComponent for History { fn set_active(&mut self, current_tab: CurrentTab) { match current_tab { CurrentTab::History => self.is_active = true, _ => { self.is_active = false; //self.table_state.select(None); //self.scroll_state = self.scroll_state.position(0); } } } } impl Component for History { 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::Validator) { self.palette.with_normal_style(style.get("normal_style").copied()); self.palette.with_hover_style(style.get("hover_style").copied()); self.palette.with_normal_border_style(style.get("normal_border_style").copied()); self.palette.with_hover_border_style(style.get("hover_border_style").copied()); self.palette.with_normal_title_style(style.get("normal_title_style").copied()); self.palette.with_hover_title_style(style.get("hover_title_style").copied()); self.palette.with_highlight_style(style.get("highlight_style").copied()); self.palette.with_scrollbar_style(style.get("scrollbar_style").copied()); } Ok(()) } fn update(&mut self, action: Action) -> Result> { match action { Action::SetValidatorEraReward(era_index, reward) => self.update_rewards(era_index, reward), Action::SetValidatorEraClaimed(era_index, is_claimed) => self.update_claims(era_index, is_claimed), Action::SetValidatorEraSlash(era_index, slash) => self.update_slashes(era_index, slash), _ => {} }; Ok(None) } fn handle_key_event(&mut self, key: KeyEvent) -> Result> { if self.is_active { match key.code { KeyCode::Up | KeyCode::Char('k') => self.previous_row(), 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(), _ => {}, }; } Ok(None) } fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { let [_, place, _] = super::validator_statistics_layout(area); let (border_style, border_type) = self.palette.create_border_style(self.is_active); let table = Table::new( self.rewards .iter() .map(|(key, value)| { let mut era_index_text = Text::from(key.to_string()).alignment(Alignment::Left); let mut slash_text = Text::from(self.prepare_u128(value.slash)).alignment(Alignment::Center); let mut reward_text = Text::from(self.prepare_u128(value.reward)).alignment(Alignment::Right); if value.is_claimed { era_index_text = era_index_text.add_modifier(Modifier::CROSSED_OUT); slash_text = slash_text.add_modifier(Modifier::CROSSED_OUT); reward_text = reward_text.add_modifier(Modifier::CROSSED_OUT); } Row::new(vec![ Cell::from(era_index_text), Cell::from(slash_text), Cell::from(reward_text), ]) }), [ Constraint::Length(4), Constraint::Fill(1), Constraint::Fill(1), ], ) .highlight_style(self.palette.create_highlight_style()) .column_spacing(1) .block(Block::bordered() .border_style(border_style) .border_type(border_type) .padding(Padding::right(2)) .title_alignment(Alignment::Right) .title_style(self.palette.create_title_style(false)) .title("Staking history")); let scrollbar = Scrollbar::default() .orientation(ScrollbarOrientation::VerticalRight) .begin_symbol(None) .end_symbol(None) .style(self.palette.create_scrollbar_style()); frame.render_stateful_widget(table, place, &mut self.table_state); frame.render_stateful_widget( scrollbar, place.inner(Margin { vertical: 1, horizontal: 1 }), &mut self.scroll_state, ); Ok(()) } }