sketch for the validators page

Signed-off-by: Uncle Stinky <uncle.stinky@ghostchain.io>
This commit is contained in:
Uncle Stinky 2025-01-19 18:04:02 +03:00
parent 85e44f0bb8
commit 2e0205a581
Signed by: st1nky
GPG Key ID: 016064BD97603B40
40 changed files with 3519 additions and 291 deletions

View File

@ -29,7 +29,18 @@
"highlight_style": "yellow bold", "highlight_style": "yellow bold",
"popup_style": "blue", "popup_style": "blue",
"popup_title_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": { "keybindings": {
"Menu": { "Menu": {
@ -49,6 +60,12 @@
"<Ctrl-c>": "Quit", "<Ctrl-c>": "Quit",
"<Ctrl-z>": "Suspend", "<Ctrl-z>": "Suspend",
}, },
"Validator": {
"<q>": "Quit",
"<Ctrl-d>": "Quit",
"<Ctrl-c>": "Quit",
"<Ctrl-z>": "Suspend",
},
"Empty": { "Empty": {
"<q>": "Quit", "<q>": "Quit",
"<Ctrl-d>": "Quit", "<Ctrl-d>": "Quit",

View File

@ -4,8 +4,8 @@ use strum::Display;
use subxt::utils::H256; use subxt::utils::H256;
use subxt::config::substrate::DigestItem; use subxt::config::substrate::DigestItem;
use crate::{ use crate::types::{
types::{SystemAccount, ActionLevel, EraInfo, CasperExtrinsicDetails}, ActionLevel, CasperExtrinsicDetails, EraInfo, Nominator, PeerInformation, SessionKeyInfo, SystemAccount
}; };
#[derive(Debug, Clone, PartialEq, Eq, Display, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Display, Serialize, Deserialize)]
@ -42,6 +42,7 @@ pub enum Action {
TransferBalance(String, [u8; 32], u128), TransferBalance(String, [u8; 32], u128),
WalletLog(String, ActionLevel), WalletLog(String, ActionLevel),
ValidatorLog(String, ActionLevel),
NewBestBlock(u32), NewBestBlock(u32),
NewBestHash(H256), NewBestHash(H256),
@ -49,6 +50,10 @@ pub enum Action {
NewFinalizedHash(H256), NewFinalizedHash(H256),
BestBlockUpdated(u32), BestBlockUpdated(u32),
ExtrinsicsLength(u32, usize), ExtrinsicsLength(u32, usize),
ValidatorsNumber(u32),
NominatorsNumber(u32),
Inflation(String),
Apy(String),
GetBlockAuthor(H256, Vec<DigestItem>), GetBlockAuthor(H256, Vec<DigestItem>),
SetBlockAuthor(H256, String), SetBlockAuthor(H256, String),
@ -59,25 +64,54 @@ pub enum Action {
GetChainName, GetChainName,
GetChainVersion, GetChainVersion,
GetPendingExtrinsics, GetPendingExtrinsics,
GetConnectedPeers,
GetSessionKeys([u8; 32]),
GetQueuedSessionKeys([u8; 32]),
GetListenAddresses,
GetLocalIdentity,
GetLatestBlock, GetLatestBlock,
GetFinalizedBlock, GetFinalizedBlock,
GetActiveEra, GetActiveEra,
GetCurrentEra,
GetEpochProgress, 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<String>), SetNodeName(Option<String>),
SetSystemHealth(Option<usize>, bool, bool), SetSystemHealth(Option<usize>, bool, bool),
SetGenesisHash(Option<H256>), SetGenesisHash(Option<H256>),
SetChainName(Option<String>), SetChainName(Option<String>),
SetChainVersion(Option<String>), SetChainVersion(Option<String>),
SetStashAccount([u8; 32]),
BestBlockInformation(H256, u32), BestBlockInformation(H256, u32),
FinalizedBlockInformation(H256, u32), FinalizedBlockInformation(H256, u32),
ExtrinsicsForBlock(u32, Vec<CasperExtrinsicDetails>), ExtrinsicsForBlock(u32, Vec<CasperExtrinsicDetails>),
SetActiveEra(EraInfo), SetActiveEra(EraInfo),
SetCurrentEra(u32),
SetEpochProgress(u64, u64), SetEpochProgress(u64, u64),
SetPendingExtrinsicsLength(usize), SetPendingExtrinsicsLength(usize),
SetConnectedPeers(Vec<PeerInformation>),
SetSessionKey(String, SessionKeyInfo),
SetListenAddresses(Vec<String>),
SetLocalIdentity(String),
SetNominatorsByValidator(Vec<Nominator>),
SetValidatorEraReward(u32, u128),
SetValidatorEraClaimed(u32, bool),
SetValidatorEraSlash(u32, u128),
SetValidatorEraUnlocking(u32, u128),
SetBondedAmount(bool),
SetStakedAmountRatio(u128, u128),
SetStakedRatio(u128, u128),
SetValidatorPrefs(u32, bool),
GetTotalIssuance, GetTotalIssuance,
GetExistentialDeposit, GetExistentialDeposit,

View File

@ -12,7 +12,8 @@ use crate::{
tui::{Event, Tui}, tui::{Event, Tui},
components::{ components::{
menu::Menu, version::Version, explorer::Explorer, wallet::Wallet, 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, Menu,
Explorer, Explorer,
Wallet, Wallet,
WalletActive, Validator,
ExplorerActive,
Empty, Empty,
EmptyActive,
} }
impl Default for Mode { impl Default for Mode {
@ -71,6 +70,7 @@ impl App {
Box::new(Version::default()), Box::new(Version::default()),
Box::new(Explorer::default()), Box::new(Explorer::default()),
Box::new(Wallet::default()), Box::new(Wallet::default()),
Box::new(Validator::default()),
Box::new(Empty::default()), Box::new(Empty::default()),
], ],
should_quite: false, should_quite: false,
@ -161,6 +161,7 @@ impl App {
fn trigger_node_fast_events(&mut self) -> Result<()> { fn trigger_node_fast_events(&mut self) -> Result<()> {
self.network_tx.send(Action::GetPendingExtrinsics)?; self.network_tx.send(Action::GetPendingExtrinsics)?;
self.network_tx.send(Action::GetConnectedPeers)?;
Ok(()) Ok(())
} }
@ -171,6 +172,8 @@ impl App {
self.network_tx.send(Action::GetChainName)?; self.network_tx.send(Action::GetChainName)?;
self.network_tx.send(Action::GetChainVersion)?; self.network_tx.send(Action::GetChainVersion)?;
self.network_tx.send(Action::GetExistentialDeposit)?; self.network_tx.send(Action::GetExistentialDeposit)?;
self.network_tx.send(Action::GetLocalIdentity)?;
self.network_tx.send(Action::GetListenAddresses)?;
Ok(()) 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 Some(component) = self.components.last_mut() {
if let Err(err) = component.draw(frame, frame.area()) { if let Err(err) = component.draw(frame, frame.area()) {

View File

@ -20,6 +20,8 @@ pub struct Health {
is_syncing: bool, is_syncing: bool,
should_have_peers: bool, should_have_peers: bool,
tx_pool_length: usize, tx_pool_length: usize,
validators_count: u32,
nominators_count: u32,
} }
impl Default for Health { impl Default for Health {
@ -36,6 +38,8 @@ impl Health {
is_syncing: true, is_syncing: true,
should_have_peers: false, should_have_peers: false,
tx_pool_length: 0, 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::SetNodeName(name) => self.name = name,
Action::SetPendingExtrinsicsLength(length) => self.tx_pool_length = length, Action::SetPendingExtrinsicsLength(length) => self.tx_pool_length = length,
Action::NominatorsNumber(number) => self.nominators_count = number,
Action::ValidatorsNumber(number) => self.validators_count = number,
_ => {} _ => {}
}; };
Ok(None) Ok(None)
@ -82,11 +88,13 @@ impl Component for Health {
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
let [place, _] = super::header_layout(area); 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.name_as_string(),
self.tx_pool_length, self.tx_pool_length,
self.peers_as_string(), 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 span = Span::styled(message, Style::new().dim());
let paragraph = Paragraph::new(span).left_aligned(); let paragraph = Paragraph::new(span).left_aligned();

View File

@ -29,8 +29,8 @@ impl Menu {
items: vec![ items: vec![
String::from("Explorer"), String::from("Explorer"),
String::from("Wallet"), String::from("Wallet"),
String::from("Validator"),
String::from("Prices"), String::from("Prices"),
String::from("Staking"),
String::from("Governance"), String::from("Governance"),
String::from("Operations"), String::from("Operations"),
], ],
@ -57,6 +57,7 @@ impl Menu {
match i { match i {
0 => Ok(Some(Action::SetMode(Mode::Explorer))), 0 => Ok(Some(Action::SetMode(Mode::Explorer))),
1 => Ok(Some(Action::SetMode(Mode::Wallet))), 1 => Ok(Some(Action::SetMode(Mode::Wallet))),
2 => Ok(Some(Action::SetMode(Mode::Validator))),
_ => Ok(Some(Action::SetMode(Mode::Empty))), _ => Ok(Some(Action::SetMode(Mode::Empty))),
} }
} }
@ -76,6 +77,7 @@ impl Menu {
match i { match i {
0 => Ok(Some(Action::SetMode(Mode::Explorer))), 0 => Ok(Some(Action::SetMode(Mode::Explorer))),
1 => Ok(Some(Action::SetMode(Mode::Wallet))), 1 => Ok(Some(Action::SetMode(Mode::Wallet))),
2 => Ok(Some(Action::SetMode(Mode::Validator))),
_ => Ok(Some(Action::SetMode(Mode::Empty))), _ => Ok(Some(Action::SetMode(Mode::Empty))),
} }
} }
@ -115,6 +117,7 @@ impl Component for Menu {
match self.list_state.selected() { match self.list_state.selected() {
Some(0) => Ok(Some(Action::SetActiveScreen(Mode::Explorer))), Some(0) => Ok(Some(Action::SetActiveScreen(Mode::Explorer))),
Some(1) => Ok(Some(Action::SetActiveScreen(Mode::Wallet))), Some(1) => Ok(Some(Action::SetActiveScreen(Mode::Wallet))),
Some(2) => Ok(Some(Action::SetActiveScreen(Mode::Validator))),
_ => Ok(Some(Action::SetActiveScreen(Mode::Empty))), _ => Ok(Some(Action::SetActiveScreen(Mode::Empty))),
} }
}, },

View File

@ -15,6 +15,7 @@ pub mod menu;
pub mod version; pub mod version;
pub mod explorer; pub mod explorer;
pub mod wallet; pub mod wallet;
pub mod validator;
pub mod empty; pub mod empty;
pub trait Component { pub trait Component {
@ -75,8 +76,8 @@ pub fn global_layout(area: Rect) -> [Rect; 2] {
pub fn header_layout(area: Rect) -> [Rect; 2] { pub fn header_layout(area: Rect) -> [Rect; 2] {
let [header, _] = global_layout(area); let [header, _] = global_layout(area);
Layout::horizontal([ Layout::horizontal([
Constraint::Percentage(50), Constraint::Fill(1),
Constraint::Percentage(50), Constraint::Length(27),
]).areas(header) ]).areas(header)
} }

View File

@ -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<chrono::Local>,
level: ActionLevel,
message: String,
}
#[derive(Debug, Default)]
pub struct EventLogs {
is_active: bool,
scroll_state: ScrollbarState,
table_state: TableState,
logs: std::collections::VecDeque<WalletLog>,
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<Option<Action>> {
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<Option<Action>> {
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(())
}
}

View File

@ -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<Sender<Action>>,
action_tx: Option<UnboundedSender<Action>>,
palette: StylePalette,
scroll_state: ScrollbarState,
table_state: TableState,
rewards: BTreeMap<u32, EraStakingInfo>,
}
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<Action>) -> Result<()> {
self.network_tx = Some(tx);
Ok(())
}
fn register_action_handler(&mut self, tx: UnboundedSender<Action>) -> Result<()> {
self.action_tx = Some(tx);
Ok(())
}
fn register_config_handler(&mut self, config: Config) -> Result<()> {
if let Some(style) = config.styles.get(&crate::app::Mode::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<Option<Action>> {
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<Option<Action>> {
if self.is_active {
match key.code {
KeyCode::Up | KeyCode::Char('k') => self.previous_row(),
KeyCode::Down | KeyCode::Char('j') => self.next_row(),
KeyCode::Char('g') => self.first_row(),
KeyCode::Char('G') => self.last_row(),
_ => {},
};
}
Ok(None)
}
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
let [_, place, _] = super::validator_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(())
}
}

View File

@ -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<UnboundedSender<Action>>,
palette: StylePalette,
scroll_state: ScrollbarState,
list_state: ListState,
listen_addresses: Vec<String>,
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<Action>) -> Result<()> {
self.action_tx = Some(tx);
Ok(())
}
fn register_config_handler(&mut self, config: Config) -> Result<()> {
if let Some(style) = config.styles.get(&crate::app::Mode::Wallet) {
self.palette.with_normal_style(style.get("normal_style").copied());
self.palette.with_hover_style(style.get("hover_style").copied());
self.palette.with_normal_border_style(style.get("normal_border_style").copied());
self.palette.with_hover_border_style(style.get("hover_border_style").copied());
self.palette.with_normal_title_style(style.get("normal_title_style").copied());
self.palette.with_hover_title_style(style.get("hover_title_style").copied());
self.palette.with_highlight_style(style.get("highlight_style").copied());
self.palette.with_scrollbar_style(style.get("scrollbar_style").copied());
}
Ok(())
}
fn update(&mut self, action: Action) -> Result<Option<Action>> {
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<Option<Action>> {
if self.is_active {
match key.code {
KeyCode::Up | KeyCode::Char('k') => self.previous_row(),
KeyCode::Down | KeyCode::Char('j') => self.next_row(),
KeyCode::Char('g') => self.first_row(),
KeyCode::Char('G') => self.last_row(),
_ => {},
};
}
Ok(None)
}
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
let [_, place] = super::validator_session_and_listen_layout(area);
let (border_style, border_type) = self.palette.create_border_style(self.is_active);
let 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(())
}
}

View File

@ -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<Box<dyn PartialComponent>>,
}
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<Action>) -> Result<()> {
for component in self.components.iter_mut() {
component.register_network_handler(tx.clone())?;
}
Ok(())
}
fn register_action_handler(&mut self, tx: UnboundedSender<Action>) -> 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<Option<Action>> {
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<Option<Action>> {
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)
}

View File

@ -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<UnboundedSender<Action>>,
palette: StylePalette,
scroll_state: ScrollbarState,
table_state: TableState,
nominators: Vec<Nominator>,
}
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<Nominator>) {
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<Action>) -> Result<()> {
self.action_tx = Some(tx);
Ok(())
}
fn register_config_handler(&mut self, config: Config) -> Result<()> {
if let Some(style) = config.styles.get(&crate::app::Mode::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<Option<Action>> {
match action {
Action::SetNominatorsByValidator(nominators) => self.update_nominators(nominators),
_ => {}
};
Ok(None)
}
fn handle_key_event(&mut self, key: KeyEvent) -> Result<Option<Action>> {
if self.is_active {
match key.code {
KeyCode::Up | KeyCode::Char('k') => self.previous_row(),
KeyCode::Down | KeyCode::Char('j') => self.next_row(),
KeyCode::Char('g') => self.first_row(),
KeyCode::Char('G') => self.last_row(),
_ => {},
};
}
Ok(None)
}
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
let [place, _, _] = super::validator_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(())
}
}

View File

@ -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<PeerInformation>,
}
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<PeerInformation>) {
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<Option<Action>> {
match action {
Action::SetConnectedPeers(peers) => self.update_peers(peers),
_ => {}
};
Ok(None)
}
fn handle_key_event(&mut self, key: KeyEvent) -> Result<Option<Action>> {
if self.is_active {
match key.code {
KeyCode::Up | KeyCode::Char('k') => self.previous_row(),
KeyCode::Down | KeyCode::Char('j') => self.next_row(),
KeyCode::Char('g') => self.first_row(),
KeyCode::Char('G') => self.last_row(),
_ => {},
};
}
Ok(None)
}
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
let [_, _, place, _] = super::validator_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(())
}
}

View File

@ -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<Option<Action>> {
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(())
}
}

View File

@ -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<Option<Action>> {
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(())
}
}

View File

@ -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<Option<Action>> {
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(())
}
}

View File

@ -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<UnboundedSender<Action>>,
network_tx: Option<Sender<Action>>,
palette: StylePalette,
scroll_state: ScrollbarState,
table_state: TableState,
stash_pair: Option<PairSigner<CasperConfig, Pair>>,
stash_address: String,
session_keys: std::collections::HashMap<String, SessionKeyInfo>,
key_names: &'static [&'static str],
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::<CasperConfig, Pair>::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::<CasperConfig, Pair>::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<Action>) -> Result<()> {
self.network_tx = Some(tx);
Ok(())
}
fn register_action_handler(&mut self, tx: UnboundedSender<Action>) -> Result<()> {
self.action_tx = Some(tx);
Ok(())
}
fn register_config_handler(&mut self, config: Config) -> Result<()> {
if let Some(style) = config.styles.get(&crate::app::Mode::Wallet) {
self.palette.with_normal_style(style.get("normal_style").copied());
self.palette.with_hover_style(style.get("hover_style").copied());
self.palette.with_normal_border_style(style.get("normal_border_style").copied());
self.palette.with_hover_border_style(style.get("hover_border_style").copied());
self.palette.with_normal_title_style(style.get("normal_title_style").copied());
self.palette.with_hover_title_style(style.get("hover_title_style").copied());
self.palette.with_highlight_style(style.get("highlight_style").copied());
self.palette.with_scrollbar_style(style.get("scrollbar_style").copied());
}
self.read_or_create_stash()?;
Ok(())
}
fn update(&mut self, action: Action) -> Result<Option<Action>> {
match action {
Action::SetSessionKey(name, key_info) => self.set_new_key(name, key_info),
_ => {}
};
Ok(None)
}
fn handle_key_event(&mut self, key: KeyEvent) -> Result<Option<Action>> {
if self.is_active {
match key.code {
KeyCode::Up | KeyCode::Char('k') => self.previous_row(),
KeyCode::Down | KeyCode::Char('j') => self.next_row(),
KeyCode::Char('g') => self.first_row(),
KeyCode::Char('G') => self.last_row(),
_ => {},
};
}
Ok(None)
}
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
let [place, _] = super::validator_session_and_listen_layout(area);
let (border_style, border_type) = self.palette.create_border_style(self.is_active);
let table = Table::new(
self.key_names
.iter()
.map(|name| {
let address_text = match self.session_keys.get(*name) {
Some(key_info) => {
let mut address_text = Text::from(key_info.key.clone()).alignment(Alignment::Center);
if !key_info.is_stored {
address_text = address_text.add_modifier(Modifier::CROSSED_OUT);
}
address_text
},
None => Text::from("-").alignment(Alignment::Center),
};
let queued_name = format!("q_{}", name);
let queued_address_text = match self.session_keys.get(&queued_name) {
Some(key_info) => {
let mut queued_address_text = Text::from(key_info.key.clone()).alignment(Alignment::Right);
if !key_info.is_stored {
queued_address_text = queued_address_text.add_modifier(Modifier::CROSSED_OUT);
}
queued_address_text
},
None => Text::from("-").alignment(Alignment::Right),
};
Row::new(vec![
Cell::from(Text::from(name.to_string()).alignment(Alignment::Left)),
Cell::from(address_text),
Cell::from(Text::from("-->".to_string()).alignment(Alignment::Center)),
Cell::from(queued_address_text),
])
}),
[
Constraint::Length(4),
Constraint::Min(0),
Constraint::Length(3),
Constraint::Min(0),
],
)
.highlight_style(self.palette.create_highlight_style())
.column_spacing(1)
.block(Block::bordered()
.border_style(border_style)
.border_type(border_type)
.title_alignment(Alignment::Right)
.title_style(self.palette.create_title_style(false))
.title(self.stash_address.clone()));
let scrollbar = Scrollbar::default()
.orientation(ScrollbarOrientation::VerticalRight)
.begin_symbol(None)
.end_symbol(None)
.style(self.palette.create_scrollbar_style());
frame.render_stateful_widget(table, place, &mut self.table_state);
frame.render_stateful_widget(
scrollbar,
place.inner(Margin { vertical: 1, horizontal: 1 }),
&mut self.scroll_state,
);
Ok(())
}
}

