diff --git a/config/config.json5 b/config/config.json5 index cb74711..3464148 100644 --- a/config/config.json5 +++ b/config/config.json5 @@ -29,7 +29,18 @@ "highlight_style": "yellow bold", "popup_style": "blue", "popup_title_style": "blue", - } + }, + "Validator": { + "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": { @@ -49,6 +60,12 @@ "": "Quit", "": "Suspend", }, + "Validator": { + "": "Quit", + "": "Quit", + "": "Quit", + "": "Suspend", + }, "Empty": { "": "Quit", "": "Quit", diff --git a/src/action.rs b/src/action.rs index d600493..2363cb9 100644 --- a/src/action.rs +++ b/src/action.rs @@ -4,8 +4,8 @@ use strum::Display; use subxt::utils::H256; use subxt::config::substrate::DigestItem; -use crate::{ - types::{SystemAccount, ActionLevel, EraInfo, CasperExtrinsicDetails}, +use crate::types::{ + ActionLevel, CasperExtrinsicDetails, EraInfo, Nominator, PeerInformation, SessionKeyInfo, SystemAccount }; #[derive(Debug, Clone, PartialEq, Eq, Display, Serialize, Deserialize)] @@ -42,6 +42,7 @@ pub enum Action { TransferBalance(String, [u8; 32], u128), WalletLog(String, ActionLevel), + ValidatorLog(String, ActionLevel), NewBestBlock(u32), NewBestHash(H256), @@ -49,6 +50,10 @@ pub enum Action { NewFinalizedHash(H256), BestBlockUpdated(u32), ExtrinsicsLength(u32, usize), + ValidatorsNumber(u32), + NominatorsNumber(u32), + Inflation(String), + Apy(String), GetBlockAuthor(H256, Vec), SetBlockAuthor(H256, String), @@ -59,25 +64,54 @@ pub enum Action { GetChainName, GetChainVersion, GetPendingExtrinsics, + GetConnectedPeers, + GetSessionKeys([u8; 32]), + GetQueuedSessionKeys([u8; 32]), + GetListenAddresses, + GetLocalIdentity, GetLatestBlock, GetFinalizedBlock, GetActiveEra, + GetCurrentEra, GetEpochProgress, - GetValidators, + GetValidatorsNumber, + GetNominatorsNumber, + GetInflation, + GetNominatorsByValidator([u8; 32]), + GetValidatorAllRewards([u8; 32]), + GetValidatorLedger([u8; 32]), + GetIsStashBonded([u8; 32]), + GetErasStakersOverview([u8; 32]), + GetValidatorPrefs([u8; 32]), SetNodeName(Option), SetSystemHealth(Option, bool, bool), SetGenesisHash(Option), SetChainName(Option), SetChainVersion(Option), + SetStashAccount([u8; 32]), BestBlockInformation(H256, u32), FinalizedBlockInformation(H256, u32), ExtrinsicsForBlock(u32, Vec), SetActiveEra(EraInfo), + SetCurrentEra(u32), SetEpochProgress(u64, u64), SetPendingExtrinsicsLength(usize), + SetConnectedPeers(Vec), + SetSessionKey(String, SessionKeyInfo), + SetListenAddresses(Vec), + SetLocalIdentity(String), + SetNominatorsByValidator(Vec), + SetValidatorEraReward(u32, u128), + SetValidatorEraClaimed(u32, bool), + SetValidatorEraSlash(u32, u128), + SetValidatorEraUnlocking(u32, u128), + SetBondedAmount(bool), + SetStakedAmountRatio(u128, u128), + SetStakedRatio(u128, u128), + SetValidatorPrefs(u32, bool), GetTotalIssuance, GetExistentialDeposit, diff --git a/src/app.rs b/src/app.rs index 801d30d..a7e6335 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, - empty::Empty, health::Health, fps::FpsCounter, Component, + validator::Validator, empty::Empty, health::Health, fps::FpsCounter, + Component, }, }; @@ -21,10 +22,8 @@ pub enum Mode { Menu, Explorer, Wallet, - WalletActive, - ExplorerActive, + Validator, Empty, - EmptyActive, } impl Default for Mode { @@ -71,6 +70,7 @@ impl App { Box::new(Version::default()), Box::new(Explorer::default()), Box::new(Wallet::default()), + Box::new(Validator::default()), Box::new(Empty::default()), ], should_quite: false, @@ -161,6 +161,7 @@ impl App { fn trigger_node_fast_events(&mut self) -> Result<()> { self.network_tx.send(Action::GetPendingExtrinsics)?; + self.network_tx.send(Action::GetConnectedPeers)?; Ok(()) } @@ -171,6 +172,8 @@ impl App { self.network_tx.send(Action::GetChainName)?; self.network_tx.send(Action::GetChainVersion)?; self.network_tx.send(Action::GetExistentialDeposit)?; + self.network_tx.send(Action::GetLocalIdentity)?; + self.network_tx.send(Action::GetListenAddresses)?; Ok(()) } @@ -252,6 +255,15 @@ impl App { } } }, + Mode::Validator => { + if let Some(component) = self.components.get_mut(6) { + 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/fps.rs b/src/components/fps.rs index 5708c7f..81fb401 100644 --- a/src/components/fps.rs +++ b/src/components/fps.rs @@ -78,7 +78,7 @@ impl Component for FpsCounter { let [_, place] = super::header_layout(area); let message = format!( - "{:.2} ticks/sec | {:.2} FPS", + " {:.2} ticks/sec | {:.2} FPS", self.ticks_per_second, self.frames_per_second ); diff --git a/src/components/health.rs b/src/components/health.rs index 21ecdc2..61c325f 100644 --- a/src/components/health.rs +++ b/src/components/health.rs @@ -20,6 +20,8 @@ pub struct Health { is_syncing: bool, should_have_peers: bool, tx_pool_length: usize, + validators_count: u32, + nominators_count: u32, } impl Default for Health { @@ -36,6 +38,8 @@ impl Health { is_syncing: true, should_have_peers: false, tx_pool_length: 0, + validators_count: 0, + nominators_count: 0, } } @@ -74,6 +78,8 @@ impl Component for Health { }, Action::SetNodeName(name) => self.name = name, Action::SetPendingExtrinsicsLength(length) => self.tx_pool_length = length, + Action::NominatorsNumber(number) => self.nominators_count = number, + Action::ValidatorsNumber(number) => self.validators_count = number, _ => {} }; Ok(None) @@ -82,11 +88,13 @@ impl Component for Health { fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { let [place, _] = super::header_layout(area); - let message = format!("{:^12} | tx.pool: {:^3} | peers: {:^3} | {:^9}", + let message = format!("{:^12} | tx.pool: {:^3} | peers: {:^3} | {:^9} | validators {:^4} | nominators {:^4} |", self.name_as_string(), self.tx_pool_length, self.peers_as_string(), - self.is_syncing_as_string()); + self.is_syncing_as_string(), + self.validators_count, + self.nominators_count); let span = Span::styled(message, Style::new().dim()); let paragraph = Paragraph::new(span).left_aligned(); diff --git a/src/components/menu.rs b/src/components/menu.rs index 0e49348..feea7de 100644 --- a/src/components/menu.rs +++ b/src/components/menu.rs @@ -29,8 +29,8 @@ impl Menu { items: vec![ String::from("Explorer"), String::from("Wallet"), + String::from("Validator"), String::from("Prices"), - String::from("Staking"), String::from("Governance"), String::from("Operations"), ], @@ -57,6 +57,7 @@ impl Menu { match i { 0 => Ok(Some(Action::SetMode(Mode::Explorer))), 1 => Ok(Some(Action::SetMode(Mode::Wallet))), + 2 => Ok(Some(Action::SetMode(Mode::Validator))), _ => Ok(Some(Action::SetMode(Mode::Empty))), } } @@ -76,6 +77,7 @@ impl Menu { match i { 0 => Ok(Some(Action::SetMode(Mode::Explorer))), 1 => Ok(Some(Action::SetMode(Mode::Wallet))), + 2 => Ok(Some(Action::SetMode(Mode::Validator))), _ => Ok(Some(Action::SetMode(Mode::Empty))), } } @@ -115,6 +117,7 @@ impl Component for Menu { match self.list_state.selected() { Some(0) => Ok(Some(Action::SetActiveScreen(Mode::Explorer))), Some(1) => Ok(Some(Action::SetActiveScreen(Mode::Wallet))), + Some(2) => Ok(Some(Action::SetActiveScreen(Mode::Validator))), _ => Ok(Some(Action::SetActiveScreen(Mode::Empty))), } }, diff --git a/src/components/mod.rs b/src/components/mod.rs index 1af2709..f609fde 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -15,6 +15,7 @@ pub mod menu; pub mod version; pub mod explorer; pub mod wallet; +pub mod validator; pub mod empty; pub trait Component { @@ -75,8 +76,8 @@ pub fn global_layout(area: Rect) -> [Rect; 2] { pub fn header_layout(area: Rect) -> [Rect; 2] { let [header, _] = global_layout(area); Layout::horizontal([ - Constraint::Percentage(50), - Constraint::Percentage(50), + Constraint::Fill(1), + Constraint::Length(27), ]).areas(header) } diff --git a/src/components/validator/event_log.rs b/src/components/validator/event_log.rs new file mode 100644 index 0000000..695dc5c --- /dev/null +++ b/src/components/validator/event_log.rs @@ -0,0 +1,197 @@ +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, Default)] +pub struct EventLogs { + is_active: bool, + scroll_state: ScrollbarState, + table_state: TableState, + logs: std::collections::VecDeque, + palette: StylePalette +} + +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::validator_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/validator/history.rs b/src/components/validator/history.rs new file mode 100644 index 0000000..df75bfc --- /dev/null +++ b/src/components/validator/history.rs @@ -0,0 +1,271 @@ +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 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) => 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(), + _ => {}, + }; + } + 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(()) + } +} diff --git a/src/components/validator/listen_addresses.rs b/src/components/validator/listen_addresses.rs new file mode 100644 index 0000000..3bc745c --- /dev/null +++ b/src/components/validator/listen_addresses.rs @@ -0,0 +1,183 @@ +use color_eyre::Result; +use crossterm::event::{KeyCode, KeyEvent}; +use ratatui::layout::Margin; +use ratatui::widgets::ListItem; +use ratatui::{ + layout::{Alignment, Rect}, + widgets::{ + Block, List, ListState, Scrollbar, + ScrollbarOrientation, ScrollbarState, + }, + Frame +}; +use tokio::sync::mpsc::UnboundedSender; + +use super::{PartialComponent, Component, CurrentTab}; +use crate::{ + action::Action, + config::Config, + palette::StylePalette, +}; + +pub struct ListenAddresses { + is_active: bool, + action_tx: Option>, + palette: StylePalette, + scroll_state: ScrollbarState, + list_state: ListState, + listen_addresses: Vec, + local_identity: String, +} + +impl Default for ListenAddresses { + fn default() -> Self { + Self::new() + } +} + +impl ListenAddresses { + pub fn new() -> Self { + Self { + is_active: false, + action_tx: None, + scroll_state: ScrollbarState::new(0), + list_state: ListState::default(), + palette: StylePalette::default(), + listen_addresses: Vec::new(), + local_identity: String::new(), + } + } + + fn first_row(&mut self) { + if self.listen_addresses.len() > 0 { + self.list_state.select(Some(0)); + self.scroll_state = self.scroll_state.position(0); + } + } + + fn next_row(&mut self) { + let i = match self.list_state.selected() { + Some(i) => { + if i >= self.listen_addresses.len() - 1 { + i + } else { + i + 1 + } + }, + None => 0, + }; + self.list_state.select(Some(i)); + self.scroll_state = self.scroll_state.position(i); + } + + fn last_row(&mut self) { + if self.listen_addresses.len() > 0 { + let last = self.listen_addresses.len() - 1; + self.list_state.select(Some(last)); + self.scroll_state = self.scroll_state.position(last); + } + } + + fn previous_row(&mut self) { + let i = match self.list_state.selected() { + Some(i) => { + if i == 0 { + 0 + } else { + i - 1 + } + }, + None => 0 + }; + self.list_state.select(Some(i)); + self.scroll_state = self.scroll_state.position(i); + } +} + +impl PartialComponent for ListenAddresses { + fn set_active(&mut self, current_tab: CurrentTab) { + match current_tab { + CurrentTab::ListenAddresses => self.is_active = true, + _ => { + self.is_active = false; + self.list_state.select(None); + self.scroll_state = self.scroll_state.position(0); + } + } + } +} + +impl Component for ListenAddresses { + 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()); + } + Ok(()) + } + + fn update(&mut self, action: Action) -> Result> { + match action { + Action::SetListenAddresses(addresses) => self.listen_addresses = addresses, + Action::SetLocalIdentity(identity) => self.local_identity = identity, + _ => {} + }; + 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 list = List::new( + self.listen_addresses + .iter() + .map(|addr| ListItem::new(addr.clone())) + ) + .highlight_style(self.palette.create_highlight_style()) + .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.local_identity.clone())); + + let scrollbar = Scrollbar::default() + .orientation(ScrollbarOrientation::VerticalRight) + .begin_symbol(None) + .end_symbol(None) + .style(self.palette.create_scrollbar_style()); + + frame.render_stateful_widget(list, place, &mut self.list_state); + frame.render_stateful_widget( + scrollbar, + place.inner(Margin { vertical: 1, horizontal: 1 }), + &mut self.scroll_state, + ); + + Ok(()) + } +} diff --git a/src/components/validator/mod.rs b/src/components/validator/mod.rs new file mode 100644 index 0000000..96b1c48 --- /dev/null +++ b/src/components/validator/mod.rs @@ -0,0 +1,223 @@ +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 peers; +mod stash_info; +mod nominators; +mod listen_addresses; +mod history; +mod withdrawals; +mod stash_details; +mod staking_details; +mod reward_details; + +use stash_details::StashDetails; +use staking_details::StakingDetails; +use reward_details::RewardDetails; +use event_log::EventLogs; +use peers::Peers; +use stash_info::StashInfo; +use listen_addresses::ListenAddresses; +use nominators::NominatorsByValidator; +use history::History; +use withdrawals::Withdrawals; + +#[derive(Debug, Clone, PartialEq)] +pub enum CurrentTab { + Nothing, + StashInfo, + ListenAddresses, + NominatorsByValidator, + History, + Withdrawals, + Peers, + EventLogs, +} + +pub trait PartialComponent: Component { + fn set_active(&mut self, current_tab: CurrentTab); +} + +pub struct Validator { + is_active: bool, + current_tab: CurrentTab, + components: Vec>, +} + +impl Default for Validator { + fn default() -> Self { + Self { + is_active: false, + current_tab: CurrentTab::Nothing, + components: vec![ + Box::new(StashInfo::default()), + Box::new(NominatorsByValidator::default()), + Box::new(StashDetails::default()), + Box::new(StakingDetails::default()), + Box::new(RewardDetails::default()), + Box::new(History::default()), + Box::new(Withdrawals::default()), + Box::new(Peers::default()), + Box::new(ListenAddresses::default()), + Box::new(EventLogs::default()), + ], + } + } +} + +impl Validator { + fn move_left(&mut self) { + match self.current_tab { + CurrentTab::EventLogs => self.current_tab = CurrentTab::Peers, + CurrentTab::Peers => self.current_tab = CurrentTab::Withdrawals, + CurrentTab::Withdrawals => self.current_tab = CurrentTab::History, + CurrentTab::History => self.current_tab = CurrentTab::NominatorsByValidator, + CurrentTab::NominatorsByValidator => self.current_tab = CurrentTab::ListenAddresses, + CurrentTab::ListenAddresses => self.current_tab = CurrentTab::StashInfo, + _ => {} + } + } + + fn move_right(&mut self) { + match self.current_tab { + CurrentTab::Nothing => self.current_tab = CurrentTab::StashInfo, + CurrentTab::StashInfo => self.current_tab = CurrentTab::ListenAddresses, + CurrentTab::ListenAddresses => self.current_tab = CurrentTab::NominatorsByValidator, + CurrentTab::NominatorsByValidator => self.current_tab = CurrentTab::History, + CurrentTab::History => self.current_tab = CurrentTab::Withdrawals, + CurrentTab::Withdrawals => self.current_tab = CurrentTab::Peers, + CurrentTab::Peers => self.current_tab = CurrentTab::EventLogs, + _ => {} + } + } +} + +impl Component for Validator { + 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 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::SetActiveScreen(Mode::Validator) => self.is_active = true, + _ => {}, + } + 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 validator_layout(area: Rect) -> [Rect; 4] { + Layout::vertical([ + Constraint::Length(16), + Constraint::Fill(1), + Constraint::Fill(1), + Constraint::Percentage(25), + ]).areas(area) +} + +pub fn validator_details_layout(area: Rect) -> [Rect; 2] { + let [place, _, _, _] = validator_layout(area); + Layout::horizontal([ + Constraint::Length(31), + Constraint::Fill(1), + ]).areas(place) +} + +pub fn validator_session_and_listen_layout(area: Rect) -> [Rect; 2] { + let [_, place] = validator_details_layout(area); + Layout::vertical([ + Constraint::Length(6), + Constraint::Fill(1), + ]).areas(place) +} + +pub fn validator_statistics_layout(area: Rect) -> [Rect; 3] { + let [_, place, _, _] = validator_layout(area); + Layout::horizontal([ + Constraint::Percentage(30), + Constraint::Percentage(40), + Constraint::Percentage(30), + ]).areas(place) +} + +pub fn validator_balance_layout(area: Rect) -> [Rect; 3] { + let [place, _] = validator_details_layout(area); + Layout::vertical([ + Constraint::Length(6), + Constraint::Length(5), + Constraint::Length(5), + ]).areas(place) +} diff --git a/src/components/validator/nominators.rs b/src/components/validator/nominators.rs new file mode 100644 index 0000000..21df0c9 --- /dev/null +++ b/src/components/validator/nominators.rs @@ -0,0 +1,210 @@ +use color_eyre::Result; +use crossterm::event::{KeyCode, KeyEvent}; +use ratatui::layout::{Constraint, Margin}; +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::Nominator; +use crate::{ + action::Action, + config::Config, + palette::StylePalette, +}; + +pub struct NominatorsByValidator { + is_active: bool, + action_tx: Option>, + palette: StylePalette, + scroll_state: ScrollbarState, + table_state: TableState, + nominators: Vec, +} + +impl Default for NominatorsByValidator { + fn default() -> Self { + Self::new() + } +} + +impl NominatorsByValidator { + const TICKER: &str = " CSPR"; + const DECIMALS: usize = 5; + + pub fn new() -> Self { + Self { + is_active: false, + action_tx: None, + scroll_state: ScrollbarState::new(0), + table_state: TableState::new(), + nominators: Vec::new(), + palette: StylePalette::default(), + } + } + + fn update_nominators(&mut self, nominators: Vec) { + if self.nominators.len() > nominators.len() { + if let Some(_) = self.table_state.selected() { + self.last_row(); + } + } + self.nominators = nominators; + } + + fn first_row(&mut self) { + if self.nominators.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.nominators.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.nominators.len() > 0 { + let last = self.nominators.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 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 NominatorsByValidator { + fn set_active(&mut self, current_tab: CurrentTab) { + match current_tab { + CurrentTab::NominatorsByValidator => self.is_active = true, + _ => { + self.is_active = false; + self.table_state.select(None); + self.scroll_state = self.scroll_state.position(0); + } + } + } +} + +impl Component for NominatorsByValidator { + 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::SetNominatorsByValidator(nominators) => self.update_nominators(nominators), + _ => {} + }; + 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_statistics_layout(area); + let (border_style, border_type) = self.palette.create_border_style(self.is_active); + let table = Table::new( + self.nominators + .iter() + .map(|info| { + Row::new(vec![ + Cell::from(Text::from(info.who.clone()).alignment(Alignment::Left)), + Cell::from(Text::from(self.prepare_u128(info.value)).alignment(Alignment::Right)), + ]) + }), + [ + Constraint::Min(0), + Constraint::Min(11), + ], + ) + .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("My Nominators")); + + 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/validator/peers.rs b/src/components/validator/peers.rs new file mode 100644 index 0000000..757ba2d --- /dev/null +++ b/src/components/validator/peers.rs @@ -0,0 +1,198 @@ +use color_eyre::Result; +use crossterm::event::{KeyCode, KeyEvent}; +use ratatui::layout::{Constraint, Margin}; +use ratatui::{ + text::Text, + layout::{Alignment, Rect}, + widgets::{ + Block, Cell, Row, Table, TableState, Scrollbar, Padding, + ScrollbarOrientation, ScrollbarState, + }, + Frame +}; + +use super::{PartialComponent, Component, CurrentTab}; +use crate::types::PeerInformation; +use crate::{ + action::Action, + config::Config, + palette::StylePalette, +}; + +pub struct Peers { + is_active: bool, + palette: StylePalette, + scroll_state: ScrollbarState, + table_state: TableState, + peers: Vec, +} + +impl Default for Peers { + fn default() -> Self { + Self::new() + } +} + +impl Peers { + pub fn new() -> Self { + Self { + is_active: false, + scroll_state: ScrollbarState::new(0), + table_state: TableState::new(), + peers: Vec::new(), + palette: StylePalette::default(), + } + } + + fn update_peers(&mut self, peers: Vec) { + if self.peers.len() > peers.len() { + if let Some(_) = self.table_state.selected() { + self.last_row(); + } + } + self.peers = peers; + } + + fn first_row(&mut self) { + if self.peers.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.peers.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.peers.len() > 0 { + let last = self.peers.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 Peers { + fn set_active(&mut self, current_tab: CurrentTab) { + match current_tab { + CurrentTab::Peers => self.is_active = true, + _ => { + self.is_active = false; + self.table_state.select(None); + self.scroll_state = self.scroll_state.position(0); + } + } + } +} + +impl Component for Peers { + 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::SetConnectedPeers(peers) => self.update_peers(peers), + _ => {} + }; + 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_layout(area); + let (border_style, border_type) = self.palette.create_border_style(self.is_active); + let table = Table::new( + 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)), + Cell::from(Text::from(info.best_hash.to_string()).alignment(Alignment::Center)), + Cell::from(Text::from(info.best_number.to_string()).alignment(Alignment::Right)), + ]) + }), + [ + Constraint::Fill(1), + Constraint::Length(11), + Constraint::Length(11), + Constraint::Length(11), + ], + ) + .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("My Peers")); + + 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/validator/reward_details.rs b/src/components/validator/reward_details.rs new file mode 100644 index 0000000..573ad2c --- /dev/null +++ b/src/components/validator/reward_details.rs @@ -0,0 +1,121 @@ +use color_eyre::Result; +use ratatui::layout::Constraint; +use ratatui::{ + text::Text, + layout::{Alignment, Rect}, + widgets::{Block, Cell, Row, Table}, + Frame +}; + +use super::{PartialComponent, Component, CurrentTab}; +use crate::{ + action::Action, + config::Config, + palette::StylePalette, +}; + +pub struct RewardDetails { + palette: StylePalette, + commission: u32, + nominators_blocked: bool, + apy: String, + inflation: String, +} + +impl Default for RewardDetails { + fn default() -> Self { + Self::new() + } +} + +impl RewardDetails { + pub fn new() -> Self { + Self { + palette: StylePalette::default(), + commission: 0, + nominators_blocked: false, + apy: String::from("0.0%"), + inflation: String::from("0.0%"), + } + } + + fn comission_to_string(&self) -> String { + if self.nominators_blocked { + "blocked".to_string() + } else { + let result = self.commission as f64 / 1_000_000_000.0; + format!("{:.1}%", result) + } + } +} + +impl PartialComponent for RewardDetails { + fn set_active(&mut self, _current_tab: CurrentTab) { } +} + +impl Component for RewardDetails { + 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::SetValidatorPrefs(commission, disabled) => { + self.commission = commission; + self.nominators_blocked = disabled; + } + Action::Apy(apy) => self.apy = apy, + Action::Inflation(inflation) => self.inflation = inflation, + _ => {} + }; + Ok(None) + } + + fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { + let [_, _, place] = super::validator_balance_layout(area); + let (border_style, border_type) = self.palette.create_border_style(false); + + let table = Table::new( + vec![ + Row::new(vec![ + Cell::from(Text::from("Nominators".to_string()).alignment(Alignment::Left)), + Cell::from(Text::from(self.comission_to_string()).alignment(Alignment::Right)), + ]), + Row::new(vec![ + Cell::from(Text::from("Current APY".to_string()).alignment(Alignment::Left)), + Cell::from(Text::from(self.apy.clone()).alignment(Alignment::Right)), + ]), + Row::new(vec![ + Cell::from(Text::from("Inflation".to_string()).alignment(Alignment::Left)), + Cell::from(Text::from(self.inflation.clone()).alignment(Alignment::Right)), + ]), + ], + [ + Constraint::Min(11), + 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("Reward details")); + + frame.render_widget(table, place); + + Ok(()) + } +} diff --git a/src/components/validator/staking_details.rs b/src/components/validator/staking_details.rs new file mode 100644 index 0000000..ad190ff --- /dev/null +++ b/src/components/validator/staking_details.rs @@ -0,0 +1,115 @@ +use color_eyre::Result; +use ratatui::layout::Constraint; +use ratatui::{ + text::Text, + layout::{Alignment, Rect}, + widgets::{Block, Cell, Row, Table}, + Frame +}; + +use super::{PartialComponent, Component, CurrentTab}; +use crate::{ + action::Action, + config::Config, + palette::StylePalette, +}; + +pub struct StakingDetails { + palette: StylePalette, + staked_own: u128, + staked_total: u128, +} + +impl Default for StakingDetails { + fn default() -> Self { + Self::new() + } +} + +impl StakingDetails { + const TICKER: &str = " CSPR"; + const DECIMALS: usize = 5; + + pub fn new() -> Self { + Self { + palette: StylePalette::default(), + staked_own: 0, + staked_total: 0, + } + } + + 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 StakingDetails { + fn set_active(&mut self, _current_tab: CurrentTab) { } +} + +impl Component for StakingDetails { + 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::SetStakedRatio(total, own) => { + self.staked_total = total; + self.staked_own = own; + } + _ => {} + }; + Ok(None) + } + + fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { + let [_, place, _] = super::validator_balance_layout(area); + let (border_style, border_type) = self.palette.create_border_style(false); + + let table = Table::new( + vec![ + Row::new(vec![ + Cell::from(Text::from("Stake value".to_string()).alignment(Alignment::Left)), + Cell::from(Text::from(self.prepare_u128(self.staked_total)).alignment(Alignment::Right)), + ]), + Row::new(vec![ + Cell::from(Text::from("Own stake".to_string()).alignment(Alignment::Left)), + Cell::from(Text::from(self.prepare_u128(self.staked_own)).alignment(Alignment::Right)), + ]), + Row::new(vec![ + Cell::from(Text::from("Other stake".to_string()).alignment(Alignment::Left)), + Cell::from(Text::from(self.prepare_u128(self.staked_total.saturating_sub(self.staked_own))).alignment(Alignment::Right)), + ]), + ], + [ + Constraint::Min(11), + 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("Staking details")); + + frame.render_widget(table, place); + + Ok(()) + } +} diff --git a/src/components/validator/stash_details.rs b/src/components/validator/stash_details.rs new file mode 100644 index 0000000..1814ed1 --- /dev/null +++ b/src/components/validator/stash_details.rs @@ -0,0 +1,140 @@ +use color_eyre::Result; +use ratatui::layout::Constraint; +use ratatui::{ + text::Text, + layout::{Alignment, Rect}, + widgets::{Block, Cell, Row, Table}, + Frame +}; + +use super::{PartialComponent, Component, CurrentTab}; +use crate::{ + action::Action, + config::Config, + palette::StylePalette, +}; + +pub struct StashDetails { + palette: StylePalette, + is_bonded: bool, + free_balance: u128, + staked_total: u128, + staked_active: u128, + stash_account_id: [u8; 32], +} + +impl Default for StashDetails { + fn default() -> Self { + Self::new() + } +} + +impl StashDetails { + const TICKER: &str = " CSPR"; + const DECIMALS: usize = 5; + + pub fn new() -> Self { + Self { + palette: StylePalette::default(), + is_bonded: false, + free_balance: 0, + staked_total: 0, + staked_active: 0, + stash_account_id: [0u8; 32], + } + } + + fn prepare_u128(&self, value: u128) -> String { + let value = value as f64 / 10f64.powi(18); + let after = Self::DECIMALS; + format!("{:.after$}{}", value, Self::TICKER) + } + + fn is_bonded_to_string(&self) -> String { + if self.is_bonded { + "bonded".to_string() + } else { + "no bond".to_string() + } + } +} + +impl PartialComponent for StashDetails { + fn set_active(&mut self, _current_tab: CurrentTab) { } +} + +impl Component for StashDetails { + 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::SetStashAccount(account_id) => self.stash_account_id = account_id, + Action::SetBondedAmount(is_bonded) => self.is_bonded = is_bonded, + Action::SetStakedAmountRatio(total, active) => { + self.staked_total = total; + self.staked_active = active; + }, + Action::BalanceResponse(account_id, balance) if account_id == self.stash_account_id => { + self.free_balance = balance.free + .saturating_sub(balance.frozen) + .saturating_sub(balance.reserved); + }, + _ => {} + }; + Ok(None) + } + + fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { + let [place, _, _] = super::validator_balance_layout(area); + let (border_style, border_type) = self.palette.create_border_style(false); + + let table = Table::new( + vec![ + Row::new(vec![ + Cell::from(Text::from("Bond ready".to_string()).alignment(Alignment::Left)), + Cell::from(Text::from(self.is_bonded_to_string()).alignment(Alignment::Right)), + ]), + Row::new(vec![ + Cell::from(Text::from("Free balance".to_string()).alignment(Alignment::Left)), + Cell::from(Text::from(self.prepare_u128(self.free_balance)).alignment(Alignment::Right)), + ]), + Row::new(vec![ + Cell::from(Text::from("Total staked".to_string()).alignment(Alignment::Left)), + Cell::from(Text::from(self.prepare_u128(self.staked_total)).alignment(Alignment::Right)), + ]), + Row::new(vec![ + Cell::from(Text::from("Active staked".to_string()).alignment(Alignment::Left)), + Cell::from(Text::from(self.prepare_u128(self.staked_active)).alignment(Alignment::Right)), + ]), + ], + [ + Constraint::Min(14), + 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("Stash details")); + + frame.render_widget(table, place); + + Ok(()) + } +} diff --git a/src/components/validator/stash_info.rs b/src/components/validator/stash_info.rs new file mode 100644 index 0000000..c87eb2a --- /dev/null +++ b/src/components/validator/stash_info.rs @@ -0,0 +1,354 @@ +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; +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], + file_path: 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"], + file_path: 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::ValidatorLog(message, level)); + } + } + + 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.file_path) { + 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); + 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.file_path), + ActionLevel::Warn); + + self.generate_and_save_new_key() + } + }, + Err(_) => { + self.log_event( + format!("file at '{:?}' not found, trying to create new key", &self.file_path), + ActionLevel::Warn); + + self.generate_and_save_new_key() + } + } + } + + fn generate_and_save_new_key(&mut self) -> Result<()> { + let (pair, seed) = Pair::generate(); // TODO: revisit + let secret_seed = hex::encode(seed); + let address = AccountId32::from(pair.public().0) + .to_ss58check_with_version(Ss58AddressFormat::custom(1996)); + let account_id = pair.public().0; + let pair_signer = PairSigner::::new(pair); + + let mut new_file = File::create(&self.file_path)?; + writeln!(new_file, "0x{}", &secret_seed)?; + + self.initiate_stash_info(account_id); + self.log_event( + format!("new stash key {} created and stored at {:?}", &address, self.file_path), + ActionLevel::Info); + + self.stash_address = address; + self.stash_pair = Some(pair_signer); + + Ok(()) + } + + fn initiate_stash_info(&self, account_id: [u8; 32]) { + if let Some(action_tx) = &self.action_tx { + let _ = action_tx.send(Action::SetStashAccount(account_id)); + } + + 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)); + } + } + + 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(()) + } +} diff --git a/src/components/validator/withdrawals.rs b/src/components/validator/withdrawals.rs new file mode 100644 index 0000000..420838e --- /dev/null +++ b/src/components/validator/withdrawals.rs @@ -0,0 +1,227 @@ +use std::collections::BTreeMap; + +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 super::{PartialComponent, Component, CurrentTab}; +use crate::{ + action::Action, + config::Config, + palette::StylePalette, +}; + +pub struct Withdrawals { + is_active: bool, + palette: StylePalette, + scroll_state: ScrollbarState, + table_state: TableState, + unlockings: BTreeMap, + current_era: u32, +} + +impl Default for Withdrawals { + fn default() -> Self { + Self::new() + } +} + +impl Withdrawals { + const TICKER: &str = " CSPR"; + const DECIMALS: usize = 5; + + pub fn new() -> Self { + Self { + is_active: false, + scroll_state: ScrollbarState::new(0), + table_state: TableState::new(), + palette: StylePalette::default(), + unlockings: BTreeMap::new(), + current_era: 0, + } + } + + fn first_row(&mut self) { + if self.unlockings.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.unlockings.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.unlockings.len() > 0 { + let last = self.unlockings.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 add_new_unlocking(&mut self, era_index: u32, unlocking: u128) { + match self.unlockings.get_mut(&era_index) { + Some(unlck) => *unlck = unlocking, + None => { + let _ = self.unlockings.insert(era_index, unlocking); + }, + } + } + + fn prepare_u128(&self, value: u128) -> String { + let value = value as f64 / 10f64.powi(18); + let after = Self::DECIMALS; + format!("{:.after$}{}", value, Self::TICKER) + } + + fn estimate_time(&self, era_index: u32) -> String { + if era_index > self.current_era { + format!("{} eras", era_index - self.current_era) + } else { + String::from("ready") + } + } +} + +impl PartialComponent for Withdrawals { + fn set_active(&mut self, current_tab: CurrentTab) { + match current_tab { + CurrentTab::Withdrawals => self.is_active = true, + _ => { + self.is_active = false; + self.table_state.select(None); + self.scroll_state = self.scroll_state.position(0); + } + } + } +} + +impl Component for Withdrawals { + 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::SetCurrentEra(current_era) => self.current_era = current_era, + Action::SetValidatorEraUnlocking(era_index, unlocking) => + self.add_new_unlocking(era_index, unlocking), + _ => {} + }; + 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_statistics_layout(area); + let (border_style, border_type) = self.palette.create_border_style(self.is_active); + let table = Table::new( + self.unlockings + .iter() + .map(|(key, value)| { + let mut era_index_text = Text::from(key.to_string()).alignment(Alignment::Left); + let mut est_era_text = Text::from(self.estimate_time(*key)).alignment(Alignment::Center); + let mut value_text = Text::from(self.prepare_u128(*value)).alignment(Alignment::Right); + + if *key > self.current_era { + era_index_text = era_index_text.add_modifier(Modifier::CROSSED_OUT); + est_era_text = est_era_text.add_modifier(Modifier::CROSSED_OUT); + value_text = value_text.add_modifier(Modifier::CROSSED_OUT); + } + + Row::new(vec![ + Cell::from(era_index_text), + Cell::from(est_era_text), + Cell::from(value_text), + ]) + }), + [ + Constraint::Length(12), + Constraint::Length(13), + 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("Withdrawals")); + + 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/wallet/accounts.rs b/src/components/wallet/accounts.rs index 278d460..3788ffa 100644 --- a/src/components/wallet/accounts.rs +++ b/src/components/wallet/accounts.rs @@ -16,7 +16,6 @@ use ratatui::{ Frame }; use subxt::{ - tx::PairSigner, ext::sp_core::{ Pair as PairT, sr25519::Pair, @@ -27,7 +26,6 @@ use tokio::sync::mpsc::UnboundedSender; use std::sync::mpsc::Sender; use super::{PartialComponent, Component, CurrentTab}; -use crate::casper::CasperConfig; use crate::types::{SystemAccount, ActionLevel}; use crate::{ action::Action, @@ -40,8 +38,6 @@ struct AccountInfo { address: String, account_id: [u8; 32], seed: String, - #[allow(dead_code)] - pair_signer: PairSigner, } pub struct Accounts { @@ -140,11 +136,10 @@ impl Accounts { } fn create_new_account(&mut self, name: String) { - let (pair, seed) = Pair::generate(); + let (pair, seed) = Pair::generate(); // TODO: generate_with_phrase() let secret_seed = hex::encode(seed); let account_id = pair.public().0; - let pair_signer = PairSigner::::new(pair); - let address = AccountId32::from(seed.clone()) + let address = AccountId32::from(seed) .to_ss58check_with_version(Ss58AddressFormat::custom(1996)); self.log_event( @@ -158,7 +153,6 @@ impl Accounts { address, account_id, seed: secret_seed, - pair_signer, }); self.last_row(); self.save_to_file(); @@ -242,7 +236,6 @@ impl Accounts { 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.send_balance_request(account_id, false); self.wallet_keys.push(AccountInfo { @@ -250,7 +243,6 @@ impl Accounts { account_id, address, seed: wallet_key.to_string(), - pair_signer, }); } self.log_event(format!("read {} wallets from disk", @@ -288,7 +280,6 @@ impl Accounts { let account_id = pair.public().0; let address = AccountId32::from(pair.public().0) .to_ss58check_with_version(Ss58AddressFormat::custom(1996)); - let pair_signer = PairSigner::::new(pair); let mut new_file = File::create(file_path)?; writeln!(new_file, "ghostie:0x{}", &secret_seed)?; @@ -299,7 +290,6 @@ impl Accounts { address, account_id, seed: secret_seed, - pair_signer, }); } }; diff --git a/src/main.rs b/src/main.rs index b0bc8da..589fd3e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,7 @@ use clap::Parser; use color_eyre::Result; use subxt::{ OnlineClient, - backend::{legacy::LegacyRpcMethods, rpc::RpcClient}, + backend::rpc::RpcClient, }; mod action; @@ -54,9 +54,7 @@ async fn main() -> Result<()> { let (action_tx, action_rx) = tokio::sync::mpsc::unbounded_channel(); let rpc_client = RpcClient::from_url(args.rpc_endpoint).await?; - let legacy_client_api = LegacyRpcMethods::::new(rpc_client.clone()); - let online_client = - OnlineClient::::from_rpc_client(rpc_client.clone()).await?; + let online_client = OnlineClient::::from_rpc_client(rpc_client.clone()).await?; let finalized_blocks_sub = online_client.blocks().subscribe_finalized().await?; let best_blocks_sub = online_client.blocks().subscribe_best().await?; @@ -66,7 +64,6 @@ async fn main() -> Result<()> { let mut network = network::Network::new( cloned_action_tx, online_client, - legacy_client_api, rpc_client, ); start_tokio_action_loop(sync_io_rx, &mut network); diff --git a/src/network/legacy_rpc_calls.rs b/src/network/legacy_rpc_calls.rs index dec77ca..7a306b4 100644 --- a/src/network/legacy_rpc_calls.rs +++ b/src/network/legacy_rpc_calls.rs @@ -1,27 +1,30 @@ use tokio::sync::mpsc::UnboundedSender; use color_eyre::Result; -use subxt::backend::legacy::rpc_methods::LegacyRpcMethods; +use subxt::{backend::{legacy::rpc_methods::SystemHealth, rpc::RpcClient}, rpc_params}; -use crate::{action::Action, casper::CasperConfig}; +use crate::{action::Action, types::PeerInformation}; pub async fn get_node_name( action_tx: &UnboundedSender, - api: &LegacyRpcMethods, + rpc_client: &RpcClient, ) -> Result<()> { - let maybe_node_name = api.system_name().await.ok(); + let maybe_node_name = rpc_client + .request("system_name", rpc_params![]) + .await + .ok(); action_tx.send(Action::SetNodeName(maybe_node_name))?; Ok(()) } pub async fn get_system_health( action_tx: &UnboundedSender, - api: &LegacyRpcMethods, + rpc_client: &RpcClient, ) -> Result<()> { - let (maybe_peers, is_syncing, should_have_peers) = api - .system_health() + let (maybe_peers, is_syncing, should_have_peers) = rpc_client + .request("system_health", rpc_params![]) .await .ok() - .map_or((None, false, false), |health| ( + .map_or((None, false, false), |health: SystemHealth| ( Some(health.peers), health.is_syncing, health.should_have_peers, @@ -35,10 +38,11 @@ pub async fn get_system_health( pub async fn get_genesis_hash( action_tx: &UnboundedSender, - api: &LegacyRpcMethods, + rpc_client: &RpcClient, ) -> Result<()> { - let maybe_genesis_hash = api - .genesis_hash() + let params = rpc_params![0u32]; + let maybe_genesis_hash = rpc_client + .request("chain_getBlockHash", params) .await .ok(); action_tx.send(Action::SetGenesisHash(maybe_genesis_hash))?; @@ -47,10 +51,10 @@ pub async fn get_genesis_hash( pub async fn get_chain_name( action_tx: &UnboundedSender, - api: &LegacyRpcMethods, + rpc_client: &RpcClient, ) -> Result<()> { - let maybe_chain_name = api - .system_chain() + let maybe_chain_name = rpc_client + .request("system_chain", rpc_params![]) .await .ok(); action_tx.send(Action::SetChainName(maybe_chain_name))?; @@ -59,12 +63,60 @@ pub async fn get_chain_name( pub async fn get_system_version( action_tx: &UnboundedSender, - api: &LegacyRpcMethods, + rpc_client: &RpcClient, ) -> Result<()> { - let maybe_system_version = api - .system_version() + let maybe_system_version = rpc_client + .request("system_version", rpc_params![]) .await .ok(); action_tx.send(Action::SetChainVersion(maybe_system_version))?; Ok(()) } + +pub async fn get_pending_extrinsics( + action_tx: &UnboundedSender, + rpc_client: &RpcClient, +) -> Result<()> { + let pending_extrinsics: Vec = rpc_client + .request("author_pendingExtrinsics", rpc_params![]) + .await + .unwrap_or_default(); + action_tx.send(Action::SetPendingExtrinsicsLength(pending_extrinsics.len()))?; + Ok(()) +} + +pub async fn get_connected_peers( + action_tx: &UnboundedSender, + rpc_client: &RpcClient, +) -> Result<()> { + let connected_peers: Vec = rpc_client + .request("system_peers", rpc_params![]) + .await + .unwrap_or_default(); + action_tx.send(Action::SetConnectedPeers(connected_peers))?; + Ok(()) +} + +pub async fn get_listen_addresses( + action_tx: &UnboundedSender, + rpc_client: &RpcClient, +) -> Result<()> { + let listen_addresses: Vec = rpc_client + .request("system_localListenAddresses", rpc_params![]) + .await + .unwrap_or_default(); + action_tx.send(Action::SetListenAddresses(listen_addresses))?; + Ok(()) +} + +pub async fn get_local_identity( + action_tx: &UnboundedSender, + rpc_client: &RpcClient, +) -> Result<()> { + let local_peer_id: String = rpc_client + .request("system_localPeerId", rpc_params![]) + .await + .unwrap_or_default(); + action_tx.send(Action::SetLocalIdentity(local_peer_id))?; + Ok(()) +} diff --git a/src/network/miscellaneous.rs b/src/network/miscellaneous.rs new file mode 100644 index 0000000..96ec0eb --- /dev/null +++ b/src/network/miscellaneous.rs @@ -0,0 +1,115 @@ +use subxt::ext::sp_runtime::Perbill; + +// generated outside, based on params +// MIN_INFLATION: u32 = 0_025_000; +// MAX_INFLATION: u32 = 0_100_000; +// IDEAL_STAKE: u32 = 0_750_000; +// FALLOFF: u32 = 0_050_000; +// MAX_PIECE_COUNT: u32 = 40; +const PIECEWISE_LINEAR_POUNTS: [(Perbill, Perbill); 33] = [ + (Perbill::from_parts(0), Perbill::from_parts(25000000)), + (Perbill::from_parts(750000000), Perbill::from_parts(100000000)), + (Perbill::from_parts(758333000), Perbill::from_parts(91817000)), + (Perbill::from_parts(766666000), Perbill::from_parts(84528000)), + (Perbill::from_parts(774999000), Perbill::from_parts(78033000)), + (Perbill::from_parts(783331000), Perbill::from_parts(72248000)), + (Perbill::from_parts(791663000), Perbill::from_parts(67094000)), + (Perbill::from_parts(799996000), Perbill::from_parts(62502000)), + (Perbill::from_parts(808328000), Perbill::from_parts(58411000)), + (Perbill::from_parts(816661000), Perbill::from_parts(54766000)), + (Perbill::from_parts(824993000), Perbill::from_parts(51519000)), + (Perbill::from_parts(833325000), Perbill::from_parts(48626000)), + (Perbill::from_parts(841656000), Perbill::from_parts(46049000)), + (Perbill::from_parts(849988000), Perbill::from_parts(43753000)), + (Perbill::from_parts(858321000), Perbill::from_parts(41707000)), + (Perbill::from_parts(866651000), Perbill::from_parts(39885000)), + (Perbill::from_parts(874984000), Perbill::from_parts(38261000)), + (Perbill::from_parts(883313000), Perbill::from_parts(36815000)), + (Perbill::from_parts(891646000), Perbill::from_parts(35526000)), + (Perbill::from_parts(899976000), Perbill::from_parts(34378000)), + (Perbill::from_parts(908308000), Perbill::from_parts(33355000)), + (Perbill::from_parts(916636000), Perbill::from_parts(32444000)), + (Perbill::from_parts(924968000), Perbill::from_parts(31632000)), + (Perbill::from_parts(933295000), Perbill::from_parts(30909000)), + (Perbill::from_parts(941619000), Perbill::from_parts(30265000)), + (Perbill::from_parts(949946000), Perbill::from_parts(29691000)), + (Perbill::from_parts(958265000), Perbill::from_parts(29180000)), + (Perbill::from_parts(966598000), Perbill::from_parts(28724000)), + (Perbill::from_parts(974925000), Perbill::from_parts(28318000)), + (Perbill::from_parts(983258000), Perbill::from_parts(27956000)), + (Perbill::from_parts(991578000), Perbill::from_parts(27634000)), + (Perbill::from_parts(999899000), Perbill::from_parts(27347000)), + (Perbill::from_parts(1000000000), Perbill::from_parts(27343000)), +]; +const MAXIMUM_INFLATION: Perbill = Perbill::from_parts(100000000); + +pub fn calculate_for_fraction(n: u128, d: u128) -> (Perbill, Perbill) { + let n = n.min(d.clone()); + + if PIECEWISE_LINEAR_POUNTS.is_empty() { + return (MAXIMUM_INFLATION, Perbill::zero()) + } + + let next_point_index = PIECEWISE_LINEAR_POUNTS.iter().position(|p| n < p.0 * d.clone()); + + let (prev, next) = if let Some(next_point_index) = next_point_index { + if let Some(previous_point_index) = next_point_index.checked_sub(1) { + (PIECEWISE_LINEAR_POUNTS[previous_point_index], PIECEWISE_LINEAR_POUNTS[next_point_index]) + } else { + // There is no previous points, take first point ordinate + let fraction = PIECEWISE_LINEAR_POUNTS.first().map(|p| p.1).unwrap_or_else(Perbill::zero); + return (MAXIMUM_INFLATION, fraction) + } + } else { + // There is no next points, take last point ordinate + let fraction = PIECEWISE_LINEAR_POUNTS.last().map(|p| p.1).unwrap_or_else(Perbill::zero); + return (MAXIMUM_INFLATION, fraction) + }; + + let delta_y = multiply_by_rational_saturating( + abs_sub(n.clone(), prev.0 * d.clone()), + abs_sub(next.1.deconstruct(), prev.1.deconstruct()), + // Must not saturate as prev abscissa > next abscissa + next.0.deconstruct().saturating_sub(prev.0.deconstruct()), + ); + + // If both subtractions are same sign then result is positive + let fraction = if (n > prev.0 * d.clone()) == (next.1.deconstruct() > prev.1.deconstruct()) { + (prev.1 * d).saturating_add(delta_y) + } else { + // Otherwise result is negative + (prev.1 * d).saturating_sub(delta_y) + }; + + (MAXIMUM_INFLATION, Perbill::from_rational(fraction, d)) +} + +fn abs_sub + Clone>(a: N, b: N) -> N where { + a.clone().max(b.clone()) - a.min(b) +} + +fn multiply_by_rational_saturating(value: u128, p: u32, q: u32) -> u128 { + let q = q.max(1); + let result_divisor_part = (value / q as u128).saturating_mul(p as u128); + let result_remainder_part = { + let rem = value % q as u128; + let rem_u32 = rem as u32; + let rem_part = rem_u32 as u64 * p as u64 / q as u64; + rem_part as u128 + }; + result_divisor_part.saturating_add(result_remainder_part) +} + +pub fn prepare_perbill_fraction_string(value: Perbill) -> String { + let d = value.deconstruct(); + let mut m = 10_000_000; + + let units = d / m; + let rest = d % m; + + for _ in 0..2 { + m /= 10; + } + + format!("{}.{}%", units, rest / m) +} diff --git a/src/network/mod.rs b/src/network/mod.rs index 29c8640..543be35 100644 --- a/src/network/mod.rs +++ b/src/network/mod.rs @@ -1,18 +1,20 @@ use tokio::sync::mpsc::UnboundedSender; use color_eyre::Result; use subxt::{ - backend::{ - legacy::LegacyRpcMethods, - rpc::RpcClient, - }, + backend::rpc::RpcClient, tx::{TxProgress, TxStatus}, utils::H256, OnlineClient, }; mod legacy_rpc_calls; -mod predefinded_calls; +mod predefined_calls; +mod predefined_txs; mod subscriptions; +mod miscellaneous; +mod raw_calls; + +pub use miscellaneous::{prepare_perbill_fraction_string, calculate_for_fraction}; use crate::{ types::ActionLevel, @@ -25,10 +27,10 @@ pub use subscriptions::{FinalizedSubscription, BestSubscription}; pub struct Network { action_tx: UnboundedSender, online_client_api: OnlineClient, - legacy_client_api: LegacyRpcMethods, rpc_client: RpcClient, best_hash: Option, finalized_hash: Option, + stash_to_watch: Option<[u8; 32]>, accounts_to_watch: std::collections::HashSet<[u8; 32]>, transactions_to_watch: Vec>>, } @@ -37,27 +39,42 @@ impl Network { pub fn new( action_tx: UnboundedSender, online_client_api: OnlineClient, - legacy_client_api: LegacyRpcMethods, rpc_client: RpcClient, ) -> Self { Self { action_tx, online_client_api, - legacy_client_api, rpc_client, best_hash: None, finalized_hash: None, + stash_to_watch: None, accounts_to_watch: Default::default(), transactions_to_watch: Default::default(), } } + fn store_stash_if_possible(&mut self, new_stash: [u8; 32]) { + match self.stash_to_watch { + Some(stash) if stash == new_stash => {}, + _ => self.stash_to_watch = Some(new_stash), + } + } + pub async fn handle_network_event(&mut self, io_event: Action) -> Result<()> { match io_event { Action::NewBestHash(hash) => { self.best_hash = Some(hash); + if let Some(stash_to_watch) = self.stash_to_watch { + predefined_calls::get_session_keys(&self.action_tx, &self.online_client_api, &self.rpc_client, &stash_to_watch).await?; + predefined_calls::get_queued_session_keys(&self.action_tx, &self.online_client_api, &self.rpc_client, &stash_to_watch).await?; + predefined_calls::get_nominators_by_validator(&self.action_tx, &self.online_client_api, &stash_to_watch).await?; + predefined_calls::get_validator_prefs(&self.action_tx, &self.online_client_api, &stash_to_watch).await?; + predefined_calls::get_staking_value_ratio(&self.action_tx, &self.online_client_api, &stash_to_watch).await?; + predefined_calls::get_is_stash_bonded(&self.action_tx, &self.online_client_api, &stash_to_watch).await?; + predefined_calls::get_validators_ledger(&self.action_tx, &self.online_client_api, &stash_to_watch).await?; + } for account_id in self.accounts_to_watch.iter() { - predefinded_calls::get_balance(&self.action_tx, &self.online_client_api, &account_id).await?; + predefined_calls::get_balance(&self.action_tx, &self.online_client_api, &account_id).await?; } Ok(()) }, @@ -100,26 +117,66 @@ impl Network { } Ok(()) }, - Action::GetSystemHealth => legacy_rpc_calls::get_system_health(&self.action_tx, &self.legacy_client_api).await, - Action::GetNodeName => legacy_rpc_calls::get_node_name(&self.action_tx, &self.legacy_client_api).await, - Action::GetGenesisHash => legacy_rpc_calls::get_genesis_hash(&self.action_tx, &self.legacy_client_api).await, - Action::GetChainName => legacy_rpc_calls::get_chain_name(&self.action_tx, &self.legacy_client_api).await, - Action::GetChainVersion => legacy_rpc_calls::get_system_version(&self.action_tx, &self.legacy_client_api).await, - Action::GetBlockAuthor(hash, logs) => predefinded_calls::get_block_author(&self.action_tx, &self.online_client_api, &logs, &hash).await, - Action::GetActiveEra => predefinded_calls::get_active_era(&self.action_tx, &self.online_client_api).await, - Action::GetEpochProgress => predefinded_calls::get_epoch_progress(&self.action_tx, &self.online_client_api).await, - Action::GetPendingExtrinsics => predefinded_calls::get_pending_extrinsics(&self.action_tx, &self.rpc_client).await, + Action::GetSystemHealth => legacy_rpc_calls::get_system_health(&self.action_tx, &self.rpc_client).await, + Action::GetNodeName => legacy_rpc_calls::get_node_name(&self.action_tx, &self.rpc_client).await, + Action::GetGenesisHash => legacy_rpc_calls::get_genesis_hash(&self.action_tx, &self.rpc_client).await, + Action::GetChainName => legacy_rpc_calls::get_chain_name(&self.action_tx, &self.rpc_client).await, + Action::GetChainVersion => legacy_rpc_calls::get_system_version(&self.action_tx, &self.rpc_client).await, + Action::GetPendingExtrinsics => legacy_rpc_calls::get_pending_extrinsics(&self.action_tx, &self.rpc_client).await, + Action::GetConnectedPeers => legacy_rpc_calls::get_connected_peers(&self.action_tx, &self.rpc_client).await, + Action::GetListenAddresses => legacy_rpc_calls::get_listen_addresses(&self.action_tx, &self.rpc_client).await, + Action::GetLocalIdentity => legacy_rpc_calls::get_local_identity(&self.action_tx, &self.rpc_client).await, - Action::GetExistentialDeposit => predefinded_calls::get_existential_deposit(&self.action_tx, &self.online_client_api).await, - Action::GetTotalIssuance => predefinded_calls::get_total_issuance(&self.action_tx, &self.online_client_api).await, + Action::GetBlockAuthor(hash, logs) => predefined_calls::get_block_author(&self.action_tx, &self.online_client_api, &logs, &hash).await, + Action::GetActiveEra => predefined_calls::get_active_era(&self.action_tx, &self.online_client_api).await, + Action::GetCurrentEra => predefined_calls::get_current_era(&self.action_tx, &self.online_client_api).await, + Action::GetEpochProgress => predefined_calls::get_epoch_progress(&self.action_tx, &self.online_client_api).await, + Action::GetExistentialDeposit => predefined_calls::get_existential_deposit(&self.action_tx, &self.online_client_api).await, + Action::GetTotalIssuance => predefined_calls::get_total_issuance(&self.action_tx, &self.online_client_api).await, + 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::GetValidatorLedger(stash) => { + self.store_stash_if_possible(stash); + predefined_calls::get_validators_ledger(&self.action_tx, &self.online_client_api, &stash).await + } + Action::GetIsStashBonded(stash) => { + self.store_stash_if_possible(stash); + predefined_calls::get_is_stash_bonded(&self.action_tx, &self.online_client_api, &stash).await + }, + Action::GetErasStakersOverview(stash) => { + self.store_stash_if_possible(stash); + predefined_calls::get_staking_value_ratio(&self.action_tx, &self.online_client_api, &stash).await + }, + Action::GetValidatorPrefs(stash) => { + self.store_stash_if_possible(stash); + predefined_calls::get_validator_prefs(&self.action_tx, &self.online_client_api, &stash).await + }, + Action::GetValidatorAllRewards(stash) => { + self.store_stash_if_possible(stash); + predefined_calls::get_validator_staking_results(&self.action_tx, &self.online_client_api, &stash).await + }, + Action::GetNominatorsByValidator(stash) => { + self.store_stash_if_possible(stash); + predefined_calls::get_nominators_by_validator(&self.action_tx, &self.online_client_api, &stash).await + }, + Action::GetQueuedSessionKeys(stash) => { + self.store_stash_if_possible(stash); + predefined_calls::get_queued_session_keys(&self.action_tx, &self.online_client_api, &self.rpc_client, &stash).await + }, + Action::GetSessionKeys(stash) => { + self.store_stash_if_possible(stash); + predefined_calls::get_session_keys(&self.action_tx, &self.online_client_api, &self.rpc_client, &stash).await + }, Action::BalanceRequest(account_id, remove) => { if remove { let _ = self.accounts_to_watch.remove(&account_id); Ok(()) } else { let _ = self.accounts_to_watch.insert(account_id); - predefinded_calls::get_balance(&self.action_tx, &self.online_client_api, &account_id).await + predefined_calls::get_balance(&self.action_tx, &self.online_client_api, &account_id).await } } Action::TransferBalance(sender, receiver, amount) => { @@ -129,7 +186,7 @@ impl Network { .try_into() .expect("stored seed is valid length; qed"); - if let Ok(tx_progress) = predefinded_calls::transfer_balance( + if let Ok(tx_progress) = predefined_txs::transfer_balance( &self.action_tx, &self.online_client_api, &sender, diff --git a/src/network/predefinded_calls.rs b/src/network/predefinded_calls.rs deleted file mode 100644 index 7fbfee2..0000000 --- a/src/network/predefinded_calls.rs +++ /dev/null @@ -1,221 +0,0 @@ -use tokio::sync::mpsc::UnboundedSender; -use color_eyre::Result; -use subxt::{ - backend::rpc::RpcClient, - client::OnlineClient, - config::substrate::DigestItem, - ext::sp_core::{ - crypto::{AccountId32, Ss58AddressFormat, Ss58Codec}, - Pair as PairT, - sr25519::Pair, - }, - rpc_params, - tx::{PairSigner, TxProgress}, - utils::H256, -}; - - -use crate::{ - action::Action, - casper_network::{ - self, - runtime_types::sp_consensus_slots, - }, - types::{SystemAccount, EraInfo, ActionLevel}, - CasperAccountId, CasperConfig -}; - -pub async fn get_block_author( - action_tx: &UnboundedSender, - api: &OnlineClient, - logs: &Vec, - at_hash: &H256, -) -> Result<()> { - use codec::Decode; - use crate::casper_network::runtime_types::sp_consensus_babe::digests::PreDigest; - - let storage_key = casper_network::storage().session().validators(); - let validators = api.storage().at(*at_hash).fetch(&storage_key).await?.unwrap_or_default(); - - let maybe_author = match logs.iter().find(|item| matches!(item, DigestItem::PreRuntime(..))) { - Some(DigestItem::PreRuntime(engine, data)) if *engine == [b'B', b'A', b'B', b'E'] => { - match PreDigest::decode(&mut &data[..]) { - Ok(PreDigest::Primary(primary)) => validators.get(primary.authority_index as usize), - Ok(PreDigest::SecondaryPlain(secondary)) => validators.get(secondary.authority_index as usize), - Ok(PreDigest::SecondaryVRF(secondary)) => validators.get(secondary.authority_index as usize), - _ => None, - } - }, - _ => None, - }; - - let validator = match maybe_author { - Some(author) => { - let extended_author = CasperAccountId::decode(&mut author.as_ref()) - .expect("author should be valid AccountId32; qed"); - let account_id = AccountId32::from(extended_author.0); - account_id.to_ss58check_with_version(Ss58AddressFormat::custom(1996)) - }, - None => "...".to_string(), - }; - - action_tx.send(Action::SetBlockAuthor(*at_hash, validator))?; - - Ok(()) -} - -pub async fn get_active_era( - action_tx: &UnboundedSender, - api: &OnlineClient, -) -> Result<()> { - let storage_key = casper_network::storage().staking().active_era(); - if let Some(active_era) = api.storage().at_latest().await?.fetch(&storage_key).await? { - action_tx.send(Action::SetActiveEra(EraInfo { - index: active_era.index, - start: active_era.start, - }))?; - } - Ok(()) -} - -pub async fn get_epoch_progress( - action_tx: &UnboundedSender, - api: &OnlineClient, -) -> Result<()> { - - let storage_key = casper_network::storage().babe().current_slot(); - let current_slot = api.storage() - .at_latest() - .await? - .fetch(&storage_key) - .await? - .unwrap_or(sp_consensus_slots::Slot(0u64)); - - let storage_key = casper_network::storage().babe().epoch_index(); - let epoch_index = api.storage() - .at_latest() - .await? - .fetch(&storage_key) - .await? - .unwrap_or_default(); - - let storage_key = casper_network::storage().babe().genesis_slot(); - let genesis_slot = api.storage() - .at_latest() - .await? - .fetch(&storage_key) - .await? - .unwrap_or(sp_consensus_slots::Slot(0u64)); - - let constant_query = casper_network::constants().babe().epoch_duration(); - let epoch_duration = api.constants().at(&constant_query)?; - - let epoch_start_slot = epoch_index * epoch_duration + genesis_slot.0; - let progress = current_slot.0.saturating_sub(epoch_start_slot); - - action_tx.send(Action::SetEpochProgress(epoch_index, progress))?; - Ok(()) -} - -pub async fn get_pending_extrinsics( - action_tx: &UnboundedSender, - rpc_client: &RpcClient, -) -> Result<()> { - let pending_extrinsics: Vec = rpc_client - .request("author_pendingExtrinsics", rpc_params![]) - .await?; - action_tx.send(Action::SetPendingExtrinsicsLength(pending_extrinsics.len()))?; - Ok(()) -} - -pub async fn get_total_issuance( - action_tx: &UnboundedSender, - api: &OnlineClient, -) -> Result<()> { - let storage_key = casper_network::storage().balances().total_issuance(); - let total_issuance = api.storage() - .at_latest() - .await? - .fetch(&storage_key) - .await? - .unwrap_or_default(); - action_tx.send(Action::SetTotalIssuance(total_issuance))?; - Ok(()) -} - -pub async fn get_existential_deposit( - action_tx: &UnboundedSender, - api: &OnlineClient, -) -> Result<()> { - let constant_query = casper_network::constants().balances().existential_deposit(); - let existential_deposit = api.constants().at(&constant_query)?; - action_tx.send(Action::SetExistentialDeposit(existential_deposit))?; - Ok(()) -} - -pub async fn get_balance( - action_tx: &UnboundedSender, - api: &OnlineClient, - account_id: &[u8; 32], -) -> Result<()> { - let account_id_converted = subxt::utils::AccountId32::from(*account_id); - let storage_key = casper_network::storage().system().account(account_id_converted); - - let maybe_balance = api - .storage() - .at_latest() - .await? - .fetch(&storage_key) - .await?; - - let balance = match maybe_balance { - Some(balance) => { - SystemAccount { - nonce: balance.nonce, - free: balance.data.free, - reserved: balance.data.reserved, - frozen: balance.data.frozen, - } - }, - None => SystemAccount::default(), - }; - - action_tx.send(Action::BalanceResponse(*account_id, balance))?; - Ok(()) -} - -pub async fn transfer_balance( - action_tx: &UnboundedSender, - api: &OnlineClient, - sender: &[u8; 32], - receiver: &[u8; 32], - amount: &u128, -) -> Result>> { - let receiver_id = subxt::utils::MultiAddress::Id( - subxt::utils::AccountId32::from(*receiver) - ); - - let transfer_tx = casper_network::tx() - .balances() - .transfer_allow_death(receiver_id, *amount); - - let pair = Pair::from_seed(sender); - let signer = PairSigner::::new(pair); - - match api - .tx() - .sign_and_submit_then_watch_default(&transfer_tx, &signer) - .await { - Ok(tx_progress) => { - action_tx.send(Action::WalletLog( - format!("transfer transaction {} sent", tx_progress.extrinsic_hash()), - ActionLevel::Info))?; - Ok(tx_progress) - }, - Err(err) => { - action_tx.send(Action::WalletLog( - format!("error during transfer: {err}"), ActionLevel::Error))?; - Err(err.into()) - } - } -} diff --git a/src/network/predefined_calls.rs b/src/network/predefined_calls.rs new file mode 100644 index 0000000..b41523f --- /dev/null +++ b/src/network/predefined_calls.rs @@ -0,0 +1,476 @@ +use tokio::sync::mpsc::UnboundedSender; +use color_eyre::Result; +use subxt::{ + backend::rpc::RpcClient, + client::OnlineClient, + config::substrate::DigestItem, + ext::sp_core::crypto::{ + AccountId32, Ss58AddressFormat, Ss58Codec, + }, + rpc_params, + utils::H256, +}; + +use crate::{ + action::Action, + casper_network::runtime_types::sp_consensus_slots, + types::{EraInfo, Nominator, SessionKeyInfo, SystemAccount}, + CasperAccountId, CasperConfig +}; + +pub async fn get_block_author( + action_tx: &UnboundedSender, + api: &OnlineClient, + logs: &Vec, + at_hash: &H256, +) -> Result<()> { + use codec::Decode; + use crate::casper_network::runtime_types::sp_consensus_babe::digests::PreDigest; + + let validators = super::raw_calls::session::validators(api, Some(at_hash)) + .await? + .unwrap_or_default(); + + let maybe_author = match logs.iter().find(|item| matches!(item, DigestItem::PreRuntime(..))) { + Some(DigestItem::PreRuntime(engine, data)) if *engine == [b'B', b'A', b'B', b'E'] => { + match PreDigest::decode(&mut &data[..]) { + Ok(PreDigest::Primary(primary)) => validators.get(primary.authority_index as usize), + Ok(PreDigest::SecondaryPlain(secondary)) => validators.get(secondary.authority_index as usize), + Ok(PreDigest::SecondaryVRF(secondary)) => validators.get(secondary.authority_index as usize), + _ => None, + } + }, + _ => None, + }; + + let validator = match maybe_author { + Some(author) => { + let extended_author = CasperAccountId::decode(&mut author.as_ref()) + .expect("author should be valid AccountId32; qed"); + let account_id = AccountId32::from(extended_author.0); + account_id.to_ss58check_with_version(Ss58AddressFormat::custom(1996)) + }, + None => "...".to_string(), + }; + + action_tx.send(Action::SetBlockAuthor(*at_hash, validator))?; + + Ok(()) +} + +pub async fn get_current_era( + action_tx: &UnboundedSender, + api: &OnlineClient, +) -> Result<()> { + let current_era = super::raw_calls::staking::current_era(api, None) + .await? + .unwrap_or_default(); + action_tx.send(Action::SetCurrentEra(current_era))?; + Ok(()) +} + +pub async fn get_active_era( + action_tx: &UnboundedSender, + api: &OnlineClient, +) -> Result<()> { + if let Some(active_era) = super::raw_calls::staking::active_era(api, None).await? { + action_tx.send(Action::SetActiveEra(EraInfo { + index: active_era.index, + start: active_era.start, + }))?; + } + Ok(()) +} + +pub async fn get_epoch_progress( + action_tx: &UnboundedSender, + api: &OnlineClient, +) -> Result<()> { + let current_slot = super::raw_calls::babe::current_slot(api, None) + .await? + .unwrap_or(sp_consensus_slots::Slot(0u64)); + + let epoch_index = super::raw_calls::babe::epoch_index(api, None) + .await? + .unwrap_or_default(); + + let genesis_slot = super::raw_calls::babe::genesis_slot(api, None) + .await? + .unwrap_or(sp_consensus_slots::Slot(0u64)); + + let epoch_duration = super::raw_calls::babe::epoch_duration(api)?; + + let epoch_start_slot = epoch_index * epoch_duration + genesis_slot.0; + let progress = current_slot.0.saturating_sub(epoch_start_slot); + + action_tx.send(Action::SetEpochProgress(epoch_index, progress))?; + Ok(()) +} + +pub async fn get_total_issuance( + action_tx: &UnboundedSender, + api: &OnlineClient, +) -> Result<()> { + let total_issuance = super::raw_calls::balances::total_issuance(api, None) + .await? + .unwrap_or_default(); + action_tx.send(Action::SetTotalIssuance(total_issuance))?; + Ok(()) +} + +pub async fn get_existential_deposit( + action_tx: &UnboundedSender, + api: &OnlineClient, +) -> Result<()> { + let existential_deposit = super::raw_calls::balances::existential_deposit(api)?; + action_tx.send(Action::SetExistentialDeposit(existential_deposit))?; + Ok(()) +} + +pub async fn get_balance( + action_tx: &UnboundedSender, + api: &OnlineClient, + account_id: &[u8; 32], +) -> Result<()> { + let maybe_balance = super::raw_calls::system::balance(api, None, account_id) + .await?; + + let balance = match maybe_balance { + Some(balance) => { + SystemAccount { + nonce: balance.nonce, + free: balance.data.free, + reserved: balance.data.reserved, + frozen: balance.data.frozen, + } + }, + None => SystemAccount::default(), + }; + + action_tx.send(Action::BalanceResponse(*account_id, balance))?; + Ok(()) +} + +pub async fn get_validators_number( + action_tx: &UnboundedSender, + api: &OnlineClient, +) -> Result<()> { + let counter_for_validators = super::raw_calls::staking::counter_for_validators(api, None) + .await? + .unwrap_or_default(); + action_tx.send(Action::ValidatorsNumber(counter_for_validators))?; + Ok(()) +} + +pub async fn get_nominators_number( + action_tx: &UnboundedSender, + api: &OnlineClient, +) -> Result<()> { + let counter_for_nominators = super::raw_calls::staking::counter_for_nominators(api, None) + .await? + .unwrap_or_default(); + action_tx.send(Action::NominatorsNumber(counter_for_nominators))?; + Ok(()) +} + +pub async fn get_inflation( + action_tx: &UnboundedSender, + api: &OnlineClient, +) -> Result<()> { + let total_issuance = super::raw_calls::balances::total_issuance(api, None) + .await? + .unwrap_or_default(); + + let active_era_index = super::raw_calls::staking::active_era(api, None) + .await? + .map(|era_info| era_info.index) + .unwrap_or_default(); + + let total_staked = super::raw_calls::staking::eras_total_stake(api, None, active_era_index) + .await? + .unwrap_or_default(); + + let (inflation, fraction) = super::calculate_for_fraction(total_staked, total_issuance); + let inflation_str = super::prepare_perbill_fraction_string(inflation); + let fraction_str = super::prepare_perbill_fraction_string(fraction); + + action_tx.send(Action::Inflation(inflation_str))?; + action_tx.send(Action::Apy(fraction_str))?; + Ok(()) +} + +pub async fn get_session_keys( + action_tx: &UnboundedSender, + api: &OnlineClient, + rpc_client: &RpcClient, + account_id: &[u8; 32], +) -> Result<()> { + let maybe_session_keys = super::raw_calls::session::next_keys(api, None, account_id).await?; + let (gran_key, babe_key, audi_key, slow_key) = match maybe_session_keys { + Some(session_keys) => { + let gran_key = format!("0x{}", hex::encode(session_keys.grandpa.0)); + let babe_key = format!("0x{}", hex::encode(session_keys.babe.0)); + let audi_key = format!("0x{}", hex::encode(session_keys.authority_discovery.0)); + let slow_key = format!("0x{}", hex::encode(session_keys.slow_clap.0)); + + (gran_key, babe_key, audi_key, slow_key) + }, + None => (String::new(), String::new(), String::new(), String::new()), + }; + + check_author_has_key(rpc_client, action_tx, &gran_key, "gran").await?; + check_author_has_key(rpc_client, action_tx, &babe_key, "babe").await?; + check_author_has_key(rpc_client, action_tx, &audi_key, "audi").await?; + check_author_has_key(rpc_client, action_tx, &slow_key, "slow").await?; + + Ok(()) +} + +pub async fn get_queued_session_keys( + action_tx: &UnboundedSender, + api: &OnlineClient, + rpc_client: &RpcClient, + account_id: &[u8; 32], +) -> Result<()> { + let account = super::raw_calls::convert_array_to_account_id(account_id); + let maybe_queued_keys = super::raw_calls::session::queued_keys(api, None).await?; + + let (gran_key, babe_key, audi_key, slow_key) = match maybe_queued_keys { + Some(session_keys) => { + match session_keys.iter().find(|tuple| tuple.0 == account) { + Some(keys) => { + let session_keys = &keys.1; + let gran_key = format!("0x{}", hex::encode(session_keys.grandpa.0)); + let babe_key = format!("0x{}", hex::encode(session_keys.babe.0)); + let audi_key = format!("0x{}", hex::encode(session_keys.authority_discovery.0)); + let slow_key = format!("0x{}", hex::encode(session_keys.slow_clap.0)); + + (gran_key, babe_key, audi_key, slow_key) + }, + None => (String::new(), String::new(), String::new(), String::new()), + } + }, + None => (String::new(), String::new(), String::new(), String::new()), + }; + + check_author_has_key(rpc_client, action_tx, &gran_key, "q_gran").await?; + check_author_has_key(rpc_client, action_tx, &babe_key, "q_babe").await?; + check_author_has_key(rpc_client, action_tx, &audi_key, "q_audi").await?; + check_author_has_key(rpc_client, action_tx, &slow_key, "q_slow").await?; + + Ok(()) +} + +async fn check_author_has_key( + rpc_client: &RpcClient, + action_tx: &UnboundedSender, + key: &str, + name: &str, +) -> Result<()> { + let params_name = if name.starts_with("q_") { + &name[2..] + } else { + name + }; + let is_stored: bool = rpc_client + .request("author_hasKey", rpc_params![key, params_name]) + .await?; + let session_key_info = SessionKeyInfo { + key: key.to_string(), + is_stored + }; + action_tx.send(Action::SetSessionKey(name.to_string(), session_key_info))?; + Ok(()) +} + +pub async fn get_validator_staking_results( + action_tx: &UnboundedSender, + api: &OnlineClient, + account_id: &[u8; 32], +) -> Result<()> { + let (start, end) = super::raw_calls::historical::stored_range(api, None) + .await? + .map(|range| (range.0 / 6, range.1 / 6)) + .unwrap_or((0, 0)); + for era_index in start..end.saturating_sub(2) { + get_validator_staking_result(action_tx, api, account_id, era_index).await?; + } + Ok(()) +} + +pub async fn get_validator_staking_result( + action_tx: &UnboundedSender, + api: &OnlineClient, + account_id: &[u8; 32], + era_index: u32, +) -> Result<()> { + get_validator_reward_in_era(action_tx, api, account_id, era_index).await?; + get_validator_claims_in_era(action_tx, api, account_id, era_index).await?; + get_validator_slashes_in_era(action_tx, api, account_id, era_index).await?; + Ok(()) +} + +async fn get_validator_reward_in_era( + action_tx: &UnboundedSender, + api: &OnlineClient, + account_id: &[u8; 32], + era_index: u32, +) -> Result<()> { + let maybe_era_reward_points = super::raw_calls::staking::eras_reward_points(api, None, era_index) + .await?; + + let era_reward = super::raw_calls::staking::eras_validator_reward(api, None, era_index) + .await? + .unwrap_or_default(); + + let my_reward = match maybe_era_reward_points { + Some(era_reward_points) => { + let my_points = era_reward_points.individual + .iter() + .find(|(acc, _)| acc.0 == *account_id) + .map(|info| info.1) + .unwrap_or_default(); + era_reward + .saturating_mul(my_points as u128) + .saturating_div(era_reward_points.total as u128) + }, + None => 0u128, + }; + + action_tx.send(Action::SetValidatorEraReward(era_index, my_reward))?; + + Ok(()) +} + +async fn get_validator_claims_in_era( + action_tx: &UnboundedSender, + api: &OnlineClient, + account_id: &[u8; 32], + era_index: u32, +) -> Result<()> { + let maybe_claimed_rewards = super::raw_calls::staking::claimed_rewards(api, None, era_index, account_id) + .await?; + + if let Some(claimed_rewards) = maybe_claimed_rewards { + let already_claimed = claimed_rewards + .first() + .map(|x| *x == 1) + .unwrap_or(false); + action_tx.send(Action::SetValidatorEraClaimed(era_index, already_claimed))?; + } + + Ok(()) +} + +async fn get_validator_slashes_in_era( + action_tx: &UnboundedSender, + api: &OnlineClient, + account_id: &[u8; 32], + era_index: u32, +) -> Result<()> { + let maybe_slash_in_era = super::raw_calls::staking::validator_slash_in_era(api, None, era_index, account_id) + .await?; + + if let Some(slash_in_era) = maybe_slash_in_era { + action_tx.send(Action::SetValidatorEraSlash(era_index, slash_in_era.1))?; + } + + Ok(()) +} + +pub async fn get_validators_ledger( + action_tx: &UnboundedSender, + api: &OnlineClient, + account_id: &[u8; 32], +) -> Result<()> { + let maybe_ledger = super::raw_calls::staking::ledger(api, None, account_id) + .await?; + + if let Some(ledger) = maybe_ledger { + action_tx.send(Action::SetStakedAmountRatio(ledger.total, ledger.active))?; + for chunk in ledger.unlocking.0.iter() { + action_tx.send(Action::SetValidatorEraUnlocking(chunk.era, chunk.value))?; + } + } + + Ok(()) +} + +pub async fn get_nominators_by_validator( + action_tx: &UnboundedSender, + api: &OnlineClient, + account_id: &[u8; 32], +) -> Result<()> { + let active_era_index = super::raw_calls::staking::active_era(api, None) + .await? + .map(|era_info| era_info.index) + .unwrap_or_default(); + + let maybe_eras_stakers = super::raw_calls::staking::eras_stakers(api, None, active_era_index, account_id) + .await?; + + let nominators = match maybe_eras_stakers { + Some(eras_stakers) => eras_stakers + .others + .iter() + .map(|info| { + Nominator { + who: AccountId32::from(info.who.0) + .to_ss58check_with_version(Ss58AddressFormat::custom(1996)), + value: info.value, + } + }) + .collect::>(), + None => Vec::new(), + }; + + action_tx.send(Action::SetNominatorsByValidator(nominators))?; + Ok(()) +} + +pub async fn get_is_stash_bonded( + action_tx: &UnboundedSender, + api: &OnlineClient, + account_id: &[u8; 32], +) -> Result<()> { + let is_bonded = super::raw_calls::staking::bonded(api, None, account_id) + .await? + .is_some(); + action_tx.send(Action::SetBondedAmount(is_bonded))?; + Ok(()) +} + +pub async fn get_staking_value_ratio( + action_tx: &UnboundedSender, + api: &OnlineClient, + account_id: &[u8; 32], +) -> Result<()> { + let active_era_index = super::raw_calls::staking::active_era(api, None) + .await? + .map(|era_info| era_info.index) + .unwrap_or_default(); + let maybe_era_stakers_overview = super::raw_calls::staking::eras_stakers_overview(api, None, active_era_index, account_id) + .await?; + let (total, own) = match maybe_era_stakers_overview { + Some(overview) => (overview.total, overview.own), + None => (0, 0), + }; + action_tx.send(Action::SetStakedRatio(total, own))?; + Ok(()) +} + +pub async fn get_validator_prefs( + action_tx: &UnboundedSender, + api: &OnlineClient, + account_id: &[u8; 32], +) -> Result<()> { + let maybe_validator_prefs = super::raw_calls::staking::validators(api, None, account_id) + .await?; + let (comission, blocked) = match maybe_validator_prefs { + Some(prefs) => (prefs.commission.0, prefs.blocked), + None => (0, false), + }; + + action_tx.send(Action::SetValidatorPrefs(comission, blocked))?; + Ok(()) +} diff --git a/src/network/predefined_txs.rs b/src/network/predefined_txs.rs new file mode 100644 index 0000000..18187ca --- /dev/null +++ b/src/network/predefined_txs.rs @@ -0,0 +1,45 @@ +use color_eyre::Result; +use subxt::{ + ext::sp_core::{Pair as PairT, sr25519::Pair}, + tx::{PairSigner, TxProgress}, + OnlineClient, +}; +use tokio::sync::mpsc::UnboundedSender; + +use crate::{action::Action, casper::CasperConfig, casper_network, types::ActionLevel}; + +pub async fn transfer_balance( + action_tx: &UnboundedSender, + api: &OnlineClient, + sender: &[u8; 32], + receiver: &[u8; 32], + amount: &u128, +) -> Result>> { + let receiver_id = subxt::utils::MultiAddress::Id( + subxt::utils::AccountId32::from(*receiver) + ); + + let transfer_tx = casper_network::tx() + .balances() + .transfer_allow_death(receiver_id, *amount); + + let pair = Pair::from_seed(sender); + let signer = PairSigner::::new(pair); + + match api + .tx() + .sign_and_submit_then_watch_default(&transfer_tx, &signer) + .await { + Ok(tx_progress) => { + action_tx.send(Action::WalletLog( + format!("transfer transaction {} sent", tx_progress.extrinsic_hash()), + ActionLevel::Info))?; + Ok(tx_progress) + }, + Err(err) => { + action_tx.send(Action::WalletLog( + format!("error during transfer: {err}"), ActionLevel::Error))?; + Err(err.into()) + } + } +} diff --git a/src/network/raw_calls/babe.rs b/src/network/raw_calls/babe.rs new file mode 100644 index 0000000..15bd9c0 --- /dev/null +++ b/src/network/raw_calls/babe.rs @@ -0,0 +1,42 @@ +use color_eyre::Result; +use subxt::{ + utils::H256, + client::OnlineClient, +}; + +use crate::{casper_network::{self, runtime_types::sp_consensus_slots}, CasperConfig}; + +pub async fn current_slot( + online_client: &OnlineClient, + at_hash: Option<&H256>, +) -> Result> { + let storage_key = casper_network::storage().babe().current_slot(); + let maybe_current_slot = super::do_storage_call(online_client, &storage_key, at_hash).await?; + Ok(maybe_current_slot) +} + +pub async fn epoch_index( + online_client: &OnlineClient, + at_hash: Option<&H256>, +) -> Result> { + let storage_key = casper_network::storage().babe().epoch_index(); + let maybe_epoch_index = super::do_storage_call(online_client, &storage_key, at_hash).await?; + Ok(maybe_epoch_index) +} + +pub async fn genesis_slot( + online_client: &OnlineClient, + at_hash: Option<&H256>, +) -> Result> { + let storage_key = casper_network::storage().babe().genesis_slot(); + let maybe_genesis_slot = super::do_storage_call(online_client, &storage_key, at_hash).await?; + Ok(maybe_genesis_slot) +} + +pub fn epoch_duration( + online_client: &OnlineClient, +) -> Result { + let constant_query = casper_network::constants().babe().epoch_duration(); + let epoch_duration = super::do_constant_call(online_client, &constant_query)?; + Ok(epoch_duration) +} diff --git a/src/network/raw_calls/balances.rs b/src/network/raw_calls/balances.rs new file mode 100644 index 0000000..c865293 --- /dev/null +++ b/src/network/raw_calls/balances.rs @@ -0,0 +1,24 @@ +use color_eyre::Result; +use subxt::{ + utils::H256, + client::OnlineClient, +}; + +use crate::{CasperConfig, casper_network}; + +pub async fn total_issuance( + online_client: &OnlineClient, + at_hash: Option<&H256>, +) -> Result> { + let storage_key = casper_network::storage().balances().total_issuance(); + let maybe_total_issuance = super::do_storage_call(online_client, &storage_key, at_hash).await?; + Ok(maybe_total_issuance) +} + +pub fn existential_deposit( + online_client: &OnlineClient, +) -> Result { + let constant_query = casper_network::constants().balances().existential_deposit(); + let existential_deposit = super::do_constant_call(online_client, &constant_query)?; + Ok(existential_deposit) +} diff --git a/src/network/raw_calls/historical.rs b/src/network/raw_calls/historical.rs new file mode 100644 index 0000000..85c32a9 --- /dev/null +++ b/src/network/raw_calls/historical.rs @@ -0,0 +1,19 @@ +use color_eyre::Result; +use subxt::{ + utils::H256, + client::OnlineClient, +}; + +use crate::{ + casper_network, + CasperConfig, +}; + +pub async fn stored_range( + online_client: &OnlineClient, + at_hash: Option<&H256>, +) -> Result> { + let storage_key = casper_network::storage().historical().stored_range(); + let maybe_stored_range = super::do_storage_call(online_client, &storage_key, at_hash).await?; + Ok(maybe_stored_range) +} diff --git a/src/network/raw_calls/mod.rs b/src/network/raw_calls/mod.rs new file mode 100644 index 0000000..19fc0cf --- /dev/null +++ b/src/network/raw_calls/mod.rs @@ -0,0 +1,54 @@ +use color_eyre::Result; +use subxt::{ + backend::BlockRef, + utils::{Yes, H256, AccountId32}, + client::OnlineClient, +}; + +use crate::CasperConfig; + +pub mod session; +pub mod staking; +pub mod system; +pub mod babe; +pub mod balances; +pub mod historical; + +pub async fn do_storage_call<'address, Addr>( + online_client: &OnlineClient, + storage_key: &'address Addr, + maybe_at_hash: Option<&H256>, +) -> Result, subxt::Error> +where + Addr: subxt::storage::Address + 'address, +{ + let at_hash = match maybe_at_hash { + Some(at_hash) => BlockRef::from_hash(*at_hash), + None => online_client + .backend() + .latest_finalized_block_ref() + .await?, + }; + + online_client + .storage() + .at(at_hash) + .fetch(storage_key) + .await +} + +pub fn do_constant_call<'address, Addr>( + online_client: &OnlineClient, + constant_query: &'address Addr, +) -> Result +where + Addr: subxt::constants::Address + 'address +{ + let constant_client = online_client.constants(); + constant_client.validate(constant_query).expect("constant query should be correct; qed"); + constant_client.at(constant_query) +} + +pub fn convert_array_to_account_id(who: &[u8; 32]) -> AccountId32 { + AccountId32::from(*who) +} diff --git a/src/network/raw_calls/session.rs b/src/network/raw_calls/session.rs new file mode 100644 index 0000000..1dd835b --- /dev/null +++ b/src/network/raw_calls/session.rs @@ -0,0 +1,36 @@ +use color_eyre::Result; +use subxt::{ + utils::{AccountId32, H256}, + client::OnlineClient, +}; + +use crate::{casper_network::{self, runtime_types::casper_runtime::opaque}, CasperConfig}; + +pub async fn validators( + online_client: &OnlineClient, + at_hash: Option<&H256>, +) -> Result>> { + let storage_key = casper_network::storage().session().validators(); + let maybe_validators = super::do_storage_call(online_client, &storage_key, at_hash).await?; + Ok(maybe_validators) +} + +pub async fn next_keys( + online_client: &OnlineClient, + at_hash: Option<&H256>, + account: &[u8; 32], +) -> Result> { + let account_id = super::convert_array_to_account_id(account); + let storage_key = casper_network::storage().session().next_keys(account_id); + let maybe_next_keys = super::do_storage_call(online_client, &storage_key, at_hash).await?; + Ok(maybe_next_keys) +} + +pub async fn queued_keys( + online_client: &OnlineClient, + at_hash: Option<&H256>, +) -> Result>> { + let storage_key = casper_network::storage().session().queued_keys(); + let maybe_queued_keys = super::do_storage_call(online_client, &storage_key, at_hash).await?; + Ok(maybe_queued_keys) +} diff --git a/src/network/raw_calls/staking.rs b/src/network/raw_calls/staking.rs new file mode 100644 index 0000000..547f669 --- /dev/null +++ b/src/network/raw_calls/staking.rs @@ -0,0 +1,164 @@ +use color_eyre::Result; +use subxt::{ + client::OnlineClient, + utils::{AccountId32, H256}, +}; + +use crate::{ + casper_network::{ + self, + runtime_types::{ + pallet_staking::{ActiveEraInfo, EraRewardPoints, StakingLedger, ValidatorPrefs}, + sp_arithmetic::per_things::Perbill, + sp_staking::{Exposure, PagedExposureMetadata}, + }, + }, + CasperConfig, +}; + +pub async fn current_era( + online_client: &OnlineClient, + at_hash: Option<&H256>, +) -> Result> { + let storage_key = casper_network::storage().staking().current_era(); + let maybe_current_era = super::do_storage_call(online_client, &storage_key, at_hash).await?; + Ok(maybe_current_era) +} + +pub async fn active_era( + online_client: &OnlineClient, + at_hash: Option<&H256>, +) -> Result> { + let storage_key = casper_network::storage().staking().active_era(); + let maybe_active_era = super::do_storage_call(online_client, &storage_key, at_hash).await?; + Ok(maybe_active_era) +} + +pub async fn counter_for_validators( + online_client: &OnlineClient, + at_hash: Option<&H256>, +) -> Result> { + let storage_key = casper_network::storage().staking().counter_for_validators(); + let maybe_counter_for_validators = super::do_storage_call(online_client, &storage_key, at_hash).await?; + Ok(maybe_counter_for_validators) +} + +pub async fn counter_for_nominators( + online_client: &OnlineClient, + at_hash: Option<&H256>, +) -> Result> { + let storage_key = casper_network::storage().staking().counter_for_nominators(); + let maybe_counter_for_nominators = super::do_storage_call(online_client, &storage_key, at_hash).await?; + Ok(maybe_counter_for_nominators) +} + +pub async fn eras_total_stake( + online_client: &OnlineClient, + at_hash: Option<&H256>, + era_index: u32, +) -> Result> { + let storage_key = casper_network::storage().staking().eras_total_stake(era_index); + let maybe_eras_total_stake = super::do_storage_call(online_client, &storage_key, at_hash).await?; + Ok(maybe_eras_total_stake) +} + +pub async fn eras_validator_reward( + online_client: &OnlineClient, + at_hash: Option<&H256>, + era_index: u32, +) -> Result> { + let storage_key = casper_network::storage().staking().eras_validator_reward(era_index); + let maybe_eras_validator_reward = super::do_storage_call(online_client, &storage_key, at_hash).await?; + Ok(maybe_eras_validator_reward) +} + +pub async fn eras_reward_points( + online_client: &OnlineClient, + at_hash: Option<&H256>, + era_index: u32, +) -> Result>> { + let storage_key = casper_network::storage().staking().eras_reward_points(era_index); + let maybe_eras_reward_points = super::do_storage_call(online_client, &storage_key, at_hash).await?; + Ok(maybe_eras_reward_points) +} + +pub async fn claimed_rewards( + online_client: &OnlineClient, + at_hash: Option<&H256>, + era_index: u32, + account: &[u8; 32], +) -> Result>> { + let account_id = super::convert_array_to_account_id(account); + let storage_key = casper_network::storage().staking().claimed_rewards(era_index, account_id); + let maybe_claimed_rewards = super::do_storage_call(online_client, &storage_key, at_hash).await?; + Ok(maybe_claimed_rewards) +} + +pub async fn validator_slash_in_era( + online_client: &OnlineClient, + at_hash: Option<&H256>, + era_index: u32, + account: &[u8; 32], +) -> Result> { + let account_id = super::convert_array_to_account_id(account); + let storage_key = casper_network::storage().staking().validator_slash_in_era(era_index, account_id); + let maybe_validator_slash_in_era = super::do_storage_call(online_client, &storage_key, at_hash).await?; + Ok(maybe_validator_slash_in_era) +} + +pub async fn ledger( + online_client: &OnlineClient, + at_hash: Option<&H256>, + account: &[u8; 32], +) -> Result> { + let account_id = super::convert_array_to_account_id(account); + let storage_key = casper_network::storage().staking().ledger(account_id); + let maybe_ledger = super::do_storage_call(online_client, &storage_key, at_hash).await?; + Ok(maybe_ledger) +} + +pub async fn eras_stakers( + online_client: &OnlineClient, + at_hash: Option<&H256>, + era_index: u32, + account: &[u8; 32], +) -> Result>> { + let account_id = super::convert_array_to_account_id(account); + let storage_key = casper_network::storage().staking().eras_stakers(era_index, account_id); + let maybe_eras_stakers = super::do_storage_call(online_client, &storage_key, at_hash).await?; + Ok(maybe_eras_stakers) +} + +pub async fn bonded( + online_client: &OnlineClient, + at_hash: Option<&H256>, + account: &[u8; 32], +) -> Result> { + let account_id = super::convert_array_to_account_id(account); + let storage_key = casper_network::storage().staking().bonded(account_id); + let maybe_bonded = super::do_storage_call(online_client, &storage_key, at_hash).await?; + Ok(maybe_bonded) +} + +pub async fn eras_stakers_overview( + online_client: &OnlineClient, + at_hash: Option<&H256>, + era_index: u32, + account: &[u8; 32], +) -> Result>> { + let account_id = super::convert_array_to_account_id(account); + let storage_key = casper_network::storage().staking().eras_stakers_overview(era_index, account_id); + let maybe_eras_stakers_overview = super::do_storage_call(online_client, &storage_key, at_hash).await?; + Ok(maybe_eras_stakers_overview) +} + +pub async fn validators( + online_client: &OnlineClient, + at_hash: Option<&H256>, + account: &[u8; 32], +) -> Result> { + let account_id = super::convert_array_to_account_id(account); + let storage_key = casper_network::storage().staking().validators(account_id); + let maybe_validators = super::do_storage_call(online_client, &storage_key, at_hash).await?; + Ok(maybe_validators) +} diff --git a/src/network/raw_calls/system.rs b/src/network/raw_calls/system.rs new file mode 100644 index 0000000..7b7c270 --- /dev/null +++ b/src/network/raw_calls/system.rs @@ -0,0 +1,24 @@ +use color_eyre::Result; +use subxt::{ + utils::H256, + client::OnlineClient, +}; + +use crate::{ + casper_network::{ + self, + runtime_types::{frame_system::AccountInfo, pallet_balances::types::AccountData}, + }, + CasperConfig, +}; + +pub async fn balance( + online_client: &OnlineClient, + at_hash: Option<&H256>, + account: &[u8; 32], +) -> Result>>> { + let account_id = super::convert_array_to_account_id(account); + let storage_key = casper_network::storage().system().account(account_id); + let maybe_balance = super::do_storage_call(online_client, &storage_key, at_hash).await?; + Ok(maybe_balance) +} diff --git a/src/network/raw_rpc.rs b/src/network/raw_rpc.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/network/subscriptions.rs b/src/network/subscriptions.rs index 4bd323d..b48a5bd 100644 --- a/src/network/subscriptions.rs +++ b/src/network/subscriptions.rs @@ -112,8 +112,12 @@ impl BestSubscription { self.network_tx.send(Action::NewBestHash(block_hash))?; self.network_tx.send(Action::GetBlockAuthor(block_hash, block.header().digest.logs.clone()))?; self.network_tx.send(Action::GetActiveEra)?; + self.network_tx.send(Action::GetCurrentEra)?; self.network_tx.send(Action::GetEpochProgress)?; self.network_tx.send(Action::GetTotalIssuance)?; + self.network_tx.send(Action::GetValidatorsNumber)?; + self.network_tx.send(Action::GetNominatorsNumber)?; + self.network_tx.send(Action::GetInflation)?; } Ok(()) } diff --git a/src/types/mod.rs b/src/types/mod.rs index 3d562d9..021b2ea 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -2,8 +2,14 @@ mod era; mod extrinsics; mod log; mod account; +mod peer; +mod session; +mod nominator; pub use extrinsics::CasperExtrinsicDetails; pub use era::EraInfo; pub use log::ActionLevel; pub use account::SystemAccount; +pub use peer::PeerInformation; +pub use session::SessionKeyInfo; +pub use nominator::Nominator; diff --git a/src/types/nominator.rs b/src/types/nominator.rs new file mode 100644 index 0000000..91fb9e5 --- /dev/null +++ b/src/types/nominator.rs @@ -0,0 +1,8 @@ +use codec::Decode; +use serde::{Serialize, Deserialize}; + +#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, Decode)] +pub struct Nominator { + pub who: String, + pub value: u128, +} diff --git a/src/types/peer.rs b/src/types/peer.rs new file mode 100644 index 0000000..0145f7f --- /dev/null +++ b/src/types/peer.rs @@ -0,0 +1,12 @@ +use subxt::utils::H256; +use codec::Decode; +use serde::{Serialize, Deserialize}; + +#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, Decode)] +#[serde(rename_all = "camelCase")] +pub struct PeerInformation { + pub peer_id: String, + pub roles: String, + pub best_hash: H256, + pub best_number: u32, +} diff --git a/src/types/session.rs b/src/types/session.rs new file mode 100644 index 0000000..1b63f55 --- /dev/null +++ b/src/types/session.rs @@ -0,0 +1,8 @@ +use codec::Decode; +use serde::{Serialize, Deserialize}; + +#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, Decode)] +pub struct SessionKeyInfo { + pub key: String, + pub is_stored: bool, +} diff --git a/src/widgets/vertical_block.rs b/src/widgets/vertical_block.rs index 61034ad..f7576c9 100644 --- a/src/widgets/vertical_block.rs +++ b/src/widgets/vertical_block.rs @@ -14,6 +14,8 @@ impl Default for VerticalBlocks { impl ToString for VerticalBlocks { fn to_string(&self) -> String { + // TODO: how it can be equal to len()?? + // do we really need super::CYCLE in denominator? self.elements[((chrono::Utc::now().timestamp_millis() % super::CYCLE) / (super::CYCLE / self.elements.len() as i64)) as usize] .to_owned()