diff --git a/config/config.json5 b/config/config.json5 index 3464148..06054ed 100644 --- a/config/config.json5 +++ b/config/config.json5 @@ -41,6 +41,17 @@ "popup_style": "blue", "popup_title_style": "blue", }, + "Nominator": { + "normal_style": "", + "hover_style": "bold yellow italic on blue", + "normal_border_style": "blue", + "hover_border_style": "blue", + "normal_title_style": "blue", + "hover_title_style": "", + "highlight_style": "yellow bold", + "popup_style": "blue", + "popup_title_style": "blue", + }, }, "keybindings": { "Menu": { @@ -66,6 +77,12 @@ "": "Quit", "": "Suspend", }, + "Nominator": { + "": "Quit", + "": "Quit", + "": "Quit", + "": "Suspend", + }, "Empty": { "": "Quit", "": "Quit", diff --git a/src/action.rs b/src/action.rs index 66754ee..c0aebc6 100644 --- a/src/action.rs +++ b/src/action.rs @@ -5,7 +5,7 @@ use subxt::utils::H256; use subxt::config::substrate::DigestItem; use crate::types::{ - ActionLevel, CasperExtrinsicDetails, EraInfo, Nominator, PeerInformation, SessionKeyInfo, SystemAccount + ActionLevel, CasperExtrinsicDetails, EraInfo, EraRewardPoints, Nominator, PeerInformation, SessionKeyInfo, SystemAccount }; #[derive(Debug, Clone, PartialEq, Eq, Display, Serialize, Deserialize)] @@ -38,8 +38,11 @@ pub enum Action { RenameAccount(String), RenameAddressBookRecord(String), + RenameKnownValidatorRecord, + UpdateAccountName(String), UpdateAddressBookRecord(String), + UpdateKnownValidator(String), TransferTo(String), TransferBalance(String, [u8; 32], u128), @@ -86,6 +89,7 @@ pub enum Action { GetIsStashBonded([u8; 32]), GetErasStakersOverview([u8; 32]), GetValidatorPrefs([u8; 32]), + GetCurrentValidatorEraRewards, SetNodeName(Option), SetSystemHealth(Option, bool, bool), @@ -114,6 +118,7 @@ pub enum Action { SetStakedAmountRatio(u128, u128), SetStakedRatio(u128, u128), SetValidatorPrefs(u32, bool), + SetCurrentValidatorEraRewards(u32, u32, Vec), GetTotalIssuance, GetExistentialDeposit, diff --git a/src/app.rs b/src/app.rs index bc004e0..8ca4327 100644 --- a/src/app.rs +++ b/src/app.rs @@ -12,7 +12,8 @@ use crate::{ tui::{Event, Tui}, components::{ menu::Menu, version::Version, explorer::Explorer, wallet::Wallet, - validator::Validator, empty::Empty, health::Health, fps::FpsCounter, + validator::Validator, nominator::Nominator, empty::Empty, + health::Health, fps::FpsCounter, Component, }, }; @@ -23,6 +24,7 @@ pub enum Mode { Explorer, Wallet, Validator, + Nominator, Empty, } @@ -71,6 +73,7 @@ impl App { Box::new(Explorer::default()), Box::new(Wallet::default()), Box::new(Validator::default()), + Box::new(Nominator::default()), Box::new(Empty::default()), ], should_quite: false, @@ -265,6 +268,15 @@ impl App { } } }, + Mode::Nominator => { + if let Some(component) = self.components.get_mut(7) { + if let Err(err) = component.draw(frame, frame.area()) { + let _ = self + .action_tx + .send(Action::Error(format!("failed to draw: {:?}", err))); + } + } + }, _ => { if let Some(component) = self.components.last_mut() { if let Err(err) = component.draw(frame, frame.area()) { diff --git a/src/components/menu.rs b/src/components/menu.rs index feea7de..a180af1 100644 --- a/src/components/menu.rs +++ b/src/components/menu.rs @@ -30,6 +30,7 @@ impl Menu { String::from("Explorer"), String::from("Wallet"), String::from("Validator"), + String::from("Nominator"), String::from("Prices"), String::from("Governance"), String::from("Operations"), @@ -58,6 +59,7 @@ impl Menu { 0 => Ok(Some(Action::SetMode(Mode::Explorer))), 1 => Ok(Some(Action::SetMode(Mode::Wallet))), 2 => Ok(Some(Action::SetMode(Mode::Validator))), + 3 => Ok(Some(Action::SetMode(Mode::Nominator))), _ => Ok(Some(Action::SetMode(Mode::Empty))), } } @@ -78,6 +80,7 @@ impl Menu { 0 => Ok(Some(Action::SetMode(Mode::Explorer))), 1 => Ok(Some(Action::SetMode(Mode::Wallet))), 2 => Ok(Some(Action::SetMode(Mode::Validator))), + 3 => Ok(Some(Action::SetMode(Mode::Nominator))), _ => Ok(Some(Action::SetMode(Mode::Empty))), } } @@ -118,6 +121,7 @@ impl Component for Menu { Some(0) => Ok(Some(Action::SetActiveScreen(Mode::Explorer))), Some(1) => Ok(Some(Action::SetActiveScreen(Mode::Wallet))), Some(2) => Ok(Some(Action::SetActiveScreen(Mode::Validator))), + Some(3) => Ok(Some(Action::SetActiveScreen(Mode::Nominator))), _ => Ok(Some(Action::SetActiveScreen(Mode::Empty))), } }, diff --git a/src/components/mod.rs b/src/components/mod.rs index f609fde..8077170 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -16,6 +16,7 @@ pub mod version; pub mod explorer; pub mod wallet; pub mod validator; +pub mod nominator; pub mod empty; pub trait Component { diff --git a/src/components/nominator/current_validators.rs b/src/components/nominator/current_validators.rs new file mode 100644 index 0000000..3b81f89 --- /dev/null +++ b/src/components/nominator/current_validators.rs @@ -0,0 +1,315 @@ +use std::fs::File; +use std::path::PathBuf; +use std::io::{Write, BufRead, BufReader}; + +use color_eyre::Result; +use crossterm::event::{KeyCode, KeyEvent}; +use ratatui::layout::{Constraint, Margin}; +use ratatui::style::{Stylize, Modifier}; +use ratatui::{ + 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::types::EraRewardPoints; +use crate::{ + action::Action, + config::Config, + palette::StylePalette, +}; + +pub struct CurrentValidators { + is_active: bool, + action_tx: Option>, + known_validators_file: PathBuf, + palette: StylePalette, + scroll_state: ScrollbarState, + table_state: TableState, + individual: Vec, + known_validators: std::collections::HashMap<[u8; 32], String>, + total_points: u32, + era_index: u32, + my_stash_id: Option<[u8; 32]>, +} + +impl Default for CurrentValidators { + fn default() -> Self { + Self::new() + } +} + +impl CurrentValidators { + const KNOWN_VALIDATORS_FILE: &str = "known-validators"; + + pub fn new() -> Self { + Self { + is_active: false, + action_tx: None, + known_validators_file: Default::default(), + scroll_state: ScrollbarState::new(0), + table_state: TableState::new(), + individual: Default::default(), + known_validators: Default::default(), + total_points: 0, + era_index: 0, + my_stash_id: None, + palette: StylePalette::default(), + } + } + + fn save_validator_name(&mut self, new_name: String) { + if let Some(index) = self.table_state.selected() { + let account_id = self.individual[index].account_id; + let _ = self.known_validators.insert(account_id, new_name); + + let mut file = File::create(&self.known_validators_file) + .expect("file should be accessible; qed"); + + for (account_id, name) in self.known_validators.iter() { + let seed = hex::encode(account_id); + writeln!(file, "{}:0x{}", &name, &seed).unwrap(); + } + } + } + + fn update_known_validator_record(&mut self) { + if self.table_state.selected().is_some() { + if let Some(action_tx) = &self.action_tx { + let _ = action_tx.send(Action::RenameKnownValidatorRecord); + } + } + } + + fn read_known_validators(&mut self, file_path: &PathBuf) -> Result<()> { + match File::open(file_path) { + Ok(file) => { + let reader = BufReader::new(file); + for line in reader.lines() { + let line = line?.replace("\n", ""); + let line_split_at = line.find(":").unwrap_or(line.len()); + let (name, seed) = line.split_at(line_split_at); + let seed_str = &seed[3..]; + + let account_id: [u8; 32] = hex::decode(seed_str) + .expect("stored seed is valid hex string; qed") + .as_slice() + .try_into() + .expect("stored seed is valid length; qed"); + + let _ = self.known_validators.insert(account_id, name.to_string()); + } + }, + Err(_) => { } + } + Ok(()) + } + + fn first_row(&mut self) { + if self.individual.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.individual.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.individual.len() > 0 { + let last = self.individual.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_era_rewards( + &mut self, + era_index: u32, + total_points: u32, + individual: &Vec, + ) { + self.individual = individual.to_vec(); + self.total_points = total_points; + self.era_index = era_index; + + if let Some(account_id) = self.my_stash_id { + if self.individual.len() > 1 { + if let Some(index) = self.individual + .iter() + .position(|item| item.account_id == account_id) { + self.individual.swap(0, index); + } + } + } + + let index = self.table_state + .selected() + .unwrap_or_default(); + self.scroll_state = self.scroll_state.position(index); + } +} + +impl PartialComponent for CurrentValidators { + fn set_active(&mut self, current_tab: CurrentTab) { + match current_tab { + CurrentTab::CurrentValidators => self.is_active = true, + _ => self.is_active = false, + } + } +} + +impl Component for CurrentValidators { + 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()); + } + let mut known_validators_file = config.config.data_dir; + known_validators_file.push(Self::KNOWN_VALIDATORS_FILE); + self.read_known_validators(&known_validators_file)?; + self.known_validators_file = known_validators_file; + Ok(()) + } + + fn update(&mut self, action: Action) -> Result> { + match action { + Action::UpdateKnownValidator(validator_name) => self.save_validator_name(validator_name), + Action::SetStashAccount(account_id) => self.my_stash_id = Some(account_id), + Action::SetCurrentValidatorEraRewards(era_index, total_points, individual) => + self.update_era_rewards(era_index, total_points, &individual), + _ => {} + }; + 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::Char('R') => self.update_known_validator_record(), + _ => {}, + }; + } + Ok(None) + } + + fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { + let [place, _] = super::validator_details_layout(area); + let (border_style, border_type) = self.palette.create_border_style(self.is_active); + let table = Table::new( + self.individual + .iter() + .enumerate() + .map(|(index, info)| { + let mut address_text = Text::from(info.address.clone()).alignment(Alignment::Center); + let mut points_text = Text::from(info.points.to_string()).alignment(Alignment::Right); + + if info.disabled { + address_text = address_text.add_modifier(Modifier::CROSSED_OUT); + points_text = points_text.add_modifier(Modifier::CROSSED_OUT); + + } + + if self.my_stash_id.is_some() && index == 0 { + let name = self.known_validators + .get(&info.account_id) + .cloned() + .unwrap_or("My stash".to_string()); + Row::new(vec![ + Cell::from(Text::from(name).alignment(Alignment::Left)), + Cell::from(address_text), + Cell::from(points_text), + ]).style(self.palette.create_highlight_style()) + } else { + let name = self.known_validators + .get(&info.account_id) + .cloned() + .unwrap_or("Ghostie".to_string()); + Row::new(vec![ + Cell::from(Text::from(name).alignment(Alignment::Left)), + Cell::from(address_text), + Cell::from(points_text), + ]) + } + }), + [ + Constraint::Length(12), + Constraint::Min(0), + Constraint::Length(6), + ], + ) + .style(self.palette.create_basic_style(false)) + .highlight_style(self.palette.create_basic_style(true)) + .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(format!("Validators | Total points: {}", self.total_points))); + + 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(()) + } +} diff --git a/src/components/nominator/event_log.rs b/src/components/nominator/event_log.rs new file mode 100644 index 0000000..47351f0 --- /dev/null +++ b/src/components/nominator/event_log.rs @@ -0,0 +1,216 @@ +use color_eyre::Result; +use crossterm::event::{KeyCode, KeyEvent}; +use ratatui::{ + layout::{Alignment, Constraint, Margin, Rect}, + style::{Color, Style}, + text::Text, + widgets::{ + Block, Padding, Cell, Row, Scrollbar, ScrollbarOrientation, + ScrollbarState, Table, TableState, + }, + Frame +}; + +use super::{Component, PartialComponent, CurrentTab}; +use crate::{ + types::ActionLevel, + action::Action, + config::Config, + palette::StylePalette, +}; + +#[derive(Debug, Default)] +struct WalletLog { + time: chrono::DateTime, + level: ActionLevel, + message: String, +} + +#[derive(Debug)] +pub struct EventLogs { + is_active: bool, + scroll_state: ScrollbarState, + table_state: TableState, + logs: std::collections::VecDeque, + palette: StylePalette +} + +// TODO: remove later +impl Default for EventLogs { + fn default() -> Self { + EventLogs { + is_active: false, + scroll_state: Default::default(), + table_state: Default::default(), + logs: std::collections::VecDeque::from(vec![ + WalletLog { + time: chrono::Local::now(), + level: ActionLevel::Warn, + message: "NOT FINALIZED PAGE! NEEDED IN ORDER TO SEE VALIDATORS POINTS".to_string(), + }, + ]), + palette: Default::default(), + } + } +} + +impl EventLogs { + //const MAX_LOGS: usize = 50; + + //fn add_new_log(&mut self, message: String, level: ActionLevel) { + // self.logs.push_front(WalletLog { + // time: chrono::Local::now(), + // level, + // message, + // }); + + // if self.logs.len() > Self::MAX_LOGS { + // let _ = self.logs.pop_back(); + // } + //} + + fn first_row(&mut self) { + if self.logs.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.logs.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.logs.len() > 0 { + let last = self.logs.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); + } +} + +impl PartialComponent for EventLogs { + fn set_active(&mut self, current_tab: CurrentTab) { + match current_tab { + CurrentTab::EventLogs => self.is_active = true, + _ => { + self.is_active = false; + self.table_state.select(None); + self.scroll_state = self.scroll_state.position(0); + } + } + } +} + +impl Component for EventLogs { + 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 handle_key_event(&mut self, key: KeyEvent) -> Result> { + match key.code { + KeyCode::Up | KeyCode::Char('k') if self.is_active => self.previous_row(), + KeyCode::Down | KeyCode::Char('j') if self.is_active => self.next_row(), + KeyCode::Char('g') if self.is_active => self.first_row(), + KeyCode::Char('G') if self.is_active => self.last_row(), + _ => {}, + }; + Ok(None) + } + + fn update(&mut self, _action: Action) -> Result> { + //match action { + // Action::ValidatorLog(message, level) => self.add_new_log(message, level), + // _ => {} + //}; + Ok(None) + } + + fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { + let [_, _, place] = super::nominator_layout(area); + + let (border_style, border_type) = self.palette.create_border_style(self.is_active); + let error_style = Style::new().fg(Color::Red); + let warn_style = Style::new().fg(Color::Yellow); + let info_style = Style::new().fg(Color::Green); + + let table = Table::new( + self.logs + .iter() + .map(|log| { + let style = match log.level { + ActionLevel::Info => info_style, + ActionLevel::Warn => warn_style, + ActionLevel::Error => error_style, + }; + Row::new(vec![ + Cell::from(Text::from(log.time.format("%H:%M:%S").to_string()).style(style).alignment(Alignment::Left)), + Cell::from(Text::from(log.message.clone()).style(style).alignment(Alignment::Left)), + ]) + }), + [ + Constraint::Max(8), + Constraint::Min(0), + ], + ) + .column_spacing(1) + .highlight_style(self.palette.create_highlight_style()) + .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("Action Logs")); + + 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(()) + } +} diff --git a/src/components/nominator/mod.rs b/src/components/nominator/mod.rs new file mode 100644 index 0000000..ec38568 --- /dev/null +++ b/src/components/nominator/mod.rs @@ -0,0 +1,182 @@ +use color_eyre::Result; +use crossterm::event::{KeyCode, KeyEvent}; +use ratatui::{ + layout::{Constraint, Layout, Rect}, + Frame, +}; + +use std::sync::mpsc::Sender; +use tokio::sync::mpsc::UnboundedSender; + +use super::Component; +use crate::{action::Action, app::Mode, config::Config}; + +mod event_log; +mod current_validators; +mod rename_known_validator; + +use event_log::EventLogs; +use current_validators::CurrentValidators; +use rename_known_validator::RenameKnownValidator; + +#[derive(Debug, Clone, PartialEq)] +pub enum CurrentTab { + Nothing, + CurrentValidators, + EventLogs, + RenameKnownValidator, +} + +pub trait PartialComponent: Component { + fn set_active(&mut self, current_tab: CurrentTab); +} + +pub struct Nominator { + is_active: bool, + current_tab: CurrentTab, + components: Vec>, +} + +impl Default for Nominator { + fn default() -> Self { + Self { + is_active: false, + current_tab: CurrentTab::Nothing, + components: vec![ + Box::new(CurrentValidators::default()), + Box::new(EventLogs::default()), + Box::new(RenameKnownValidator::default()), + ], + } + } +} + +impl Nominator { + fn move_left(&mut self) { + match self.current_tab { + CurrentTab::EventLogs => self.current_tab = CurrentTab::CurrentValidators, + _ => {} + } + } + + fn move_right(&mut self) { + match self.current_tab { + CurrentTab::CurrentValidators => self.current_tab = CurrentTab::EventLogs, + _ => {} + } + } +} + +impl Component for Nominator { + fn register_network_handler(&mut self, tx: Sender) -> Result<()> { + for component in self.components.iter_mut() { + component.register_network_handler(tx.clone())?; + } + Ok(()) + } + + fn register_action_handler(&mut self, tx: UnboundedSender) -> Result<()> { + for component in self.components.iter_mut() { + component.register_action_handler(tx.clone())?; + } + Ok(()) + } + + fn register_config_handler(&mut self, config: Config) -> Result<()> { + for component in self.components.iter_mut() { + component.register_config_handler(config.clone())?; + } + Ok(()) + } + + fn handle_key_event(&mut self, key: KeyEvent) -> Result> { + if !self.is_active { return Ok(None) } + + match self.current_tab { + CurrentTab::RenameKnownValidator => match key.code { + KeyCode::Esc => { + self.current_tab = CurrentTab::CurrentValidators; + 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()); + } + }, + _ => { + for component in self.components.iter_mut() { + component.handle_key_event(key)?; + } + } + } + } + Ok(None) + } + + fn update(&mut self, action: Action) -> Result> { + match action { + Action::RenameKnownValidatorRecord => + self.current_tab = CurrentTab::RenameKnownValidator, + Action::UpdateKnownValidator(_) => + self.current_tab = CurrentTab::CurrentValidators, + Action::SetActiveScreen(Mode::Nominator) => { + self.is_active = true; + self.current_tab = CurrentTab::CurrentValidators; + }, + _ => {}, + } + for component in self.components.iter_mut() { + component.set_active(self.current_tab.clone()); + component.update(action.clone())?; + } + Ok(None) + } + + fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { + let screen = super::screen_layout(area); + for component in self.components.iter_mut() { + component.draw(frame, screen)?; + } + Ok(()) + } +} + +pub fn nominator_layout(area: Rect) -> [Rect; 3] { + Layout::vertical([ + Constraint::Percentage(25), + Constraint::Percentage(50), + Constraint::Percentage(25), + ]).areas(area) +} + +pub fn validator_details_layout(area: Rect) -> [Rect; 2] { + let [place, _, _] = nominator_layout(area); + Layout::horizontal([ + Constraint::Percentage(70), + Constraint::Percentage(30), + ]).areas(place) +} diff --git a/src/components/nominator/rename_known_validator.rs b/src/components/nominator/rename_known_validator.rs new file mode 100644 index 0000000..36a0eba --- /dev/null +++ b/src/components/nominator/rename_known_validator.rs @@ -0,0 +1,139 @@ +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 super::{Component, PartialComponent, CurrentTab}; +use crate::{ + widgets::{Input, InputRequest}, + action::Action, + config::Config, + palette::StylePalette, +}; + +#[derive(Debug)] +pub struct RenameKnownValidator { + is_active: bool, + action_tx: Option>, + name: Input, + palette: StylePalette +} + +impl Default for RenameKnownValidator { + fn default() -> Self { + Self::new() + } +} + +impl RenameKnownValidator { + pub fn new() -> Self { + Self { + is_active: false, + action_tx: None, + name: Input::new(String::new()), + palette: StylePalette::default(), + } + } +} + +impl RenameKnownValidator { + fn submit_message(&mut self) { + if let Some(action_tx) = &self.action_tx { + let _ = action_tx.send(Action::UpdateKnownValidator( + self.name.value().to_string())); + } + } + + fn enter_char(&mut self, new_char: char) { + let _ = self.name.handle(InputRequest::InsertChar(new_char)); + } + + fn delete_char(&mut self) { + let _ = self.name.handle(InputRequest::DeletePrevChar); + } + + fn move_cursor_right(&mut self) { + let _ = self.name.handle(InputRequest::GoToNextChar); + } + + fn move_cursor_left(&mut self) { + let _ = self.name.handle(InputRequest::GoToPrevChar); + } +} + +impl PartialComponent for RenameKnownValidator { + fn set_active(&mut self, current_tab: CurrentTab) { + match current_tab { + CurrentTab::RenameKnownValidator => self.is_active = true, + _ => { + self.is_active = false; + self.name = Input::new(String::new()); + }, + }; + } +} + +impl Component for RenameKnownValidator { + 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 draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { + if self.is_active { + let size = area.as_size(); + let area = Rect::new(size.width / 2, size.height / 2, 51, 3); + let (border_style, border_type) = self.palette.create_popup_style(); + + let input = Paragraph::new(self.name.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("Know validator name")); + 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.name.cursor() as u16 + 1, + area.y + 1 + )); + } + Ok(()) + } +} diff --git a/src/components/validator/peers.rs b/src/components/validator/peers.rs index 757ba2d..37e5733 100644 --- a/src/components/validator/peers.rs +++ b/src/components/validator/peers.rs @@ -155,7 +155,6 @@ impl Component for Peers { self.peers .iter() .map(|info| { - Row::new(vec![ Cell::from(Text::from(info.peer_id.clone()).alignment(Alignment::Left)), Cell::from(Text::from(info.roles.clone()).alignment(Alignment::Center)), diff --git a/src/network/mod.rs b/src/network/mod.rs index 2c283d2..974c092 100644 --- a/src/network/mod.rs +++ b/src/network/mod.rs @@ -157,6 +157,7 @@ impl Network { Action::GetValidatorsNumber => predefined_calls::get_validators_number(&self.action_tx, &self.online_client_api).await, Action::GetNominatorsNumber => predefined_calls::get_nominators_number(&self.action_tx, &self.online_client_api).await, Action::GetInflation => predefined_calls::get_inflation(&self.action_tx, &self.online_client_api).await, + Action::GetCurrentValidatorEraRewards => predefined_calls::get_current_validator_reward_in_era(&self.action_tx, &self.online_client_api).await, Action::SetSender(seed, maybe_nonce) => { self.store_sender_nonce(&seed, maybe_nonce); diff --git a/src/network/predefined_calls.rs b/src/network/predefined_calls.rs index c9fd25c..2f175df 100644 --- a/src/network/predefined_calls.rs +++ b/src/network/predefined_calls.rs @@ -14,7 +14,7 @@ use subxt::{ use crate::{ action::Action, casper_network::runtime_types::sp_consensus_slots, - types::{EraInfo, Nominator, SessionKeyInfo, SystemAccount}, + types::{EraInfo, EraRewardPoints, Nominator, SessionKeyInfo, SystemAccount}, CasperAccountId, CasperConfig }; @@ -303,6 +303,50 @@ pub async fn get_validator_staking_result( Ok(()) } +pub async fn get_current_validator_reward_in_era( + action_tx: &UnboundedSender, + api: &OnlineClient, +) -> Result<()> { + let era_index = super::raw_calls::staking::current_era(api, None) + .await? + .unwrap_or_default(); + + let disabled_validators = super::raw_calls::staking::disabled_validators(api, None) + .await? + .unwrap_or_default(); + + let maybe_era_reward_points = super::raw_calls::staking::eras_reward_points(api, None, era_index) + .await?; + + let (total_points, individual) = match maybe_era_reward_points { + Some(era_reward_points) => { + ( + era_reward_points.total, + era_reward_points.individual + .iter() + .enumerate() + .map(|(index, (account_id, points))| { + let address = AccountId32::from(account_id.0) + .to_ss58check_with_version(Ss58AddressFormat::custom(1996)); + EraRewardPoints { + address, + account_id: account_id.0, + points: *points, + disabled: disabled_validators.contains(&(index as u32)), + } + }) + .collect(), + ) + }, + None => (0, Vec::new()), + }; + + action_tx.send(Action::SetCurrentValidatorEraRewards( + era_index, total_points, individual))?; + + Ok(()) +} + async fn get_validator_reward_in_era( action_tx: &UnboundedSender, api: &OnlineClient, diff --git a/src/network/raw_calls/staking.rs b/src/network/raw_calls/staking.rs index 547f669..6626de4 100644 --- a/src/network/raw_calls/staking.rs +++ b/src/network/raw_calls/staking.rs @@ -162,3 +162,12 @@ pub async fn validators( let maybe_validators = super::do_storage_call(online_client, &storage_key, at_hash).await?; Ok(maybe_validators) } + +pub async fn disabled_validators( + online_client: &OnlineClient, + at_hash: Option<&H256>, +) -> Result>> { + let storage_key = casper_network::storage().staking().disabled_validators(); + let maybe_disabled_validators = super::do_storage_call(online_client, &storage_key, at_hash).await?; + Ok(maybe_disabled_validators) +} diff --git a/src/network/subscriptions.rs b/src/network/subscriptions.rs index b48a5bd..8123cc2 100644 --- a/src/network/subscriptions.rs +++ b/src/network/subscriptions.rs @@ -118,6 +118,7 @@ impl BestSubscription { self.network_tx.send(Action::GetValidatorsNumber)?; self.network_tx.send(Action::GetNominatorsNumber)?; self.network_tx.send(Action::GetInflation)?; + self.network_tx.send(Action::GetCurrentValidatorEraRewards)?; } Ok(()) } diff --git a/src/types/era.rs b/src/types/era.rs index 882533d..fcb6302 100644 --- a/src/types/era.rs +++ b/src/types/era.rs @@ -6,3 +6,11 @@ pub struct EraInfo { pub index: u32, pub start: Option, } + +#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, Decode)] +pub struct EraRewardPoints { + pub address: String, + pub account_id: [u8; 32], + pub points: u32, + pub disabled: bool, +} diff --git a/src/types/mod.rs b/src/types/mod.rs index 021b2ea..c196a5b 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -7,7 +7,7 @@ mod session; mod nominator; pub use extrinsics::CasperExtrinsicDetails; -pub use era::EraInfo; +pub use era::{EraRewardPoints, EraInfo}; pub use log::ActionLevel; pub use account::SystemAccount; pub use peer::PeerInformation; diff --git a/src/types/points.rs b/src/types/points.rs new file mode 100644 index 0000000..937a290 --- /dev/null +++ b/src/types/points.rs @@ -0,0 +1,10 @@ +use codec::Decode; +use serde::{Serialize, Deserialize}; + +#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, Decode)] +pub struct EraRewardPoints { + pub nonce: u32, + pub free: u128, + pub reserved: u128, + pub frozen: u128, +}