View File

@ -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<u32, u128>,
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<Option<Action>> {
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<Option<Action>> {
if self.is_active {
match key.code {
KeyCode::Up | KeyCode::Char('k') => self.previous_row(),
KeyCode::Down | KeyCode::Char('j') => self.next_row(),
KeyCode::Char('g') => self.first_row(),
KeyCode::Char('G') => self.last_row(),
_ => {},
};
}
Ok(None)
}
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
let [_, _, place] = super::validator_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(())
}
}

View File

@ -16,7 +16,6 @@ use ratatui::{
Frame Frame
}; };
use subxt::{ use subxt::{
tx::PairSigner,
ext::sp_core::{ ext::sp_core::{
Pair as PairT, Pair as PairT,
sr25519::Pair, sr25519::Pair,
@ -27,7 +26,6 @@ use tokio::sync::mpsc::UnboundedSender;
use std::sync::mpsc::Sender; use std::sync::mpsc::Sender;
use super::{PartialComponent, Component, CurrentTab}; use super::{PartialComponent, Component, CurrentTab};
use crate::casper::CasperConfig;
use crate::types::{SystemAccount, ActionLevel}; use crate::types::{SystemAccount, ActionLevel};
use crate::{ use crate::{
action::Action, action::Action,
@ -40,8 +38,6 @@ struct AccountInfo {
address: String, address: String,
account_id: [u8; 32], account_id: [u8; 32],
seed: String, seed: String,
#[allow(dead_code)]
pair_signer: PairSigner<CasperConfig, Pair>,
} }
pub struct Accounts { pub struct Accounts {
@ -140,11 +136,10 @@ impl Accounts {
} }
fn create_new_account(&mut self, name: String) { 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 secret_seed = hex::encode(seed);
let account_id = pair.public().0; let account_id = pair.public().0;
let pair_signer = PairSigner::<CasperConfig, Pair>::new(pair); let address = AccountId32::from(seed)
let address = AccountId32::from(seed.clone())
.to_ss58check_with_version(Ss58AddressFormat::custom(1996)); .to_ss58check_with_version(Ss58AddressFormat::custom(1996));
self.log_event( self.log_event(
@ -158,7 +153,6 @@ impl Accounts {
address, address,
account_id, account_id,
seed: secret_seed, seed: secret_seed,
pair_signer,
}); });
self.last_row(); self.last_row();
self.save_to_file(); self.save_to_file();
@ -242,7 +236,6 @@ impl Accounts {
let account_id = pair.public().0; let account_id = pair.public().0;
let address = AccountId32::from(account_id) let address = AccountId32::from(account_id)
.to_ss58check_with_version(Ss58AddressFormat::custom(1996)); .to_ss58check_with_version(Ss58AddressFormat::custom(1996));
let pair_signer = PairSigner::<CasperConfig, Pair>::new(pair);
self.send_balance_request(account_id, false); self.send_balance_request(account_id, false);
self.wallet_keys.push(AccountInfo { self.wallet_keys.push(AccountInfo {
@ -250,7 +243,6 @@ impl Accounts {
account_id, account_id,
address, address,
seed: wallet_key.to_string(), seed: wallet_key.to_string(),
pair_signer,
}); });
} }
self.log_event(format!("read {} wallets from disk", self.log_event(format!("read {} wallets from disk",
@ -288,7 +280,6 @@ impl Accounts {
let account_id = pair.public().0; let account_id = pair.public().0;
let address = AccountId32::from(pair.public().0) let address = AccountId32::from(pair.public().0)
.to_ss58check_with_version(Ss58AddressFormat::custom(1996)); .to_ss58check_with_version(Ss58AddressFormat::custom(1996));
let pair_signer = PairSigner::<CasperConfig, Pair>::new(pair);
let mut new_file = File::create(file_path)?; let mut new_file = File::create(file_path)?;
writeln!(new_file, "ghostie:0x{}", &secret_seed)?; writeln!(new_file, "ghostie:0x{}", &secret_seed)?;
@ -299,7 +290,6 @@ impl Accounts {
address, address,
account_id, account_id,
seed: secret_seed, seed: secret_seed,
pair_signer,
}); });
} }
}; };

View File

@ -2,7 +2,7 @@ use clap::Parser;
use color_eyre::Result; use color_eyre::Result;
use subxt::{ use subxt::{
OnlineClient, OnlineClient,
backend::{legacy::LegacyRpcMethods, rpc::RpcClient}, backend::rpc::RpcClient,
}; };
mod action; mod action;
@ -54,9 +54,7 @@ async fn main() -> Result<()> {
let (action_tx, action_rx) = tokio::sync::mpsc::unbounded_channel(); let (action_tx, action_rx) = tokio::sync::mpsc::unbounded_channel();
let rpc_client = RpcClient::from_url(args.rpc_endpoint).await?; let rpc_client = RpcClient::from_url(args.rpc_endpoint).await?;
let legacy_client_api = LegacyRpcMethods::<CasperConfig>::new(rpc_client.clone()); let online_client = OnlineClient::<CasperConfig>::from_rpc_client(rpc_client.clone()).await?;
let online_client =
OnlineClient::<CasperConfig>::from_rpc_client(rpc_client.clone()).await?;
let finalized_blocks_sub = online_client.blocks().subscribe_finalized().await?; let finalized_blocks_sub = online_client.blocks().subscribe_finalized().await?;
let best_blocks_sub = online_client.blocks().subscribe_best().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( let mut network = network::Network::new(
cloned_action_tx, cloned_action_tx,
online_client, online_client,
legacy_client_api,
rpc_client, rpc_client,
); );
start_tokio_action_loop(sync_io_rx, &mut network); start_tokio_action_loop(sync_io_rx, &mut network);

View File

@ -1,27 +1,30 @@
use tokio::sync::mpsc::UnboundedSender; use tokio::sync::mpsc::UnboundedSender;
use color_eyre::Result; 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( pub async fn get_node_name(
action_tx: &UnboundedSender<Action>, action_tx: &UnboundedSender<Action>,
api: &LegacyRpcMethods<CasperConfig>, rpc_client: &RpcClient,
) -> Result<()> { ) -> 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))?; action_tx.send(Action::SetNodeName(maybe_node_name))?;
Ok(()) Ok(())
} }
pub async fn get_system_health( pub async fn get_system_health(
action_tx: &UnboundedSender<Action>, action_tx: &UnboundedSender<Action>,
api: &LegacyRpcMethods<CasperConfig>, rpc_client: &RpcClient,
) -> Result<()> { ) -> Result<()> {
let (maybe_peers, is_syncing, should_have_peers) = api let (maybe_peers, is_syncing, should_have_peers) = rpc_client
.system_health() .request("system_health", rpc_params![])
.await .await
.ok() .ok()
.map_or((None, false, false), |health| ( .map_or((None, false, false), |health: SystemHealth| (
Some(health.peers), Some(health.peers),
health.is_syncing, health.is_syncing,
health.should_have_peers, health.should_have_peers,
@ -35,10 +38,11 @@ pub async fn get_system_health(
pub async fn get_genesis_hash( pub async fn get_genesis_hash(
action_tx: &UnboundedSender<Action>, action_tx: &UnboundedSender<Action>,
api: &LegacyRpcMethods<CasperConfig>, rpc_client: &RpcClient,
) -> Result<()> { ) -> Result<()> {
let maybe_genesis_hash = api let params = rpc_params![0u32];
.genesis_hash() let maybe_genesis_hash = rpc_client
.request("chain_getBlockHash", params)
.await .await
.ok(); .ok();
action_tx.send(Action::SetGenesisHash(maybe_genesis_hash))?; action_tx.send(Action::SetGenesisHash(maybe_genesis_hash))?;
@ -47,10 +51,10 @@ pub async fn get_genesis_hash(
pub async fn get_chain_name( pub async fn get_chain_name(
action_tx: &UnboundedSender<Action>, action_tx: &UnboundedSender<Action>,
api: &LegacyRpcMethods<CasperConfig>, rpc_client: &RpcClient,
) -> Result<()> { ) -> Result<()> {
let maybe_chain_name = api let maybe_chain_name = rpc_client
.system_chain() .request("system_chain", rpc_params![])
.await .await
.ok(); .ok();
action_tx.send(Action::SetChainName(maybe_chain_name))?; action_tx.send(Action::SetChainName(maybe_chain_name))?;
@ -59,12 +63,60 @@ pub async fn get_chain_name(
pub async fn get_system_version( pub async fn get_system_version(
action_tx: &UnboundedSender<Action>, action_tx: &UnboundedSender<Action>,
api: &LegacyRpcMethods<CasperConfig>, rpc_client: &RpcClient,
) -> Result<()> { ) -> Result<()> {
let maybe_system_version = api let maybe_system_version = rpc_client
.system_version() .request("system_version", rpc_params![])
.await .await
.ok(); .ok();
action_tx.send(Action::SetChainVersion(maybe_system_version))?; action_tx.send(Action::SetChainVersion(maybe_system_version))?;
Ok(()) Ok(())
} }
pub async fn get_pending_extrinsics(
action_tx: &UnboundedSender<Action>,
rpc_client: &RpcClient,
) -> Result<()> {
let pending_extrinsics: Vec<String> = 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<Action>,
rpc_client: &RpcClient,
) -> Result<()> {
let connected_peers: Vec<PeerInformation> = 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<Action>,
rpc_client: &RpcClient,
) -> Result<()> {
let listen_addresses: Vec<String> = 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<Action>,
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(())
}

View File

@ -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<N: Ord + core::ops::Sub<Output = N> + 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)
}

View File

@ -1,18 +1,20 @@
use tokio::sync::mpsc::UnboundedSender; use tokio::sync::mpsc::UnboundedSender;
use color_eyre::Result; use color_eyre::Result;
use subxt::{ use subxt::{
backend::{ backend::rpc::RpcClient,
legacy::LegacyRpcMethods,
rpc::RpcClient,
},
tx::{TxProgress, TxStatus}, tx::{TxProgress, TxStatus},
utils::H256, utils::H256,
OnlineClient, OnlineClient,
}; };
mod legacy_rpc_calls; mod legacy_rpc_calls;
mod predefinded_calls; mod predefined_calls;
mod predefined_txs;
mod subscriptions; mod subscriptions;
mod miscellaneous;
mod raw_calls;
pub use miscellaneous::{prepare_perbill_fraction_string, calculate_for_fraction};
use crate::{ use crate::{
types::ActionLevel, types::ActionLevel,
@ -25,10 +27,10 @@ pub use subscriptions::{FinalizedSubscription, BestSubscription};
pub struct Network { pub struct Network {
action_tx: UnboundedSender<Action>, action_tx: UnboundedSender<Action>,
online_client_api: OnlineClient<CasperConfig>, online_client_api: OnlineClient<CasperConfig>,
legacy_client_api: LegacyRpcMethods<CasperConfig>,
rpc_client: RpcClient, rpc_client: RpcClient,
best_hash: Option<H256>, best_hash: Option<H256>,
finalized_hash: Option<H256>, finalized_hash: Option<H256>,
stash_to_watch: Option<[u8; 32]>,
accounts_to_watch: std::collections::HashSet<[u8; 32]>, accounts_to_watch: std::collections::HashSet<[u8; 32]>,
transactions_to_watch: Vec<TxProgress<CasperConfig, OnlineClient<CasperConfig>>>, transactions_to_watch: Vec<TxProgress<CasperConfig, OnlineClient<CasperConfig>>>,
} }
@ -37,27 +39,42 @@ impl Network {
pub fn new( pub fn new(
action_tx: UnboundedSender<Action>, action_tx: UnboundedSender<Action>,
online_client_api: OnlineClient<CasperConfig>, online_client_api: OnlineClient<CasperConfig>,
legacy_client_api: LegacyRpcMethods<CasperConfig>,
rpc_client: RpcClient, rpc_client: RpcClient,
) -> Self { ) -> Self {
Self { Self {
action_tx, action_tx,
online_client_api, online_client_api,
legacy_client_api,
rpc_client, rpc_client,
best_hash: None, best_hash: None,
finalized_hash: None, finalized_hash: None,
stash_to_watch: None,
accounts_to_watch: Default::default(), accounts_to_watch: Default::default(),
transactions_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<()> { pub async fn handle_network_event(&mut self, io_event: Action) -> Result<()> {
match io_event { match io_event {
Action::NewBestHash(hash) => { Action::NewBestHash(hash) => {
self.best_hash = Some(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() { 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(()) Ok(())
}, },
@ -100,26 +117,66 @@ impl Network {
} }
Ok(()) Ok(())
}, },
Action::GetSystemHealth => legacy_rpc_calls::get_system_health(&self.action_tx, &self.legacy_client_api).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.legacy_client_api).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.legacy_client_api).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.legacy_client_api).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.legacy_client_api).await, Action::GetChainVersion => legacy_rpc_calls::get_system_version(&self.action_tx, &self.rpc_client).await,
Action::GetBlockAuthor(hash, logs) => predefinded_calls::get_block_author(&self.action_tx, &self.online_client_api, &logs, &hash).await, Action::GetPendingExtrinsics => legacy_rpc_calls::get_pending_extrinsics(&self.action_tx, &self.rpc_client).await,
Action::GetActiveEra => predefinded_calls::get_active_era(&self.action_tx, &self.online_client_api).await, Action::GetConnectedPeers => legacy_rpc_calls::get_connected_peers(&self.action_tx, &self.rpc_client).await,
Action::GetEpochProgress => predefinded_calls::get_epoch_progress(&self.action_tx, &self.online_client_api).await, Action::GetListenAddresses => legacy_rpc_calls::get_listen_addresses(&self.action_tx, &self.rpc_client).await,
Action::GetPendingExtrinsics => predefinded_calls::get_pending_extrinsics(&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::GetBlockAuthor(hash, logs) => predefined_calls::get_block_author(&self.action_tx, &self.online_client_api, &logs, &hash).await,
Action::GetTotalIssuance => predefinded_calls::get_total_issuance(&self.action_tx, &self.online_client_api).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) => { Action::BalanceRequest(account_id, remove) => {
if remove { if remove {
let _ = self.accounts_to_watch.remove(&account_id); let _ = self.accounts_to_watch.remove(&account_id);
Ok(()) Ok(())
} else { } else {
let _ = self.accounts_to_watch.insert(account_id); 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) => { Action::TransferBalance(sender, receiver, amount) => {
@ -129,7 +186,7 @@ impl Network {
.try_into() .try_into()
.expect("stored seed is valid length; qed"); .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.action_tx,
&self.online_client_api, &self.online_client_api,
&sender, &sender,

View File

@ -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<Action>,
api: &OnlineClient<CasperConfig>,
logs: &Vec<DigestItem>,
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<Action>,
api: &OnlineClient<CasperConfig>,
) -> 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<Action>,
api: &OnlineClient<CasperConfig>,
) -> 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<Action>,
rpc_client: &RpcClient,
) -> Result<()> {
let pending_extrinsics: Vec<String> = 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<Action>,
api: &OnlineClient<CasperConfig>,
) -> 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<Action>,
api: &OnlineClient<CasperConfig>,
) -> 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<Action>,
api: &OnlineClient<CasperConfig>,
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<Action>,
api: &OnlineClient<CasperConfig>,
sender: &[u8; 32],
receiver: &[u8; 32],
amount: &u128,
) -> Result<TxProgress<CasperConfig, OnlineClient<CasperConfig>>> {
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::<CasperConfig, Pair>::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())
}
}
}

View File

@ -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<Action>,
api: &OnlineClient<CasperConfig>,
logs: &Vec<DigestItem>,
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<Action>,
api: &OnlineClient<CasperConfig>,
) -> 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<Action>,
api: &OnlineClient<CasperConfig>,
) -> 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<Action>,
api: &OnlineClient<CasperConfig>,
) -> 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<Action>,
api: &OnlineClient<CasperConfig>,
) -> 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<Action>,
api: &OnlineClient<CasperConfig>,
) -> 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<Action>,
api: &OnlineClient<CasperConfig>,
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<Action>,
api: &OnlineClient<CasperConfig>,
) -> 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<Action>,
api: &OnlineClient<CasperConfig>,
) -> 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<Action>,
api: &OnlineClient<CasperConfig>,
) -> 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<Action>,
api: &OnlineClient<CasperConfig>,
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<Action>,
api: &OnlineClient<CasperConfig>,
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<Action>,
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<Action>,
api: &OnlineClient<CasperConfig>,
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<Action>,
api: &OnlineClient<CasperConfig>,
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<Action>,
api: &OnlineClient<CasperConfig>,
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<Action>,
api: &OnlineClient<CasperConfig>,
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<Action>,
api: &OnlineClient<CasperConfig>,
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<Action>,
api: &OnlineClient<CasperConfig>,
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<Action>,
api: &OnlineClient<CasperConfig>,
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::<Vec<_>>(),
None => Vec::new(),
};
action_tx.send(Action::SetNominatorsByValidator(nominators))?;
Ok(())
}
pub async fn get_is_stash_bonded(
action_tx: &UnboundedSender<Action>,
api: &OnlineClient<CasperConfig>,
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<Action>,
api: &OnlineClient<CasperConfig>,
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<Action>,
api: &OnlineClient<CasperConfig>,
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(())
}

View File

@ -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<Action>,
api: &OnlineClient<CasperConfig>,
sender: &[u8; 32],
receiver: &[u8; 32],
amount: &u128,
) -> Result<TxProgress<CasperConfig, OnlineClient<CasperConfig>>> {
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::<CasperConfig, Pair>::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())
}
}
}

View File

@ -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<CasperConfig>,
at_hash: Option<&H256>,
) -> Result<Option<sp_consensus_slots::Slot>> {
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<CasperConfig>,
at_hash: Option<&H256>,
) -> Result<Option<u64>> {
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<CasperConfig>,
at_hash: Option<&H256>,
) -> Result<Option<sp_consensus_slots::Slot>> {
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<CasperConfig>,
) -> Result<u64> {
let constant_query = casper_network::constants().babe().epoch_duration();
let epoch_duration = super::do_constant_call(online_client, &constant_query)?;
Ok(epoch_duration)
}

View File

@ -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<CasperConfig>,
at_hash: Option<&H256>,
) -> Result<Option<u128>> {
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<CasperConfig>,
) -> Result<u128> {
let constant_query = casper_network::constants().balances().existential_deposit();
let existential_deposit = super::do_constant_call(online_client, &constant_query)?;
Ok(existential_deposit)
}

View File

@ -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<CasperConfig>,
at_hash: Option<&H256>,
) -> Result<Option<(u32, u32)>> {
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)
}

