ghost-eye/src/components/validator/stash_info.rs
Uncle Stretch 10b8337f8d
withdraw unbonded functionality added
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-02-11 16:09:34 +03:00

359 lines
13 KiB
Rust

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<UnboundedSender<Action>>,
network_tx: Option<Sender<Action>>,
palette: StylePalette,
scroll_state: ScrollbarState,
table_state: TableState,
stash_pair: Option<PairSigner<CasperConfig, Pair>>,
stash_address: String,
session_keys: std::collections::HashMap<String, SessionKeyInfo>,
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::<CasperConfig, Pair>::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::<CasperConfig, Pair>::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<Action>) -> Result<()> {
self.network_tx = Some(tx);
Ok(())
}
fn register_action_handler(&mut self, tx: UnboundedSender<Action>) -> 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<Option<Action>> {
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<Option<Action>> {
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(())
}
}