use std::path::PathBuf; use std::fs::File; use std::io::{Write, BufRead, BufReader}; use color_eyre::Result; use crossterm::event::{KeyCode, KeyEvent}; use ratatui::layout::{Constraint, Margin}; use ratatui::style::{Modifier, Stylize}; use ratatui::{ text::Text, layout::{Alignment, Rect}, widgets::{ Block, Cell, Row, Table, TableState, Scrollbar, ScrollbarOrientation, ScrollbarState, }, Frame }; use subxt::{ tx::PairSigner, ext::sp_core::{ Pair as PairT, sr25519::Pair, crypto::{Ss58Codec, Ss58AddressFormat, AccountId32}, }, }; use tokio::sync::mpsc::UnboundedSender; use std::sync::mpsc::Sender; use super::{PartialComponent, Component, CurrentTab}; use crate::casper::CasperConfig; use crate::types::{ActionLevel, ActionTarget}; use crate::{ types::SessionKeyInfo, action::Action, config::Config, palette::StylePalette, }; pub struct StashInfo { is_active: bool, action_tx: Option>, network_tx: Option>, palette: StylePalette, scroll_state: ScrollbarState, table_state: TableState, stash_pair: Option>, stash_address: String, session_keys: std::collections::HashMap, key_names: &'static [&'static str], stash_filepath: PathBuf, } impl Default for StashInfo { fn default() -> Self { Self::new() } } impl StashInfo { pub fn new() -> Self { Self { is_active: false, action_tx: None, network_tx: None, scroll_state: ScrollbarState::new(0), table_state: TableState::new(), palette: StylePalette::default(), stash_address: String::new(), stash_pair: None, session_keys: Default::default(), key_names: &["gran", "babe", "audi", "slow"], stash_filepath: PathBuf::from("/etc/ghost/stash-key"), } } fn log_event(&mut self, message: String, level: ActionLevel) { if let Some(action_tx) = &self.action_tx { let _ = action_tx.send( Action::EventLog(message, level, ActionTarget::ValidatorLog)); } } fn first_row(&mut self) { if self.session_keys.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.session_keys.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.session_keys.len() > 0 { let last = self.session_keys.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 read_or_create_stash(&mut self) -> Result<()> { match File::open(&self.stash_filepath) { Ok(file) => { let reader = BufReader::new(file); if let Some(Ok(line)) = reader.lines().next() { let stash_key = line.replace("\n", ""); let stash_key = &stash_key[2..]; let seed: [u8; 32] = hex::decode(stash_key) .expect("stored seed is valid hex string; qed") .as_slice() .try_into() .expect("stored seed is valid length; qed"); let pair = Pair::from_seed(&seed); let account_id = pair.public().0; let address = AccountId32::from(account_id) .to_ss58check_with_version(Ss58AddressFormat::custom(1996)); let pair_signer = PairSigner::::new(pair); self.initiate_stash_info(account_id, seed); self.log_event( format!("stash key {address} read from disk"), ActionLevel::Info); self.stash_address = address; self.stash_pair = Some(pair_signer); Ok(()) } else { self.log_event( format!("file at '{:?}' is empty, trying to create new key", &self.stash_filepath), ActionLevel::Warn); self.generate_and_save_new_key() } }, Err(_) => { self.log_event( format!("file at '{:?}' not found, trying to create new key", &self.stash_filepath), ActionLevel::Warn); self.generate_and_save_new_key() } } } fn generate_and_save_new_key(&mut self) -> Result<()> { let (pair, seed) = Pair::generate(); let secret_seed = hex::encode(seed); let account_id = pair.public().0; let address = AccountId32::from(account_id) .to_ss58check_with_version(Ss58AddressFormat::custom(1996)); let pair_signer = PairSigner::::new(pair); let mut new_file = File::create(&self.stash_filepath)?; writeln!(new_file, "0x{}", &secret_seed)?; self.initiate_stash_info(account_id, seed); self.log_event( format!("new stash key {} created and stored at {:?}", &address, self.stash_filepath), ActionLevel::Info); self.stash_address = address; self.stash_pair = Some(pair_signer); Ok(()) } fn initiate_stash_info(&self, account_id: [u8; 32], secret_seed: [u8; 32]) { if let Some(action_tx) = &self.action_tx { let _ = action_tx.send(Action::SetStashAccount(account_id)); let _ = action_tx.send(Action::SetStashSecret(secret_seed)); } if let Some(network_tx) = &self.network_tx { let _ = network_tx.send(Action::BalanceRequest(account_id, false)); let _ = network_tx.send(Action::GetValidatorLedger(account_id)); let _ = network_tx.send(Action::GetIsStashBonded(account_id)); let _ = network_tx.send(Action::GetErasStakersOverview(account_id)); let _ = network_tx.send(Action::GetValidatorPrefs(account_id)); let _ = network_tx.send(Action::GetNominatorsByValidator(account_id)); let _ = network_tx.send(Action::GetQueuedSessionKeys(account_id)); let _ = network_tx.send(Action::GetSessionKeys(account_id)); let _ = network_tx.send(Action::GetValidatorAllRewards(account_id)); let _ = network_tx.send(Action::GetSlashingSpans(account_id)); } } fn set_new_key(&mut self, name: String, key_info: SessionKeyInfo) { if let Some(info) = self.session_keys.get_mut(&name) { let key_changed = info.key != key_info.key; let is_stored_changed = info.is_stored != key_info.is_stored; if key_changed || is_stored_changed { *info = key_info; } } else { let _ = self.session_keys.insert(name, key_info); } } } impl PartialComponent for StashInfo { fn set_active(&mut self, current_tab: CurrentTab) { match current_tab { CurrentTab::StashInfo => self.is_active = true, _ => { self.is_active = false; self.table_state.select(None); self.scroll_state = self.scroll_state.position(0); } } } } impl Component for StashInfo { 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_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()); } self.read_or_create_stash()?; Ok(()) } fn update(&mut self, action: Action) -> Result> { match action { Action::SetSessionKey(name, key_info) => self.set_new_key(name, key_info), _ => {} }; 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(), _ => {}, }; } Ok(None) } fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { let [place, _] = super::validator_session_and_listen_layout(area); let (border_style, border_type) = self.palette.create_border_style(self.is_active); let table = Table::new( self.key_names .iter() .map(|name| { let address_text = match self.session_keys.get(*name) { Some(key_info) => { let mut address_text = Text::from(key_info.key.clone()).alignment(Alignment::Center); if !key_info.is_stored { address_text = address_text.add_modifier(Modifier::CROSSED_OUT); } address_text }, None => Text::from("-").alignment(Alignment::Center), }; let queued_name = format!("q_{}", name); let queued_address_text = match self.session_keys.get(&queued_name) { Some(key_info) => { let mut queued_address_text = Text::from(key_info.key.clone()).alignment(Alignment::Right); if !key_info.is_stored { queued_address_text = queued_address_text.add_modifier(Modifier::CROSSED_OUT); } queued_address_text }, None => Text::from("-").alignment(Alignment::Right), }; Row::new(vec![ Cell::from(Text::from(name.to_string()).alignment(Alignment::Left)), Cell::from(address_text), Cell::from(Text::from("-->".to_string()).alignment(Alignment::Center)), Cell::from(queued_address_text), ]) }), [ Constraint::Length(4), Constraint::Min(0), Constraint::Length(3), Constraint::Min(0), ], ) .highlight_style(self.palette.create_highlight_style()) .column_spacing(1) .block(Block::bordered() .border_style(border_style) .border_type(border_type) .title_alignment(Alignment::Right) .title_style(self.palette.create_title_style(false)) .title(self.stash_address.clone())); 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(()) } }