View File

@ -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<CasperConfig>,
storage_key: &'address Addr,
maybe_at_hash: Option<&H256>,
) -> Result<Option<Addr::Target>, subxt::Error>
where
Addr: subxt::storage::Address<IsFetchable = Yes> + '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<CasperConfig>,
constant_query: &'address Addr,
) -> Result<Addr::Target, subxt::Error>
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)
}

View File

@ -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<CasperConfig>,
at_hash: Option<&H256>,
) -> Result<Option<Vec<AccountId32>>> {
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<CasperConfig>,
at_hash: Option<&H256>,
account: &[u8; 32],
) -> Result<Option<opaque::SessionKeys>> {
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<CasperConfig>,
at_hash: Option<&H256>,
) -> Result<Option<Vec<(AccountId32, opaque::SessionKeys)>>> {
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)
}

View File

@ -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<CasperConfig>,
at_hash: Option<&H256>,
) -> Result<Option<u32>> {
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<CasperConfig>,
at_hash: Option<&H256>,
) -> Result<Option<ActiveEraInfo>> {
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<CasperConfig>,
at_hash: Option<&H256>,
) -> Result<Option<u32>> {
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<CasperConfig>,
at_hash: Option<&H256>,
) -> Result<Option<u32>> {
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<CasperConfig>,
at_hash: Option<&H256>,
era_index: u32,
) -> Result<Option<u128>> {
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<CasperConfig>,
at_hash: Option<&H256>,
era_index: u32,
) -> Result<Option<u128>> {
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<CasperConfig>,
at_hash: Option<&H256>,
era_index: u32,
) -> Result<Option<EraRewardPoints<AccountId32>>> {
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<CasperConfig>,
at_hash: Option<&H256>,
era_index: u32,
account: &[u8; 32],
) -> Result<Option<Vec<u32>>> {
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<CasperConfig>,
at_hash: Option<&H256>,
era_index: u32,
account: &[u8; 32],
) -> Result<Option<(Perbill, u128)>> {
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<CasperConfig>,
at_hash: Option<&H256>,
account: &[u8; 32],
) -> Result<Option<StakingLedger>> {
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<CasperConfig>,
at_hash: Option<&H256>,
era_index: u32,
account: &[u8; 32],
) -> Result<Option<Exposure<AccountId32, u128>>> {
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<CasperConfig>,
at_hash: Option<&H256>,
account: &[u8; 32],
) -> Result<Option<AccountId32>> {
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<CasperConfig>,
at_hash: Option<&H256>,
era_index: u32,
account: &[u8; 32],
) -> Result<Option<PagedExposureMetadata<u128>>> {
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<CasperConfig>,
at_hash: Option<&H256>,
account: &[u8; 32],
) -> Result<Option<ValidatorPrefs>> {
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)
}

