use std::fs::File; use std::path::PathBuf; use std::io::{Write, BufRead, BufReader}; use subxt::ext::sp_core::crypto::{AccountId32, Ss58AddressFormat, Ss58Codec}; use color_eyre::Result; use crossterm::event::{KeyCode, KeyEvent}; use ratatui::layout::{Constraint, Margin}; use ratatui::style::{Stylize, Modifier}; use ratatui::widgets::Clear; use ratatui::{ text::{Line, Text}, layout::{Alignment, Rect}, widgets::{ Block, Cell, Row, Table, TableState, Scrollbar, Padding, ScrollbarOrientation, ScrollbarState, }, Frame }; use tokio::sync::mpsc::UnboundedSender; use std::sync::mpsc::Sender; use super::{PartialComponent, Component, CurrentTab}; use crate::types::{ActionLevel, Nominations, ActionTarget, EraRewardPoints}; use crate::{ action::Action, config::Config, palette::StylePalette, }; pub struct CurrentValidators { is_active: bool, network_tx: Option>, action_tx: Option>, known_validators_file: PathBuf, palette: StylePalette, scroll_state: ScrollbarState, table_state: TableState, individual: Vec, not_active_nominations: Vec, known_validators: std::collections::HashMap<[u8; 32], String>, checked_validators: std::collections::HashSet<[u8; 32]>, total_points: u32, era_index: u32, my_stash_id: Option<[u8; 32]>, account_id: [u8; 32], account_secret_seed: [u8; 32], my_nominations: std::collections::HashMap<[u8; 32], Nominations>, filtered_vector: Vec, } 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, network_tx: None, action_tx: None, known_validators_file: Default::default(), scroll_state: ScrollbarState::new(0), table_state: TableState::new(), individual: Default::default(), known_validators: Default::default(), checked_validators: Default::default(), total_points: 0, era_index: 0, my_stash_id: None, account_id: [0u8; 32], account_secret_seed: [0u8; 32], palette: StylePalette::default(), my_nominations: Default::default(), not_active_nominations: Default::default(), filtered_vector: Default::default(), } } fn close_popup(&mut self) { self.is_active = false; if let Some(action_tx) = &self.action_tx { let _ = action_tx.send(Action::ClosePopup); } } fn move_selected(&mut self, index: usize) { if self.filtered_vector.len() > 0 { self.table_state.select(Some(index)); self.scroll_state = self.scroll_state.position(index); self.update_choosen_details(index); } } fn update_used_account(&mut self, account_id: [u8; 32], secret_seed_str: String) { let secret_seed: [u8; 32] = hex::decode(secret_seed_str) .expect("stored seed is valid hex string; qed") .as_slice() .try_into() .expect("stored seed is valid length; qed"); self.account_id = account_id; self.account_secret_seed = secret_seed; } fn store_nominators(&mut self, nominations: Nominations, account_id: [u8; 32]) { if self.account_id == account_id { self.not_active_nominations.clear(); for account in nominations.targets.iter() { if !self.individual.iter().any(|r| r.account_id == *account) { self.not_active_nominations.push(EraRewardPoints { account_id: *account, address: AccountId32::from(*account) .to_ss58check_with_version(Ss58AddressFormat::custom(1996)), points: 0, disabled: true, }); } } } self.my_nominations.insert(account_id, nominations); } fn update_choosen_details(&self, index: usize) { if let Some(action_tx) = &self.action_tx { let (selected_account_id, selected_points) = self.filtered_vector .get(index) .map(|data| (data.account_id, data.points)) .unwrap_or_default(); let _ = action_tx.send(Action::SetChoosenValidator( selected_account_id, selected_points, self.total_points)); } } fn clear_choosen(&mut self) { self.checked_validators.clear(); } fn choose_current_nominated(&mut self) { self.clear_choosen(); self.checked_validators.extend(self.my_nominations .get(&self.account_id) .map(|nom| nom.targets.clone()) .unwrap_or_default()); } fn choose_all_validators(&mut self) { self.clear_choosen(); self.checked_validators.extend(self.individual .iter().map(|ind| ind.account_id)); } fn swap_choosen_filter(&mut self) { let is_individual = self.filtered_vector.len() == self.individual.len(); self.filtered_vector = self.individual .iter() .filter_map(|data| { let is_good = !is_individual || self.checked_validators.contains(&data.account_id); is_good.then(|| data.clone()) }) .collect::>(); self.scroll_state = self.scroll_state.content_length(self.filtered_vector.len()); if self.filtered_vector.len() == 0 { self.table_state.select(None); self.scroll_state = self.scroll_state.position(0); } else { self.first_row(); } } fn flip_validator_check(&mut self) { if let Some(index) = self.table_state.selected() { if let Some(indiv) = self.filtered_vector.get(index) { let current_account_id = indiv.account_id; if self.checked_validators.contains(¤t_account_id) { self.checked_validators.remove(¤t_account_id); } else { self.checked_validators.insert(current_account_id); } } } } fn save_validator_name(&mut self, new_name: String) { if let Some(index) = self.table_state.selected() { let account_id = self.filtered_vector[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<()> { if let Ok(file) = File::open(file_path) { 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()); } } Ok(()) } fn first_row(&mut self) { if self.filtered_vector.len() > 0 { self.move_selected(0); } } fn next_row(&mut self) { let i = match self.table_state.selected() { Some(i) => { if i >= self.filtered_vector.len() - 1 { i } else { i + 1 } }, None => 0, }; self.move_selected(i); } fn last_row(&mut self) { if self.filtered_vector.len() > 0 { let last = self.filtered_vector.len() - 1; self.move_selected(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.move_selected(i); } fn update_era_rewards( &mut self, era_index: u32, total_points: u32, individual: &Vec, ) { let previous_length = self.individual.len(); self.individual = individual.to_vec(); self.total_points = total_points; self.era_index = era_index; if individual.len() > 0 { if let Some(account_id) = self.my_stash_id { if let Some(index) = self.individual .iter() .position(|item| item.account_id == account_id) { self.individual.swap(0, index); } } if previous_length == 0 { self.filtered_vector = self.individual.clone(); self.scroll_state = self.scroll_state.content_length(self.individual.len()); self.first_row(); } } } fn nominate_choosen(&mut self) { if self.my_stash_id.map(|acc| acc == self.account_id).unwrap_or_default() { if let Some(action_tx) = &self.action_tx { let _ = action_tx.send(Action::EventLog( "nomination from stash account will stop node validation, use another account for nomination".to_string(), ActionLevel::Error, ActionTarget::WalletLog)); } } else { if let Some(network_tx) = &self.network_tx { let nominate_targets: Vec<[u8; 32]> = self.checked_validators .clone() .into_iter() .collect::>(); let _ = network_tx.send(Action::NominateTargets( self.account_secret_seed, nominate_targets)); } } self.close_popup(); } fn prepare_nomination_line(&self) -> String { let empty_nominations = Nominations::default(); let nominations = self.my_nominations .get(&self.account_id) .unwrap_or(&empty_nominations); if nominations.targets.len() == 0 { "No nominations found".to_string() } else { let status = if nominations.suppressed { "Suppressed" } else { "Active" }; format!("Submitted at era #{} | {} ", nominations.submitted_in, status, ) } } } impl PartialComponent for CurrentValidators { fn set_active(&mut self, current_tab: CurrentTab) { match current_tab { CurrentTab::CurrentValidatorsPopup => 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_network_handler(&mut self, tx: Sender) -> Result<()> { self.network_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_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::UsedAccount(account_id, secret_seed) => self.update_used_account(account_id, secret_seed), Action::SetNominatorsByAccount(nominations, account_id) => self.store_nominators(nominations, account_id), 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(), KeyCode::Char('c') => self.clear_choosen(), KeyCode::Char('m') => self.choose_current_nominated(), KeyCode::Char('a') => self.choose_all_validators(), KeyCode::Char('f') => self.swap_choosen_filter(), KeyCode::Char('N') => self.nominate_choosen(), KeyCode::Enter => self.flip_validator_check(), KeyCode::Esc => self.close_popup(), _ => {}, }; } Ok(None) } fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { if self.is_active { let (border_style, border_type) = self.palette.create_border_style(self.is_active); let [place, _] = super::nominator_layout(area); let top_title = format!("Validators {} | Total points: {}", self.individual.len(), self.total_points); let bottom_title = self.prepare_nomination_line(); let table = Table::new( self.filtered_vector .iter() .chain(&self.not_active_nominations) .enumerate() .map(|(index, info)| { let is_validator_choosen = self.checked_validators.contains(&info.account_id); let is_current_nomination = self.my_nominations .get(&self.account_id) .map(|x| x.targets.contains(&info.account_id)) .unwrap_or_default(); 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); let (row_style, is_choosen_text) = if is_validator_choosen { address_text = address_text.add_modifier(Modifier::ITALIC); points_text = points_text.add_modifier(Modifier::ITALIC); (self.palette.create_highlight_style(), ">") } else if is_current_nomination { (Default::default(), "*") } else { (Default::default(), "") }; if info.disabled { address_text = address_text.add_modifier(Modifier::CROSSED_OUT); points_text = points_text.add_modifier(Modifier::CROSSED_OUT); } let default_name = if index == 0 && self.my_stash_id .map(|account_id| account_id == info.account_id) .unwrap_or_default() == true { "My stash" } else { "Ghostie" }; let name = self.known_validators .get(&info.account_id) .cloned() .unwrap_or(default_name.to_string()); Row::new(vec![ Cell::from(Text::from(is_choosen_text).alignment(Alignment::Left)), Cell::from(Text::from(name).alignment(Alignment::Left)), Cell::from(address_text), Cell::from(points_text), ]).style(row_style) }), [ Constraint::Length(1), 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_style(self.palette.create_title_style(false)) .title_bottom(Line::from(bottom_title).left_aligned()) .title_top(Line::from(top_title).right_aligned())); let scrollbar = Scrollbar::default() .orientation(ScrollbarOrientation::VerticalRight) .begin_symbol(None) .end_symbol(None) .style(self.palette.create_scrollbar_style()); frame.render_widget(Clear, place); 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(()) } }