View File

@ -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<CasperConfig>,
at_hash: Option<&H256>,
account: &[u8; 32],
) -> Result<Option<AccountInfo<u32, AccountData<u128>>>> {
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)
}

0
src/network/raw_rpc.rs Normal file
View File

View File

@ -112,8 +112,12 @@ impl BestSubscription {
self.network_tx.send(Action::NewBestHash(block_hash))?; 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::GetBlockAuthor(block_hash, block.header().digest.logs.clone()))?;
self.network_tx.send(Action::GetActiveEra)?; self.network_tx.send(Action::GetActiveEra)?;
self.network_tx.send(Action::GetCurrentEra)?;
self.network_tx.send(Action::GetEpochProgress)?; self.network_tx.send(Action::GetEpochProgress)?;
self.network_tx.send(Action::GetTotalIssuance)?; 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(()) Ok(())
} }

View File

@ -2,8 +2,14 @@ mod era;
mod extrinsics; mod extrinsics;
mod log; mod log;
mod account; mod account;
mod peer;
mod session;
mod nominator;
pub use extrinsics::CasperExtrinsicDetails; pub use extrinsics::CasperExtrinsicDetails;
pub use era::EraInfo; pub use era::EraInfo;
pub use log::ActionLevel; pub use log::ActionLevel;
pub use account::SystemAccount; pub use account::SystemAccount;
pub use peer::PeerInformation;
pub use session::SessionKeyInfo;
pub use nominator::Nominator;

8
src/types/nominator.rs Normal file
View File

@ -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,
}

12
src/types/peer.rs Normal file
View File

@ -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,
}

8
src/types/session.rs Normal file
View File

@ -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,
}

View File

@ -14,6 +14,8 @@ impl Default for VerticalBlocks {
impl ToString for VerticalBlocks { impl ToString for VerticalBlocks {
fn to_string(&self) -> String { 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) self.elements[((chrono::Utc::now().timestamp_millis() % super::CYCLE)
/ (super::CYCLE / self.elements.len() as i64)) as usize] / (super::CYCLE / self.elements.len() as i64)) as usize]
.to_owned() .to_owned()