Compare commits
97 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0a28acede7 | |||
| 5db96fa8f1 | |||
| 0f9c0aa1f6 | |||
| 682deadb48 | |||
| b9c863e2ea | |||
| ef81076f71 | |||
| 9afda5a701 | |||
| 29622e6ec3 | |||
| a6ebb90ede | |||
| 0d329e4340 | |||
| 6a43dab6dd | |||
| 8eb3871e34 | |||
| 7561c525e3 | |||
| 0e4cb343dc | |||
| 5de724fc19 | |||
| ddc598a8a4 | |||
| a24a35aa51 | |||
| aed9fae2af | |||
| 987d8e544d | |||
| 0af68ca624 | |||
| 9e7cdffd29 | |||
| ac67bb4310 | |||
| 756a1089f6 | |||
| edd1b6d616 | |||
| 13295288ce | |||
| 105ca64dc6 | |||
| 2d01ef73c7 | |||
| 39818f848e | |||
| 48eb85efa3 | |||
| de1732372e | |||
| 814854b286 | |||
| f8b4215546 | |||
| bb5a0e8eba | |||
| ee5640349a | |||
| 3557e5fa4e | |||
| 3284d5d07e | |||
| d42bb97d36 | |||
| c4f0f6f35c | |||
| e16d319b72 | |||
| 2635792dab | |||
| 33027f96d5 | |||
| 9dad47210b | |||
| 22db763d31 | |||
| 5e90456fdf | |||
| 6e7104f229 | |||
| 9d346172b6 | |||
| 08e0f3d576 | |||
| 4fc26712a5 | |||
| d53e0242fb | |||
| 309c97d60e | |||
| 76d87aecbf | |||
| 2afc38068a | |||
| fafc5bd2c2 | |||
| 90c07aa339 | |||
| c7cdafe3b6 | |||
| a2584f8212 | |||
| 5d73989a43 | |||
| 91fb7380d6 | |||
| 10b8337f8d | |||
| 0cb651728e | |||
| adf1d94cbe | |||
| 0777e6ebf0 | |||
| b1ff74c637 | |||
| 3e22e933ab | |||
| 38e95f58ed | |||
| 26b8700455 | |||
| 8555ddceec | |||
| 5b0ef1c4b7 | |||
| 7e9e6ac329 | |||
| 4f8c8f5262 | |||
| afd11aa294 | |||
| 853bdcd76e | |||
| 9b1136e4ca | |||
| 507c03959d | |||
| 8f63c7483d | |||
| 493a4db663 | |||
| 9e045ca6cc | |||
| a365acc2a1 | |||
| 44bd27e8f0 | |||
| 87eca056a3 | |||
| ba79a4cba8 | |||
| 80fd07bcfe | |||
| b922aa75f7 | |||
| 8b302a0814 | |||
| 25d0a37aa8 | |||
| 5323363b35 | |||
| 9ebb1d9543 | |||
| 6347474633 | |||
| f9233c6291 | |||
| 9e77dd816d | |||
| ddc8eb062a | |||
| 3662f9c666 | |||
| 3b7aaa7fd2 | |||
| 2e0205a581 | |||
| 85e44f0bb8 | |||
| cbf3aa95a5 | |||
| 10a9ca166c |
@ -2,8 +2,10 @@
|
||||
name = "ghost-eye"
|
||||
authors = ["str3tch <stretch@ghostchain.io>"]
|
||||
description = "Application for interacting with Casper/Ghost nodes that are exposing RPC only to the localhost"
|
||||
version = "0.2.6"
|
||||
version = "0.3.70"
|
||||
edition = "2021"
|
||||
homepage = "https://git.ghostchain.io/ghostchain"
|
||||
repository = "https://git.ghostchain.io/ghostchain/ghost-eye"
|
||||
|
||||
[dependencies]
|
||||
better-panic = "0.3.0"
|
||||
|
||||
Binary file not shown.
@ -9,6 +9,15 @@
|
||||
"hover_title_style": "",
|
||||
"highlight_style": "yellow italic",
|
||||
},
|
||||
"Help": {
|
||||
"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 italic",
|
||||
},
|
||||
"Explorer": {
|
||||
"normal_style": "",
|
||||
"hover_style": "bold yellow italic on blue",
|
||||
@ -29,7 +38,18 @@
|
||||
"highlight_style": "yellow bold",
|
||||
"popup_style": "blue",
|
||||
"popup_title_style": "blue",
|
||||
}
|
||||
},
|
||||
"Validator": {
|
||||
"normal_style": "",
|
||||
"hover_style": "bold yellow italic on blue",
|
||||
"normal_border_style": "blue",
|
||||
"hover_border_style": "blue",
|
||||
"normal_title_style": "blue",
|
||||
"hover_title_style": "",
|
||||
"highlight_style": "yellow bold",
|
||||
"popup_style": "blue",
|
||||
"popup_title_style": "blue",
|
||||
},
|
||||
},
|
||||
"keybindings": {
|
||||
"Menu": {
|
||||
@ -45,6 +65,11 @@
|
||||
"<Ctrl-z>": "Suspend",
|
||||
},
|
||||
"Wallet": {
|
||||
"<Ctrl-d>": "Quit",
|
||||
"<Ctrl-c>": "Quit",
|
||||
"<Ctrl-z>": "Suspend",
|
||||
},
|
||||
"Validator": {
|
||||
"<q>": "Quit",
|
||||
"<Ctrl-d>": "Quit",
|
||||
"<Ctrl-c>": "Quit",
|
||||
|
||||
@ -4,8 +4,10 @@ use strum::Display;
|
||||
use subxt::utils::H256;
|
||||
use subxt::config::substrate::DigestItem;
|
||||
|
||||
use crate::{
|
||||
types::{SystemAccount, ActionLevel, EraInfo, CasperExtrinsicDetails},
|
||||
use crate::types::{
|
||||
ActionLevel, ActionTarget, CasperExtrinsicDetails, EraInfo, EraRewardPoints,
|
||||
Nominator, Nominations, PeerInformation, SessionKeyInfo, UnlockChunk, SystemAccount,
|
||||
RewardDestination, Gatekeeper, BlockRange,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Display, Serialize, Deserialize)]
|
||||
@ -19,29 +21,52 @@ pub enum Action {
|
||||
ClearScreen,
|
||||
Error(String),
|
||||
Help,
|
||||
CheckPendingTransactions,
|
||||
|
||||
SetMode(crate::app::Mode),
|
||||
SetActiveScreen(crate::app::Mode),
|
||||
UsedExplorerBlock(Option<u32>),
|
||||
UsedExplorerLog(Option<String>),
|
||||
UsedAccount(String),
|
||||
UsedAccount([u8; 32], String),
|
||||
NewAccount(String),
|
||||
NewAddressBookRecord(String, String),
|
||||
SetSender(String, Option<u32>),
|
||||
RemoveEraToWatch(u32),
|
||||
|
||||
ClosePopup,
|
||||
RotateSessionKeys,
|
||||
PayoutValidatorPopup(u32),
|
||||
PayoutAllValidatorPopup(Vec<u32>),
|
||||
WithdrawValidatorPopup,
|
||||
|
||||
BalanceRequest([u8; 32], bool),
|
||||
BalanceResponse([u8; 32], SystemAccount),
|
||||
BalanceResponse([u8; 32], Option<SystemAccount>),
|
||||
BalanceSetActive(Option<SystemAccount>),
|
||||
|
||||
RenameAccount(String),
|
||||
RenameAddressBookRecord(String),
|
||||
RenameKnownValidatorRecord,
|
||||
|
||||
UpdateAccountName(String),
|
||||
UpdateAddressBookRecord(String),
|
||||
UpdateKnownValidator(String),
|
||||
TransferTo(String),
|
||||
AccountDetailsOf(String, Option<SystemAccount>),
|
||||
StoreRotatedKeys(String),
|
||||
|
||||
TransferBalance(String, [u8; 32], u128),
|
||||
WalletLog(String, ActionLevel),
|
||||
BondValidatorExtraFrom([u8; 32], u128, ActionTarget),
|
||||
BondValidatorFrom([u8; 32], u128, ActionTarget),
|
||||
SetPayee([u8; 32], RewardDestination, ActionTarget),
|
||||
PayoutStakers([u8; 32], [u8; 32], u32),
|
||||
SetSessionKeys([u8; 32], String),
|
||||
ValidateFrom([u8; 32], u32),
|
||||
ChillFrom([u8; 32]),
|
||||
UnbondFrom([u8; 32], u128),
|
||||
RebondFrom([u8; 32], u128),
|
||||
WithdrawUnbondedFrom([u8; 32], u32),
|
||||
NominateTargets([u8; 32], Vec<[u8; 32]>),
|
||||
EventLog(String, ActionLevel, ActionTarget),
|
||||
|
||||
NewBestBlock(u32),
|
||||
NewBestHash(H256),
|
||||
@ -49,6 +74,12 @@ pub enum Action {
|
||||
NewFinalizedHash(H256),
|
||||
BestBlockUpdated(u32),
|
||||
ExtrinsicsLength(u32, usize),
|
||||
ValidatorsNumber(u32),
|
||||
NominatorsNumber(u32),
|
||||
Inflation(String),
|
||||
Apy(String),
|
||||
TreasuryApy(String),
|
||||
NextReward(u128),
|
||||
|
||||
GetBlockAuthor(H256, Vec<DigestItem>),
|
||||
SetBlockAuthor(H256, String),
|
||||
@ -59,29 +90,78 @@ pub enum Action {
|
||||
GetChainName,
|
||||
GetChainVersion,
|
||||
GetPendingExtrinsics,
|
||||
GetConnectedPeers,
|
||||
GetSessionKeys([u8; 32], bool),
|
||||
GetQueuedSessionKeys([u8; 32], bool),
|
||||
GetListenAddresses,
|
||||
GetLocalIdentity,
|
||||
|
||||
GetLatestBlock,
|
||||
GetFinalizedBlock,
|
||||
GetActiveEra,
|
||||
GetCurrentEra,
|
||||
GetEpochProgress,
|
||||
GetValidators,
|
||||
GetValidatorsNumber,
|
||||
GetNominatorsNumber,
|
||||
GetInflation,
|
||||
GetNominatorsByValidator([u8; 32], bool),
|
||||
GetNominatorsByAccount([u8; 32], bool),
|
||||
GetValidatorAllRewards([u8; 32], bool),
|
||||
GetValidatorLedger([u8; 32], bool),
|
||||
GetIsStashBonded([u8; 32], bool),
|
||||
GetErasStakersOverview([u8; 32], bool),
|
||||
GetValidatorPrefs([u8; 32], bool),
|
||||
GetSlashingSpans([u8; 32], bool),
|
||||
GetValidatorLatestClaim([u8; 32], bool),
|
||||
GetStakingPayee([u8; 32], bool),
|
||||
GetValidatorIsDisabled([u8; 32], bool),
|
||||
GetCurrentValidatorEraRewards,
|
||||
|
||||
SetNodeName(Option<String>),
|
||||
SetSystemHealth(Option<usize>, bool, bool),
|
||||
SetGenesisHash(Option<H256>),
|
||||
SetChainName(Option<String>),
|
||||
SetChainVersion(Option<String>),
|
||||
SetBlockRange(u64, BlockRange),
|
||||
SetStashAccount([u8; 32]),
|
||||
SetStashSecret([u8; 32]),
|
||||
SetChoosenValidator([u8; 32], u32, u32),
|
||||
SetChoosenGatekeeper(u64),
|
||||
SetSlashingSpansLength(usize, [u8; 32]),
|
||||
|
||||
BestBlockInformation(H256, u32),
|
||||
FinalizedBlockInformation(H256, u32),
|
||||
ExtrinsicsForBlock(u32, Vec<CasperExtrinsicDetails>),
|
||||
SetActiveEra(EraInfo),
|
||||
SetCurrentEra(u32),
|
||||
SetEpochProgress(u64, u64),
|
||||
SetPendingExtrinsicsLength(usize),
|
||||
SetConnectedPeers(Vec<PeerInformation>),
|
||||
SetSessionKey(String, SessionKeyInfo),
|
||||
SetListenAddresses(Vec<String>),
|
||||
SetLocalIdentity(String),
|
||||
SetNominatorsByValidator(Vec<Nominator>, [u8; 32]),
|
||||
SetNominatorsByAccount(Nominations, [u8; 32]),
|
||||
SetValidatorEraReward(u32, u128),
|
||||
SetValidatorEraClaimed(u32, bool),
|
||||
SetValidatorEraSlash(u32, u128),
|
||||
SetValidatorEraUnlocking(Vec<UnlockChunk>, [u8; 32]),
|
||||
SetValidatorLatestClaim(u32, [u8; 32]),
|
||||
SetStakingPayee(RewardDestination, [u8; 32]),
|
||||
SetIsBonded(bool, [u8; 32]),
|
||||
SetStakedAmountRatio(Option<u128>, Option<u128>, [u8; 32]),
|
||||
SetStakedRatio(u128, u128, [u8; 32]),
|
||||
SetValidatorPrefs(Option<u32>, bool, [u8; 32]),
|
||||
SetCurrentValidatorEraRewards(u32, u32, Vec<EraRewardPoints>),
|
||||
|
||||
GetTotalIssuance,
|
||||
GetExistentialDeposit,
|
||||
GetMinValidatorBond,
|
||||
GetGatekeepedNetwork(u64),
|
||||
GetBlockRange,
|
||||
|
||||
SetExistentialDeposit(u128),
|
||||
SetTotalIssuance(u128),
|
||||
SetMinValidatorBond(u128),
|
||||
SetGatekeepedNetwork(Gatekeeper),
|
||||
SetTotalIssuance(Option<u128>),
|
||||
}
|
||||
|
||||
57
src/app.rs
57
src/app.rs
@ -1,7 +1,6 @@
|
||||
use color_eyre::Result;
|
||||
use crossterm::event::KeyEvent;
|
||||
use ratatui::prelude::Rect;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use tokio::sync::mpsc::{UnboundedSender, UnboundedReceiver};
|
||||
use std::sync::mpsc::Sender;
|
||||
use tracing::info;
|
||||
@ -12,26 +11,12 @@ use crate::{
|
||||
tui::{Event, Tui},
|
||||
components::{
|
||||
menu::Menu, version::Version, explorer::Explorer, wallet::Wallet,
|
||||
empty::Empty, health::Health, fps::FpsCounter, Component,
|
||||
validator::Validator, empty::Empty, help::Help, health::Health,
|
||||
fps::FpsCounter, Component,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum Mode {
|
||||
Menu,
|
||||
Explorer,
|
||||
Wallet,
|
||||
WalletActive,
|
||||
ExplorerActive,
|
||||
Empty,
|
||||
EmptyActive,
|
||||
}
|
||||
|
||||
impl Default for Mode {
|
||||
fn default() -> Self {
|
||||
Self::Explorer
|
||||
}
|
||||
}
|
||||
pub use crate::modes::Mode;
|
||||
|
||||
pub struct App {
|
||||
network_tx: Sender<Action>,
|
||||
@ -69,8 +54,10 @@ impl App {
|
||||
Box::new(FpsCounter::default()),
|
||||
Box::new(Health::default()),
|
||||
Box::new(Version::default()),
|
||||
Box::new(Help::default()),
|
||||
Box::new(Explorer::default()),
|
||||
Box::new(Wallet::default()),
|
||||
Box::new(Validator::default()),
|
||||
Box::new(Empty::default()),
|
||||
],
|
||||
should_quite: false,
|
||||
@ -161,6 +148,9 @@ impl App {
|
||||
|
||||
fn trigger_node_fast_events(&mut self) -> Result<()> {
|
||||
self.network_tx.send(Action::GetPendingExtrinsics)?;
|
||||
self.network_tx.send(Action::GetConnectedPeers)?;
|
||||
self.network_tx.send(Action::CheckPendingTransactions)?;
|
||||
self.network_tx.send(Action::GetBlockRange)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -171,6 +161,8 @@ impl App {
|
||||
self.network_tx.send(Action::GetChainName)?;
|
||||
self.network_tx.send(Action::GetChainVersion)?;
|
||||
self.network_tx.send(Action::GetExistentialDeposit)?;
|
||||
self.network_tx.send(Action::GetLocalIdentity)?;
|
||||
self.network_tx.send(Action::GetListenAddresses)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -225,17 +217,9 @@ impl App {
|
||||
|
||||
fn render(&mut self, tui: &mut Tui) -> Result<()> {
|
||||
tui.draw(|frame| {
|
||||
for component in self.components.iter_mut().take(4) {
|
||||
if let Err(err) = (*component).draw(frame, frame.area()) {
|
||||
let _ = self
|
||||
.action_tx
|
||||
.send(Action::Error(format!("failed to draw: {:?}", err)));
|
||||
}
|
||||
}
|
||||
|
||||
match self.mode {
|
||||
Mode::Explorer => {
|
||||
if let Some(component) = self.components.get_mut(4) {
|
||||
if let Some(component) = self.components.get_mut(5) {
|
||||
if let Err(err) = component.draw(frame, frame.area()) {
|
||||
let _ = self
|
||||
.action_tx
|
||||
@ -244,7 +228,16 @@ impl App {
|
||||
}
|
||||
},
|
||||
Mode::Wallet => {
|
||||
if let Some(component) = self.components.get_mut(5) {
|
||||
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)));
|
||||
}
|
||||
}
|
||||
},
|
||||
Mode::Validator => {
|
||||
if let Some(component) = self.components.get_mut(7) {
|
||||
if let Err(err) = component.draw(frame, frame.area()) {
|
||||
let _ = self
|
||||
.action_tx
|
||||
@ -262,6 +255,14 @@ impl App {
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
for component in self.components.iter_mut().take(5) {
|
||||
if let Err(err) = (*component).draw(frame, frame.area()) {
|
||||
let _ = self
|
||||
.action_tx
|
||||
.send(Action::Error(format!("failed to draw: {:?}", err)));
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
use subxt::{
|
||||
Config, blocks::Block, client::OnlineClient,
|
||||
config::{DefaultExtrinsicParams, SubstrateConfig},
|
||||
config::{DefaultExtrinsicParamsBuilder, DefaultExtrinsicParams, SubstrateConfig},
|
||||
};
|
||||
|
||||
/// Default set of commonly used type by Casper nodes.
|
||||
@ -26,9 +26,9 @@ pub type CasperAccountId = subxt::utils::AccountId32;
|
||||
pub type CasperBlock = Block<CasperConfig, OnlineClient<CasperConfig>>;
|
||||
|
||||
/// A struct representing the signed extra and additional parameters required to construct a
|
||||
/// transaction for a polkadot node.
|
||||
/// transaction for a casper node.
|
||||
pub type CasperExtrinsicParams<T> = DefaultExtrinsicParams<T>;
|
||||
|
||||
///// A builder which leads to [`CasperExtrinsicParams`] being constructed. This is what we provide
|
||||
///// to methods like `sign_and_submit()`.
|
||||
//pub type CasperExtrinsicParamsBuilder<T> = DefaultExtrinsicParamsBuilder<T>;
|
||||
/// A builder which leads to [`CasperExtrinsicParams`] being constructed. This is what we provide
|
||||
/// to methods like `sign_and_submit()`.
|
||||
pub type CasperExtrinsicParamsBuilder<T> = DefaultExtrinsicParamsBuilder<T>;
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
use color_eyre::Result;
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
use crossterm::event::{KeyEvent, KeyCode};
|
||||
use ratatui::{
|
||||
layout::{Alignment, Rect},
|
||||
@ -10,13 +9,20 @@ use ratatui::{
|
||||
|
||||
use super::Component;
|
||||
use crate::{
|
||||
components::generic::Activatable,
|
||||
action::Action, app::Mode
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Empty {
|
||||
is_active: bool,
|
||||
action_tx: Option<UnboundedSender<Action>>,
|
||||
}
|
||||
|
||||
impl Activatable for Empty {
|
||||
fn is_active(&self) -> bool { self.is_active }
|
||||
fn is_inactive(&self) -> bool { !self.is_active }
|
||||
fn set_inactive(&mut self) { self.is_active = false; }
|
||||
fn set_active(&mut self) { self.is_active = true; }
|
||||
}
|
||||
|
||||
impl Empty {
|
||||
@ -88,20 +94,15 @@ impl Empty {
|
||||
}
|
||||
|
||||
fn move_out(&mut self) -> Result<Option<Action>> {
|
||||
self.is_active = false;
|
||||
self.set_inactive();
|
||||
Ok(Some(Action::SetActiveScreen(Mode::Menu)))
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for Empty {
|
||||
fn register_action_handler(&mut self, tx: UnboundedSender<Action>) -> Result<()> {
|
||||
self.action_tx = Some(tx);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update(&mut self, action: Action) -> Result<Option<Action>> {
|
||||
match action {
|
||||
Action::SetActiveScreen(Mode::Empty) => self.is_active = true,
|
||||
Action::SetActiveScreen(Mode::Empty) => self.set_active(),
|
||||
_ => {}
|
||||
};
|
||||
Ok(None)
|
||||
@ -109,15 +110,20 @@ impl Component for Empty {
|
||||
|
||||
fn handle_key_event(&mut self, key: KeyEvent) -> Result<Option<Action>> {
|
||||
match key.code {
|
||||
KeyCode::Esc if self.is_active => self.move_out(),
|
||||
KeyCode::Esc if self.is_active() => self.move_out(),
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
|
||||
let screen = super::screen_layout(area);
|
||||
let screen = super::layouts::screen_layout(area);
|
||||
|
||||
let lines = if self.is_active() {
|
||||
self.prepare_active_text()
|
||||
} else {
|
||||
self.prepare_inactive_text()
|
||||
};
|
||||
|
||||
let lines = if self.is_active { self.prepare_active_text() } else { self.prepare_inactive_text() };
|
||||
let lines_len = lines.len() as u16;
|
||||
let padding_top = screen
|
||||
.as_size()
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
use color_eyre::Result;
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
use crossterm::event::KeyEvent;
|
||||
use ratatui::layout::{Constraint, Margin};
|
||||
use ratatui::widgets::{Padding, Scrollbar, ScrollbarOrientation};
|
||||
use ratatui::{
|
||||
@ -9,13 +9,11 @@ use ratatui::{
|
||||
Frame
|
||||
};
|
||||
use subxt::utils::H256;
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
|
||||
use super::{Component, PartialComponent, CurrentTab};
|
||||
use super::{Component, CurrentTab};
|
||||
use crate::{
|
||||
action::Action,
|
||||
config::Config,
|
||||
palette::StylePalette,
|
||||
components::generic::{Activatable, Scrollable, PartialComponent},
|
||||
action::Action, config::Config, palette::StylePalette,
|
||||
};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
@ -26,7 +24,6 @@ struct BlockInfo {
|
||||
|
||||
pub struct BlockExplorer {
|
||||
is_active: bool,
|
||||
action_tx: Option<UnboundedSender<Action>>,
|
||||
blocks: std::collections::VecDeque<BlockInfo>,
|
||||
block_headers: std::collections::HashMap<u32, H256>,
|
||||
block_authors: std::collections::HashMap<H256, String>,
|
||||
@ -41,6 +38,49 @@ impl Default for BlockExplorer {
|
||||
}
|
||||
}
|
||||
|
||||
impl Activatable for BlockExplorer {
|
||||
fn is_active(&self) -> bool { self.is_active }
|
||||
fn is_inactive(&self) -> bool { !self.is_active }
|
||||
fn set_inactive(&mut self) { self.is_active = false; }
|
||||
fn set_active(&mut self) { self.is_active = true; }
|
||||
}
|
||||
|
||||
impl Scrollable for BlockExplorer {
|
||||
type IndexType = usize;
|
||||
|
||||
fn selected_index(&self) -> Option<Self::IndexType> {
|
||||
self.table_state.selected()
|
||||
}
|
||||
|
||||
fn items_length(&self) -> Self::IndexType {
|
||||
self.blocks.len()
|
||||
}
|
||||
|
||||
fn apply_next_row(&mut self, new_index: Self::IndexType) -> Result<Option<Action>> {
|
||||
self.table_state.select(Some(new_index));
|
||||
self.scroll_state = self.scroll_state.position(new_index);
|
||||
self.send_used_explorer_block(new_index)
|
||||
}
|
||||
|
||||
fn apply_prev_row(&mut self, new_index: Self::IndexType) -> Result<Option<Action>> {
|
||||
self.apply_next_row(new_index)
|
||||
}
|
||||
|
||||
fn apply_first_row(&mut self) -> Result<Option<Action>> {
|
||||
match self.items_length() > 0 {
|
||||
true => self.apply_next_row(0),
|
||||
false => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_last_row(&mut self) -> Result<Option<Action>> {
|
||||
match self.items_length().checked_sub(1) {
|
||||
Some(last_idx) => self.apply_next_row(last_idx),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BlockExplorer {
|
||||
const MAX_BLOCKS: usize = 50;
|
||||
|
||||
@ -48,7 +88,6 @@ impl BlockExplorer {
|
||||
Self {
|
||||
is_active: false,
|
||||
blocks: Default::default(),
|
||||
action_tx: None,
|
||||
block_authors: Default::default(),
|
||||
block_headers: Default::default(),
|
||||
scroll_state: ScrollbarState::new(0),
|
||||
@ -61,7 +100,7 @@ impl BlockExplorer {
|
||||
&mut self,
|
||||
hash: H256,
|
||||
block_number: u32,
|
||||
) {
|
||||
) -> Result<Option<Action>> {
|
||||
let front_block_number = self.blocks
|
||||
.front()
|
||||
.map(|block| block.block_number)
|
||||
@ -74,7 +113,7 @@ impl BlockExplorer {
|
||||
});
|
||||
|
||||
self.block_headers.insert(block_number, hash);
|
||||
if self.blocks.len() > Self::MAX_BLOCKS {
|
||||
if self.items_length() > Self::MAX_BLOCKS {
|
||||
if let Some(block) = self.blocks.pop_back() {
|
||||
if let Some(hash) = self.block_headers.remove(&block.block_number) {
|
||||
self.block_authors.remove(&hash);
|
||||
@ -82,19 +121,21 @@ impl BlockExplorer {
|
||||
}
|
||||
}
|
||||
|
||||
self.scroll_state = self.scroll_state.content_length(self.blocks.len());
|
||||
if self.table_state.selected().is_some() {
|
||||
self.next_row();
|
||||
self.scroll_state = self.scroll_state.content_length(self.items_length());
|
||||
return match self.table_state.selected() {
|
||||
Some(_) => self.next_row(),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn update_finalized_block_info(
|
||||
&mut self,
|
||||
header: H256,
|
||||
block_number: u32,
|
||||
) {
|
||||
for idx in 0..self.blocks.len() {
|
||||
) -> Result<Option<Action>> {
|
||||
for idx in 0..self.items_length() {
|
||||
if self.blocks[idx].finalized { break; }
|
||||
else if self.blocks[idx].block_number > block_number { continue; }
|
||||
else {
|
||||
@ -102,86 +143,30 @@ impl BlockExplorer {
|
||||
self.blocks[idx].finalized = true;
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn send_used_explorer_block(&mut self, index: usize) {
|
||||
if let Some(action_tx) = &self.action_tx {
|
||||
let maybe_block_number = self.blocks.get(index).map(|info| info.block_number);
|
||||
let _ = action_tx.send(Action::UsedExplorerBlock(maybe_block_number));
|
||||
}
|
||||
}
|
||||
|
||||
fn first_row(&mut self) {
|
||||
if self.blocks.len() > 0 {
|
||||
self.table_state.select(Some(0));
|
||||
self.scroll_state = self.scroll_state.position(0);
|
||||
self.send_used_explorer_block(0);
|
||||
}
|
||||
}
|
||||
|
||||
fn next_row(&mut self) {
|
||||
let i = match self.table_state.selected() {
|
||||
Some(i) => {
|
||||
if i >= self.blocks.len() - 1 {
|
||||
i
|
||||
} else {
|
||||
i + 1
|
||||
}
|
||||
},
|
||||
None => 0,
|
||||
};
|
||||
self.table_state.select(Some(i));
|
||||
self.scroll_state = self.scroll_state.position(i);
|
||||
self.send_used_explorer_block(i);
|
||||
}
|
||||
|
||||
fn last_row(&mut self) {
|
||||
if self.blocks.len() > 0 {
|
||||
let last = self.blocks.len() - 1;
|
||||
self.table_state.select(Some(last));
|
||||
self.scroll_state = self.scroll_state.position(last);
|
||||
self.send_used_explorer_block(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);
|
||||
self.send_used_explorer_block(i);
|
||||
fn send_used_explorer_block(&mut self, index: usize) -> Result<Option<Action>> {
|
||||
let maybe_block_number = self.blocks.get(index).map(|info| info.block_number);
|
||||
Ok(Some(Action::UsedExplorerBlock(maybe_block_number)))
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialComponent for BlockExplorer {
|
||||
fn set_active(&mut self, current_tab: CurrentTab) {
|
||||
impl PartialComponent<CurrentTab> for BlockExplorer {
|
||||
fn set_active_tab(&mut self, current_tab: CurrentTab) {
|
||||
match current_tab {
|
||||
CurrentTab::Blocks => self.is_active = true,
|
||||
CurrentTab::Extrinsics => self.is_active = false,
|
||||
CurrentTab::Blocks => self.set_active(),
|
||||
CurrentTab::Extrinsics => self.set_inactive(),
|
||||
CurrentTab::Nothing => {
|
||||
self.is_active = false;
|
||||
self.set_inactive();
|
||||
self.table_state.select(None);
|
||||
self.scroll_state = self.scroll_state.position(0);
|
||||
self.send_used_explorer_block(usize::MAX);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for BlockExplorer {
|
||||
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::Explorer) {
|
||||
self.palette.with_normal_style(style.get("normal_style").copied());
|
||||
@ -202,28 +187,23 @@ impl Component for BlockExplorer {
|
||||
Action::FinalizedBlockInformation(header, block_number) => self.update_finalized_block_info(header, block_number),
|
||||
Action::SetBlockAuthor(header, author) => {
|
||||
let _ = self.block_authors.insert(header, author);
|
||||
Ok(None)
|
||||
},
|
||||
_ => {},
|
||||
};
|
||||
Ok(None)
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_key_event(&mut self, key: KeyEvent) -> Result<Option<Action>> {
|
||||
match key.code {
|
||||
KeyCode::Char('k') | KeyCode::Up if self.is_active => self.previous_row(),
|
||||
KeyCode::Char('j') | KeyCode::Down 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)
|
||||
match self.is_active() {
|
||||
true => self.handle_scrollable_key_codes(key.code),
|
||||
false => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
|
||||
let [place, _] = super::explorer_scrollbars_layout(area);
|
||||
let [place, _] = super::layouts::explorer_scrollbars_layout(area);
|
||||
|
||||
let (border_style, border_type) = self.palette.create_border_style(self.is_active);
|
||||
let (border_style, border_type) = self.palette.create_border_style(self.is_active());
|
||||
let default_hash = H256::repeat_byte(69u8);
|
||||
let default_author = "...".to_string();
|
||||
let rows = self.blocks
|
||||
|
||||
@ -7,12 +7,11 @@ use ratatui::{
|
||||
Frame
|
||||
};
|
||||
|
||||
use super::{Component, PartialComponent, CurrentTab};
|
||||
use super::{Component, CurrentTab};
|
||||
use crate::{
|
||||
config::Config,
|
||||
components::generic::PartialComponent,
|
||||
config::Config, action::Action, palette::StylePalette,
|
||||
widgets::{BigText, PixelSize},
|
||||
action::Action,
|
||||
palette::StylePalette,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
@ -37,18 +36,15 @@ impl BlockTicker {
|
||||
}
|
||||
}
|
||||
|
||||
fn block_found(&mut self, block: u32) -> Result<()> {
|
||||
fn block_found(&mut self, block: u32) {
|
||||
if self.last_block < block {
|
||||
self.last_block_time = Instant::now();
|
||||
self.last_block = block;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialComponent for BlockTicker {
|
||||
fn set_active(&mut self, _current_tab: CurrentTab) {}
|
||||
}
|
||||
impl PartialComponent<CurrentTab> for BlockTicker {}
|
||||
|
||||
impl Component for BlockTicker {
|
||||
fn register_config_handler(&mut self, config: Config) -> Result<()> {
|
||||
@ -65,14 +61,14 @@ impl Component for BlockTicker {
|
||||
|
||||
fn update(&mut self, action: Action) -> Result<Option<Action>> {
|
||||
match action {
|
||||
Action::BestBlockUpdated(block) => self.block_found(block)?,
|
||||
Action::BestBlockUpdated(block) => self.block_found(block),
|
||||
_ => {}
|
||||
};
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
|
||||
let [place, _, _] = super::explorer_block_info_layout(area);
|
||||
let [place, _, _] = super::layouts::explorer_block_info_layout(area);
|
||||
|
||||
let passed = (Instant::now() - self.last_block_time).as_secs_f64();
|
||||
let text = if passed < 60.0 {
|
||||
|
||||
@ -6,11 +6,10 @@ use ratatui::{
|
||||
Frame,
|
||||
};
|
||||
|
||||
use super::{Component, PartialComponent, CurrentTab};
|
||||
use super::{Component, CurrentTab};
|
||||
use crate::{
|
||||
config::Config,
|
||||
action::Action,
|
||||
palette::StylePalette,
|
||||
components::generic::PartialComponent,
|
||||
config::Config, action::Action, palette::StylePalette,
|
||||
widgets::{PixelSize, BigText},
|
||||
};
|
||||
|
||||
@ -26,17 +25,9 @@ impl CurrentEpoch {
|
||||
const SESSION_LENGTH: u64 = 2_400;
|
||||
const SECONDS_IN_DAY: u64 = 86_400;
|
||||
const SECONDS_IN_HOUR: u64 = 3_600;
|
||||
|
||||
fn update_epoch_progress(&mut self, number: u64, progress: u64) -> Result<()> {
|
||||
self.number = number;
|
||||
self.progress = progress;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialComponent for CurrentEpoch {
|
||||
fn set_active(&mut self, _current_tab: CurrentTab) {}
|
||||
}
|
||||
impl PartialComponent<CurrentTab> for CurrentEpoch {}
|
||||
|
||||
impl Component for CurrentEpoch {
|
||||
fn register_config_handler(&mut self, config: Config) -> Result<()> {
|
||||
@ -53,15 +44,17 @@ impl Component for CurrentEpoch {
|
||||
|
||||
fn update(&mut self, action: Action) -> Result<Option<Action>> {
|
||||
match action {
|
||||
Action::SetEpochProgress(number, progress) =>
|
||||
self.update_epoch_progress(number, progress)?,
|
||||
Action::SetEpochProgress(number, progress) => {
|
||||
self.number = number;
|
||||
self.progress = progress;
|
||||
},
|
||||
_ => {}
|
||||
};
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
|
||||
let [place, _] = super::explorer_era_info_layout(area);
|
||||
let [place, _] = super::layouts::explorer_era_info_layout(area);
|
||||
|
||||
let seconds_to_next = Self::SESSION_LENGTH.saturating_sub(self.progress) * Self::SECONDS_IN_BLOCK;
|
||||
let hours = (seconds_to_next % Self::SECONDS_IN_DAY) / Self::SECONDS_IN_HOUR;
|
||||
|
||||
@ -6,13 +6,10 @@ use ratatui::{
|
||||
Frame,
|
||||
};
|
||||
|
||||
use super::{Component, PartialComponent, CurrentTab};
|
||||
use super::{Component, CurrentTab};
|
||||
use crate::{
|
||||
config::Config,
|
||||
action::Action,
|
||||
palette::StylePalette,
|
||||
types::EraInfo,
|
||||
widgets::{PixelSize, BigText},
|
||||
config::Config, action::Action, palette::StylePalette, types::EraInfo,
|
||||
components::generic::PartialComponent, widgets::{PixelSize, BigText},
|
||||
};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
@ -28,16 +25,9 @@ impl CurrentEra {
|
||||
const MILLIS_IN_DAY: u64 = 86_400_000;
|
||||
const MILLIS_IN_HOUR: u64 = 3_600_000;
|
||||
const MILLIS_IN_MINUTE: u64 = 60_000;
|
||||
|
||||
fn update_era(&mut self, era_info: EraInfo) -> Result<()> {
|
||||
self.era = era_info;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialComponent for CurrentEra {
|
||||
fn set_active(&mut self, _current_tab: CurrentTab) {}
|
||||
}
|
||||
impl PartialComponent<CurrentTab> for CurrentEra {}
|
||||
|
||||
impl Component for CurrentEra {
|
||||
fn register_config_handler(&mut self, config: Config) -> Result<()> {
|
||||
@ -54,14 +44,14 @@ impl Component for CurrentEra {
|
||||
|
||||
fn update(&mut self, action: Action) -> Result<Option<Action>> {
|
||||
match action {
|
||||
Action::SetActiveEra(era_info) => self.update_era(era_info)?,
|
||||
_ => {}
|
||||
Action::SetActiveEra(era_info) => self.era = era_info,
|
||||
_ => {},
|
||||
};
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
|
||||
let [_, place] = super::explorer_era_info_layout(area);
|
||||
let [_, place] = super::layouts::explorer_era_info_layout(area);
|
||||
|
||||
let current_time = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::usize;
|
||||
use color_eyre::Result;
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
use crossterm::event::KeyEvent;
|
||||
use ratatui::layout::{Constraint, Margin};
|
||||
use ratatui::widgets::{Padding, Scrollbar, ScrollbarOrientation};
|
||||
use ratatui::{
|
||||
@ -10,19 +10,16 @@ use ratatui::{
|
||||
widgets::{Block, ScrollbarState, Cell, Row, Table, TableState},
|
||||
Frame
|
||||
};
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
|
||||
use super::{Component, CurrentTab, PartialComponent};
|
||||
use super::{Component, CurrentTab};
|
||||
use crate::{
|
||||
types::CasperExtrinsicDetails,
|
||||
action::Action,
|
||||
config::Config,
|
||||
palette::StylePalette,
|
||||
components::generic::{Activatable, Scrollable, PartialComponent},
|
||||
types::CasperExtrinsicDetails,
|
||||
action::Action, config::Config, palette::StylePalette,
|
||||
};
|
||||
|
||||
pub struct ExtrinsicExplorer {
|
||||
is_active: bool,
|
||||
action_tx: Option<UnboundedSender<Action>>,
|
||||
extrinsics: HashMap<u32, Vec<CasperExtrinsicDetails>>,
|
||||
current_extrinsics: Option<Vec<CasperExtrinsicDetails>>,
|
||||
block_numbers: VecDeque<u32>,
|
||||
@ -37,13 +34,58 @@ impl Default for ExtrinsicExplorer {
|
||||
}
|
||||
}
|
||||
|
||||
impl Activatable for ExtrinsicExplorer {
|
||||
fn is_active(&self) -> bool { self.is_active }
|
||||
fn is_inactive(&self) -> bool { !self.is_active }
|
||||
fn set_inactive(&mut self) { self.is_active = false; }
|
||||
fn set_active(&mut self) { self.is_active = true; }
|
||||
}
|
||||
|
||||
impl Scrollable for ExtrinsicExplorer {
|
||||
type IndexType = usize;
|
||||
|
||||
fn selected_index(&self) -> Option<Self::IndexType> {
|
||||
self.table_state.selected()
|
||||
}
|
||||
|
||||
fn items_length(&self) -> Self::IndexType {
|
||||
self.current_extrinsics
|
||||
.as_ref()
|
||||
.map(|exts| exts.len())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn apply_next_row(&mut self, new_index: Self::IndexType) -> Result<Option<Action>> {
|
||||
self.table_state.select(Some(new_index));
|
||||
self.scroll_state = self.scroll_state.position(new_index);
|
||||
self.send_used_explorer_log(new_index)
|
||||
}
|
||||
|
||||
fn apply_prev_row(&mut self, new_index: Self::IndexType) -> Result<Option<Action>> {
|
||||
self.apply_next_row(new_index)
|
||||
}
|
||||
|
||||
fn apply_first_row(&mut self) -> Result<Option<Action>> {
|
||||
match self.items_length() > 0 {
|
||||
true => self.apply_next_row(0),
|
||||
false => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_last_row(&mut self) -> Result<Option<Action>> {
|
||||
match self.items_length().checked_sub(1) {
|
||||
Some(last_idx) => self.apply_next_row(last_idx),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ExtrinsicExplorer {
|
||||
const MAX_BLOCKS: usize = 50;
|
||||
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
is_active: false,
|
||||
action_tx: None,
|
||||
current_extrinsics: None,
|
||||
extrinsics: Default::default(),
|
||||
block_numbers: Default::default(),
|
||||
@ -53,6 +95,20 @@ impl ExtrinsicExplorer {
|
||||
}
|
||||
}
|
||||
|
||||
fn update_used_explorer_block(&mut self, block_number: u32) {
|
||||
let maybe_exts = self.extrinsics
|
||||
.get(&block_number)
|
||||
.map(|exts| exts.to_vec());
|
||||
let exts_length = self.extrinsics
|
||||
.get(&block_number)
|
||||
.map(|exts| exts.len())
|
||||
.unwrap_or_default();
|
||||
|
||||
self.current_extrinsics = maybe_exts;
|
||||
self.scroll_state = self.scroll_state.content_length(exts_length);
|
||||
}
|
||||
|
||||
|
||||
fn update_extrinsics_for_header(
|
||||
&mut self,
|
||||
block_number: u32,
|
||||
@ -68,109 +124,30 @@ impl ExtrinsicExplorer {
|
||||
}
|
||||
}
|
||||
|
||||
fn send_used_explorer_log(&mut self, index: usize) {
|
||||
if let Some(action_tx) = &self.action_tx {
|
||||
let maybe_log = self.current_extrinsics
|
||||
.as_ref()
|
||||
.map(|ext| {
|
||||
ext.get(index).map(|ext| {
|
||||
hex::encode(&ext.field_bytes.clone())
|
||||
})
|
||||
})
|
||||
.flatten();
|
||||
let _ = action_tx.send(Action::UsedExplorerLog(maybe_log.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
fn first_row(&mut self) {
|
||||
match &self.current_extrinsics {
|
||||
Some(exts) if exts.len() > 0 => self.table_state.select(Some(0)),
|
||||
_ => self.table_state.select(None),
|
||||
}
|
||||
self.scroll_state = self.scroll_state.position(0);
|
||||
self.send_used_explorer_log(0);
|
||||
}
|
||||
|
||||
fn next_row(&mut self) {
|
||||
match &self.current_extrinsics {
|
||||
Some(exts) => {
|
||||
let i = match self.table_state.selected() {
|
||||
Some(i) => {
|
||||
if i >= exts.len() - 1 {
|
||||
i
|
||||
} else {
|
||||
i + 1
|
||||
}
|
||||
},
|
||||
None => 0,
|
||||
};
|
||||
self.table_state.select(Some(i));
|
||||
self.scroll_state = self.scroll_state.position(i);
|
||||
self.send_used_explorer_log(i);
|
||||
},
|
||||
None => {
|
||||
self.table_state.select(None);
|
||||
self.scroll_state = self.scroll_state.position(0);
|
||||
self.send_used_explorer_log(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn last_row(&mut self) {
|
||||
match &self.current_extrinsics {
|
||||
Some(exts) => {
|
||||
let last = exts.len().saturating_sub(1);
|
||||
self.table_state.select(Some(last));
|
||||
self.scroll_state = self.scroll_state.position(last);
|
||||
self.send_used_explorer_log(last);
|
||||
},
|
||||
None => {
|
||||
self.table_state.select(None);
|
||||
self.scroll_state = self.scroll_state.position(0);
|
||||
self.send_used_explorer_log(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn previous_row(&mut self) {
|
||||
match &self.current_extrinsics {
|
||||
Some(_) => {
|
||||
let i = self.table_state
|
||||
.selected()
|
||||
.map(|i| i.saturating_sub(1))
|
||||
.unwrap_or_default();
|
||||
self.table_state.select(Some(i));
|
||||
self.scroll_state = self.scroll_state.position(i);
|
||||
self.send_used_explorer_log(i);
|
||||
},
|
||||
None => {
|
||||
self.table_state.select(None);
|
||||
self.scroll_state = self.scroll_state.position(0);
|
||||
self.send_used_explorer_log(0);
|
||||
}
|
||||
}
|
||||
fn send_used_explorer_log(&mut self, index: usize) -> Result<Option<Action>> {
|
||||
let maybe_log = self.current_extrinsics
|
||||
.as_ref()
|
||||
.map(|ext| ext.get(index).map(|ext| {
|
||||
hex::encode(&ext.field_bytes.clone())
|
||||
}))
|
||||
.flatten();
|
||||
Ok(Some(Action::UsedExplorerLog(maybe_log.clone())))
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialComponent for ExtrinsicExplorer {
|
||||
fn set_active(&mut self, current_tab: CurrentTab) {
|
||||
impl PartialComponent<CurrentTab> for ExtrinsicExplorer {
|
||||
fn set_active_tab(&mut self, current_tab: CurrentTab) {
|
||||
if current_tab == CurrentTab::Extrinsics {
|
||||
self.is_active = true;
|
||||
self.set_active();
|
||||
} else {
|
||||
self.is_active = false;
|
||||
self.set_inactive();
|
||||
self.table_state.select(None);
|
||||
self.scroll_state = self.scroll_state.position(0);
|
||||
self.send_used_explorer_log(usize::MAX);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for ExtrinsicExplorer {
|
||||
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::Explorer) {
|
||||
self.palette.with_normal_style(style.get("normal_style").copied());
|
||||
@ -187,16 +164,8 @@ impl Component for ExtrinsicExplorer {
|
||||
|
||||
fn update(&mut self, action: Action) -> Result<Option<Action>> {
|
||||
match action {
|
||||
Action::UsedExplorerBlock(maybe_block_number) => {
|
||||
let block_number = maybe_block_number.unwrap_or_default();
|
||||
if let Some(exts) = self.extrinsics.get(&block_number) {
|
||||
self.current_extrinsics = Some(exts.to_vec());
|
||||
self.scroll_state = self.scroll_state.content_length(exts.len());
|
||||
} else {
|
||||
self.current_extrinsics = None;
|
||||
self.scroll_state = self.scroll_state.content_length(0);
|
||||
}
|
||||
},
|
||||
Action::UsedExplorerBlock(maybe_block_number) =>
|
||||
self.update_used_explorer_block(maybe_block_number.unwrap_or_default()),
|
||||
Action::ExtrinsicsForBlock(block_number, extrinsics) =>
|
||||
self.update_extrinsics_for_header(block_number, extrinsics),
|
||||
_ => {},
|
||||
@ -205,21 +174,15 @@ impl Component for ExtrinsicExplorer {
|
||||
}
|
||||
|
||||
fn handle_key_event(&mut self, key: KeyEvent) -> Result<Option<Action>> {
|
||||
match key.code {
|
||||
KeyCode::Char('k') | KeyCode::Up if self.is_active => self.previous_row(),
|
||||
KeyCode::Char('j') | KeyCode::Down 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)
|
||||
match self.is_active() {
|
||||
true => self.handle_scrollable_key_codes(key.code),
|
||||
false => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
|
||||
let [_, place] = super::explorer_scrollbars_layout(area);
|
||||
|
||||
let (border_style, border_type) = self.palette.create_border_style(self.is_active);
|
||||
let [_, place] = super::layouts::explorer_scrollbars_layout(area);
|
||||
let (border_style, border_type) = self.palette.create_border_style(self.is_active());
|
||||
|
||||
let mut longest_pallet_name_length = 0;
|
||||
let mut longest_variant_name_length = 0;
|
||||
|
||||
@ -7,8 +7,11 @@ use ratatui::{
|
||||
Frame,
|
||||
};
|
||||
|
||||
use super::{Component, PartialComponent, CurrentTab};
|
||||
use crate::{palette::StylePalette, config::Config, action::Action};
|
||||
use super::{Component, CurrentTab};
|
||||
use crate::{
|
||||
components::generic::PartialComponent,
|
||||
palette::StylePalette, config::Config, action::Action,
|
||||
};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ExtrinsicsChart {
|
||||
@ -70,9 +73,7 @@ impl ExtrinsicsChart {
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialComponent for ExtrinsicsChart {
|
||||
fn set_active(&mut self, _current_tab: CurrentTab) {}
|
||||
}
|
||||
impl PartialComponent<CurrentTab> for ExtrinsicsChart {}
|
||||
|
||||
impl Component for ExtrinsicsChart {
|
||||
fn register_config_handler(&mut self, config: Config) -> Result<()> {
|
||||
@ -96,7 +97,7 @@ impl Component for ExtrinsicsChart {
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
|
||||
let [_, place] = super::explorer_header_layout(area);
|
||||
let [_, place] = super::layouts::explorer_header_layout(area);
|
||||
frame.render_widget(self.extrinsics_bar_chart(place.as_size().width), place);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -5,11 +5,10 @@ use ratatui::{
|
||||
Frame,
|
||||
};
|
||||
|
||||
use super::{Component, PartialComponent, CurrentTab};
|
||||
use super::{Component, CurrentTab};
|
||||
use crate::{
|
||||
config::Config,
|
||||
action::Action,
|
||||
palette::StylePalette,
|
||||
components::generic::PartialComponent,
|
||||
config::Config, action::Action, palette::StylePalette,
|
||||
widgets::{PixelSize, BigText},
|
||||
};
|
||||
|
||||
@ -26,9 +25,7 @@ impl FinalizedBlock {
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialComponent for FinalizedBlock {
|
||||
fn set_active(&mut self, _current_tab: CurrentTab) {}
|
||||
}
|
||||
impl PartialComponent<CurrentTab> for FinalizedBlock {}
|
||||
|
||||
impl Component for FinalizedBlock {
|
||||
fn register_config_handler(&mut self, config: Config) -> Result<()> {
|
||||
@ -52,7 +49,7 @@ impl Component for FinalizedBlock {
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
|
||||
let [_, _, place] = super::explorer_block_info_layout(area);
|
||||
let [_, _, place] = super::layouts::explorer_block_info_layout(area);
|
||||
|
||||
let text = self.number.to_string();
|
||||
let (border_style, border_type) = self.palette.create_border_style(false);
|
||||
|
||||
@ -5,11 +5,10 @@ use ratatui::{
|
||||
Frame,
|
||||
};
|
||||
|
||||
use super::{Component, PartialComponent, CurrentTab};
|
||||
use super::{Component, CurrentTab};
|
||||
use crate::{
|
||||
config::Config,
|
||||
action::Action,
|
||||
palette::StylePalette,
|
||||
components::generic::PartialComponent,
|
||||
config::Config, action::Action, palette::StylePalette,
|
||||
widgets::{PixelSize, BigText},
|
||||
};
|
||||
|
||||
@ -26,9 +25,7 @@ impl LatestBlock {
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialComponent for LatestBlock {
|
||||
fn set_active(&mut self, _current_tab: CurrentTab) {}
|
||||
}
|
||||
impl PartialComponent<CurrentTab> for LatestBlock {}
|
||||
|
||||
impl Component for LatestBlock {
|
||||
fn register_config_handler(&mut self, config: Config) -> Result<()> {
|
||||
@ -52,7 +49,7 @@ impl Component for LatestBlock {
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
|
||||
let [_, place, _] = super::explorer_block_info_layout(area);
|
||||
let [_, place, _] = super::layouts::explorer_block_info_layout(area);
|
||||
|
||||
let text = self.number.to_string();
|
||||
let (border_style, border_type) = self.palette.create_border_style(false);
|
||||
|
||||
50
src/components/explorer/layouts.rs
Normal file
50
src/components/explorer/layouts.rs
Normal file
@ -0,0 +1,50 @@
|
||||
use ratatui::layout::{Constraint, Flex, Layout, Rect};
|
||||
|
||||
pub fn explorer_layout(area: Rect) -> [Rect; 3] {
|
||||
Layout::vertical([
|
||||
Constraint::Percentage(30),
|
||||
Constraint::Percentage(40),
|
||||
Constraint::Percentage(30),
|
||||
]).areas(area)
|
||||
}
|
||||
|
||||
pub fn explorer_header_layout(area: Rect) -> [Rect; 2] {
|
||||
let [header, _, _] = explorer_layout(area);
|
||||
Layout::horizontal([
|
||||
Constraint::Percentage(50),
|
||||
Constraint::Percentage(50),
|
||||
]).areas(header)
|
||||
}
|
||||
|
||||
pub fn explorer_info_layout(area: Rect) -> [Rect; 2] {
|
||||
let [info, _] = explorer_header_layout(area);
|
||||
Layout::vertical([
|
||||
Constraint::Percentage(100),
|
||||
Constraint::Percentage(100),
|
||||
]).areas(info)
|
||||
}
|
||||
|
||||
pub fn explorer_block_info_layout(area: Rect) -> [Rect; 3] {
|
||||
let [blocks, _] = explorer_info_layout(area);
|
||||
Layout::horizontal([
|
||||
Constraint::Percentage(33),
|
||||
Constraint::Percentage(33),
|
||||
Constraint::Percentage(33),
|
||||
]).flex(Flex::SpaceBetween).areas(blocks)
|
||||
}
|
||||
|
||||
pub fn explorer_era_info_layout(area: Rect) -> [Rect; 2] {
|
||||
let [_, blocks] = explorer_info_layout(area);
|
||||
Layout::horizontal([
|
||||
Constraint::Percentage(50),
|
||||
Constraint::Percentage(50),
|
||||
]).flex(Flex::SpaceBetween).areas(blocks)
|
||||
}
|
||||
|
||||
pub fn explorer_scrollbars_layout(area: Rect) -> [Rect; 2] {
|
||||
let [_, place, _] = explorer_layout(area);
|
||||
Layout::horizontal([
|
||||
Constraint::Percentage(50),
|
||||
Constraint::Percentage(50),
|
||||
]).areas(place)
|
||||
}
|
||||
@ -6,38 +6,19 @@ use ratatui::{
|
||||
Frame
|
||||
};
|
||||
|
||||
use super::{Component, CurrentTab, PartialComponent};
|
||||
use super::{Component, CurrentTab};
|
||||
use crate::{
|
||||
action::Action,
|
||||
config::Config,
|
||||
palette::StylePalette,
|
||||
components::generic::PartialComponent,
|
||||
action::Action, config::Config, palette::StylePalette
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct LogExplorer {
|
||||
is_active: bool,
|
||||
maybe_log: Option<String>,
|
||||
palette: StylePalette,
|
||||
}
|
||||
|
||||
impl Default for LogExplorer {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl LogExplorer {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
is_active: false,
|
||||
maybe_log: None,
|
||||
palette: StylePalette::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialComponent for LogExplorer {
|
||||
fn set_active(&mut self, _current_tab: CurrentTab) {}
|
||||
}
|
||||
impl PartialComponent<CurrentTab> for LogExplorer {}
|
||||
|
||||
impl Component for LogExplorer {
|
||||
fn register_config_handler(&mut self, config: Config) -> Result<()> {
|
||||
@ -58,9 +39,9 @@ impl Component for LogExplorer {
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
|
||||
let [_, _, place] = super::explorer_layout(area);
|
||||
let [_, _, place] = super::layouts::explorer_layout(area);
|
||||
|
||||
let (border_style, border_type) = self.palette.create_border_style(self.is_active);
|
||||
let (border_style, border_type) = self.palette.create_border_style(false);
|
||||
|
||||
let line = match &self.maybe_log {
|
||||
Some(log) => Line::from(hex::encode(log)),
|
||||
|
||||
@ -1,14 +1,15 @@
|
||||
use color_eyre::Result;
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
use ratatui::{
|
||||
layout::{Constraint, Flex, Layout, Rect},
|
||||
Frame,
|
||||
};
|
||||
use ratatui::{layout::Rect, Frame};
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
|
||||
use super::Component;
|
||||
use super::{
|
||||
generic::{Activatable, PartialComponent},
|
||||
Component,
|
||||
};
|
||||
use crate::{action::Action, app::Mode, config::Config};
|
||||
|
||||
pub mod layouts;
|
||||
mod latest_block;
|
||||
mod finalized_block;
|
||||
mod block_ticker;
|
||||
@ -29,21 +30,17 @@ use block_explorer::BlockExplorer;
|
||||
use extrinsic_explorer::ExtrinsicExplorer;
|
||||
use log_explorer::LogExplorer;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum CurrentTab {
|
||||
Nothing,
|
||||
Blocks,
|
||||
Extrinsics,
|
||||
}
|
||||
|
||||
pub trait PartialComponent: Component {
|
||||
fn set_active(&mut self, current_tab: CurrentTab);
|
||||
}
|
||||
|
||||
pub struct Explorer {
|
||||
is_active: bool,
|
||||
current_tab: CurrentTab,
|
||||
components: Vec<Box<dyn PartialComponent>>
|
||||
components: Vec<Box<dyn PartialComponent<CurrentTab>>>
|
||||
}
|
||||
|
||||
impl Default for Explorer {
|
||||
@ -82,10 +79,15 @@ impl Explorer {
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialComponent for Explorer {
|
||||
fn set_active(&mut self, _current_tab: CurrentTab) {}
|
||||
impl Activatable for Explorer {
|
||||
fn is_active(&self) -> bool { self.is_active }
|
||||
fn is_inactive(&self) -> bool { !self.is_active }
|
||||
fn set_inactive(&mut self) { self.is_active = false; }
|
||||
fn set_active(&mut self) { self.is_active = true; }
|
||||
}
|
||||
|
||||
impl PartialComponent<CurrentTab> for Explorer {}
|
||||
|
||||
impl Component for Explorer {
|
||||
fn register_action_handler(&mut self, tx: UnboundedSender<Action>) -> Result<()> {
|
||||
for component in self.components.iter_mut() {
|
||||
@ -104,31 +106,35 @@ impl Component for Explorer {
|
||||
}
|
||||
|
||||
fn handle_key_event(&mut self, key: KeyEvent) -> Result<Option<Action>> {
|
||||
if !self.is_active { return Ok(None); }
|
||||
if self.is_inactive() { return Ok(None); }
|
||||
|
||||
match key.code {
|
||||
KeyCode::Esc => {
|
||||
self.is_active = false;
|
||||
self.set_inactive();
|
||||
self.current_tab = CurrentTab::Nothing;
|
||||
for component in self.components.iter_mut() {
|
||||
component.set_active(self.current_tab.clone());
|
||||
component.set_active_tab(self.current_tab);
|
||||
}
|
||||
return Ok(Some(Action::SetActiveScreen(Mode::Menu)));
|
||||
},
|
||||
KeyCode::Enter | KeyCode::Char('l') | KeyCode::Right => {
|
||||
self.move_right();
|
||||
for component in self.components.iter_mut() {
|
||||
component.set_active(self.current_tab.clone());
|
||||
component.set_active_tab(self.current_tab);
|
||||
}
|
||||
},
|
||||
KeyCode::Char('h') | KeyCode::Left => {
|
||||
self.move_left();
|
||||
for component in self.components.iter_mut() {
|
||||
component.set_active(self.current_tab.clone());
|
||||
component.set_active_tab(self.current_tab);
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
for component in self.components.iter_mut() {
|
||||
component.handle_key_event(key)?;
|
||||
let maybe_action = component.handle_key_event(key);
|
||||
if let Ok(Some(_)) = maybe_action {
|
||||
return maybe_action;
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
@ -137,10 +143,10 @@ impl Component for Explorer {
|
||||
|
||||
fn update(&mut self, action: Action) -> Result<Option<Action>> {
|
||||
if let Action::SetActiveScreen(Mode::Explorer) = action {
|
||||
self.is_active = true;
|
||||
self.set_active();
|
||||
self.current_tab = CurrentTab::Blocks;
|
||||
for component in self.components.iter_mut() {
|
||||
component.set_active(self.current_tab.clone());
|
||||
component.set_active_tab(self.current_tab);
|
||||
}
|
||||
}
|
||||
for component in self.components.iter_mut() {
|
||||
@ -150,59 +156,10 @@ impl Component for Explorer {
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
|
||||
let screen = super::screen_layout(area);
|
||||
let screen = super::layouts::screen_layout(area);
|
||||
for component in self.components.iter_mut() {
|
||||
component.draw(frame, screen)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn explorer_layout(area: Rect) -> [Rect; 3] {
|
||||
Layout::vertical([
|
||||
Constraint::Percentage(30),
|
||||
Constraint::Percentage(40),
|
||||
Constraint::Percentage(30),
|
||||
]).areas(area)
|
||||
}
|
||||
|
||||
pub fn explorer_header_layout(area: Rect) -> [Rect; 2] {
|
||||
let [header, _, _] = explorer_layout(area);
|
||||
Layout::horizontal([
|
||||
Constraint::Percentage(50),
|
||||
Constraint::Percentage(50),
|
||||
]).areas(header)
|
||||
}
|
||||
|
||||
pub fn explorer_info_layout(area: Rect) -> [Rect; 2] {
|
||||
let [info, _] = explorer_header_layout(area);
|
||||
Layout::vertical([
|
||||
Constraint::Percentage(100),
|
||||
Constraint::Percentage(100),
|
||||
]).areas(info)
|
||||
}
|
||||
|
||||
pub fn explorer_block_info_layout(area: Rect) -> [Rect; 3] {
|
||||
let [blocks, _] = explorer_info_layout(area);
|
||||
Layout::horizontal([
|
||||
Constraint::Percentage(33),
|
||||
Constraint::Percentage(33),
|
||||
Constraint::Percentage(33),
|
||||
]).flex(Flex::SpaceBetween).areas(blocks)
|
||||
}
|
||||
|
||||
pub fn explorer_era_info_layout(area: Rect) -> [Rect; 2] {
|
||||
let [_, blocks] = explorer_info_layout(area);
|
||||
Layout::horizontal([
|
||||
Constraint::Percentage(50),
|
||||
Constraint::Percentage(50),
|
||||
]).flex(Flex::SpaceBetween).areas(blocks)
|
||||
}
|
||||
|
||||
pub fn explorer_scrollbars_layout(area: Rect) -> [Rect; 2] {
|
||||
let [_, place, _] = explorer_layout(area);
|
||||
Layout::horizontal([
|
||||
Constraint::Percentage(50),
|
||||
Constraint::Percentage(50),
|
||||
]).areas(place)
|
||||
}
|
||||
|
||||
@ -75,10 +75,10 @@ impl Component for FpsCounter {
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
|
||||
let [_, place] = super::header_layout(area);
|
||||
let [_, place] = super::layouts::header_layout(area);
|
||||
|
||||
let message = format!(
|
||||
"{:.2} ticks/sec | {:.2} FPS",
|
||||
" {:.2} ticks/sec | {:.2} FPS",
|
||||
self.ticks_per_second,
|
||||
self.frames_per_second
|
||||
);
|
||||
|
||||
6
src/components/generic/activatable.rs
Normal file
6
src/components/generic/activatable.rs
Normal file
@ -0,0 +1,6 @@
|
||||
pub trait Activatable {
|
||||
fn is_active(&self) -> bool;
|
||||
fn is_inactive(&self) -> bool;
|
||||
fn set_active(&mut self);
|
||||
fn set_inactive(&mut self);
|
||||
}
|
||||
11
src/components/generic/helpable.rs
Normal file
11
src/components/generic/helpable.rs
Normal file
@ -0,0 +1,11 @@
|
||||
use color_eyre::Result;
|
||||
use crate::action::Action;
|
||||
|
||||
use super::Activatable;
|
||||
|
||||
pub trait Helpable: Activatable {
|
||||
fn open_help_popup(&mut self) -> Result<Option<Action>> {
|
||||
self.set_inactive();
|
||||
Ok(Some(Action::Help))
|
||||
}
|
||||
}
|
||||
9
src/components/generic/mod.rs
Normal file
9
src/components/generic/mod.rs
Normal file
@ -0,0 +1,9 @@
|
||||
mod activatable;
|
||||
mod helpable;
|
||||
mod scrollable;
|
||||
mod partial;
|
||||
|
||||
pub use activatable::Activatable;
|
||||
pub use helpable::Helpable;
|
||||
pub use scrollable::Scrollable;
|
||||
pub use partial::PartialComponent;
|
||||
8
src/components/generic/partial.rs
Normal file
8
src/components/generic/partial.rs
Normal file
@ -0,0 +1,8 @@
|
||||
use crate::components::Component;
|
||||
|
||||
pub trait PartialComponent<TabType>: Component
|
||||
where
|
||||
TabType: std::fmt::Debug + Clone + Copy + PartialEq,
|
||||
{
|
||||
fn set_active_tab(&mut self, _current_tab: TabType) {}
|
||||
}
|
||||
86
src/components/generic/scrollable.rs
Normal file
86
src/components/generic/scrollable.rs
Normal file
@ -0,0 +1,86 @@
|
||||
use std::{
|
||||
cmp::{PartialEq, PartialOrd}, ops::{Add, Sub}
|
||||
};
|
||||
use crossterm::event::KeyCode;
|
||||
|
||||
use color_eyre::Result;
|
||||
use crate::action::Action;
|
||||
|
||||
pub trait Scrollable {
|
||||
type IndexType: Add<Output = Self::IndexType>
|
||||
+ Sub<Output = Self::IndexType>
|
||||
+ PartialOrd
|
||||
+ PartialEq
|
||||
+ From<u8>
|
||||
+ Copy;
|
||||
|
||||
fn selected_index(&self) -> Option<Self::IndexType>;
|
||||
fn items_length(&self) -> Self::IndexType;
|
||||
|
||||
fn apply_next_row(&mut self, _new_index: Self::IndexType) -> Result<Option<Action>> { Ok(None) }
|
||||
fn apply_prev_row(&mut self, _new_index: Self::IndexType) -> Result<Option<Action>> { Ok(None) }
|
||||
fn apply_first_row(&mut self) -> Result<Option<Action>> { Ok(None) }
|
||||
fn apply_last_row(&mut self) -> Result<Option<Action>> { Ok(None) }
|
||||
|
||||
|
||||
fn next_row(&mut self) -> Result<Option<Action>> {
|
||||
let length = self.items_length();
|
||||
if length == 0.into() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let new_index = match self.selected_index() {
|
||||
Some(index) => {
|
||||
if index < length - 1.into() {
|
||||
index + 1.into()
|
||||
} else {
|
||||
index
|
||||
}
|
||||
},
|
||||
None => 0.into(),
|
||||
};
|
||||
self.apply_next_row(new_index)
|
||||
}
|
||||
|
||||
fn prev_row(&mut self) -> Result<Option<Action>> {
|
||||
if self.items_length() == 0.into() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let new_index = match self.selected_index() {
|
||||
Some(index) => {
|
||||
if index == 0.into() {
|
||||
0.into()
|
||||
} else {
|
||||
index - 1.into()
|
||||
}
|
||||
}
|
||||
None => 0.into()
|
||||
};
|
||||
self.apply_prev_row(new_index)
|
||||
}
|
||||
|
||||
fn first_row(&mut self) -> Result<Option<Action>> {
|
||||
if self.items_length() == 0.into() {
|
||||
return Ok(None);
|
||||
}
|
||||
self.apply_first_row()
|
||||
}
|
||||
|
||||
fn last_row(&mut self) -> Result<Option<Action>> {
|
||||
if self.items_length() == 0.into() {
|
||||
return Ok(None);
|
||||
}
|
||||
self.apply_last_row()
|
||||
}
|
||||
|
||||
fn handle_scrollable_key_codes(&mut self, key_code: KeyCode) -> Result<Option<Action>> {
|
||||
match key_code {
|
||||
KeyCode::Char('k') | KeyCode::Up => self.prev_row(),
|
||||
KeyCode::Char('j') | KeyCode::Down => self.next_row(),
|
||||
KeyCode::Char('g') => self.first_row(),
|
||||
KeyCode::Char('G') => self.last_row(),
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -20,6 +20,8 @@ pub struct Health {
|
||||
is_syncing: bool,
|
||||
should_have_peers: bool,
|
||||
tx_pool_length: usize,
|
||||
validators_count: u32,
|
||||
nominators_count: u32,
|
||||
}
|
||||
|
||||
impl Default for Health {
|
||||
@ -36,6 +38,8 @@ impl Health {
|
||||
is_syncing: true,
|
||||
should_have_peers: false,
|
||||
tx_pool_length: 0,
|
||||
validators_count: 0,
|
||||
nominators_count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
@ -48,18 +52,16 @@ impl Health {
|
||||
}
|
||||
|
||||
pub fn peers_as_string(&self) -> String {
|
||||
if self.peers.is_some() {
|
||||
self.peers.unwrap().to_string()
|
||||
} else {
|
||||
DotSpinner::default().to_string()
|
||||
match self.peers {
|
||||
Some(peers) => peers.to_string(),
|
||||
None => DotSpinner::default().to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name_as_string(&self) -> String {
|
||||
if self.name.is_some() {
|
||||
self.name.clone().unwrap()
|
||||
} else {
|
||||
OghamCenter::default().to_string()
|
||||
match &self.name {
|
||||
Some(name) => name.clone(),
|
||||
None => OghamCenter::default().to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -74,19 +76,23 @@ impl Component for Health {
|
||||
},
|
||||
Action::SetNodeName(name) => self.name = name,
|
||||
Action::SetPendingExtrinsicsLength(length) => self.tx_pool_length = length,
|
||||
Action::NominatorsNumber(number) => self.nominators_count = number,
|
||||
Action::ValidatorsNumber(number) => self.validators_count = number,
|
||||
_ => {}
|
||||
};
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
|
||||
let [place, _] = super::header_layout(area);
|
||||
let [place, _] = super::layouts::header_layout(area);
|
||||
|
||||
let message = format!("{:^12} | tx.pool: {:^3} | peers: {:^3} | {:^9}",
|
||||
let message = format!("{:^12} | tx.pool: {:^3} | peers: {:^3} | {:^9} | validators {:^4} | nominators {:^4} |",
|
||||
self.name_as_string(),
|
||||
self.tx_pool_length,
|
||||
self.peers_as_string(),
|
||||
self.is_syncing_as_string());
|
||||
self.is_syncing_as_string(),
|
||||
self.validators_count,
|
||||
self.nominators_count);
|
||||
|
||||
let span = Span::styled(message, Style::new().dim());
|
||||
let paragraph = Paragraph::new(span).left_aligned();
|
||||
|
||||
202
src/components/help.rs
Normal file
202
src/components/help.rs
Normal file
@ -0,0 +1,202 @@
|
||||
use color_eyre::Result;
|
||||
use crossterm::event::{KeyEvent, KeyCode};
|
||||
use ratatui::{
|
||||
layout::{Alignment, Constraint, Flex, Layout, Margin, Rect},
|
||||
style::{palette::tailwind, Style, Modifier},
|
||||
text::Text,
|
||||
widgets::{
|
||||
Block, BorderType, Cell, Clear, HighlightSpacing, Paragraph, Row,
|
||||
Scrollbar, ScrollbarOrientation, ScrollbarState, Table, TableState,
|
||||
},
|
||||
Frame
|
||||
};
|
||||
|
||||
use super::palette::StylePalette;
|
||||
use super::generic::{Activatable, Helpable, Scrollable};
|
||||
use super::Component;
|
||||
|
||||
use crate::{action::Action, app::Mode, config::Config};
|
||||
|
||||
const ITEM_HEIGHT: usize = 3;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Help {
|
||||
is_active: bool,
|
||||
current_mode: Mode,
|
||||
palette: StylePalette,
|
||||
scroll_state: ScrollbarState,
|
||||
table_state: TableState,
|
||||
}
|
||||
|
||||
impl Help {
|
||||
fn move_index(&mut self, new_index: usize) -> Result<Option<Action>> {
|
||||
self.table_state.select(Some(new_index));
|
||||
self.scroll_state = self.scroll_state.position(new_index * ITEM_HEIGHT);
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Help {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
is_active: false,
|
||||
current_mode: Mode::Menu,
|
||||
palette: Default::default(),
|
||||
scroll_state: ScrollbarState::new((Mode::Menu.get_help_data().len() - 1) * ITEM_HEIGHT),
|
||||
table_state: TableState::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Activatable for Help {
|
||||
fn is_active(&self) -> bool { self.is_active }
|
||||
fn is_inactive(&self) -> bool { !self.is_active }
|
||||
fn set_inactive(&mut self) { self.is_active = false; }
|
||||
fn set_active(&mut self) { self.is_active = true; }
|
||||
}
|
||||
|
||||
impl Helpable for Help { }
|
||||
|
||||
impl Scrollable for Help {
|
||||
type IndexType = usize;
|
||||
|
||||
fn selected_index(&self) -> Option<Self::IndexType> {
|
||||
self.table_state.selected()
|
||||
}
|
||||
|
||||
fn items_length(&self) -> Self::IndexType {
|
||||
self.current_mode.get_help_data().len()
|
||||
}
|
||||
|
||||
fn apply_next_row(&mut self, new_index: Self::IndexType) -> Result<Option<Action>> {
|
||||
self.move_index(new_index)
|
||||
}
|
||||
|
||||
fn apply_prev_row(&mut self, new_index: Self::IndexType) -> Result<Option<Action>> {
|
||||
self.move_index(new_index)
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for Help {
|
||||
fn register_config_handler(&mut self, config: Config) -> Result<()> {
|
||||
if let Some(style) = config.styles.get(&crate::app::Mode::Help) {
|
||||
self.palette.with_normal_style(style.get("normal_style").copied());
|
||||
self.palette.with_normal_border_style(style.get("normal_border_style").copied());
|
||||
self.palette.with_normal_title_style(style.get("normal_title_style").copied());
|
||||
self.palette.with_popup_style(style.get("popup_style").copied());
|
||||
self.palette.with_popup_title_style(style.get("popup_title_style").copied());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update(&mut self, action: Action) -> Result<Option<Action>> {
|
||||
match action {
|
||||
Action::Help if self.is_inactive() => self.set_active(),
|
||||
Action::SetActiveScreen(mode) => {
|
||||
if self.current_mode != mode {
|
||||
self.current_mode = mode;
|
||||
self.table_state.select(None);
|
||||
self.scroll_state = ScrollbarState::new((self.current_mode.get_help_data().len() - 1) * ITEM_HEIGHT);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn handle_key_event(&mut self, key: KeyEvent) -> Result<Option<Action>> {
|
||||
match key.code {
|
||||
KeyCode::Up | KeyCode::Char('k') if self.is_active() => self.next_row(),
|
||||
KeyCode::Down | KeyCode::Char('j') if self.is_active() => self.prev_row(),
|
||||
KeyCode::Esc if self.is_active() => {
|
||||
self.set_inactive();
|
||||
Ok(Some(Action::SetActiveScreen(self.current_mode)))
|
||||
},
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
|
||||
if self.is_active() {
|
||||
let highlight_symbol = Text::from(vec![
|
||||
"".into(),
|
||||
" █ ".into(),
|
||||
"".into(),
|
||||
]);
|
||||
|
||||
let table = Table::new(
|
||||
self.current_mode
|
||||
.get_help_data()
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, content)| {
|
||||
let color = match i % 2 {
|
||||
0 => tailwind::SLATE.c950,
|
||||
_ => tailwind::SLATE.c900,
|
||||
};
|
||||
|
||||
Row::from(content
|
||||
.into_iter()
|
||||
.map(|data| Cell::from(Text::from(*data)))
|
||||
.collect::<Row>()
|
||||
.style(Style::default().fg(tailwind::BLUE.c200).bg(color))
|
||||
.height(ITEM_HEIGHT as u16))
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
self.current_mode.get_help_constraints()
|
||||
)
|
||||
.header(self.current_mode
|
||||
.get_help_headers()
|
||||
.into_iter()
|
||||
.map(|h| Cell::from(Text::from(*h)))
|
||||
.collect::<Row>()
|
||||
.style(Style::default()
|
||||
.fg(tailwind::SLATE.c200)
|
||||
.bg(tailwind::BLUE.c900)
|
||||
)
|
||||
.height(1)
|
||||
)
|
||||
.highlight_style(Style::default()
|
||||
.add_modifier(Modifier::REVERSED)
|
||||
.fg(tailwind::BLUE.c400)
|
||||
.bg(tailwind::YELLOW.c200))
|
||||
.highlight_spacing(HighlightSpacing::Always)
|
||||
.highlight_symbol(highlight_symbol)
|
||||
.block(Block::default()
|
||||
.style(Style::default().bg(tailwind::SLATE.c950))
|
||||
.title_alignment(Alignment::Right)
|
||||
.title_style(tailwind::YELLOW.c200)
|
||||
.title(self.current_mode.get_help_title()));
|
||||
|
||||
let scrollbar = Scrollbar::default()
|
||||
.orientation(ScrollbarOrientation::VerticalRight)
|
||||
.begin_symbol(None)
|
||||
.end_symbol(None);
|
||||
|
||||
let footer = Paragraph::new(Text::from(self.current_mode.get_help_text()))
|
||||
.style(Style::default().fg(tailwind::SLATE.c200).bg(tailwind::SLATE.c950))
|
||||
.centered()
|
||||
.block(Block::bordered()
|
||||
.border_type(BorderType::Double)
|
||||
.border_style(Style::default().fg(tailwind::BLUE.c400))
|
||||
);
|
||||
|
||||
let v = Layout::vertical([Constraint::Length(23)]).flex(Flex::Center);
|
||||
let h = Layout::horizontal([Constraint::Length(65)]).flex(Flex::Center);
|
||||
|
||||
let [area] = v.areas(area);
|
||||
let [area] = h.areas(area);
|
||||
|
||||
let [main_area, footer_area] = Layout::vertical([
|
||||
Constraint::Fill(1),
|
||||
Constraint::Length(3),
|
||||
]).areas(area);
|
||||
|
||||
frame.render_widget(Clear, area);
|
||||
frame.render_stateful_widget(table, main_area, &mut self.table_state);
|
||||
frame.render_stateful_widget(scrollbar, main_area.inner(Margin::new(1, 2)), &mut self.scroll_state);
|
||||
frame.render_widget(footer, footer_area);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
37
src/components/layouts.rs
Normal file
37
src/components/layouts.rs
Normal file
@ -0,0 +1,37 @@
|
||||
use ratatui::layout::{Constraint, Layout, Rect};
|
||||
|
||||
pub fn global_layout(area: Rect) -> [Rect; 2] {
|
||||
Layout::vertical([
|
||||
Constraint::Length(1),
|
||||
Constraint::Fill(1),
|
||||
]).areas(area)
|
||||
}
|
||||
|
||||
pub fn header_layout(area: Rect) -> [Rect; 2] {
|
||||
let [header, _] = global_layout(area);
|
||||
Layout::horizontal([
|
||||
Constraint::Fill(1),
|
||||
Constraint::Length(27),
|
||||
]).areas(header)
|
||||
}
|
||||
|
||||
pub fn main_layout(area: Rect) -> [Rect; 2] {
|
||||
let [_, main] = global_layout(area);
|
||||
Layout::horizontal([
|
||||
Constraint::Max(30),
|
||||
Constraint::Fill(1),
|
||||
]).areas(main)
|
||||
}
|
||||
|
||||
pub fn menu_layout(area: Rect) -> [Rect; 2] {
|
||||
let [menu, _] = main_layout(area);
|
||||
Layout::vertical([
|
||||
Constraint::Min(0),
|
||||
Constraint::Length(5),
|
||||
]).areas(menu)
|
||||
}
|
||||
|
||||
pub fn screen_layout(area: Rect) -> Rect {
|
||||
let [_, screen] = main_layout(area);
|
||||
screen
|
||||
}
|
||||
@ -1,14 +1,14 @@
|
||||
use color_eyre::Result;
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
use crossterm::event::{KeyEvent, KeyCode};
|
||||
|
||||
use super::Component;
|
||||
use super::palette::StylePalette;
|
||||
use crate::{config::Config, action::Action, app::Mode};
|
||||
|
||||
use super::generic::{Activatable, Helpable, Scrollable};
|
||||
|
||||
pub struct Menu {
|
||||
command_tx: Option<UnboundedSender<Action>>,
|
||||
list_state: ListState,
|
||||
items: Vec<String>,
|
||||
is_active: bool,
|
||||
@ -16,21 +16,47 @@ pub struct Menu {
|
||||
}
|
||||
|
||||
impl Default for Menu {
|
||||
fn default() -> Self {
|
||||
Menu::new()
|
||||
fn default() -> Self { Menu::new() }
|
||||
}
|
||||
|
||||
impl Activatable for Menu {
|
||||
fn is_active(&self) -> bool { self.is_active }
|
||||
fn is_inactive(&self) -> bool { !self.is_active }
|
||||
fn set_inactive(&mut self) { self.is_active = false; }
|
||||
fn set_active(&mut self) { self.is_active = true; }
|
||||
}
|
||||
|
||||
impl Helpable for Menu { }
|
||||
|
||||
impl Scrollable for Menu {
|
||||
type IndexType = usize;
|
||||
|
||||
fn selected_index(&self) -> Option<Self::IndexType> {
|
||||
self.list_state.selected()
|
||||
}
|
||||
|
||||
fn items_length(&self) -> Self::IndexType {
|
||||
self.items.len()
|
||||
}
|
||||
|
||||
fn apply_next_row(&mut self, new_index: Self::IndexType) -> Result<Option<Action>> {
|
||||
self.move_index(new_index)
|
||||
}
|
||||
|
||||
fn apply_prev_row(&mut self, new_index: Self::IndexType) -> Result<Option<Action>> {
|
||||
self.move_index(new_index)
|
||||
}
|
||||
}
|
||||
|
||||
impl Menu {
|
||||
pub fn new() -> Self {
|
||||
let mut new_list = Self {
|
||||
command_tx: None,
|
||||
list_state: ListState::default(),
|
||||
items: vec![
|
||||
String::from("Explorer"),
|
||||
String::from("Wallet"),
|
||||
String::from("Validator"),
|
||||
String::from("Prices"),
|
||||
String::from("Staking"),
|
||||
String::from("Governance"),
|
||||
String::from("Operations"),
|
||||
],
|
||||
@ -42,40 +68,12 @@ impl Menu {
|
||||
new_list
|
||||
}
|
||||
|
||||
fn next_row(&mut self) -> Result<Option<Action>> {
|
||||
let i = match self.list_state.selected() {
|
||||
Some(i) => {
|
||||
if i >= self.items.len() - 1 {
|
||||
i
|
||||
} else {
|
||||
i + 1
|
||||
}
|
||||
},
|
||||
None => 0,
|
||||
};
|
||||
self.list_state.select(Some(i));
|
||||
match i {
|
||||
0 => Ok(Some(Action::SetMode(Mode::Explorer))),
|
||||
1 => Ok(Some(Action::SetMode(Mode::Wallet))),
|
||||
_ => Ok(Some(Action::SetMode(Mode::Empty))),
|
||||
}
|
||||
}
|
||||
|
||||
fn previous_row(&mut self) -> Result<Option<Action>> {
|
||||
let i = match self.list_state.selected() {
|
||||
Some(i) => {
|
||||
if i == 0 {
|
||||
0
|
||||
} else {
|
||||
i - 1
|
||||
}
|
||||
},
|
||||
None => 0
|
||||
};
|
||||
self.list_state.select(Some(i));
|
||||
match i {
|
||||
fn move_index(&mut self, new_index: usize) -> Result<Option<Action>> {
|
||||
self.list_state.select(Some(new_index));
|
||||
match new_index {
|
||||
0 => Ok(Some(Action::SetMode(Mode::Explorer))),
|
||||
1 => Ok(Some(Action::SetMode(Mode::Wallet))),
|
||||
2 => Ok(Some(Action::SetMode(Mode::Validator))),
|
||||
_ => Ok(Some(Action::SetMode(Mode::Empty))),
|
||||
}
|
||||
}
|
||||
@ -84,17 +82,12 @@ impl Menu {
|
||||
impl Component for Menu {
|
||||
fn update(&mut self, action: Action) -> Result<Option<Action>> {
|
||||
match action {
|
||||
Action::SetActiveScreen(Mode::Menu) => self.is_active = true,
|
||||
Action::SetActiveScreen(Mode::Menu) => self.set_active(),
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn register_action_handler(&mut self, tx: UnboundedSender<Action>) -> Result<()> {
|
||||
self.command_tx = Some(tx);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn register_config_handler(&mut self, config: Config) -> Result<()> {
|
||||
if let Some(style) = config.styles.get(&crate::app::Mode::Menu) {
|
||||
self.palette.with_normal_style(style.get("normal_style").copied());
|
||||
@ -108,24 +101,26 @@ impl Component for Menu {
|
||||
|
||||
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::Enter | KeyCode::Char('l') | KeyCode::Right if self.is_active => {
|
||||
self.is_active = false;
|
||||
KeyCode::Up | KeyCode::Char('k') if self.is_active() => self.prev_row(),
|
||||
KeyCode::Down | KeyCode::Char('j') if self.is_active() => self.next_row(),
|
||||
KeyCode::Enter | KeyCode::Char('l') | KeyCode::Right if self.is_active() => {
|
||||
self.set_inactive();
|
||||
match self.list_state.selected() {
|
||||
Some(0) => Ok(Some(Action::SetActiveScreen(Mode::Explorer))),
|
||||
Some(1) => Ok(Some(Action::SetActiveScreen(Mode::Wallet))),
|
||||
Some(2) => Ok(Some(Action::SetActiveScreen(Mode::Validator))),
|
||||
_ => Ok(Some(Action::SetActiveScreen(Mode::Empty))),
|
||||
}
|
||||
},
|
||||
KeyCode::Char('?') if self.is_active() => self.open_help_popup(),
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
|
||||
let [menu, _] = super::menu_layout(area);
|
||||
let [menu, _] = super::layouts::menu_layout(area);
|
||||
|
||||
let (color, border_type) = self.palette.create_border_style(self.is_active);
|
||||
let (color, border_type) = self.palette.create_border_style(self.is_active());
|
||||
let block = Block::bordered()
|
||||
.border_style(color)
|
||||
.border_type(border_type);
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
use color_eyre::Result;
|
||||
use crossterm::event::{KeyEvent, MouseEvent};
|
||||
use ratatui::{
|
||||
layout::{Constraint, Layout, Rect, Size},
|
||||
layout::{Rect, Size},
|
||||
Frame,
|
||||
};
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
@ -9,13 +9,17 @@ use std::sync::mpsc::Sender;
|
||||
|
||||
use crate::{palette, action::Action, config::Config, tui::Event};
|
||||
|
||||
pub mod layouts;
|
||||
pub mod fps;
|
||||
pub mod health;
|
||||
pub mod menu;
|
||||
pub mod version;
|
||||
pub mod explorer;
|
||||
pub mod wallet;
|
||||
pub mod validator;
|
||||
pub mod empty;
|
||||
pub mod help;
|
||||
pub mod generic;
|
||||
|
||||
pub trait Component {
|
||||
fn register_network_handler(&mut self, tx: Sender<Action>) -> Result<()> {
|
||||
@ -64,39 +68,3 @@ pub trait Component {
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()>;
|
||||
}
|
||||
|
||||
pub fn global_layout(area: Rect) -> [Rect; 2] {
|
||||
Layout::vertical([
|
||||
Constraint::Length(1),
|
||||
Constraint::Fill(1),
|
||||
]).areas(area)
|
||||
}
|
||||
|
||||
pub fn header_layout(area: Rect) -> [Rect; 2] {
|
||||
let [header, _] = global_layout(area);
|
||||
Layout::horizontal([
|
||||
Constraint::Percentage(50),
|
||||
Constraint::Percentage(50),
|
||||
]).areas(header)
|
||||
}
|
||||
|
||||
pub fn main_layout(area: Rect) -> [Rect; 2] {
|
||||
let [_, main] = global_layout(area);
|
||||
Layout::horizontal([
|
||||
Constraint::Max(30),
|
||||
Constraint::Fill(1),
|
||||
]).areas(main)
|
||||
}
|
||||
|
||||
pub fn menu_layout(area: Rect) -> [Rect; 2] {
|
||||
let [menu, _] = main_layout(area);
|
||||
Layout::vertical([
|
||||
Constraint::Min(0),
|
||||
Constraint::Length(5),
|
||||
]).areas(menu)
|
||||
}
|
||||
|
||||
pub fn screen_layout(area: Rect) -> Rect {
|
||||
let [_, screen] = main_layout(area);
|
||||
screen
|
||||
}
|
||||
|
||||
199
src/components/validator/bond_popup.rs
Normal file
199
src/components/validator/bond_popup.rs
Normal file
@ -0,0 +1,199 @@
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind};
|
||||
use color_eyre::Result;
|
||||
use ratatui::{
|
||||
layout::{Position, Alignment, Constraint, Flex, Layout, Rect},
|
||||
widgets::{Block, Clear, Paragraph},
|
||||
Frame
|
||||
};
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
use super::{Component, PartialComponent, CurrentTab};
|
||||
use crate::{
|
||||
action::Action,
|
||||
config::Config,
|
||||
palette::StylePalette,
|
||||
types::{ActionLevel, ActionTarget},
|
||||
widgets::{Input, InputRequest},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct BondPopup {
|
||||
is_active: bool,
|
||||
action_tx: Option<UnboundedSender<Action>>,
|
||||
network_tx: Option<Sender<Action>>,
|
||||
stash_secret_seed: [u8; 32],
|
||||
stash_account_id: [u8; 32],
|
||||
minimal_bond: u128,
|
||||
is_bonded: bool,
|
||||
amount: Input,
|
||||
palette: StylePalette
|
||||
}
|
||||
|
||||
impl Default for BondPopup {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl BondPopup {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
is_active: false,
|
||||
stash_secret_seed: [0u8; 32],
|
||||
stash_account_id: [0u8; 32],
|
||||
action_tx: None,
|
||||
network_tx: None,
|
||||
minimal_bond: 0u128,
|
||||
is_bonded: false,
|
||||
amount: Input::new(String::new()),
|
||||
palette: StylePalette::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn close_popup(&mut self) {
|
||||
self.is_active = false;
|
||||
if let Some(action_tx) = &self.action_tx {
|
||||
let _ = action_tx.send(Action::ClosePopup);
|
||||
}
|
||||
}
|
||||
|
||||
fn log_event(&mut self, message: String, level: ActionLevel) {
|
||||
if let Some(action_tx) = &self.action_tx {
|
||||
let _ = action_tx.send(
|
||||
Action::EventLog(message, level, ActionTarget::ValidatorLog));
|
||||
}
|
||||
}
|
||||
|
||||
fn submit_message(&mut self) {
|
||||
if let Some(network_tx) = &self.network_tx {
|
||||
let str_amount = self.amount.value();
|
||||
let str_amount = if str_amount.starts_with('.') {
|
||||
&format!("0{}", str_amount)[..]
|
||||
} else {
|
||||
str_amount
|
||||
};
|
||||
match str_amount.parse::<f64>() {
|
||||
Ok(value) => {
|
||||
let amount = (value * 1_000_000_000_000_000_000.0) as u128;
|
||||
let log_target = ActionTarget::ValidatorLog;
|
||||
let _ = if self.is_bonded {
|
||||
network_tx.send(Action::BondValidatorExtraFrom(self.stash_secret_seed, amount, log_target))
|
||||
} else {
|
||||
network_tx.send(Action::BondValidatorFrom(self.stash_secret_seed, amount, log_target))
|
||||
};
|
||||
if let Some(action_tx) = &self.action_tx {
|
||||
let _ = action_tx.send(Action::ClosePopup);
|
||||
}
|
||||
},
|
||||
Err(err) => self.log_event(
|
||||
format!("invalid amount, error: {err}"), ActionLevel::Error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn enter_char(&mut self, new_char: char) {
|
||||
let is_separator_needed = !self.amount.value().contains('.') && new_char == '.';
|
||||
if new_char.is_digit(10) || is_separator_needed {
|
||||
let _ = self.amount.handle(InputRequest::InsertChar(new_char));
|
||||
}
|
||||
}
|
||||
|
||||
fn delete_char(&mut self) {
|
||||
let _ = self.amount.handle(InputRequest::DeletePrevChar);
|
||||
}
|
||||
|
||||
fn move_cursor_right(&mut self) {
|
||||
let _ = self.amount.handle(InputRequest::GoToNextChar);
|
||||
}
|
||||
|
||||
fn move_cursor_left(&mut self) {
|
||||
let _ = self.amount.handle(InputRequest::GoToPrevChar);
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialComponent for BondPopup {
|
||||
fn set_active(&mut self, current_tab: CurrentTab) {
|
||||
match current_tab {
|
||||
CurrentTab::BondPopup => self.is_active = true,
|
||||
_ => {
|
||||
self.is_active = false;
|
||||
self.amount = Input::new(String::new());
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for BondPopup {
|
||||
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_normal_border_style(style.get("normal_border_style").copied());
|
||||
self.palette.with_normal_title_style(style.get("normal_title_style").copied());
|
||||
self.palette.with_popup_style(style.get("popup_style").copied());
|
||||
self.palette.with_popup_title_style(style.get("popup_title_style").copied());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn handle_key_event(&mut self, key: KeyEvent) -> Result<Option<Action>> {
|
||||
if self.is_active && key.kind == KeyEventKind::Press {
|
||||
match key.code {
|
||||
KeyCode::Enter => self.submit_message(),
|
||||
KeyCode::Char(to_insert) => self.enter_char(to_insert),
|
||||
KeyCode::Backspace => self.delete_char(),
|
||||
KeyCode::Left => self.move_cursor_left(),
|
||||
KeyCode::Right => self.move_cursor_right(),
|
||||
KeyCode::Esc => self.close_popup(),
|
||||
_ => {},
|
||||
};
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn update(&mut self, action: Action) -> Result<Option<Action>> {
|
||||
match action {
|
||||
Action::SetIsBonded(is_bonded, account_id) if self.stash_account_id == account_id =>
|
||||
self.is_bonded = is_bonded,
|
||||
Action::SetMinValidatorBond(minimal_bond) => self.minimal_bond = minimal_bond,
|
||||
Action::SetStashSecret(secret_seed) => self.stash_secret_seed = secret_seed,
|
||||
Action::SetStashAccount(account_id) => self.stash_account_id = account_id,
|
||||
_ => {}
|
||||
};
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
|
||||
if self.is_active {
|
||||
let (border_style, border_type) = self.palette.create_popup_style();
|
||||
let input = Paragraph::new(self.amount.value())
|
||||
.block(Block::bordered()
|
||||
.border_style(border_style)
|
||||
.border_type(border_type)
|
||||
.title_style(self.palette.create_popup_title_style())
|
||||
.title_alignment(Alignment::Right)
|
||||
.title(format!("Staking bond amount")));
|
||||
let v = Layout::vertical([Constraint::Max(3)]).flex(Flex::Center);
|
||||
let h = Layout::horizontal([Constraint::Max(50)]).flex(Flex::Center);
|
||||
let [area] = v.areas(area);
|
||||
let [area] = h.areas(area);
|
||||
|
||||
frame.render_widget(Clear, area);
|
||||
frame.render_widget(input, area);
|
||||
frame.set_cursor_position(Position::new(
|
||||
area.x + self.amount.cursor() as u16 + 1,
|
||||
area.y + 1
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
130
src/components/validator/chill_popup.rs
Normal file
130
src/components/validator/chill_popup.rs
Normal file
@ -0,0 +1,130 @@
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind};
|
||||
use color_eyre::Result;
|
||||
use ratatui::{
|
||||
layout::{Alignment, Constraint, Flex, Layout, Rect},
|
||||
widgets::{Block, Clear, Paragraph},
|
||||
Frame
|
||||
};
|
||||
use std::sync::mpsc::Sender;
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
|
||||
use super::{Component, PartialComponent, CurrentTab};
|
||||
use crate::{
|
||||
action::Action,
|
||||
config::Config,
|
||||
palette::StylePalette,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ChillPopup {
|
||||
is_active: bool,
|
||||
action_tx: Option<UnboundedSender<Action>>,
|
||||
network_tx: Option<Sender<Action>>,
|
||||
secret_seed: [u8; 32],
|
||||
palette: StylePalette
|
||||
}
|
||||
|
||||
impl Default for ChillPopup {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl ChillPopup {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
is_active: false,
|
||||
secret_seed: [0u8; 32],
|
||||
network_tx: None,
|
||||
action_tx: None,
|
||||
palette: StylePalette::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn close_popup(&mut self) {
|
||||
self.is_active = false;
|
||||
if let Some(action_tx) = &self.action_tx {
|
||||
let _ = action_tx.send(Action::ClosePopup);
|
||||
}
|
||||
}
|
||||
|
||||
fn submit_message(&mut self) {
|
||||
if let Some(network_tx) = &self.network_tx {
|
||||
let _ = network_tx.send(Action::ChillFrom(self.secret_seed));
|
||||
if let Some(action_tx) = &self.action_tx {
|
||||
let _ = action_tx.send(Action::ClosePopup);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialComponent for ChillPopup {
|
||||
fn set_active(&mut self, current_tab: CurrentTab) {
|
||||
match current_tab {
|
||||
CurrentTab::ChillPopup => self.is_active = true,
|
||||
_ => self.is_active = false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for ChillPopup {
|
||||
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_normal_border_style(style.get("normal_border_style").copied());
|
||||
self.palette.with_normal_title_style(style.get("normal_title_style").copied());
|
||||
self.palette.with_popup_style(style.get("popup_style").copied());
|
||||
self.palette.with_popup_title_style(style.get("popup_title_style").copied());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn handle_key_event(&mut self, key: KeyEvent) -> Result<Option<Action>> {
|
||||
if self.is_active && key.kind == KeyEventKind::Press {
|
||||
match key.code {
|
||||
KeyCode::Enter => self.submit_message(),
|
||||
KeyCode::Esc => self.close_popup(),
|
||||
_ => {},
|
||||
};
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn update(&mut self, action: Action) -> Result<Option<Action>> {
|
||||
match action {
|
||||
Action::SetStashSecret(secret_seed) => self.secret_seed = secret_seed,
|
||||
_ => {}
|
||||
};
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
|
||||
if self.is_active {
|
||||
let (border_style, border_type) = self.palette.create_popup_style();
|
||||
let input = Paragraph::new(" Do you want to chill stash account and stop validation?")
|
||||
.block(Block::bordered()
|
||||
.border_style(border_style)
|
||||
.border_type(border_type)
|
||||
.title_style(self.palette.create_popup_title_style())
|
||||
.title_alignment(Alignment::Right)
|
||||
.title(format!("Chill stash")));
|
||||
let v = Layout::vertical([Constraint::Max(3)]).flex(Flex::Center);
|
||||
let h = Layout::horizontal([Constraint::Max(57)]).flex(Flex::Center);
|
||||
let [area] = v.areas(area);
|
||||
let [area] = h.areas(area);
|
||||
|
||||
frame.render_widget(Clear, area);
|
||||
frame.render_widget(input, area);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
195
src/components/validator/event_log.rs
Normal file
195
src/components/validator/event_log.rs
Normal file
@ -0,0 +1,195 @@
|
||||
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::{
|
||||
action::Action, config::Config, palette::StylePalette, types::{ActionLevel, ActionTarget}
|
||||
};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct LogDetails {
|
||||
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<LogDetails>,
|
||||
palette: StylePalette
|
||||
}
|
||||
|
||||
impl EventLogs {
|
||||
const MAX_LOGS: usize = 50;
|
||||
|
||||
fn add_new_log(&mut self, message: String, level: ActionLevel) {
|
||||
self.logs.push_front(LogDetails {
|
||||
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::EventLog(message, level, target) if target == ActionTarget::ValidatorLog =>
|
||||
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(())
|
||||
}
|
||||
}
|
||||
120
src/components/validator/gatekeeper_details.rs
Normal file
120
src/components/validator/gatekeeper_details.rs
Normal file
@ -0,0 +1,120 @@
|
||||
use std::collections::HashMap;
|
||||
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::types::BlockRange;
|
||||
use crate::{
|
||||
action::Action,
|
||||
config::Config,
|
||||
palette::StylePalette,
|
||||
};
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
struct Details {
|
||||
incoming_fee: String,
|
||||
outgoing_fee: String,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct GatekeeperDetails {
|
||||
palette: StylePalette,
|
||||
gatekeeper_details: HashMap<u64, Details>,
|
||||
block_ranges: HashMap<u64, BlockRange>,
|
||||
selected_chain_id: u64,
|
||||
}
|
||||
|
||||
impl PartialComponent for GatekeeperDetails {
|
||||
fn set_active(&mut self, _current_tab: CurrentTab) { }
|
||||
}
|
||||
|
||||
impl Component for GatekeeperDetails {
|
||||
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::SetChoosenGatekeeper(chain_id) => self.selected_chain_id = chain_id,
|
||||
Action::SetBlockRange(chain_id, block_range) => {
|
||||
let _ = self.block_ranges.insert(chain_id, block_range);
|
||||
},
|
||||
Action::SetGatekeepedNetwork(network) => {
|
||||
self.gatekeeper_details.insert(network.chain_id, Details {
|
||||
incoming_fee: format!("{:.5}%", network.incoming_fee as f64 / 10_000_000.0),
|
||||
outgoing_fee: format!("{:.5}%", network.outgoing_fee as f64 / 10_000_000.0),
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
|
||||
let [_, place] = super::validator_gatekeeped_networks_layout(area);
|
||||
let (border_style, border_type) = self.palette.create_border_style(false);
|
||||
|
||||
let current_gatekeeper_details = self.gatekeeper_details
|
||||
.get(&self.selected_chain_id)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
let current_block_range = self.block_ranges
|
||||
.get(&self.selected_chain_id)
|
||||
.copied()
|
||||
.unwrap_or_default();
|
||||
|
||||
let table = Table::new(
|
||||
vec![
|
||||
Row::new(vec![
|
||||
Cell::from(Text::from("From block".to_string()).alignment(Alignment::Left)),
|
||||
Cell::from(Text::from(current_block_range.from_block.to_string()).alignment(Alignment::Right)),
|
||||
]),
|
||||
Row::new(vec![
|
||||
Cell::from(Text::from("To block".to_string()).alignment(Alignment::Left)),
|
||||
Cell::from(Text::from(current_block_range.to_block.to_string()).alignment(Alignment::Right)),
|
||||
]),
|
||||
Row::new(vec![
|
||||
Cell::from(Text::from("Incoming fee".to_string()).alignment(Alignment::Left)),
|
||||
Cell::from(Text::from(current_gatekeeper_details.incoming_fee).alignment(Alignment::Right)),
|
||||
]),
|
||||
Row::new(vec![
|
||||
Cell::from(Text::from("Outgoing fee".to_string()).alignment(Alignment::Left)),
|
||||
Cell::from(Text::from(current_gatekeeper_details.outgoing_fee).alignment(Alignment::Right)),
|
||||
]),
|
||||
],
|
||||
[
|
||||
Constraint::Length(12),
|
||||
Constraint::Fill(1),
|
||||
],
|
||||
)
|
||||
.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(format!("Chain ID: {}", self.selected_chain_id)));
|
||||
|
||||
frame.render_widget(table, place);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
234
src/components/validator/gatekeepers.rs
Normal file
234
src/components/validator/gatekeepers.rs
Normal file
@ -0,0 +1,234 @@
|
||||
use std::collections::HashMap;
|
||||
use color_eyre::Result;
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
use ratatui::layout::Margin;
|
||||
use ratatui::{
|
||||
layout::{Alignment, Rect},
|
||||
widgets::{
|
||||
Block, List, ListState, ListItem, Scrollbar,
|
||||
ScrollbarOrientation, ScrollbarState,
|
||||
},
|
||||
Frame
|
||||
};
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
|
||||
use super::{PartialComponent, Component, CurrentTab};
|
||||
use crate::types::Gatekeeper;
|
||||
use crate::{
|
||||
action::Action,
|
||||
config::Config,
|
||||
palette::StylePalette,
|
||||
};
|
||||
|
||||
struct NetworkDetails {
|
||||
chain_id: u64,
|
||||
chain_name: String,
|
||||
chain_type: String,
|
||||
}
|
||||
|
||||
pub struct Gatekeepers {
|
||||
is_active: bool,
|
||||
action_tx: Option<UnboundedSender<Action>>,
|
||||
palette: StylePalette,
|
||||
scroll_state: ScrollbarState,
|
||||
list_state: ListState,
|
||||
gatekeepers: Vec<NetworkDetails>,
|
||||
chain_ids: HashMap<u64, usize>,
|
||||
}
|
||||
|
||||
impl Default for Gatekeepers {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Gatekeepers {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
is_active: false,
|
||||
action_tx: None,
|
||||
scroll_state: ScrollbarState::new(0),
|
||||
list_state: ListState::default(),
|
||||
gatekeepers: Vec::new(),
|
||||
palette: StylePalette::default(),
|
||||
chain_ids: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn change_choosen_gatekeeper(&mut self) {
|
||||
if let Some(action_tx) = &self.action_tx {
|
||||
if let Some(chain_id) = self.list_state
|
||||
.selected()
|
||||
.map(|index| self.gatekeepers
|
||||
.get(index)
|
||||
.map(|data| data.chain_id)
|
||||
)
|
||||
.flatten()
|
||||
{
|
||||
let _ = action_tx.send(Action::SetChoosenGatekeeper(chain_id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_gatekeeped_network(&mut self, network: Gatekeeper) {
|
||||
if let Some(index) = self.chain_ids.get(&network.chain_id) {
|
||||
self.gatekeepers[*index] = NetworkDetails {
|
||||
chain_id: network.chain_id,
|
||||
chain_name: network.chain_name,
|
||||
chain_type: network.chain_type,
|
||||
};
|
||||
} else {
|
||||
let position = self.gatekeepers.len();
|
||||
self.chain_ids.insert(network.chain_id, position);
|
||||
self.gatekeepers.push(NetworkDetails {
|
||||
chain_id: network.chain_id,
|
||||
chain_name: network.chain_name,
|
||||
chain_type: network.chain_type,
|
||||
});
|
||||
|
||||
if position == 0 {
|
||||
self.first_row();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn first_row(&mut self) {
|
||||
if self.gatekeepers.len() > 0 {
|
||||
self.list_state.select(Some(0));
|
||||
self.scroll_state = self.scroll_state.position(0);
|
||||
}
|
||||
self.change_choosen_gatekeeper();
|
||||
}
|
||||
|
||||
fn next_row(&mut self) {
|
||||
let i = match self.list_state.selected() {
|
||||
Some(i) => {
|
||||
if i >= self.gatekeepers.len() - 1 {
|
||||
i
|
||||
} else {
|
||||
i + 1
|
||||
}
|
||||
},
|
||||
None => 0,
|
||||
};
|
||||
self.list_state.select(Some(i));
|
||||
self.scroll_state = self.scroll_state.position(i);
|
||||
self.change_choosen_gatekeeper();
|
||||
}
|
||||
|
||||
fn last_row(&mut self) {
|
||||
if self.gatekeepers.len() > 0 {
|
||||
let last = self.gatekeepers.len() - 1;
|
||||
self.list_state.select(Some(last));
|
||||
self.scroll_state = self.scroll_state.position(last);
|
||||
}
|
||||
self.change_choosen_gatekeeper();
|
||||
}
|
||||
|
||||
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);
|
||||
self.change_choosen_gatekeeper();
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialComponent for Gatekeepers {
|
||||
fn set_active(&mut self, current_tab: CurrentTab) {
|
||||
match current_tab {
|
||||
CurrentTab::Gatekeepers => self.is_active = true,
|
||||
_ => {
|
||||
self.is_active = false;
|
||||
self.list_state.select(None);
|
||||
self.scroll_state = self.scroll_state.position(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for Gatekeepers {
|
||||
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::SetGatekeepedNetwork(network) =>
|
||||
self.update_gatekeeped_network(network),
|
||||
_ => {}
|
||||
};
|
||||
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_gatekeeped_networks_layout(area);
|
||||
let (border_style, border_type) = self.palette.create_border_style(self.is_active);
|
||||
let list = List::new(
|
||||
self.gatekeepers
|
||||
.iter()
|
||||
.map(|network| ListItem::new(format!("{} (type {})",
|
||||
network.chain_name,
|
||||
network.chain_type))
|
||||
)
|
||||
)
|
||||
.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("Gatekeeped Networks"));
|
||||
|
||||
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(())
|
||||
}
|
||||
}
|
||||
348
src/components/validator/history.rs
Normal file
348
src/components/validator/history.rs
Normal file
@ -0,0 +1,348 @@
|
||||
use std::collections::{HashSet, 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::types::{ActionLevel, ActionTarget};
|
||||
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>,
|
||||
pending_payout: HashSet<u32>,
|
||||
}
|
||||
|
||||
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: Default::default(),
|
||||
pending_payout: Default::default(),
|
||||
palette: StylePalette::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn payout_all_available(&mut self) {
|
||||
let unclaimed_keys = self.rewards
|
||||
.iter()
|
||||
.filter_map(|(k, v)| (!v.is_claimed && v.reward > 0).then(|| *k))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if let Some(action_tx) = &self.action_tx {
|
||||
let _ = if unclaimed_keys.len() == 0 {
|
||||
action_tx.send(Action::EventLog(
|
||||
String::from("no available payouts found for current validator"),
|
||||
ActionLevel::Warn,
|
||||
ActionTarget::ValidatorLog))
|
||||
} else {
|
||||
self.pending_payout.extend(&unclaimed_keys);
|
||||
action_tx.send(Action::PayoutAllValidatorPopup(unclaimed_keys))
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn payout_by_era_index(&mut self) {
|
||||
if let Some(index) = self.table_state.selected() {
|
||||
let rev_index = self.rewards.len()
|
||||
.saturating_sub(index)
|
||||
.saturating_sub(1);
|
||||
|
||||
if let Some(era_index) = self.rewards.keys().nth(rev_index) {
|
||||
let is_claimed = self.rewards
|
||||
.get(&era_index)
|
||||
.map(|x| x.is_claimed)
|
||||
.expect("BTreeMap of rewards is indexed; qed");
|
||||
|
||||
if let Some(action_tx) = &self.action_tx {
|
||||
let _ = match (is_claimed, self.pending_payout.contains(era_index)) {
|
||||
(false, false) => {
|
||||
self.pending_payout.insert(*era_index);
|
||||
action_tx.send(Action::PayoutValidatorPopup(*era_index))
|
||||
}
|
||||
(false, true) => action_tx.send(Action::EventLog(
|
||||
format!("payout for era #{} is in-flight already", era_index),
|
||||
ActionLevel::Warn,
|
||||
ActionTarget::ValidatorLog)),
|
||||
(true, _) => action_tx.send(Action::EventLog(
|
||||
format!("staking rewards for era index #{} already claimed", era_index),
|
||||
ActionLevel::Warn,
|
||||
ActionTarget::ValidatorLog)),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
self.scroll_state = self.scroll_state.content_length(self.rewards.len());
|
||||
}
|
||||
|
||||
fn update_claims(&mut self, era_index: u32, is_claimed: bool) {
|
||||
match self.rewards.get_mut(&era_index) {
|
||||
Some(reward_item) => {
|
||||
if reward_item.is_claimed == false && is_claimed == true {
|
||||
self.pending_payout.remove(&era_index);
|
||||
if let Some(network_tx) = &self.network_tx {
|
||||
let _ = network_tx.send(Action::RemoveEraToWatch(era_index));
|
||||
}
|
||||
}
|
||||
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.pending_payout = Default::default();
|
||||
},
|
||||
_ => self.is_active = false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(),
|
||||
KeyCode::Char('H') => self.payout_all_available(),
|
||||
KeyCode::Enter => self.payout_by_era_index(),
|
||||
_ => {},
|
||||
};
|
||||
}
|
||||
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()
|
||||
.rev()
|
||||
.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),
|
||||
]).style(self.palette.create_highlight_style())
|
||||
} else {
|
||||
if self.pending_payout.contains(key) {
|
||||
era_index_text = era_index_text.add_modifier(Modifier::SLOW_BLINK);
|
||||
slash_text = slash_text.add_modifier(Modifier::SLOW_BLINK);
|
||||
reward_text = reward_text.add_modifier(Modifier::SLOW_BLINK);
|
||||
}
|
||||
|
||||
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_basic_style(true))
|
||||
.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(())
|
||||
}
|
||||
}
|
||||
183
src/components/validator/listen_addresses.rs
Normal file
183
src/components/validator/listen_addresses.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
338
src/components/validator/mod.rs
Normal file
338
src/components/validator/mod.rs
Normal file
@ -0,0 +1,338 @@
|
||||
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 gatekeepers;
|
||||
mod gatekeeper_details;
|
||||
mod history;
|
||||
mod withdrawals;
|
||||
mod stash_details;
|
||||
mod staking_details;
|
||||
mod reward_details;
|
||||
mod bond_popup;
|
||||
mod payout_popup;
|
||||
mod payout_all_popup;
|
||||
mod rotate_popup;
|
||||
mod validate_popup;
|
||||
mod chill_popup;
|
||||
mod unbond_popup;
|
||||
mod rebond_popup;
|
||||
mod withdraw_popup;
|
||||
mod payee_popup;
|
||||
|
||||
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 gatekeepers::Gatekeepers;
|
||||
use gatekeeper_details::GatekeeperDetails;
|
||||
use nominators::NominatorsByValidator;
|
||||
use history::History;
|
||||
use withdrawals::Withdrawals;
|
||||
use bond_popup::BondPopup;
|
||||
use payout_popup::PayoutPopup;
|
||||
use payout_all_popup::PayoutAllPopup;
|
||||
use rotate_popup::RotatePopup;
|
||||
use validate_popup::ValidatePopup;
|
||||
use chill_popup::ChillPopup;
|
||||
use unbond_popup::UnbondPopup;
|
||||
use rebond_popup::RebondPopup;
|
||||
use withdraw_popup::WithdrawPopup;
|
||||
use payee_popup::PayeePopup;
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
pub enum CurrentTab {
|
||||
Nothing,
|
||||
ListenAddresses,
|
||||
Gatekeepers,
|
||||
NominatorsByValidator,
|
||||
History,
|
||||
Withdrawals,
|
||||
Peers,
|
||||
EventLogs,
|
||||
BondPopup,
|
||||
PayoutPopup,
|
||||
PayoutAllPopup,
|
||||
RotatePopup,
|
||||
ValidatePopup,
|
||||
ChillPopup,
|
||||
UnbondPopup,
|
||||
RebondPopup,
|
||||
WithdrawPopup,
|
||||
PayeePopup,
|
||||
}
|
||||
|
||||
pub trait PartialComponent: Component {
|
||||
fn set_active(&mut self, current_tab: CurrentTab);
|
||||
}
|
||||
|
||||
pub struct Validator {
|
||||
is_active: bool,
|
||||
current_tab: CurrentTab,
|
||||
previous_tab: CurrentTab,
|
||||
components: Vec<Box<dyn PartialComponent>>,
|
||||
}
|
||||
|
||||
impl Default for Validator {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
is_active: false,
|
||||
current_tab: CurrentTab::Nothing,
|
||||
previous_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(GatekeeperDetails::default()),
|
||||
Box::new(History::default()),
|
||||
Box::new(Withdrawals::default()),
|
||||
Box::new(Peers::default()),
|
||||
Box::new(ListenAddresses::default()),
|
||||
Box::new(Gatekeepers::default()),
|
||||
Box::new(EventLogs::default()),
|
||||
Box::new(BondPopup::default()),
|
||||
Box::new(PayoutPopup::default()),
|
||||
Box::new(PayoutAllPopup::default()),
|
||||
Box::new(RotatePopup::default()),
|
||||
Box::new(ValidatePopup::default()),
|
||||
Box::new(ChillPopup::default()),
|
||||
Box::new(UnbondPopup::default()),
|
||||
Box::new(RebondPopup::default()),
|
||||
Box::new(WithdrawPopup::default()),
|
||||
Box::new(PayeePopup::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::Gatekeepers,
|
||||
CurrentTab::ListenAddresses => self.current_tab = CurrentTab::Gatekeepers,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn move_right(&mut self) {
|
||||
match self.current_tab {
|
||||
CurrentTab::ListenAddresses => self.current_tab = CurrentTab::Gatekeepers,
|
||||
CurrentTab::Gatekeepers => self.current_tab = CurrentTab::NominatorsByValidator,
|
||||
CurrentTab::Nothing => 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 self.current_tab {
|
||||
CurrentTab::BondPopup |
|
||||
CurrentTab::RotatePopup |
|
||||
CurrentTab::ValidatePopup |
|
||||
CurrentTab::ChillPopup |
|
||||
CurrentTab::UnbondPopup |
|
||||
CurrentTab::RebondPopup |
|
||||
CurrentTab::WithdrawPopup |
|
||||
CurrentTab::PayoutPopup |
|
||||
CurrentTab::PayoutAllPopup => {
|
||||
for component in self.components.iter_mut() {
|
||||
component.handle_key_event(key)?;
|
||||
}
|
||||
},
|
||||
_ => 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('R') => {
|
||||
self.previous_tab = self.current_tab;
|
||||
self.current_tab = CurrentTab::RotatePopup;
|
||||
},
|
||||
KeyCode::Char('V') => {
|
||||
self.previous_tab = self.current_tab;
|
||||
self.current_tab = CurrentTab::ValidatePopup;
|
||||
},
|
||||
KeyCode::Char('U') => {
|
||||
self.previous_tab = self.current_tab;
|
||||
self.current_tab = CurrentTab::UnbondPopup;
|
||||
},
|
||||
KeyCode::Char('C') => {
|
||||
self.previous_tab = self.current_tab;
|
||||
self.current_tab = CurrentTab::ChillPopup;
|
||||
},
|
||||
KeyCode::Char('B') => {
|
||||
self.previous_tab = self.current_tab;
|
||||
self.current_tab = CurrentTab::BondPopup;
|
||||
},
|
||||
KeyCode::Char('E') => {
|
||||
self.previous_tab = self.current_tab;
|
||||
self.current_tab = CurrentTab::RebondPopup;
|
||||
},
|
||||
KeyCode::Char('W') => {
|
||||
self.previous_tab = self.current_tab;
|
||||
self.current_tab = CurrentTab::WithdrawPopup;
|
||||
},
|
||||
KeyCode::Char('L') => {
|
||||
self.previous_tab = self.current_tab;
|
||||
self.current_tab = CurrentTab::ListenAddresses;
|
||||
},
|
||||
KeyCode::Char('I') => {
|
||||
self.previous_tab = self.current_tab;
|
||||
self.current_tab = CurrentTab::PayeePopup;
|
||||
},
|
||||
KeyCode::Char('l') | KeyCode::Right => self.move_right(),
|
||||
KeyCode::Char('h') | KeyCode::Left => self.move_left(),
|
||||
_ => {
|
||||
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;
|
||||
self.previous_tab = CurrentTab::NominatorsByValidator;
|
||||
self.current_tab = CurrentTab::NominatorsByValidator;
|
||||
}
|
||||
Action::PayoutValidatorPopup(_) => {
|
||||
self.previous_tab = self.current_tab;
|
||||
self.current_tab = CurrentTab::PayoutPopup;
|
||||
}
|
||||
Action::PayoutAllValidatorPopup(_) => {
|
||||
self.previous_tab = self.current_tab;
|
||||
self.current_tab = CurrentTab::PayoutAllPopup;
|
||||
}
|
||||
Action::WithdrawValidatorPopup => {
|
||||
self.previous_tab = self.current_tab;
|
||||
self.current_tab = CurrentTab::WithdrawPopup;
|
||||
},
|
||||
Action::ClosePopup => self.current_tab = self.previous_tab,
|
||||
_ => {},
|
||||
}
|
||||
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::layouts::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(18),
|
||||
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; 3] {
|
||||
let [_, place] = validator_details_layout(area);
|
||||
Layout::vertical([
|
||||
Constraint::Length(6),
|
||||
Constraint::Length(6),
|
||||
Constraint::Fill(1),
|
||||
]).areas(place)
|
||||
}
|
||||
|
||||
pub fn validator_gatekeeped_networks_layout(area: Rect) -> [Rect; 2] {
|
||||
let [_, _, place] = validator_session_and_listen_layout(area);
|
||||
Layout::horizontal([
|
||||
Constraint::Fill(1),
|
||||
Constraint::Length(30),
|
||||
]).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(6),
|
||||
Constraint::Fill(1),
|
||||
]).areas(place)
|
||||
}
|
||||
214
src/components/validator/nominators.rs
Normal file
214
src/components/validator/nominators.rs
Normal file
@ -0,0 +1,214 @@
|
||||
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>,
|
||||
stash: [u8; 32],
|
||||
}
|
||||
|
||||
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(),
|
||||
stash: [0u8; 32],
|
||||
palette: StylePalette::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn update_nominators(&mut self, nominators: Vec<Nominator>) {
|
||||
if self.nominators.len() > nominators.len() {
|
||||
self.table_state.select(None);
|
||||
self.scroll_state = self.scroll_state.position(0);
|
||||
}
|
||||
self.nominators = nominators;
|
||||
self.scroll_state = self.scroll_state.content_length(self.nominators.len());
|
||||
}
|
||||
|
||||
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::SetStashAccount(stash) => self.stash = stash,
|
||||
Action::SetNominatorsByValidator(nominators, account_id) if self.stash == account_id =>
|
||||
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.address.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(())
|
||||
}
|
||||
}
|
||||
365
src/components/validator/payee_popup.rs
Normal file
365
src/components/validator/payee_popup.rs
Normal file
@ -0,0 +1,365 @@
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind};
|
||||
use color_eyre::Result;
|
||||
use ratatui::{
|
||||
layout::{Alignment, Constraint, Flex, Layout, Position, Rect},
|
||||
text::Text,
|
||||
widgets::{Block, Cell, Clear, Paragraph, Row, Table, TableState},
|
||||
Frame,
|
||||
};
|
||||
use subxt::ext::sp_core::crypto::{
|
||||
ByteArray, Ss58Codec, Ss58AddressFormat, AccountId32,
|
||||
};
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
use super::{Component, PartialComponent, CurrentTab};
|
||||
use crate::{
|
||||
action::Action,
|
||||
config::Config,
|
||||
palette::StylePalette,
|
||||
types::{ActionLevel, ActionTarget, RewardDestination},
|
||||
widgets::{Input, InputRequest},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PayeePopup {
|
||||
is_active: bool,
|
||||
action_tx: Option<UnboundedSender<Action>>,
|
||||
network_tx: Option<Sender<Action>>,
|
||||
table_state: TableState,
|
||||
stash_secret_seed: [u8; 32],
|
||||
stash_account_id: [u8; 32],
|
||||
proposed_account_id: Option<[u8; 32]>,
|
||||
is_bonded: bool,
|
||||
is_account_chosen: bool,
|
||||
is_input_active: bool,
|
||||
address: Input,
|
||||
possible_payee_options: &'static [(&'static str, &'static str)],
|
||||
current_reward_destination: RewardDestination,
|
||||
palette: StylePalette
|
||||
}
|
||||
|
||||
impl Default for PayeePopup {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl PayeePopup {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
is_active: false,
|
||||
stash_secret_seed: [0u8; 32],
|
||||
stash_account_id: [0u8; 32],
|
||||
proposed_account_id: None,
|
||||
action_tx: None,
|
||||
network_tx: None,
|
||||
table_state: TableState::new(),
|
||||
is_bonded: false,
|
||||
is_account_chosen: false,
|
||||
is_input_active: false,
|
||||
address: Input::new(String::new()),
|
||||
current_reward_destination: Default::default(),
|
||||
possible_payee_options: &[
|
||||
("Re-stake", "(pay into the stash account, increasing the amount at stake accordingly)"),
|
||||
("Stake", "(pay into the stash account, not increasing the amount at stake)"),
|
||||
("Account", "(pay into a specified account different from stash)"),
|
||||
("None", "(refuse to receive all rewards from staking)"),
|
||||
],
|
||||
palette: StylePalette::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn close_popup(&mut self) {
|
||||
self.is_active = false;
|
||||
if let Some(action_tx) = &self.action_tx {
|
||||
let _ = action_tx.send(Action::ClosePopup);
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_index_from_destination(&mut self) -> usize {
|
||||
let (index, address) = match self.current_reward_destination {
|
||||
RewardDestination::Staked => (0, Default::default()),
|
||||
RewardDestination::Stash => (1, Default::default()),
|
||||
RewardDestination::Account(account_id) => {
|
||||
let address = AccountId32::from(account_id)
|
||||
.to_ss58check_with_version(Ss58AddressFormat::custom(1996));
|
||||
(2, address)
|
||||
},
|
||||
RewardDestination::None => (3, Default::default()),
|
||||
_ => (0, Default::default()),
|
||||
};
|
||||
self.address = Input::new(address);
|
||||
index
|
||||
}
|
||||
|
||||
fn move_to_row(&mut self, index: usize) {
|
||||
self.table_state.select(Some(index));
|
||||
self.is_account_chosen = index == 2;
|
||||
}
|
||||
|
||||
fn next_row(&mut self) {
|
||||
let i = match self.table_state.selected() {
|
||||
Some(i) => {
|
||||
if i >= self.possible_payee_options.len() - 1 {
|
||||
i
|
||||
} else {
|
||||
i + 1
|
||||
}
|
||||
},
|
||||
None => 0,
|
||||
};
|
||||
self.move_to_row(i);
|
||||
}
|
||||
|
||||
|
||||
fn previous_row(&mut self) {
|
||||
let i = match self.table_state.selected() {
|
||||
Some(i) => {
|
||||
if i == 0 {
|
||||
0
|
||||
} else {
|
||||
i - 1
|
||||
}
|
||||
},
|
||||
None => 0
|
||||
};
|
||||
self.move_to_row(i);
|
||||
}
|
||||
|
||||
fn log_event(&mut self, message: String, level: ActionLevel) {
|
||||
if let Some(action_tx) = &self.action_tx {
|
||||
let _ = action_tx.send(Action::EventLog(
|
||||
message,
|
||||
level,
|
||||
ActionTarget::ValidatorLog));
|
||||
}
|
||||
}
|
||||
|
||||
fn trigger_address_input(&mut self) {
|
||||
self.is_input_active = !self.is_input_active;
|
||||
}
|
||||
|
||||
fn submit_new_input(&mut self) {
|
||||
match AccountId32::from_ss58check_with_version(self.address.value()) {
|
||||
Ok((account_id, format)) => {
|
||||
if format != Ss58AddressFormat::custom(1996) {
|
||||
self.log_event(
|
||||
format!("provided public address for {} is not part of Casper/Ghost ecosystem", self.address.value()),
|
||||
ActionLevel::Error);
|
||||
}
|
||||
let seed_vec = account_id.to_raw_vec();
|
||||
let mut account_id = [0u8; 32];
|
||||
account_id.copy_from_slice(&seed_vec);
|
||||
|
||||
self.proposed_account_id = Some(account_id);
|
||||
self.submit_new_payee();
|
||||
},
|
||||
_ => {
|
||||
self.log_event(
|
||||
format!("could not create valid account id from {}", self.address.value()),
|
||||
ActionLevel::Error);
|
||||
self.proposed_account_id = None;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
fn submit_new_payee(&mut self) {
|
||||
if let Some(index) = self.table_state.selected() {
|
||||
let new_destination = match index {
|
||||
0 => RewardDestination::Staked,
|
||||
1 => RewardDestination::Stash,
|
||||
2 => {
|
||||
let account_id = self.proposed_account_id
|
||||
.expect("checked before in submit_new_input; qed");
|
||||
RewardDestination::Account(account_id)
|
||||
}
|
||||
3 => RewardDestination::None,
|
||||
_ => RewardDestination::Staked,
|
||||
};
|
||||
|
||||
if !self.is_bonded {
|
||||
self.log_event(
|
||||
"no bond detected, stake minimum bond amount first".to_string(),
|
||||
ActionLevel::Warn);
|
||||
} else if new_destination == self.current_reward_destination {
|
||||
self.log_event(
|
||||
"same destination choosen, no need for transaction".to_string(),
|
||||
ActionLevel::Warn);
|
||||
} else {
|
||||
if let Some(network_tx) = &self.network_tx {
|
||||
let _ = network_tx.send(Action::SetPayee(
|
||||
self.stash_secret_seed,
|
||||
new_destination,
|
||||
ActionTarget::ValidatorLog));
|
||||
}
|
||||
}
|
||||
if let Some(action_tx) = &self.action_tx {
|
||||
let _ = action_tx.send(Action::ClosePopup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn enter_char(&mut self, new_char: char) {
|
||||
let _ = self.address.handle(InputRequest::InsertChar(new_char));
|
||||
}
|
||||
|
||||
fn delete_char(&mut self) {
|
||||
let _ = self.address.handle(InputRequest::DeletePrevChar);
|
||||
}
|
||||
|
||||
fn move_cursor_right(&mut self) {
|
||||
let _ = self.address.handle(InputRequest::GoToNextChar);
|
||||
}
|
||||
|
||||
fn move_cursor_left(&mut self) {
|
||||
let _ = self.address.handle(InputRequest::GoToPrevChar);
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialComponent for PayeePopup {
|
||||
fn set_active(&mut self, current_tab: CurrentTab) {
|
||||
match current_tab {
|
||||
CurrentTab::PayeePopup => self.is_active = true,
|
||||
_ => {
|
||||
self.is_active = false;
|
||||
self.is_account_chosen = false;
|
||||
self.is_input_active = false;
|
||||
|
||||
let index = self.parse_index_from_destination();
|
||||
self.move_to_row(index);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for PayeePopup {
|
||||
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_normal_border_style(style.get("normal_border_style").copied());
|
||||
self.palette.with_normal_title_style(style.get("normal_title_style").copied());
|
||||
self.palette.with_popup_style(style.get("popup_style").copied());
|
||||
self.palette.with_popup_title_style(style.get("popup_title_style").copied());
|
||||
self.palette.with_highlight_style(style.get("highlight_style").copied());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_key_event(&mut self, key: KeyEvent) -> Result<Option<Action>> {
|
||||
if self.is_active && key.kind == KeyEventKind::Press {
|
||||
if self.is_input_active {
|
||||
match key.code {
|
||||
KeyCode::Enter => self.submit_new_input(),
|
||||
KeyCode::Char(to_insert) => self.enter_char(to_insert),
|
||||
KeyCode::Backspace => self.delete_char(),
|
||||
KeyCode::Left => self.move_cursor_left(),
|
||||
KeyCode::Right => self.move_cursor_right(),
|
||||
KeyCode::Esc => self.trigger_address_input(),
|
||||
_ => {},
|
||||
};
|
||||
} else {
|
||||
match key.code {
|
||||
KeyCode::Enter if !self.is_account_chosen => self.submit_new_payee(),
|
||||
KeyCode::Enter if self.is_account_chosen => self.trigger_address_input(),
|
||||
KeyCode::Up | KeyCode::Char('k') => self.previous_row(),
|
||||
KeyCode::Down | KeyCode::Char('j') => self.next_row(),
|
||||
KeyCode::Esc => self.close_popup(),
|
||||
_ => {},
|
||||
};
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn update(&mut self, action: Action) -> Result<Option<Action>> {
|
||||
match action {
|
||||
Action::SetStashAccount(account_id) => self.stash_account_id = account_id,
|
||||
Action::SetStashSecret(secret_seed) => self.stash_secret_seed = secret_seed,
|
||||
Action::SetIsBonded(is_bonded, account_id) if self.stash_account_id == account_id =>
|
||||
self.is_bonded = is_bonded,
|
||||
Action::SetStakingPayee(reward_destination, account_id) if self.stash_account_id == account_id => {
|
||||
let destination_changed = self.current_reward_destination != reward_destination;
|
||||
self.current_reward_destination = reward_destination;
|
||||
if destination_changed || self.table_state.selected().is_none() {
|
||||
let index = self.parse_index_from_destination();
|
||||
self.move_to_row(index);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
|
||||
if self.is_active {
|
||||
let (border_style, border_type) = self.palette.create_popup_style();
|
||||
let size = area.as_size();
|
||||
let input_area = Rect::new(size.width / 2, size.height / 2, 51, 3);
|
||||
|
||||
let table = Table::new(
|
||||
self.possible_payee_options
|
||||
.iter()
|
||||
.map(|data| {
|
||||
Row::new(vec![
|
||||
Cell::from(Text::from(data.0.to_string()).alignment(Alignment::Left)),
|
||||
Cell::from(Text::from(data.1.to_string()).alignment(Alignment::Left)),
|
||||
])
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
[Constraint::Length(8), 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_style(self.palette.create_popup_title_style())
|
||||
.title_alignment(Alignment::Right)
|
||||
.title("Select reward destination"));
|
||||
|
||||
let v = Layout::vertical([Constraint::Max(6)]).flex(Flex::Center);
|
||||
let h = Layout::horizontal([Constraint::Max(83)]).flex(Flex::Center);
|
||||
let [area] = v.areas(area);
|
||||
let [area] = h.areas(area);
|
||||
|
||||
frame.render_widget(Clear, area);
|
||||
frame.render_stateful_widget(table, area, &mut self.table_state);
|
||||
|
||||
if self.is_input_active {
|
||||
let input_amount = Paragraph::new(self.address.value())
|
||||
.block(Block::bordered()
|
||||
.border_style(border_style)
|
||||
.border_type(border_type)
|
||||
.title_style(self.palette.create_popup_title_style())
|
||||
.title_alignment(Alignment::Right)
|
||||
.title("Destination account"));
|
||||
|
||||
let v = Layout::vertical([Constraint::Max(8)]).flex(Flex::Center);
|
||||
let h = Layout::horizontal([Constraint::Max(51)]).flex(Flex::Center);
|
||||
let [input_area] = v.areas(input_area);
|
||||
let [input_area] = h.areas(input_area);
|
||||
|
||||
frame.render_widget(Clear, input_area);
|
||||
frame.render_widget(input_amount, input_area);
|
||||
|
||||
frame.set_cursor_position(Position::new(
|
||||
input_area.x + self.address.cursor() as u16 + 1,
|
||||
input_area.y + 1
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
145
src/components/validator/payout_all_popup.rs
Normal file
145
src/components/validator/payout_all_popup.rs
Normal file
@ -0,0 +1,145 @@
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind};
|
||||
use color_eyre::Result;
|
||||
use ratatui::{
|
||||
layout::{Alignment, Constraint, Flex, Layout, Rect},
|
||||
widgets::{Block, Clear, Paragraph},
|
||||
Frame
|
||||
};
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
use super::{Component, PartialComponent, CurrentTab};
|
||||
use crate::{
|
||||
action::Action,
|
||||
config::Config,
|
||||
palette::StylePalette,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PayoutAllPopup {
|
||||
is_active: bool,
|
||||
action_tx: Option<UnboundedSender<Action>>,
|
||||
network_tx: Option<Sender<Action>>,
|
||||
secret_seed: [u8; 32],
|
||||
stash_account_id: [u8; 32],
|
||||
era_indexes: Vec<u32>,
|
||||
palette: StylePalette
|
||||
}
|
||||
|
||||
impl Default for PayoutAllPopup {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl PayoutAllPopup {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
is_active: false,
|
||||
secret_seed: [0u8; 32],
|
||||
stash_account_id: [0u8; 32],
|
||||
era_indexes: Default::default(),
|
||||
action_tx: None,
|
||||
network_tx: None,
|
||||
palette: StylePalette::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn close_popup(&mut self) {
|
||||
self.is_active = false;
|
||||
self.era_indexes = Default::default();
|
||||
if let Some(action_tx) = &self.action_tx {
|
||||
let _ = action_tx.send(Action::ClosePopup);
|
||||
}
|
||||
}
|
||||
|
||||
fn start_payout(&mut self) {
|
||||
if let Some(network_tx) = &self.network_tx {
|
||||
for era_index in &self.era_indexes {
|
||||
let _ = network_tx.send(Action::PayoutStakers(
|
||||
self.secret_seed,
|
||||
self.stash_account_id,
|
||||
*era_index));
|
||||
}
|
||||
}
|
||||
self.close_popup();
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialComponent for PayoutAllPopup {
|
||||
fn set_active(&mut self, current_tab: CurrentTab) {
|
||||
match current_tab {
|
||||
CurrentTab::PayoutAllPopup => self.is_active = true,
|
||||
_ => self.is_active = false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for PayoutAllPopup {
|
||||
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_normal_border_style(style.get("normal_border_style").copied());
|
||||
self.palette.with_normal_title_style(style.get("normal_title_style").copied());
|
||||
self.palette.with_popup_style(style.get("popup_style").copied());
|
||||
self.palette.with_popup_title_style(style.get("popup_title_style").copied());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_key_event(&mut self, key: KeyEvent) -> Result<Option<Action>> {
|
||||
if self.is_active && key.kind == KeyEventKind::Press {
|
||||
match key.code {
|
||||
KeyCode::Enter => self.start_payout(),
|
||||
KeyCode::Esc => self.close_popup(),
|
||||
_ => {},
|
||||
};
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn update(&mut self, action: Action) -> Result<Option<Action>> {
|
||||
match action {
|
||||
Action::PayoutAllValidatorPopup(era_indexes) => self.era_indexes = era_indexes,
|
||||
Action::SetStashSecret(secret_seed) => self.secret_seed = secret_seed,
|
||||
Action::SetStashAccount(account_id) => self.stash_account_id = account_id,
|
||||
_ => {}
|
||||
};
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
|
||||
if self.is_active {
|
||||
let (border_style, border_type) = self.palette.create_popup_style();
|
||||
let popup = Paragraph::new(
|
||||
format!(" Do payout for {} eras, from {} to {}",
|
||||
self.era_indexes.len(),
|
||||
self.era_indexes.first().expect("Length of unclaimed indexes always more then one; qed"),
|
||||
self.era_indexes.last().expect("Length of unclaimed indexes always more then one; qed")))
|
||||
.block(Block::bordered()
|
||||
.border_style(border_style)
|
||||
.border_type(border_type)
|
||||
.title_style(self.palette.create_popup_title_style())
|
||||
.title_alignment(Alignment::Right)
|
||||
.title("Enter to proceed / Esc to close"));
|
||||
let v = Layout::vertical([Constraint::Max(3)]).flex(Flex::Center);
|
||||
let h = Layout::horizontal([Constraint::Max(50)]).flex(Flex::Center);
|
||||
let [area] = v.areas(area);
|
||||
let [area] = h.areas(area);
|
||||
|
||||
frame.render_widget(Clear, area);
|
||||
frame.render_widget(popup, area);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
140
src/components/validator/payout_popup.rs
Normal file
140
src/components/validator/payout_popup.rs
Normal file
@ -0,0 +1,140 @@
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind};
|
||||
use color_eyre::Result;
|
||||
use ratatui::{
|
||||
layout::{Alignment, Constraint, Flex, Layout, Rect},
|
||||
widgets::{Block, Clear, Paragraph},
|
||||
Frame
|
||||
};
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
use super::{Component, PartialComponent, CurrentTab};
|
||||
use crate::{
|
||||
action::Action,
|
||||
config::Config,
|
||||
palette::StylePalette,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PayoutPopup {
|
||||
is_active: bool,
|
||||
action_tx: Option<UnboundedSender<Action>>,
|
||||
network_tx: Option<Sender<Action>>,
|
||||
secret_seed: [u8; 32],
|
||||
stash_account_id: [u8; 32],
|
||||
era_index: u32,
|
||||
palette: StylePalette
|
||||
}
|
||||
|
||||
impl Default for PayoutPopup {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl PayoutPopup {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
is_active: false,
|
||||
secret_seed: [0u8; 32],
|
||||
stash_account_id: [0u8; 32],
|
||||
era_index: 0u32,
|
||||
action_tx: None,
|
||||
network_tx: None,
|
||||
palette: StylePalette::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn close_popup(&mut self) {
|
||||
self.is_active = false;
|
||||
if let Some(action_tx) = &self.action_tx {
|
||||
let _ = action_tx.send(Action::ClosePopup);
|
||||
}
|
||||
}
|
||||
|
||||
fn start_payout(&mut self) {
|
||||
if let Some(network_tx) = &self.network_tx {
|
||||
let _ = network_tx.send(Action::PayoutStakers(
|
||||
self.secret_seed,
|
||||
self.stash_account_id,
|
||||
self.era_index));
|
||||
}
|
||||
if let Some(action_tx) = &self.action_tx {
|
||||
let _ = action_tx.send(Action::ClosePopup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialComponent for PayoutPopup {
|
||||
fn set_active(&mut self, current_tab: CurrentTab) {
|
||||
match current_tab {
|
||||
CurrentTab::PayoutPopup => self.is_active = true,
|
||||
_ => self.is_active = false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for PayoutPopup {
|
||||
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_normal_border_style(style.get("normal_border_style").copied());
|
||||
self.palette.with_normal_title_style(style.get("normal_title_style").copied());
|
||||
self.palette.with_popup_style(style.get("popup_style").copied());
|
||||
self.palette.with_popup_title_style(style.get("popup_title_style").copied());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_key_event(&mut self, key: KeyEvent) -> Result<Option<Action>> {
|
||||
if self.is_active && key.kind == KeyEventKind::Press {
|
||||
match key.code {
|
||||
KeyCode::Enter => self.start_payout(),
|
||||
KeyCode::Esc => self.close_popup(),
|
||||
_ => {},
|
||||
};
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn update(&mut self, action: Action) -> Result<Option<Action>> {
|
||||
match action {
|
||||
Action::PayoutValidatorPopup(era_index) => self.era_index = era_index,
|
||||
Action::SetStashSecret(secret_seed) => self.secret_seed = secret_seed,
|
||||
Action::SetStashAccount(account_id) => self.stash_account_id = account_id,
|
||||
_ => {}
|
||||
};
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
|
||||
if self.is_active {
|
||||
let (border_style, border_type) = self.palette.create_popup_style();
|
||||
let popup = Paragraph::new(format!(" Do payout for era #{}", self.era_index))
|
||||
.block(Block::bordered()
|
||||
.border_style(border_style)
|
||||
.border_type(border_type)
|
||||
.title_style(self.palette.create_popup_title_style())
|
||||
.title_alignment(Alignment::Right)
|
||||
.title("Enter to proceed / Esc to close"));
|
||||
let v = Layout::vertical([Constraint::Max(3)]).flex(Flex::Center);
|
||||
let h = Layout::horizontal([Constraint::Max(50)]).flex(Flex::Center);
|
||||
let [area] = v.areas(area);
|
||||
let [area] = h.areas(area);
|
||||
|
||||
frame.render_widget(Clear, area);
|
||||
frame.render_widget(popup, area);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
198
src/components/validator/peers.rs
Normal file
198
src/components/validator/peers.rs
Normal 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;
|
||||
self.scroll_state = self.scroll_state.content_length(self.peers.len());
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
}
|
||||
184
src/components/validator/rebond_popup.rs
Normal file
184
src/components/validator/rebond_popup.rs
Normal file
@ -0,0 +1,184 @@
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind};
|
||||
use color_eyre::Result;
|
||||
use ratatui::{
|
||||
layout::{Position, Alignment, Constraint, Flex, Layout, Rect},
|
||||
widgets::{Block, Clear, Paragraph},
|
||||
Frame
|
||||
};
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
use super::{Component, PartialComponent, CurrentTab};
|
||||
use crate::{
|
||||
action::Action,
|
||||
config::Config,
|
||||
palette::StylePalette,
|
||||
types::{ActionLevel, ActionTarget},
|
||||
widgets::{Input, InputRequest},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RebondPopup {
|
||||
is_active: bool,
|
||||
action_tx: Option<UnboundedSender<Action>>,
|
||||
network_tx: Option<Sender<Action>>,
|
||||
secret_seed: [u8; 32],
|
||||
amount: Input,
|
||||
palette: StylePalette
|
||||
}
|
||||
|
||||
impl Default for RebondPopup {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl RebondPopup {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
is_active: false,
|
||||
secret_seed: [0u8; 32],
|
||||
action_tx: None,
|
||||
network_tx: None,
|
||||
amount: Input::new(String::new()),
|
||||
palette: StylePalette::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn close_popup(&mut self) {
|
||||
self.is_active = false;
|
||||
if let Some(action_tx) = &self.action_tx {
|
||||
let _ = action_tx.send(Action::ClosePopup);
|
||||
}
|
||||
}
|
||||
|
||||
fn log_event(&mut self, message: String, level: ActionLevel) {
|
||||
if let Some(action_tx) = &self.action_tx {
|
||||
let _ = action_tx.send(
|
||||
Action::EventLog(message, level, ActionTarget::ValidatorLog));
|
||||
}
|
||||
}
|
||||
|
||||
fn submit_message(&mut self) {
|
||||
if let Some(network_tx) = &self.network_tx {
|
||||
let str_amount = self.amount.value();
|
||||
let str_amount = if str_amount.starts_with('.') {
|
||||
&format!("0{}", str_amount)[..]
|
||||
} else {
|
||||
str_amount
|
||||
};
|
||||
match str_amount.parse::<f64>() {
|
||||
Ok(value) => {
|
||||
let amount = (value * 1_000_000_000_000_000_000.0) as u128;
|
||||
let _ = network_tx.send(Action::RebondFrom(self.secret_seed, amount));
|
||||
if let Some(action_tx) = &self.action_tx {
|
||||
let _ = action_tx.send(Action::ClosePopup);
|
||||
}
|
||||
},
|
||||
Err(err) => self.log_event(
|
||||
format!("invalid amount, error: {err}"), ActionLevel::Error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn enter_char(&mut self, new_char: char) {
|
||||
let is_separator_needed = !self.amount.value().contains('.') && new_char == '.';
|
||||
if new_char.is_digit(10) || is_separator_needed {
|
||||
let _ = self.amount.handle(InputRequest::InsertChar(new_char));
|
||||
}
|
||||
}
|
||||
|
||||
fn delete_char(&mut self) {
|
||||
let _ = self.amount.handle(InputRequest::DeletePrevChar);
|
||||
}
|
||||
|
||||
fn move_cursor_right(&mut self) {
|
||||
let _ = self.amount.handle(InputRequest::GoToNextChar);
|
||||
}
|
||||
|
||||
fn move_cursor_left(&mut self) {
|
||||
let _ = self.amount.handle(InputRequest::GoToPrevChar);
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialComponent for RebondPopup {
|
||||
fn set_active(&mut self, current_tab: CurrentTab) {
|
||||
match current_tab {
|
||||
CurrentTab::RebondPopup => self.is_active = true,
|
||||
_ => {
|
||||
self.is_active = false;
|
||||
self.amount = Input::new(String::new());
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for RebondPopup {
|
||||
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_normal_border_style(style.get("normal_border_style").copied());
|
||||
self.palette.with_normal_title_style(style.get("normal_title_style").copied());
|
||||
self.palette.with_popup_style(style.get("popup_style").copied());
|
||||
self.palette.with_popup_title_style(style.get("popup_title_style").copied());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn handle_key_event(&mut self, key: KeyEvent) -> Result<Option<Action>> {
|
||||
if self.is_active && key.kind == KeyEventKind::Press {
|
||||
match key.code {
|
||||
KeyCode::Enter => self.submit_message(),
|
||||
KeyCode::Char(to_insert) => self.enter_char(to_insert),
|
||||
KeyCode::Backspace => self.delete_char(),
|
||||
KeyCode::Left => self.move_cursor_left(),
|
||||
KeyCode::Right => self.move_cursor_right(),
|
||||
KeyCode::Esc => self.close_popup(),
|
||||
_ => {},
|
||||
};
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn update(&mut self, action: Action) -> Result<Option<Action>> {
|
||||
match action {
|
||||
Action::SetStashSecret(secret_seed) => self.secret_seed = secret_seed,
|
||||
_ => {}
|
||||
};
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
|
||||
if self.is_active {
|
||||
let (border_style, border_type) = self.palette.create_popup_style();
|
||||
let input = Paragraph::new(self.amount.value())
|
||||
.block(Block::bordered()
|
||||
.border_style(border_style)
|
||||
.border_type(border_type)
|
||||
.title_style(self.palette.create_popup_title_style())
|
||||
.title_alignment(Alignment::Right)
|
||||
.title(format!("Amount to rebond")));
|
||||
let v = Layout::vertical([Constraint::Max(3)]).flex(Flex::Center);
|
||||
let h = Layout::horizontal([Constraint::Max(50)]).flex(Flex::Center);
|
||||
let [area] = v.areas(area);
|
||||
let [area] = h.areas(area);
|
||||
|
||||
frame.render_widget(Clear, area);
|
||||
frame.render_widget(input, area);
|
||||
frame.set_cursor_position(Position::new(
|
||||
area.x + self.amount.cursor() as u16 + 1,
|
||||
area.y + 1
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
177
src/components/validator/reward_details.rs
Normal file
177
src/components/validator/reward_details.rs
Normal file
@ -0,0 +1,177 @@
|
||||
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::types::EraRewardPoints;
|
||||
use crate::widgets::DotSpinner;
|
||||
use crate::{
|
||||
action::Action,
|
||||
config::Config,
|
||||
palette::StylePalette,
|
||||
};
|
||||
|
||||
pub struct RewardDetails {
|
||||
palette: StylePalette,
|
||||
commission: Option<u32>,
|
||||
nominators_blocked: bool,
|
||||
apy: String,
|
||||
treasury_apy: String,
|
||||
inflation: String,
|
||||
stash: [u8; 32],
|
||||
in_staking_validators: bool,
|
||||
in_queued_keys: bool,
|
||||
in_next_keys: bool,
|
||||
is_disabled: bool,
|
||||
in_rewards: bool,
|
||||
}
|
||||
|
||||
impl Default for RewardDetails {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl RewardDetails {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
palette: StylePalette::default(),
|
||||
commission: None,
|
||||
nominators_blocked: false,
|
||||
apy: String::from("0.0%"),
|
||||
treasury_apy: String::from("0.0%"),
|
||||
inflation: String::from("0.0%"),
|
||||
stash: [0u8; 32],
|
||||
in_staking_validators: false,
|
||||
in_queued_keys: false,
|
||||
in_next_keys: false,
|
||||
is_disabled: false,
|
||||
in_rewards: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn comission_to_string(&self) -> String {
|
||||
match self.commission {
|
||||
Some(commission) => {
|
||||
if self.nominators_blocked { "blocked".to_string() }
|
||||
else { format!("{:.2}%", commission as f64 / 10_000_000.0) }
|
||||
},
|
||||
None => DotSpinner::default().to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn update_individual(&mut self, individual: &Vec<EraRewardPoints>) {
|
||||
let (is_disabled, in_rewards) = individual
|
||||
.iter()
|
||||
.find(|data| data.account_id == self.stash)
|
||||
.map(|data| (data.disabled, true))
|
||||
.unwrap_or((false, false));
|
||||
self.is_disabled = is_disabled;
|
||||
self.in_rewards = in_rewards;
|
||||
}
|
||||
}
|
||||
|
||||
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::SetStashAccount(stash) => self.stash = stash,
|
||||
Action::SetCurrentValidatorEraRewards(_, _, individual) => self.update_individual(&individual),
|
||||
Action::SetValidatorPrefs(commission, disabled, account_id) if self.stash == account_id => {
|
||||
self.commission = commission;
|
||||
self.in_staking_validators = commission.is_some();
|
||||
self.nominators_blocked = disabled;
|
||||
}
|
||||
Action::SetSessionKey(name, session_key_info) => {
|
||||
if name.starts_with("q_") {
|
||||
self.in_queued_keys = !session_key_info.key.is_empty();
|
||||
} else {
|
||||
self.in_next_keys = !session_key_info.key.is_empty();
|
||||
}
|
||||
},
|
||||
Action::Apy(apy) => self.apy = apy,
|
||||
Action::TreasuryApy(apy) => self.treasury_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 status = if self.is_disabled {
|
||||
"Disabled"
|
||||
} else {
|
||||
let is_fully_in_session = self.in_queued_keys && self.in_next_keys;
|
||||
match (self.in_staking_validators, is_fully_in_session) {
|
||||
(true, true) => "Active",
|
||||
(true, false) => "Rotating",
|
||||
(false, true) if self.in_rewards => { "Stopping" }
|
||||
(false, true) if !self.in_rewards => { "Chill" }
|
||||
_ => "Nothing",
|
||||
}
|
||||
};
|
||||
let staking_status = format!("Staking status: {status}");
|
||||
|
||||
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("Validator APY".to_string()).alignment(Alignment::Left)),
|
||||
Cell::from(Text::from(self.apy.clone()).alignment(Alignment::Right)),
|
||||
]),
|
||||
Row::new(vec![
|
||||
Cell::from(Text::from("Treasury APY".to_string()).alignment(Alignment::Left)),
|
||||
Cell::from(Text::from(self.treasury_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(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(staking_status));
|
||||
|
||||
frame.render_widget(table, place);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
188
src/components/validator/rotate_popup.rs
Normal file
188
src/components/validator/rotate_popup.rs
Normal file
@ -0,0 +1,188 @@
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind};
|
||||
use color_eyre::Result;
|
||||
use ratatui::{
|
||||
layout::{Alignment, Constraint, Flex, Layout, Rect},
|
||||
text::Text,
|
||||
widgets::{Block, Cell, Clear, Row, Table},
|
||||
Frame,
|
||||
};
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
use super::{Component, PartialComponent, CurrentTab};
|
||||
use crate::{
|
||||
action::Action,
|
||||
config::Config,
|
||||
palette::StylePalette,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RotatePopup {
|
||||
is_active: bool,
|
||||
action_tx: Option<UnboundedSender<Action>>,
|
||||
network_tx: Option<Sender<Action>>,
|
||||
cached_keys: String,
|
||||
secret_seed: [u8; 32],
|
||||
palette: StylePalette
|
||||
}
|
||||
|
||||
impl Default for RotatePopup {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl RotatePopup {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
is_active: false,
|
||||
action_tx: None,
|
||||
network_tx: None,
|
||||
cached_keys: String::new(),
|
||||
secret_seed: [0u8; 32],
|
||||
palette: StylePalette::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn close_popup(&mut self) {
|
||||
self.is_active = false;
|
||||
if let Some(action_tx) = &self.action_tx {
|
||||
let _ = action_tx.send(Action::ClosePopup);
|
||||
}
|
||||
}
|
||||
|
||||
fn rotate_keys(&mut self) {
|
||||
if !self.cached_keys.is_empty() && self.cached_keys.len() == 258 {
|
||||
if let Some(network_tx) = &self.network_tx {
|
||||
let _ = network_tx.send(Action::SetSessionKeys(
|
||||
self.secret_seed, self.cached_keys.clone()));
|
||||
}
|
||||
if let Some(action_tx) = &self.action_tx {
|
||||
let _ = action_tx.send(Action::ClosePopup);
|
||||
}
|
||||
} else {
|
||||
if let Some(network_tx) = &self.network_tx {
|
||||
let _ = network_tx.send(Action::RotateSessionKeys);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_session_keys(&self) -> (String, String, String, String) {
|
||||
if !self.cached_keys.is_empty() && self.cached_keys.len() == 258 {
|
||||
let gran_key = format!("0x{}", &self.cached_keys[2..66]);
|
||||
let babe_key = format!("0x{}", &self.cached_keys[66..130]);
|
||||
let audi_key = format!("0x{}", &self.cached_keys[130..194]);
|
||||
let slow_key = format!("0x{}", &self.cached_keys[194..258]);
|
||||
|
||||
(gran_key, babe_key, audi_key, slow_key)
|
||||
} else {
|
||||
(
|
||||
String::from("0x0000000000000000000000000000000000000000000000000000000000000000"),
|
||||
String::from("0x0000000000000000000000000000000000000000000000000000000000000000"),
|
||||
String::from("0x0000000000000000000000000000000000000000000000000000000000000000"),
|
||||
String::from("0x0000000000000000000000000000000000000000000000000000000000000000"),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialComponent for RotatePopup {
|
||||
fn set_active(&mut self, current_tab: CurrentTab) {
|
||||
match current_tab {
|
||||
CurrentTab::RotatePopup => self.is_active = true,
|
||||
_ => self.is_active = false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for RotatePopup {
|
||||
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_normal_border_style(style.get("normal_border_style").copied());
|
||||
self.palette.with_normal_title_style(style.get("normal_title_style").copied());
|
||||
self.palette.with_popup_style(style.get("popup_style").copied());
|
||||
self.palette.with_popup_title_style(style.get("popup_title_style").copied());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update(&mut self, action: Action) -> Result<Option<Action>> {
|
||||
match action {
|
||||
Action::StoreRotatedKeys(cached_keys) => self.cached_keys = cached_keys,
|
||||
Action::SetStashSecret(secret_seed) => self.secret_seed = secret_seed,
|
||||
_ => {}
|
||||
};
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn handle_key_event(&mut self, key: KeyEvent) -> Result<Option<Action>> {
|
||||
if self.is_active && key.kind == KeyEventKind::Press {
|
||||
match key.code {
|
||||
KeyCode::Enter => self.rotate_keys(),
|
||||
KeyCode::Esc => self.close_popup(),
|
||||
_ => {},
|
||||
};
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
|
||||
if self.is_active {
|
||||
let (border_style, border_type) = self.palette.create_popup_style();
|
||||
|
||||
let (gran_key, babe_key, audi_key, slow_key) = self.parse_session_keys();
|
||||
let table = Table::new(
|
||||
vec![
|
||||
Row::new(vec![
|
||||
Cell::from(Text::from("gran".to_string()).alignment(Alignment::Left)),
|
||||
Cell::from(Text::from(gran_key).alignment(Alignment::Right)),
|
||||
]),
|
||||
Row::new(vec![
|
||||
Cell::from(Text::from("babe".to_string()).alignment(Alignment::Left)),
|
||||
Cell::from(Text::from(babe_key).alignment(Alignment::Right)),
|
||||
]),
|
||||
Row::new(vec![
|
||||
Cell::from(Text::from("audi".to_string()).alignment(Alignment::Left)),
|
||||
Cell::from(Text::from(audi_key).alignment(Alignment::Right)),
|
||||
]),
|
||||
Row::new(vec![
|
||||
Cell::from(Text::from("slow".to_string()).alignment(Alignment::Left)),
|
||||
Cell::from(Text::from(slow_key).alignment(Alignment::Right)),
|
||||
]),
|
||||
],
|
||||
[
|
||||
Constraint::Length(4),
|
||||
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("Rotate session keys (Enter to proceed / Esc to close)"));
|
||||
|
||||
let v = Layout::vertical([Constraint::Max(6)]).flex(Flex::Center);
|
||||
let h = Layout::horizontal([Constraint::Max(73)]).flex(Flex::Center);
|
||||
let [area] = v.areas(area);
|
||||
let [area] = h.areas(area);
|
||||
|
||||
frame.render_widget(Clear, area);
|
||||
frame.render_widget(table, area);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
148
src/components/validator/staking_details.rs
Normal file
148
src/components/validator/staking_details.rs
Normal file
@ -0,0 +1,148 @@
|
||||
use color_eyre::Result;
|
||||
use ratatui::layout::Constraint;
|
||||
use ratatui::{
|
||||
text::Text,
|
||||
layout::{Alignment, Rect},
|
||||
widgets::{Block, Cell, Row, Table},
|
||||
Frame
|
||||
};
|
||||
use subxt::ext::sp_core::crypto::{Ss58Codec, Ss58AddressFormat, AccountId32};
|
||||
|
||||
use super::{PartialComponent, Component, CurrentTab};
|
||||
use crate::types::RewardDestination;
|
||||
use crate::{
|
||||
action::Action,
|
||||
config::Config,
|
||||
palette::StylePalette,
|
||||
};
|
||||
|
||||
pub struct StakingDetails {
|
||||
palette: StylePalette,
|
||||
staked_own: u128,
|
||||
staked_total: u128,
|
||||
next_reward: u128,
|
||||
stash: [u8; 32],
|
||||
reward_destination: RewardDestination,
|
||||
}
|
||||
|
||||
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,
|
||||
next_reward: 0,
|
||||
stash: [0u8; 32],
|
||||
reward_destination: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
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 get_reward_destination(&self) -> String {
|
||||
match self.reward_destination {
|
||||
RewardDestination::Staked => "re-stake".to_string(),
|
||||
RewardDestination::Stash => "stake".to_string(),
|
||||
RewardDestination::Controller => "controller".to_string(),
|
||||
RewardDestination::None => "empty".to_string(),
|
||||
RewardDestination::Account(account_id) => {
|
||||
let address = AccountId32::from(account_id)
|
||||
.to_ss58check_with_version(Ss58AddressFormat::custom(1996));
|
||||
let tail = address.len().saturating_sub(5);
|
||||
format!("{}..{}", &address[..5], &address[tail..])
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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::SetStashAccount(account_id) => self.stash = account_id,
|
||||
Action::NextReward(next_reward) => self.next_reward = next_reward,
|
||||
Action::SetStakingPayee(destination, account_id) if self.stash == account_id =>
|
||||
self.reward_destination = destination,
|
||||
Action::SetStakedRatio(total, own, account_id) if self.stash == account_id => {
|
||||
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 title = format!("Staking details: {}", self.get_reward_destination());
|
||||
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)),
|
||||
]),
|
||||
Row::new(vec![
|
||||
Cell::from(Text::from("Next reward".to_string()).alignment(Alignment::Left)),
|
||||
Cell::from(Text::from(self.prepare_u128(self.next_reward)).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(title));
|
||||
|
||||
frame.render_widget(table, place);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
163
src/components/validator/stash_details.rs
Normal file
163
src/components/validator/stash_details.rs
Normal file
@ -0,0 +1,163 @@
|
||||
use color_eyre::Result;
|
||||
use ratatui::layout::Constraint;
|
||||
use ratatui::{
|
||||
text::Text,
|
||||
layout::{Alignment, Rect},
|
||||
widgets::{Block, Cell, Row, Table},
|
||||
Frame
|
||||
};
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
use super::{PartialComponent, Component, CurrentTab};
|
||||
use crate::widgets::DotSpinner;
|
||||
use crate::{
|
||||
action::Action,
|
||||
config::Config,
|
||||
palette::StylePalette,
|
||||
};
|
||||
|
||||
pub struct StashDetails {
|
||||
palette: StylePalette,
|
||||
network_tx: Option<Sender<Action>>,
|
||||
is_bonded: bool,
|
||||
free_balance: Option<u128>,
|
||||
staked_total: Option<u128>,
|
||||
staked_active: Option<u128>,
|
||||
stash_account_id: [u8; 32],
|
||||
stash_secret: [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(),
|
||||
network_tx: None,
|
||||
is_bonded: false,
|
||||
free_balance: None,
|
||||
staked_total: None,
|
||||
staked_active: None,
|
||||
stash_account_id: [0u8; 32],
|
||||
stash_secret: [0u8; 32],
|
||||
}
|
||||
}
|
||||
|
||||
fn prepare_u128(&self, maybe_value: Option<u128>) -> String {
|
||||
match maybe_value {
|
||||
Some(value) => {
|
||||
let value = value as f64 / 10f64.powi(18);
|
||||
let after = Self::DECIMALS;
|
||||
format!("{:.after$}{}", value, Self::TICKER)
|
||||
},
|
||||
None => format!("{}{}", DotSpinner::default().to_string(), 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_network_handler(&mut self, tx: Sender<Action>) -> Result<()> {
|
||||
self.network_tx = Some(tx);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn register_config_handler(&mut self, config: Config) -> Result<()> {
|
||||
if let Some(style) = config.styles.get(&crate::app::Mode::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::SetStashSecret(secret) => self.stash_secret = secret,
|
||||
Action::SetStashAccount(account_id) => self.stash_account_id = account_id,
|
||||
Action::SetIsBonded(is_bonded, account_id) if self.stash_account_id == account_id =>
|
||||
self.is_bonded = is_bonded,
|
||||
Action::SetStakedAmountRatio(total, active, account_id) if self.stash_account_id == account_id => {
|
||||
self.staked_total = total;
|
||||
self.staked_active = active;
|
||||
},
|
||||
Action::BalanceResponse(account_id, maybe_balance) if account_id == self.stash_account_id => {
|
||||
if let Some(network_tx) = &self.network_tx {
|
||||
let _ = network_tx.send(Action::SetSender(
|
||||
hex::encode(self.stash_secret),
|
||||
maybe_balance.clone().map(|b| b.nonce)));
|
||||
}
|
||||
self.free_balance = maybe_balance.map(|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(())
|
||||
}
|
||||
}
|
||||
273
src/components/validator/stash_info.rs
Normal file
273
src/components/validator/stash_info.rs
Normal file
@ -0,0 +1,273 @@
|
||||
use std::path::PathBuf;
|
||||
use std::fs::File;
|
||||
use std::io::{Write, BufRead, BufReader};
|
||||
|
||||
use color_eyre::Result;
|
||||
use ratatui::layout::Constraint;
|
||||
use ratatui::style::{Modifier, Stylize};
|
||||
use ratatui::{
|
||||
text::Text,
|
||||
layout::{Alignment, Rect},
|
||||
widgets::{Block, Cell, Row, Table},
|
||||
Frame
|
||||
};
|
||||
|
||||
use subxt::{
|
||||
tx::PairSigner,
|
||||
ext::sp_core::{
|
||||
Pair as PairT,
|
||||
sr25519::Pair,
|
||||
crypto::{Ss58Codec, Ss58AddressFormat, AccountId32},
|
||||
},
|
||||
};
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
use super::{PartialComponent, Component, CurrentTab};
|
||||
use crate::casper::CasperConfig;
|
||||
use crate::types::{ActionLevel, ActionTarget};
|
||||
use crate::{
|
||||
types::SessionKeyInfo,
|
||||
action::Action,
|
||||
config::Config,
|
||||
palette::StylePalette,
|
||||
};
|
||||
|
||||
pub struct StashInfo {
|
||||
is_active: bool,
|
||||
action_tx: Option<UnboundedSender<Action>>,
|
||||
network_tx: Option<Sender<Action>>,
|
||||
palette: StylePalette,
|
||||
stash_pair: Option<PairSigner<CasperConfig, Pair>>,
|
||||
stash_address: String,
|
||||
session_keys: std::collections::HashMap<String, SessionKeyInfo>,
|
||||
key_names: &'static [&'static str],
|
||||
stash_filepath: PathBuf,
|
||||
}
|
||||
|
||||
impl Default for StashInfo {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl StashInfo {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
is_active: false,
|
||||
action_tx: None,
|
||||
network_tx: None,
|
||||
palette: StylePalette::default(),
|
||||
stash_address: String::new(),
|
||||
stash_pair: None,
|
||||
session_keys: Default::default(),
|
||||
key_names: &["gran", "babe", "audi", "slow"],
|
||||
stash_filepath: PathBuf::from("/etc/ghost/stash-key"),
|
||||
}
|
||||
}
|
||||
|
||||
fn log_event(&mut self, message: String, level: ActionLevel) {
|
||||
if let Some(action_tx) = &self.action_tx {
|
||||
let _ = action_tx.send(
|
||||
Action::EventLog(message, level, ActionTarget::ValidatorLog));
|
||||
}
|
||||
}
|
||||
|
||||
fn read_or_create_stash(&mut self) -> Result<()> {
|
||||
match File::open(&self.stash_filepath) {
|
||||
Ok(file) => {
|
||||
let reader = BufReader::new(file);
|
||||
if let Some(Ok(line)) = reader.lines().next() {
|
||||
let stash_key = line.replace("\n", "");
|
||||
let stash_key = &stash_key[2..];
|
||||
|
||||
let seed: [u8; 32] = hex::decode(stash_key)
|
||||
.expect("stored seed is valid hex string; qed")
|
||||
.as_slice()
|
||||
.try_into()
|
||||
.expect("stored seed is valid length; qed");
|
||||
|
||||
let pair = Pair::from_seed(&seed);
|
||||
let account_id = pair.public().0;
|
||||
let address = AccountId32::from(account_id)
|
||||
.to_ss58check_with_version(Ss58AddressFormat::custom(1996));
|
||||
let pair_signer = PairSigner::<CasperConfig, Pair>::new(pair);
|
||||
|
||||
self.initiate_stash_info(account_id, seed);
|
||||
self.log_event(
|
||||
format!("stash key {address} read from disk"),
|
||||
ActionLevel::Info);
|
||||
|
||||
self.stash_address = address;
|
||||
self.stash_pair = Some(pair_signer);
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
self.log_event(
|
||||
format!("file at '{:?}' is empty, trying to create new key", &self.stash_filepath),
|
||||
ActionLevel::Warn);
|
||||
|
||||
self.generate_and_save_new_key()
|
||||
}
|
||||
},
|
||||
Err(_) => {
|
||||
self.log_event(
|
||||
format!("file at '{:?}' not found, trying to create new key", &self.stash_filepath),
|
||||
ActionLevel::Warn);
|
||||
|
||||
self.generate_and_save_new_key()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_and_save_new_key(&mut self) -> Result<()> {
|
||||
let (pair, seed) = Pair::generate();
|
||||
let secret_seed = hex::encode(seed);
|
||||
let account_id = pair.public().0;
|
||||
let address = AccountId32::from(account_id)
|
||||
.to_ss58check_with_version(Ss58AddressFormat::custom(1996));
|
||||
let pair_signer = PairSigner::<CasperConfig, Pair>::new(pair);
|
||||
|
||||
let mut new_file = File::create(&self.stash_filepath)?;
|
||||
writeln!(new_file, "0x{}", &secret_seed)?;
|
||||
|
||||
self.initiate_stash_info(account_id, seed);
|
||||
self.log_event(
|
||||
format!("new stash key {} created and stored at {:?}", &address, self.stash_filepath),
|
||||
ActionLevel::Info);
|
||||
|
||||
self.stash_address = address;
|
||||
self.stash_pair = Some(pair_signer);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn initiate_stash_info(&self, account_id: [u8; 32], secret_seed: [u8; 32]) {
|
||||
if let Some(action_tx) = &self.action_tx {
|
||||
let _ = action_tx.send(Action::SetStashAccount(account_id));
|
||||
let _ = action_tx.send(Action::SetStashSecret(secret_seed));
|
||||
}
|
||||
|
||||
if let Some(network_tx) = &self.network_tx {
|
||||
let _ = network_tx.send(Action::BalanceRequest(account_id, false));
|
||||
let _ = network_tx.send(Action::GetValidatorLedger(account_id, true));
|
||||
let _ = network_tx.send(Action::GetIsStashBonded(account_id, true));
|
||||
let _ = network_tx.send(Action::GetErasStakersOverview(account_id, true));
|
||||
let _ = network_tx.send(Action::GetValidatorPrefs(account_id, true));
|
||||
let _ = network_tx.send(Action::GetNominatorsByValidator(account_id, true));
|
||||
let _ = network_tx.send(Action::GetQueuedSessionKeys(account_id, true));
|
||||
let _ = network_tx.send(Action::GetSessionKeys(account_id, true));
|
||||
let _ = network_tx.send(Action::GetValidatorAllRewards(account_id, true));
|
||||
let _ = network_tx.send(Action::GetSlashingSpans(account_id, true));
|
||||
let _ = network_tx.send(Action::GetValidatorIsDisabled(account_id, true));
|
||||
let _ = network_tx.send(Action::GetStakingPayee(account_id, true));
|
||||
}
|
||||
}
|
||||
|
||||
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) { }
|
||||
}
|
||||
|
||||
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 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()));
|
||||
|
||||
frame.render_widget(table, place);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
198
src/components/validator/unbond_popup.rs
Normal file
198
src/components/validator/unbond_popup.rs
Normal file
@ -0,0 +1,198 @@
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind};
|
||||
use color_eyre::Result;
|
||||
use ratatui::{
|
||||
layout::{Position, Alignment, Constraint, Flex, Layout, Rect},
|
||||
widgets::{Block, Clear, Paragraph},
|
||||
Frame
|
||||
};
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
use super::{Component, PartialComponent, CurrentTab};
|
||||
use crate::{
|
||||
action::Action,
|
||||
config::Config,
|
||||
palette::StylePalette,
|
||||
types::{ActionLevel, ActionTarget},
|
||||
widgets::{Input, InputRequest},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct UnbondPopup {
|
||||
is_active: bool,
|
||||
action_tx: Option<UnboundedSender<Action>>,
|
||||
network_tx: Option<Sender<Action>>,
|
||||
stash_secret_seed: [u8; 32],
|
||||
stash_account_id: [u8; 32],
|
||||
is_bonded: bool,
|
||||
amount: Input,
|
||||
palette: StylePalette
|
||||
}
|
||||
|
||||
impl Default for UnbondPopup {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl UnbondPopup {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
is_active: false,
|
||||
stash_secret_seed: [0u8; 32],
|
||||
stash_account_id: [0u8; 32],
|
||||
action_tx: None,
|
||||
network_tx: None,
|
||||
is_bonded: false,
|
||||
amount: Input::new(String::new()),
|
||||
palette: StylePalette::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn close_popup(&mut self) {
|
||||
self.is_active = false;
|
||||
if let Some(action_tx) = &self.action_tx {
|
||||
let _ = action_tx.send(Action::ClosePopup);
|
||||
}
|
||||
}
|
||||
|
||||
fn log_event(&mut self, message: String, level: ActionLevel) {
|
||||
if let Some(action_tx) = &self.action_tx {
|
||||
let _ = action_tx.send(
|
||||
Action::EventLog(message, level, ActionTarget::ValidatorLog));
|
||||
}
|
||||
}
|
||||
|
||||
fn submit_message(&mut self) {
|
||||
if let Some(network_tx) = &self.network_tx {
|
||||
let str_amount = self.amount.value();
|
||||
let str_amount = if str_amount.starts_with('.') {
|
||||
&format!("0{}", str_amount)[..]
|
||||
} else {
|
||||
str_amount
|
||||
};
|
||||
match str_amount.parse::<f64>() {
|
||||
Ok(value) => {
|
||||
if self.is_bonded {
|
||||
let amount = (value * 1_000_000_000_000_000_000.0) as u128;
|
||||
let _ = network_tx.send(Action::UnbondFrom(
|
||||
self.stash_secret_seed, amount));
|
||||
} else {
|
||||
self.log_event(
|
||||
format!("current stash doesn't have bond yet"),
|
||||
ActionLevel::Warn);
|
||||
}
|
||||
if let Some(action_tx) = &self.action_tx {
|
||||
let _ = action_tx.send(Action::ClosePopup);
|
||||
}
|
||||
},
|
||||
Err(err) => self.log_event(
|
||||
format!("invalid amount, error: {err}"), ActionLevel::Error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn enter_char(&mut self, new_char: char) {
|
||||
let is_separator_needed = !self.amount.value().contains('.') && new_char == '.';
|
||||
if new_char.is_digit(10) || is_separator_needed {
|
||||
let _ = self.amount.handle(InputRequest::InsertChar(new_char));
|
||||
}
|
||||
}
|
||||
|
||||
fn delete_char(&mut self) {
|
||||
let _ = self.amount.handle(InputRequest::DeletePrevChar);
|
||||
}
|
||||
|
||||
fn move_cursor_right(&mut self) {
|
||||
let _ = self.amount.handle(InputRequest::GoToNextChar);
|
||||
}
|
||||
|
||||
fn move_cursor_left(&mut self) {
|
||||
let _ = self.amount.handle(InputRequest::GoToPrevChar);
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialComponent for UnbondPopup {
|
||||
fn set_active(&mut self, current_tab: CurrentTab) {
|
||||
match current_tab {
|
||||
CurrentTab::UnbondPopup => self.is_active = true,
|
||||
_ => {
|
||||
self.is_active = false;
|
||||
self.amount = Input::new(String::new());
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for UnbondPopup {
|
||||
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_normal_border_style(style.get("normal_border_style").copied());
|
||||
self.palette.with_normal_title_style(style.get("normal_title_style").copied());
|
||||
self.palette.with_popup_style(style.get("popup_style").copied());
|
||||
self.palette.with_popup_title_style(style.get("popup_title_style").copied());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn handle_key_event(&mut self, key: KeyEvent) -> Result<Option<Action>> {
|
||||
if self.is_active && key.kind == KeyEventKind::Press {
|
||||
match key.code {
|
||||
KeyCode::Enter => self.submit_message(),
|
||||
KeyCode::Char(to_insert) => self.enter_char(to_insert),
|
||||
KeyCode::Backspace => self.delete_char(),
|
||||
KeyCode::Left => self.move_cursor_left(),
|
||||
KeyCode::Right => self.move_cursor_right(),
|
||||
KeyCode::Esc => self.close_popup(),
|
||||
_ => {},
|
||||
};
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn update(&mut self, action: Action) -> Result<Option<Action>> {
|
||||
match action {
|
||||
Action::SetIsBonded(is_bonded, account_id) if self.stash_account_id == account_id =>
|
||||
self.is_bonded = is_bonded,
|
||||
Action::SetStashSecret(secret_seed) => self.stash_secret_seed = secret_seed,
|
||||
Action::SetStashAccount(account_id) => self.stash_account_id = account_id,
|
||||
_ => {}
|
||||
};
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
|
||||
if self.is_active {
|
||||
let (border_style, border_type) = self.palette.create_popup_style();
|
||||
let input = Paragraph::new(self.amount.value())
|
||||
.block(Block::bordered()
|
||||
.border_style(border_style)
|
||||
.border_type(border_type)
|
||||
.title_style(self.palette.create_popup_title_style())
|
||||
.title_alignment(Alignment::Right)
|
||||
.title(format!("Unbond amount")));
|
||||
let v = Layout::vertical([Constraint::Max(3)]).flex(Flex::Center);
|
||||
let h = Layout::horizontal([Constraint::Max(50)]).flex(Flex::Center);
|
||||
let [area] = v.areas(area);
|
||||
let [area] = h.areas(area);
|
||||
|
||||
frame.render_widget(Clear, area);
|
||||
frame.render_widget(input, area);
|
||||
frame.set_cursor_position(Position::new(
|
||||
area.x + self.amount.cursor() as u16 + 1,
|
||||
area.y + 1
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
184
src/components/validator/validate_popup.rs
Normal file
184
src/components/validator/validate_popup.rs
Normal file
@ -0,0 +1,184 @@
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind};
|
||||
use color_eyre::Result;
|
||||
use ratatui::{
|
||||
layout::{Position, Alignment, Constraint, Flex, Layout, Rect},
|
||||
widgets::{Block, Clear, Paragraph},
|
||||
Frame
|
||||
};
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
use super::{Component, PartialComponent, CurrentTab};
|
||||
use crate::{
|
||||
action::Action,
|
||||
config::Config,
|
||||
palette::StylePalette,
|
||||
types::{ActionLevel, ActionTarget},
|
||||
widgets::{Input, InputRequest},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ValidatePopup {
|
||||
is_active: bool,
|
||||
action_tx: Option<UnboundedSender<Action>>,
|
||||
network_tx: Option<Sender<Action>>,
|
||||
secret_seed: [u8; 32],
|
||||
amount: Input,
|
||||
palette: StylePalette
|
||||
}
|
||||
|
||||
impl Default for ValidatePopup {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl ValidatePopup {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
is_active: false,
|
||||
secret_seed: [0u8; 32],
|
||||
action_tx: None,
|
||||
network_tx: None,
|
||||
amount: Input::new(String::new()),
|
||||
palette: StylePalette::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn close_popup(&mut self) {
|
||||
self.is_active = false;
|
||||
if let Some(action_tx) = &self.action_tx {
|
||||
let _ = action_tx.send(Action::ClosePopup);
|
||||
}
|
||||
}
|
||||
|
||||
fn log_event(&mut self, message: String, level: ActionLevel) {
|
||||
if let Some(action_tx) = &self.action_tx {
|
||||
let _ = action_tx.send(
|
||||
Action::EventLog(message, level, ActionTarget::ValidatorLog));
|
||||
}
|
||||
}
|
||||
|
||||
fn submit_message(&mut self) {
|
||||
if let Some(network_tx) = &self.network_tx {
|
||||
let str_amount = self.amount.value();
|
||||
let str_amount = if str_amount.starts_with('.') {
|
||||
&format!("0{}", str_amount)[..]
|
||||
} else {
|
||||
str_amount
|
||||
};
|
||||
match str_amount.parse::<f64>() {
|
||||
Ok(value) => {
|
||||
let amount = (value * 10_000_000.0).round() as u32;
|
||||
let _ = network_tx.send(Action::ValidateFrom(self.secret_seed, amount));
|
||||
if let Some(action_tx) = &self.action_tx {
|
||||
let _ = action_tx.send(Action::ClosePopup);
|
||||
}
|
||||
},
|
||||
Err(err) => self.log_event(
|
||||
format!("invalid amount, error: {err}"), ActionLevel::Error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn enter_char(&mut self, new_char: char) {
|
||||
let is_separator_needed = !self.amount.value().contains('.') && new_char == '.';
|
||||
if new_char.is_digit(10) || is_separator_needed {
|
||||
let _ = self.amount.handle(InputRequest::InsertChar(new_char));
|
||||
}
|
||||
}
|
||||
|
||||
fn delete_char(&mut self) {
|
||||
let _ = self.amount.handle(InputRequest::DeletePrevChar);
|
||||
}
|
||||
|
||||
fn move_cursor_right(&mut self) {
|
||||
let _ = self.amount.handle(InputRequest::GoToNextChar);
|
||||
}
|
||||
|
||||
fn move_cursor_left(&mut self) {
|
||||
let _ = self.amount.handle(InputRequest::GoToPrevChar);
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialComponent for ValidatePopup {
|
||||
fn set_active(&mut self, current_tab: CurrentTab) {
|
||||
match current_tab {
|
||||
CurrentTab::ValidatePopup => self.is_active = true,
|
||||
_ => {
|
||||
self.is_active = false;
|
||||
self.amount = Input::new(String::new());
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for ValidatePopup {
|
||||
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_normal_border_style(style.get("normal_border_style").copied());
|
||||
self.palette.with_normal_title_style(style.get("normal_title_style").copied());
|
||||
self.palette.with_popup_style(style.get("popup_style").copied());
|
||||
self.palette.with_popup_title_style(style.get("popup_title_style").copied());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn handle_key_event(&mut self, key: KeyEvent) -> Result<Option<Action>> {
|
||||
if self.is_active && key.kind == KeyEventKind::Press {
|
||||
match key.code {
|
||||
KeyCode::Enter => self.submit_message(),
|
||||
KeyCode::Char(to_insert) => self.enter_char(to_insert),
|
||||
KeyCode::Backspace => self.delete_char(),
|
||||
KeyCode::Left => self.move_cursor_left(),
|
||||
KeyCode::Right => self.move_cursor_right(),
|
||||
KeyCode::Esc => self.close_popup(),
|
||||
_ => {},
|
||||
};
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn update(&mut self, action: Action) -> Result<Option<Action>> {
|
||||
match action {
|
||||
Action::SetStashSecret(secret_seed) => self.secret_seed = secret_seed,
|
||||
_ => {}
|
||||
};
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
|
||||
if self.is_active {
|
||||
let (border_style, border_type) = self.palette.create_popup_style();
|
||||
let input = Paragraph::new(self.amount.value())
|
||||
.block(Block::bordered()
|
||||
.border_style(border_style)
|
||||
.border_type(border_type)
|
||||
.title_style(self.palette.create_popup_title_style())
|
||||
.title_alignment(Alignment::Right)
|
||||
.title(format!("Commission for nominators")));
|
||||
let v = Layout::vertical([Constraint::Max(3)]).flex(Flex::Center);
|
||||
let h = Layout::horizontal([Constraint::Max(50)]).flex(Flex::Center);
|
||||
let [area] = v.areas(area);
|
||||
let [area] = h.areas(area);
|
||||
|
||||
frame.render_widget(Clear, area);
|
||||
frame.render_widget(input, area);
|
||||
frame.set_cursor_position(Position::new(
|
||||
area.x + self.amount.cursor() as u16 + 1,
|
||||
area.y + 1
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
170
src/components/validator/withdraw_popup.rs
Normal file
170
src/components/validator/withdraw_popup.rs
Normal file
@ -0,0 +1,170 @@
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind};
|
||||
use color_eyre::Result;
|
||||
use ratatui::{
|
||||
layout::{Alignment, Constraint, Flex, Layout, Rect},
|
||||
widgets::{Block, Clear, Paragraph},
|
||||
Frame
|
||||
};
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
use super::{Component, PartialComponent, CurrentTab};
|
||||
use crate::{
|
||||
action::Action,
|
||||
config::Config,
|
||||
palette::StylePalette,
|
||||
types::{ActionLevel, ActionTarget},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct WithdrawPopup {
|
||||
is_active: bool,
|
||||
action_tx: Option<UnboundedSender<Action>>,
|
||||
network_tx: Option<Sender<Action>>,
|
||||
stash_account: [u8; 32],
|
||||
secret_seed: [u8; 32],
|
||||
slashing_spans_length: u32,
|
||||
unlocking_is_empty: bool,
|
||||
existential_deposit: u128,
|
||||
active_balance: u128,
|
||||
palette: StylePalette
|
||||
}
|
||||
|
||||
impl Default for WithdrawPopup {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl WithdrawPopup {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
is_active: false,
|
||||
stash_account: [0u8; 32],
|
||||
secret_seed: [0u8; 32],
|
||||
slashing_spans_length: 0u32,
|
||||
unlocking_is_empty: false,
|
||||
existential_deposit: 0u128,
|
||||
active_balance: 0u128,
|
||||
action_tx: None,
|
||||
network_tx: None,
|
||||
palette: StylePalette::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn close_popup(&mut self) {
|
||||
self.is_active = false;
|
||||
if let Some(action_tx) = &self.action_tx {
|
||||
let _ = action_tx.send(Action::ClosePopup);
|
||||
}
|
||||
}
|
||||
|
||||
fn stash_should_be_killed(&self) -> bool {
|
||||
self.unlocking_is_empty && self.active_balance < self.existential_deposit
|
||||
}
|
||||
|
||||
fn proceed(&mut self) {
|
||||
if let Some(network_tx) = &self.network_tx {
|
||||
let spans_needed = if self.stash_should_be_killed() {
|
||||
if let Some(action_tx) = &self.action_tx {
|
||||
let _ = action_tx.send(Action::EventLog(
|
||||
"Current stash account will be killed during this transaction".to_string(),
|
||||
ActionLevel::Warn,
|
||||
ActionTarget::ValidatorLog));
|
||||
}
|
||||
self.slashing_spans_length
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let _ = network_tx.send(Action::WithdrawUnbondedFrom(self.secret_seed, spans_needed));
|
||||
if let Some(action_tx) = &self.action_tx {
|
||||
let _ = action_tx.send(Action::ClosePopup);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialComponent for WithdrawPopup {
|
||||
fn set_active(&mut self, current_tab: CurrentTab) {
|
||||
match current_tab {
|
||||
CurrentTab::WithdrawPopup => self.is_active = true,
|
||||
_ => self.is_active = false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for WithdrawPopup {
|
||||
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_normal_border_style(style.get("normal_border_style").copied());
|
||||
self.palette.with_normal_title_style(style.get("normal_title_style").copied());
|
||||
self.palette.with_popup_style(style.get("popup_style").copied());
|
||||
self.palette.with_popup_title_style(style.get("popup_title_style").copied());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_key_event(&mut self, key: KeyEvent) -> Result<Option<Action>> {
|
||||
if self.is_active && key.kind == KeyEventKind::Press {
|
||||
match key.code {
|
||||
KeyCode::Enter => self.proceed(),
|
||||
KeyCode::Esc => self.close_popup(),
|
||||
_ => {},
|
||||
};
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn update(&mut self, action: Action) -> Result<Option<Action>> {
|
||||
match action {
|
||||
Action::SetExistentialDeposit(existential_deposit) => self.existential_deposit = existential_deposit,
|
||||
Action::SetStashAccount(account_id) => self.stash_account = account_id,
|
||||
Action::SetStashSecret(secret_seed) => self.secret_seed = secret_seed,
|
||||
Action::SetSlashingSpansLength(length, account_id) if self.stash_account == account_id =>
|
||||
self.slashing_spans_length = length as u32,
|
||||
Action::SetStakedAmountRatio(_, active_balance, account_id) if self.stash_account == account_id =>
|
||||
self.active_balance = active_balance.unwrap_or_default(),
|
||||
Action::SetValidatorEraUnlocking(unlockings, account_id) if self.stash_account == account_id =>
|
||||
self.unlocking_is_empty = unlockings.is_empty(),
|
||||
_ => {}
|
||||
};
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
|
||||
if self.is_active {
|
||||
let (border_style, border_type) = self.palette.create_popup_style();
|
||||
let text = if self.stash_should_be_killed() {
|
||||
" Do you want to kill current stash account?"
|
||||
} else {
|
||||
" Do you want to withdraw all unbonded funds?"
|
||||
};
|
||||
let input = Paragraph::new(text)
|
||||
.block(Block::bordered()
|
||||
.border_style(border_style)
|
||||
.border_type(border_type)
|
||||
.title_style(self.palette.create_popup_title_style())
|
||||
.title_alignment(Alignment::Right)
|
||||
.title("Enter to proceed / Esc to close"));
|
||||
let v = Layout::vertical([Constraint::Max(3)]).flex(Flex::Center);
|
||||
let h = Layout::horizontal([Constraint::Max(50)]).flex(Flex::Center);
|
||||
let [area] = v.areas(area);
|
||||
let [area] = h.areas(area);
|
||||
|
||||
frame.render_widget(Clear, area);
|
||||
frame.render_widget(input, area);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
257
src/components/validator/withdrawals.rs
Normal file
257
src/components/validator/withdrawals.rs
Normal file
@ -0,0 +1,257 @@
|
||||
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 tokio::sync::mpsc::UnboundedSender;
|
||||
|
||||
use super::{PartialComponent, Component, CurrentTab};
|
||||
use crate::types::{ActionTarget, ActionLevel, UnlockChunk};
|
||||
use crate::{
|
||||
action::Action,
|
||||
config::Config,
|
||||
palette::StylePalette,
|
||||
};
|
||||
|
||||
pub struct Withdrawals {
|
||||
is_active: bool,
|
||||
action_tx: Option<UnboundedSender<Action>>,
|
||||
palette: StylePalette,
|
||||
scroll_state: ScrollbarState,
|
||||
table_state: TableState,
|
||||
unlockings: Vec<UnlockChunk>,
|
||||
stash_account: [u8; 32],
|
||||
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,
|
||||
action_tx: None,
|
||||
scroll_state: ScrollbarState::new(0),
|
||||
table_state: TableState::new(),
|
||||
palette: StylePalette::default(),
|
||||
unlockings: Vec::new(),
|
||||
stash_account: [0u8; 32],
|
||||
current_era: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn try_open_popup(&mut self) {
|
||||
if let Some(action_tx) = &self.action_tx {
|
||||
if let Some(index) = self.table_state.selected() {
|
||||
if index == 0 && self.unlockings[0].era < self.current_era {
|
||||
let _ = action_tx.send(Action::WithdrawValidatorPopup);
|
||||
} else {
|
||||
let _ = action_tx.send(Action::EventLog(
|
||||
"Nothing to be witdrawn yet on the selected unlocking".to_string(),
|
||||
ActionLevel::Info,
|
||||
ActionTarget::ValidatorLog));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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, unlockings: Vec<UnlockChunk>) {
|
||||
let mut updated_unlockings = Vec::new();
|
||||
for chunk in unlockings {
|
||||
if chunk.era > self.current_era {
|
||||
updated_unlockings.push(chunk);
|
||||
} else {
|
||||
match updated_unlockings.get_mut(0) {
|
||||
Some(stored_chunk) => (*stored_chunk).value += chunk.value,
|
||||
None => updated_unlockings.push(chunk),
|
||||
}
|
||||
}
|
||||
}
|
||||
self.unlockings = updated_unlockings;
|
||||
self.scroll_state = self.scroll_state
|
||||
.content_length(self.unlockings.len());
|
||||
}
|
||||
|
||||
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_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::SetStashAccount(account_id) => self.stash_account = account_id,
|
||||
Action::SetActiveEra(era_info) => self.current_era = era_info.index,
|
||||
Action::SetValidatorEraUnlocking(unlockings, account_id) if self.stash_account == account_id =>
|
||||
self.add_new_unlocking(unlockings),
|
||||
_ => {}
|
||||
};
|
||||
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(),
|
||||
KeyCode::Enter => self.try_open_popup(),
|
||||
_ => {},
|
||||
};
|
||||
}
|
||||
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(|unlock| {
|
||||
let mut est_era_text = Text::from(self.estimate_time(unlock.era)).alignment(Alignment::Center);
|
||||
let mut value_text = Text::from(self.prepare_u128(unlock.value)).alignment(Alignment::Right);
|
||||
|
||||
if unlock.era > self.current_era {
|
||||
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(est_era_text),
|
||||
Cell::from(value_text),
|
||||
])
|
||||
}),
|
||||
[
|
||||
Constraint::Length(7),
|
||||
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(())
|
||||
}
|
||||
}
|
||||
@ -52,7 +52,7 @@ impl Component for Version {
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
|
||||
let [_, version] = super::menu_layout(area);
|
||||
let [_, version] = super::layouts::menu_layout(area);
|
||||
|
||||
let text_style = self.palette.create_basic_style(false);
|
||||
let (border_style, border_type) = self.palette.create_border_style(false);
|
||||
|
||||
202
src/components/wallet/account_details.rs
Normal file
202
src/components/wallet/account_details.rs
Normal file
@ -0,0 +1,202 @@
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind};
|
||||
use color_eyre::Result;
|
||||
use ratatui::{
|
||||
layout::{Alignment, Constraint, Flex, Layout, Rect},
|
||||
widgets::{Block, Cell, Clear, Row, Table},
|
||||
text::Text,
|
||||
Frame,
|
||||
};
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
|
||||
use super::{Component, PartialComponent, CurrentTab};
|
||||
use crate::{
|
||||
action::Action,
|
||||
config::Config,
|
||||
palette::StylePalette,
|
||||
widgets::DotSpinner,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AccountDetails {
|
||||
is_active: bool,
|
||||
action_tx: Option<UnboundedSender<Action>>,
|
||||
palette: StylePalette,
|
||||
name: String,
|
||||
transferable_balance: Option<u128>,
|
||||
locked_balance: Option<u128>,
|
||||
reserved_balance: Option<u128>,
|
||||
total_balance: Option<u128>,
|
||||
nonce: Option<u32>,
|
||||
}
|
||||
|
||||
impl Default for AccountDetails {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl AccountDetails {
|
||||
const TICKER: &str = " CSPR";
|
||||
const DECIMALS: usize = 6;
|
||||
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
is_active: false,
|
||||
action_tx: None,
|
||||
palette: StylePalette::default(),
|
||||
name: String::new(),
|
||||
transferable_balance: None,
|
||||
locked_balance: None,
|
||||
reserved_balance: None,
|
||||
total_balance: None,
|
||||
nonce: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn close_popup(&mut self) {
|
||||
self.is_active = false;
|
||||
if let Some(action_tx) = &self.action_tx {
|
||||
let _ = action_tx.send(Action::ClosePopup);
|
||||
}
|
||||
}
|
||||
|
||||
fn prepare_u128(&self, maybe_value: Option<u128>) -> String {
|
||||
match maybe_value {
|
||||
Some(value) => {
|
||||
let value = value as f64 / 10f64.powi(18);
|
||||
let after = Self::DECIMALS;
|
||||
format!("{:.after$}{}", value, Self::TICKER)
|
||||
},
|
||||
None => format!("{}{}", DotSpinner::default().to_string(), Self::TICKER)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialComponent for AccountDetails {
|
||||
fn set_active(&mut self, current_tab: CurrentTab) {
|
||||
match current_tab {
|
||||
CurrentTab::AccountDetails => self.is_active = true,
|
||||
_ => {
|
||||
if self.is_active {
|
||||
self.is_active = false;
|
||||
self.name = String::new();
|
||||
self.transferable_balance = None;
|
||||
self.locked_balance = None;
|
||||
self.reserved_balance = None;
|
||||
self.total_balance = None;
|
||||
self.nonce = None;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for AccountDetails {
|
||||
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_normal_border_style(style.get("normal_border_style").copied());
|
||||
self.palette.with_normal_title_style(style.get("normal_title_style").copied());
|
||||
self.palette.with_popup_style(style.get("popup_style").copied());
|
||||
self.palette.with_popup_title_style(style.get("popup_title_style").copied());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update(&mut self, action: Action) -> Result<Option<Action>> {
|
||||
match action {
|
||||
Action::AccountDetailsOf(name, maybe_account_info) => {
|
||||
self.name = name;
|
||||
match maybe_account_info {
|
||||
Some(account_info) => {
|
||||
self.total_balance = Some(account_info.free);
|
||||
self.locked_balance = Some(account_info.frozen);
|
||||
self.reserved_balance = Some(account_info.reserved);
|
||||
self.nonce = Some(account_info.nonce);
|
||||
|
||||
let transferable = account_info.free
|
||||
.saturating_sub(account_info.reserved)
|
||||
.saturating_sub(account_info.frozen);
|
||||
self.transferable_balance = Some(transferable);
|
||||
},
|
||||
None => {
|
||||
self.transferable_balance = None;
|
||||
self.locked_balance = None;
|
||||
self.reserved_balance = None;
|
||||
self.total_balance = None;
|
||||
self.nonce = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn handle_key_event(&mut self, key: KeyEvent) -> Result<Option<Action>> {
|
||||
if self.is_active && key.kind == KeyEventKind::Press {
|
||||
match key.code {
|
||||
KeyCode::Esc | KeyCode::Enter => self.close_popup(),
|
||||
_ => {},
|
||||
};
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
|
||||
if self.is_active {
|
||||
let (border_style, border_type) = self.palette.create_popup_style();
|
||||
let table = Table::new(
|
||||
[
|
||||
Row::new(vec![
|
||||
Cell::from(Text::from("nonce: ".to_string()).alignment(Alignment::Left)),
|
||||
Cell::from(Text::from(self.nonce
|
||||
.map(|n| n.to_string())
|
||||
.unwrap_or(DotSpinner::default().to_string())
|
||||
).alignment(Alignment::Right)),
|
||||
]),
|
||||
Row::new(vec![
|
||||
Cell::from(Text::from("total: ".to_string()).alignment(Alignment::Left)),
|
||||
Cell::from(Text::from(self.prepare_u128(self.total_balance)).alignment(Alignment::Right))
|
||||
]),
|
||||
Row::new(vec![
|
||||
Cell::from(Text::from("free: ".to_string()).alignment(Alignment::Left)),
|
||||
Cell::from(Text::from(self.prepare_u128(self.transferable_balance)).alignment(Alignment::Right))
|
||||
]),
|
||||
Row::new(vec![
|
||||
Cell::from(Text::from("locked: ".to_string()).alignment(Alignment::Left)),
|
||||
Cell::from(Text::from(self.prepare_u128(self.locked_balance)).alignment(Alignment::Right)),
|
||||
]),
|
||||
Row::new(vec![
|
||||
Cell::from(Text::from("reserved: ".to_string()).alignment(Alignment::Left)),
|
||||
Cell::from(Text::from(self.prepare_u128(self.reserved_balance)).alignment(Alignment::Right)),
|
||||
]),
|
||||
],
|
||||
[
|
||||
Constraint::Max(10),
|
||||
Constraint::Min(14),
|
||||
]
|
||||
)
|
||||
.block(Block::bordered()
|
||||
.border_style(border_style)
|
||||
.border_type(border_type)
|
||||
.title_alignment(Alignment::Right)
|
||||
.title_style(self.palette.create_title_style(false))
|
||||
.title(format!("Details for {}", &self.name)));
|
||||
|
||||
let v = Layout::vertical([Constraint::Max(7)]).flex(Flex::Center);
|
||||
let h = Layout::horizontal([Constraint::Max(50)]).flex(Flex::Center);
|
||||
let [area] = v.areas(area);
|
||||
let [area] = h.areas(area);
|
||||
|
||||
frame.render_widget(Clear, area);
|
||||
frame.render_widget(table, area);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@ -15,20 +15,17 @@ use ratatui::{
|
||||
},
|
||||
Frame
|
||||
};
|
||||
use subxt::{
|
||||
tx::PairSigner,
|
||||
ext::sp_core::{
|
||||
Pair as PairT,
|
||||
sr25519::Pair,
|
||||
crypto::{Ss58Codec, Ss58AddressFormat, AccountId32},
|
||||
},
|
||||
use subxt::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::{SystemAccount, ActionLevel};
|
||||
use crate::types::{ActionLevel, ActionTarget, SystemAccount};
|
||||
use crate::widgets::DotSpinner;
|
||||
use crate::{
|
||||
action::Action,
|
||||
config::Config,
|
||||
@ -40,8 +37,6 @@ struct AccountInfo {
|
||||
address: String,
|
||||
account_id: [u8; 32],
|
||||
seed: String,
|
||||
#[allow(dead_code)]
|
||||
pair_signer: PairSigner<CasperConfig, Pair>,
|
||||
}
|
||||
|
||||
pub struct Accounts {
|
||||
@ -63,6 +58,9 @@ impl Default for Accounts {
|
||||
}
|
||||
|
||||
impl Accounts {
|
||||
const TICKER: &str = " CSPR";
|
||||
const DECIMALS: usize = 3;
|
||||
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
is_active: false,
|
||||
@ -77,16 +75,37 @@ impl Accounts {
|
||||
}
|
||||
}
|
||||
|
||||
fn set_sender_nonce(&mut self, index: usize) {
|
||||
if let Some(network_tx) = &self.network_tx {
|
||||
let used_seed = self.wallet_keys[index].seed.clone();
|
||||
let account_id = self.wallet_keys[index].account_id;
|
||||
let used_nonce = self.balances
|
||||
.get(&account_id)
|
||||
.map(|info| info.nonce);
|
||||
let _ = network_tx.send(Action::SetSender(used_seed, used_nonce));
|
||||
}
|
||||
}
|
||||
|
||||
fn set_used_account(&mut self, index: usize) {
|
||||
let used_seed = self.wallet_keys[index].seed.clone();
|
||||
let account_id = self.wallet_keys[index].account_id;
|
||||
if let Some(action_tx) = &self.action_tx {
|
||||
let _ = action_tx.send(Action::UsedAccount(
|
||||
self.wallet_keys[index].seed.clone()));
|
||||
account_id,
|
||||
used_seed.clone()));
|
||||
}
|
||||
if let Some(network_tx) = &self.network_tx {
|
||||
let _ = network_tx.send(Action::GetNominatorsByAccount(
|
||||
account_id,
|
||||
false));
|
||||
}
|
||||
self.set_sender_nonce(index);
|
||||
}
|
||||
|
||||
fn log_event(&mut self, message: String, level: ActionLevel) {
|
||||
if let Some(action_tx) = &self.action_tx {
|
||||
let _ = action_tx.send(Action::WalletLog(message, level));
|
||||
let _ = action_tx.send(
|
||||
Action::EventLog(message, level, ActionTarget::WalletLog));
|
||||
}
|
||||
}
|
||||
|
||||
@ -133,7 +152,7 @@ impl Accounts {
|
||||
format!("address {} copied to clipboard", &address),
|
||||
ActionLevel::Warn),
|
||||
_ => self.log_event(
|
||||
"could not use `xclip` to copy".to_string(),
|
||||
"command `xclip` not found, consider installing `xclip` on your machine".to_string(),
|
||||
ActionLevel::Error),
|
||||
}
|
||||
}
|
||||
@ -143,8 +162,7 @@ impl Accounts {
|
||||
let (pair, seed) = Pair::generate();
|
||||
let secret_seed = hex::encode(seed);
|
||||
let account_id = pair.public().0;
|
||||
let pair_signer = PairSigner::<CasperConfig, Pair>::new(pair);
|
||||
let address = AccountId32::from(seed.clone())
|
||||
let address = AccountId32::from(account_id)
|
||||
.to_ss58check_with_version(Ss58AddressFormat::custom(1996));
|
||||
|
||||
self.log_event(
|
||||
@ -158,7 +176,6 @@ impl Accounts {
|
||||
address,
|
||||
account_id,
|
||||
seed: secret_seed,
|
||||
pair_signer,
|
||||
});
|
||||
self.last_row();
|
||||
self.save_to_file();
|
||||
@ -242,7 +259,6 @@ impl Accounts {
|
||||
let account_id = pair.public().0;
|
||||
let address = AccountId32::from(account_id)
|
||||
.to_ss58check_with_version(Ss58AddressFormat::custom(1996));
|
||||
let pair_signer = PairSigner::<CasperConfig, Pair>::new(pair);
|
||||
|
||||
self.send_balance_request(account_id, false);
|
||||
self.wallet_keys.push(AccountInfo {
|
||||
@ -250,7 +266,6 @@ impl Accounts {
|
||||
account_id,
|
||||
address,
|
||||
seed: wallet_key.to_string(),
|
||||
pair_signer,
|
||||
});
|
||||
}
|
||||
self.log_event(format!("read {} wallets from disk",
|
||||
@ -286,9 +301,8 @@ impl Accounts {
|
||||
|
||||
let secret_seed = hex::encode(seed);
|
||||
let account_id = pair.public().0;
|
||||
let address = AccountId32::from(pair.public().0)
|
||||
let address = AccountId32::from(account_id)
|
||||
.to_ss58check_with_version(Ss58AddressFormat::custom(1996));
|
||||
let pair_signer = PairSigner::<CasperConfig, Pair>::new(pair);
|
||||
|
||||
let mut new_file = File::create(file_path)?;
|
||||
writeln!(new_file, "ghostie:0x{}", &secret_seed)?;
|
||||
@ -299,17 +313,77 @@ impl Accounts {
|
||||
address,
|
||||
account_id,
|
||||
seed: secret_seed,
|
||||
pair_signer,
|
||||
});
|
||||
}
|
||||
};
|
||||
self.table_state.select(Some(0));
|
||||
self.scroll_state = self.scroll_state.content_length(self.wallet_keys.len());
|
||||
self.set_balance_active(0);
|
||||
self.set_used_account(0);
|
||||
self.first_row();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn load_initial_keys(&mut self) {
|
||||
self.try_load_and_store_keys("/etc/ghost/wallet-key", "ghostie");
|
||||
self.try_load_and_store_keys("/etc/ghost/stash-key", "stash");
|
||||
}
|
||||
|
||||
fn try_load_and_store_keys(&mut self, path_to_key: &str, name_for_key: &str) {
|
||||
match std::fs::read_to_string(path_to_key) {
|
||||
Ok(content) => {
|
||||
let content = content.replace("\n", "");
|
||||
let content = &content[2..];
|
||||
let seed: [u8; 32] = hex::decode(content)
|
||||
.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 secret_seed = hex::encode(seed);
|
||||
let account_id = pair.public().0;
|
||||
let address = AccountId32::from(account_id)
|
||||
.to_ss58check_with_version(Ss58AddressFormat::custom(1996));
|
||||
|
||||
if self.wallet_keys.iter().any(|key| key.seed == secret_seed) {
|
||||
self.log_event(
|
||||
format!("{} from `{}` already loaded", name_for_key, path_to_key),
|
||||
ActionLevel::Warn);
|
||||
} else {
|
||||
self.log_event(
|
||||
format!("{} from `{}` loaded to ghost-eye", name_for_key, path_to_key),
|
||||
ActionLevel::Warn);
|
||||
|
||||
self.send_balance_request(account_id, false);
|
||||
self.wallet_keys.push(AccountInfo {
|
||||
name: name_for_key.to_string(),
|
||||
address,
|
||||
account_id,
|
||||
seed: secret_seed,
|
||||
});
|
||||
self.save_to_file();
|
||||
}
|
||||
|
||||
}
|
||||
Err(_) => {
|
||||
self.log_event(
|
||||
format!("nothing found inside `{}`", path_to_key),
|
||||
ActionLevel::Warn);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn push_to_address_book(&mut self) {
|
||||
if let Some(index) = self.table_state.selected() {
|
||||
if let Some(action_tx) = &self.action_tx {
|
||||
let wallet_key = self.wallet_keys
|
||||
.get(index)
|
||||
.expect("wallet key index should be in range; qed");
|
||||
let _ = action_tx.send(Action::NewAddressBookRecord(
|
||||
wallet_key.name.clone(),
|
||||
wallet_key.address.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn first_row(&mut self) {
|
||||
if self.wallet_keys.len() > 0 {
|
||||
self.table_state.select(Some(0));
|
||||
@ -363,9 +437,15 @@ impl Accounts {
|
||||
self.set_used_account(i);
|
||||
}
|
||||
|
||||
fn prepare_u128(&self, value: u128, after: usize, ticker: Option<&str>) -> String {
|
||||
let value = value as f64 / 10f64.powi(18);
|
||||
format!("{:.after$}{}", value, ticker.unwrap_or_default())
|
||||
fn prepare_u128(&self, maybe_value: Option<u128>) -> String {
|
||||
match maybe_value {
|
||||
Some(value) => {
|
||||
let value = value as f64 / 10f64.powi(18);
|
||||
let after = Self::DECIMALS;
|
||||
format!("{:.after$}{}", value, Self::TICKER)
|
||||
}
|
||||
None => format!("{}{}", DotSpinner::default().to_string(), Self::TICKER)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -412,12 +492,16 @@ impl Component for Accounts {
|
||||
match action {
|
||||
Action::NewAccount(name) => self.create_new_account(name),
|
||||
Action::UpdateAccountName(new_name) => self.rename_account(new_name),
|
||||
Action::BalanceResponse(account_id, balance) => {
|
||||
Action::BalanceResponse(account_id, maybe_balance) => {
|
||||
if self.wallet_keys.iter().any(|wallet| wallet.account_id == account_id) {
|
||||
let _ = self.balances.insert(account_id, balance);
|
||||
let _ = match maybe_balance {
|
||||
Some(balance) => self.balances.insert(account_id, balance),
|
||||
None => self.balances.remove(&account_id),
|
||||
};
|
||||
if let Some(index) = self.table_state.selected() {
|
||||
if self.wallet_keys[index].account_id == account_id {
|
||||
self.set_balance_active(index);
|
||||
self.set_sender_nonce(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -439,6 +523,8 @@ impl Component for Accounts {
|
||||
KeyCode::Char('D') => self.delete_row(),
|
||||
KeyCode::Char('R') => self.update_account_name(),
|
||||
KeyCode::Char('Y') => self.copy_to_clipboard(),
|
||||
KeyCode::Char('L') => self.load_initial_keys(),
|
||||
KeyCode::Char('P') => self.push_to_address_book(),
|
||||
_ => {},
|
||||
};
|
||||
}
|
||||
@ -446,7 +532,7 @@ impl Component for Accounts {
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
|
||||
let [_, place, _] = super::account_layout(area);
|
||||
let [_, place, _, _] = super::account_layout(area);
|
||||
let (border_style, border_type) = self.palette.create_border_style(self.is_active);
|
||||
|
||||
let table = Table::new(
|
||||
@ -455,12 +541,11 @@ impl Component for Accounts {
|
||||
.map(|info| {
|
||||
let balance = self.balances
|
||||
.get(&info.account_id)
|
||||
.map(|b| b.free)
|
||||
.unwrap_or_default();
|
||||
.map(|b| b.free);
|
||||
Row::new(vec![
|
||||
Cell::from(Text::from(info.name.clone()).alignment(Alignment::Left)),
|
||||
Cell::from(Text::from(info.address.clone()).alignment(Alignment::Center)),
|
||||
Cell::from(Text::from(self.prepare_u128(balance, 2, Some(" CSPR"))).alignment(Alignment::Right)),
|
||||
Cell::from(Text::from(self.prepare_u128(balance)).alignment(Alignment::Right)),
|
||||
])
|
||||
}),
|
||||
[
|
||||
|
||||
@ -38,12 +38,19 @@ impl AddAccount {
|
||||
palette: StylePalette::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AddAccount {
|
||||
fn close_popup(&mut self) {
|
||||
self.is_active = false;
|
||||
if let Some(action_tx) = &self.action_tx {
|
||||
let _ = action_tx.send(Action::ClosePopup);
|
||||
}
|
||||
}
|
||||
|
||||
fn submit_message(&mut self) {
|
||||
if let Some(action_tx) = &self.action_tx {
|
||||
let _ = action_tx.send(Action::NewAccount(self.name.value().to_string()));
|
||||
let _ = action_tx.send(Action::NewAccount(
|
||||
self.name.value().to_string()));
|
||||
let _ = action_tx.send(Action::ClosePopup);
|
||||
}
|
||||
}
|
||||
|
||||
@ -101,7 +108,7 @@ impl Component for AddAccount {
|
||||
KeyCode::Backspace => self.delete_char(),
|
||||
KeyCode::Left => self.move_cursor_left(),
|
||||
KeyCode::Right => self.move_cursor_right(),
|
||||
KeyCode::Esc => self.is_active = false,
|
||||
KeyCode::Esc => self.close_popup(),
|
||||
_ => {},
|
||||
};
|
||||
}
|
||||
|
||||
@ -48,9 +48,14 @@ impl AddAddressBookRecord {
|
||||
palette: StylePalette::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AddAddressBookRecord {
|
||||
fn close_popup(&mut self) {
|
||||
self.is_active = false;
|
||||
if let Some(action_tx) = &self.action_tx {
|
||||
let _ = action_tx.send(Action::ClosePopup);
|
||||
}
|
||||
}
|
||||
|
||||
fn submit_message(&mut self) {
|
||||
match self.name_or_address {
|
||||
NameOrAddress::Name => self.name_or_address = NameOrAddress::Address,
|
||||
@ -58,6 +63,7 @@ impl AddAddressBookRecord {
|
||||
let _ = action_tx.send(Action::NewAddressBookRecord(
|
||||
self.name.value().to_string(),
|
||||
self.address.value().to_string()));
|
||||
let _ = action_tx.send(Action::ClosePopup);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -150,7 +156,7 @@ impl Component for AddAddressBookRecord {
|
||||
KeyCode::Backspace => self.delete_char(),
|
||||
KeyCode::Left => self.move_cursor_left(),
|
||||
KeyCode::Right => self.move_cursor_right(),
|
||||
KeyCode::Esc => self.is_active = false,
|
||||
KeyCode::Esc => self.close_popup(),
|
||||
_ => {},
|
||||
};
|
||||
}
|
||||
@ -160,8 +166,8 @@ impl Component for AddAddressBookRecord {
|
||||
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
|
||||
if self.is_active {
|
||||
let size = area.as_size();
|
||||
let name_area = Rect::new(size.width / 2, size.height / 2, 50, 3);
|
||||
let address_area = Rect::new(size.width / 2, size.height / 2 + 3, 50, 3);
|
||||
let name_area = Rect::new(size.width / 2, size.height / 2, 51, 3);
|
||||
let address_area = Rect::new(size.width / 2, size.height / 2 + 3, 51, 3);
|
||||
let (border_style, border_type) = self.palette.create_popup_style();
|
||||
|
||||
let input_name = Paragraph::new(self.name.value())
|
||||
@ -180,8 +186,8 @@ impl Component for AddAddressBookRecord {
|
||||
.title_alignment(Alignment::Right)
|
||||
.title("Address for new name"));
|
||||
|
||||
let v = Layout::vertical([Constraint::Max(3)]).flex(Flex::Center);
|
||||
let h = Layout::horizontal([Constraint::Max(50)]).flex(Flex::Center);
|
||||
let v = Layout::vertical([Constraint::Length(3)]).flex(Flex::Center);
|
||||
let h = Layout::horizontal([Constraint::Length(51)]).flex(Flex::Center);
|
||||
|
||||
let [name_area] = v.areas(name_area);
|
||||
let [name_area] = h.areas(name_area);
|
||||
|
||||
@ -19,7 +19,7 @@ use tokio::sync::mpsc::UnboundedSender;
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
use super::{Component, PartialComponent, CurrentTab};
|
||||
use crate::types::{ActionLevel, SystemAccount};
|
||||
use crate::types::{ActionLevel, ActionTarget, SystemAccount};
|
||||
use crate::widgets::DotSpinner;
|
||||
use crate::{
|
||||
action::Action,
|
||||
@ -53,6 +53,9 @@ impl Default for AddressBook {
|
||||
}
|
||||
|
||||
impl AddressBook {
|
||||
const TICKER: &str = " CSPR";
|
||||
const DECIMALS: usize = 3;
|
||||
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
is_active: false,
|
||||
@ -91,7 +94,8 @@ impl AddressBook {
|
||||
|
||||
fn log_event(&mut self, message: String, level: ActionLevel) {
|
||||
if let Some(action_tx) = &self.action_tx {
|
||||
let _ = action_tx.send(Action::WalletLog(message, level));
|
||||
let _ = action_tx.send(
|
||||
Action::EventLog(message, level, ActionTarget::WalletLog));
|
||||
}
|
||||
}
|
||||
|
||||
@ -192,24 +196,40 @@ impl AddressBook {
|
||||
ActionLevel::Error);
|
||||
}
|
||||
|
||||
self.log_event(
|
||||
format!("account {} with address {} added to address book", &name, &address),
|
||||
ActionLevel::Info);
|
||||
match self.address_book.iter().position(|record| record.address == address) {
|
||||
Some(index) => {
|
||||
let record = self.address_book
|
||||
.get(index)
|
||||
.expect("record should be in range of address book; qed");
|
||||
self.log_event(
|
||||
format!("record with name `{}` already stores address {}", &record.name, &record.address),
|
||||
ActionLevel::Warn);
|
||||
self.table_state.select(Some(index));
|
||||
self.scroll_state = self.scroll_state.position(index);
|
||||
},
|
||||
None => {
|
||||
let seed_vec = account_id.to_raw_vec();
|
||||
let mut account_id = [0u8; 32];
|
||||
account_id.copy_from_slice(&seed_vec);
|
||||
let seed_str = hex::encode(seed_vec);
|
||||
|
||||
let seed_vec = account_id.to_raw_vec();
|
||||
let mut account_id = [0u8; 32];
|
||||
account_id.copy_from_slice(&seed_vec);
|
||||
let seed_str = hex::encode(seed_vec);
|
||||
self.log_event(
|
||||
format!("account {} with address {} added to address book", &name, &address),
|
||||
ActionLevel::Info);
|
||||
|
||||
self.address_book.push(BookRecord {
|
||||
name,
|
||||
address,
|
||||
account_id,
|
||||
seed: seed_str,
|
||||
});
|
||||
self.save_to_file();
|
||||
self.last_row();
|
||||
self.send_balance_request(account_id, false);
|
||||
self.address_book.push(BookRecord {
|
||||
name,
|
||||
address,
|
||||
account_id,
|
||||
seed: seed_str,
|
||||
});
|
||||
|
||||
self.scroll_state = self.scroll_state.content_length(self.address_book.len());
|
||||
self.save_to_file();
|
||||
self.send_balance_request(account_id, false);
|
||||
self.last_row();
|
||||
},
|
||||
}
|
||||
},
|
||||
Err(_) => self.log_event(
|
||||
format!("provided account address {} is invalid", &address),
|
||||
@ -227,6 +247,19 @@ impl AddressBook {
|
||||
}
|
||||
}
|
||||
|
||||
fn show_account_details(&mut self) {
|
||||
if let Some(index) = self.table_state.selected() {
|
||||
if let Some(action_tx) = &self.action_tx {
|
||||
let _ = action_tx.send(Action::AccountDetailsOf(
|
||||
self.address_book[index].name.clone(),
|
||||
self.balances
|
||||
.get(&self.address_book[index].account_id)
|
||||
.map(|data| data.clone()),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn swap_up(&mut self) {
|
||||
if let Some(src_index) = self.table_state.selected() {
|
||||
let dst_index = src_index.saturating_sub(1);
|
||||
@ -305,13 +338,14 @@ impl AddressBook {
|
||||
self.scroll_state = self.scroll_state.position(i);
|
||||
}
|
||||
|
||||
fn prepare_u128(&self, maybe_value: Option<u128>, ticker: Option<&str>, after: usize) -> String {
|
||||
fn prepare_u128(&self, maybe_value: Option<u128>) -> String {
|
||||
match maybe_value {
|
||||
Some(value) => {
|
||||
let value = value as f64 / 10f64.powi(18);
|
||||
format!("{:.after$}{}", value, ticker.unwrap_or_default())
|
||||
let after = Self::DECIMALS;
|
||||
format!("{:.after$}{}", value, Self::TICKER)
|
||||
},
|
||||
None => DotSpinner::default().to_string(),
|
||||
None => format!("{}{}", DotSpinner::default().to_string(), Self::TICKER)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -360,9 +394,12 @@ impl Component for AddressBook {
|
||||
self.rename_record(new_name),
|
||||
Action::NewAddressBookRecord(name, address) =>
|
||||
self.add_new_record(name, address),
|
||||
Action::BalanceResponse(account_id, balance) => {
|
||||
Action::BalanceResponse(account_id, maybe_balance) => {
|
||||
if self.address_book.iter().any(|record| record.account_id == account_id) {
|
||||
let _ = self.balances.insert(account_id, balance);
|
||||
let _ = match maybe_balance {
|
||||
Some(balance) => self.balances.insert(account_id, balance),
|
||||
None => self.balances.remove(&account_id),
|
||||
};
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
@ -382,6 +419,7 @@ impl Component for AddressBook {
|
||||
KeyCode::Char('J') => self.swap_down(),
|
||||
KeyCode::Char('D') => self.delete_row(),
|
||||
KeyCode::Char('R') => self.update_address_book_record(),
|
||||
KeyCode::Char('I') => self.show_account_details(),
|
||||
KeyCode::Enter => self.send_transfer_to(),
|
||||
_ => {},
|
||||
};
|
||||
@ -399,15 +437,11 @@ impl Component for AddressBook {
|
||||
.map(|info| {
|
||||
let balance = self.balances
|
||||
.get(&info.account_id)
|
||||
.map(|inner_balance| {
|
||||
inner_balance.free
|
||||
.saturating_add(inner_balance.reserved)
|
||||
.saturating_add(inner_balance.frozen)
|
||||
});
|
||||
.map(|inner_balance| inner_balance.free);
|
||||
Row::new(vec![
|
||||
Cell::from(Text::from(info.name.clone()).alignment(Alignment::Left)),
|
||||
Cell::from(Text::from(info.address.clone()).alignment(Alignment::Center)),
|
||||
Cell::from(Text::from(self.prepare_u128(balance, Some(" CSPR"), 2)).alignment(Alignment::Right)),
|
||||
Cell::from(Text::from(self.prepare_u128(balance)).alignment(Alignment::Right)),
|
||||
])
|
||||
}),
|
||||
[
|
||||
|
||||
@ -20,7 +20,7 @@ pub struct Balance {
|
||||
total_balance: Option<u128>,
|
||||
transferable_balance: Option<u128>,
|
||||
locked_balance: Option<u128>,
|
||||
bonded_balance: Option<u128>,
|
||||
reserved_balance: Option<u128>,
|
||||
nonce: Option<u32>,
|
||||
palette: StylePalette
|
||||
}
|
||||
@ -32,7 +32,8 @@ impl Default for Balance {
|
||||
}
|
||||
|
||||
impl Balance {
|
||||
const DECIMALS_FOR_BALANCE: usize = 5;
|
||||
const TICKER: &str = " CSPR";
|
||||
const DECIMALS: usize = 6;
|
||||
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
@ -40,19 +41,20 @@ impl Balance {
|
||||
total_balance: None,
|
||||
transferable_balance: None,
|
||||
locked_balance: None,
|
||||
bonded_balance: None,
|
||||
reserved_balance: None,
|
||||
nonce: None,
|
||||
palette: StylePalette::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn prepare_u128(&self, maybe_value: Option<u128>, ticker: Option<&str>, after: usize) -> String {
|
||||
fn prepare_u128(&self, maybe_value: Option<u128>) -> String {
|
||||
match maybe_value {
|
||||
Some(value) => {
|
||||
let value = value as f64 / 10f64.powi(18);
|
||||
format!("{:.after$}{}", value, ticker.unwrap_or_default())
|
||||
let after = Self::DECIMALS;
|
||||
format!("{:.after$}{}", value, Self::TICKER)
|
||||
},
|
||||
None => DotSpinner::default().to_string()
|
||||
None => format!("{}{}", DotSpinner::default().to_string(), Self::TICKER)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -84,20 +86,20 @@ impl Component for Balance {
|
||||
Action::BalanceSetActive(maybe_balance) => {
|
||||
match maybe_balance {
|
||||
Some(balance) => {
|
||||
self.transferable_balance = Some(balance.free);
|
||||
self.locked_balance = Some(balance.reserved);
|
||||
self.bonded_balance = Some(balance.frozen);
|
||||
|
||||
let total_balance = balance.free
|
||||
.saturating_add(balance.reserved)
|
||||
.saturating_add(balance.frozen);
|
||||
self.total_balance = Some(total_balance);
|
||||
self.total_balance = Some(balance.free);
|
||||
self.locked_balance = Some(balance.frozen);
|
||||
self.reserved_balance = Some(balance.reserved);
|
||||
self.nonce = Some(balance.nonce);
|
||||
|
||||
let transferable = balance.free
|
||||
.saturating_sub(balance.reserved)
|
||||
.saturating_sub(balance.frozen);
|
||||
self.transferable_balance = Some(transferable);
|
||||
},
|
||||
None => {
|
||||
self.transferable_balance = None;
|
||||
self.locked_balance = None;
|
||||
self.bonded_balance = None;
|
||||
self.reserved_balance = None;
|
||||
self.total_balance = None;
|
||||
self.nonce = None;
|
||||
}
|
||||
@ -109,7 +111,7 @@ impl Component for Balance {
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
|
||||
let [_, _, place] = super::account_layout(area);
|
||||
let [_, _, place, _] = super::account_layout(area);
|
||||
let (border_style, border_type) = self.palette
|
||||
.create_border_style(self.is_active);
|
||||
|
||||
@ -117,38 +119,26 @@ impl Component for Balance {
|
||||
[
|
||||
Row::new(vec![
|
||||
Cell::from(Text::from("nonce: ".to_string()).alignment(Alignment::Left)),
|
||||
Cell::from(Text::from(self.prepare_u128(
|
||||
self.nonce.map(|n| n as u128),
|
||||
None,
|
||||
0)).alignment(Alignment::Right)),
|
||||
Cell::from(Text::from(self.nonce
|
||||
.map(|n| n.to_string())
|
||||
.unwrap_or(DotSpinner::default().to_string())
|
||||
).alignment(Alignment::Right)),
|
||||
]),
|
||||
Row::new(vec![
|
||||
Cell::from(Text::from("account: ".to_string()).alignment(Alignment::Left)),
|
||||
Cell::from(Text::from(self.prepare_u128(
|
||||
self.total_balance,
|
||||
Some(" CSPR"),
|
||||
Self::DECIMALS_FOR_BALANCE)).alignment(Alignment::Right)),
|
||||
Cell::from(Text::from(self.prepare_u128(self.total_balance)).alignment(Alignment::Right)),
|
||||
]),
|
||||
Row::new(vec![
|
||||
Cell::from(Text::from("free: ".to_string()).alignment(Alignment::Left)),
|
||||
Cell::from(Text::from(self.prepare_u128(
|
||||
self.transferable_balance,
|
||||
Some(" CSPR"),
|
||||
Self::DECIMALS_FOR_BALANCE)).alignment(Alignment::Right))
|
||||
Cell::from(Text::from(self.prepare_u128(self.transferable_balance)).alignment(Alignment::Right))
|
||||
]),
|
||||
Row::new(vec![
|
||||
Cell::from(Text::from("locked: ".to_string()).alignment(Alignment::Left)),
|
||||
Cell::from(Text::from(self.prepare_u128(
|
||||
self.locked_balance,
|
||||
Some(" CSPR"),
|
||||
Self::DECIMALS_FOR_BALANCE)).alignment(Alignment::Right)),
|
||||
Cell::from(Text::from(self.prepare_u128(self.locked_balance)).alignment(Alignment::Right)),
|
||||
]),
|
||||
Row::new(vec![
|
||||
Cell::from(Text::from("bonded: ".to_string()).alignment(Alignment::Left)),
|
||||
Cell::from(Text::from(self.prepare_u128(
|
||||
self.bonded_balance,
|
||||
Some(" CSPR"),
|
||||
Self::DECIMALS_FOR_BALANCE)).alignment(Alignment::Right)),
|
||||
Cell::from(Text::from("reserved: ".to_string()).alignment(Alignment::Left)),
|
||||
Cell::from(Text::from(self.prepare_u128(self.reserved_balance)).alignment(Alignment::Right)),
|
||||
]),
|
||||
],
|
||||
[
|
||||
|
||||
209
src/components/wallet/bond_popup.rs
Normal file
209
src/components/wallet/bond_popup.rs
Normal file
@ -0,0 +1,209 @@
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind};
|
||||
use color_eyre::Result;
|
||||
use ratatui::{
|
||||
layout::{Position, Alignment, Constraint, Flex, Layout, Rect},
|
||||
widgets::{Block, Clear, Paragraph},
|
||||
Frame
|
||||
};
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
use super::{Component, PartialComponent, CurrentTab};
|
||||
use crate::{
|
||||
action::Action,
|
||||
config::Config,
|
||||
palette::StylePalette,
|
||||
types::{ActionLevel, ActionTarget},
|
||||
widgets::{Input, InputRequest},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct BondPopup {
|
||||
is_active: bool,
|
||||
action_tx: Option<UnboundedSender<Action>>,
|
||||
network_tx: Option<Sender<Action>>,
|
||||
account_secret_seed: [u8; 32],
|
||||
account_id: [u8; 32],
|
||||
minimal_bond: u128,
|
||||
is_bonded: bool,
|
||||
amount: Input,
|
||||
palette: StylePalette
|
||||
}
|
||||
|
||||
impl Default for BondPopup {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl BondPopup {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
is_active: false,
|
||||
account_secret_seed: [0u8; 32],
|
||||
account_id: [0u8; 32],
|
||||
action_tx: None,
|
||||
network_tx: None,
|
||||
minimal_bond: 0u128,
|
||||
is_bonded: false,
|
||||
amount: Input::new(String::new()),
|
||||
palette: StylePalette::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn log_event(&mut self, message: String, level: ActionLevel) {
|
||||
if let Some(action_tx) = &self.action_tx {
|
||||
let _ = action_tx.send(
|
||||
Action::EventLog(message, level, ActionTarget::WalletLog));
|
||||
}
|
||||
}
|
||||
|
||||
fn close_popup(&mut self) {
|
||||
self.is_active = false;
|
||||
if let Some(action_tx) = &self.action_tx {
|
||||
let _ = action_tx.send(Action::ClosePopup);
|
||||
}
|
||||
}
|
||||
|
||||
fn update_used_account(&mut self, account_id: [u8; 32], secret_seed_str: String) {
|
||||
let secret_seed: [u8; 32] = hex::decode(secret_seed_str)
|
||||
.expect("stored seed is valid hex string; qed")
|
||||
.as_slice()
|
||||
.try_into()
|
||||
.expect("stored seed is valid length; qed");
|
||||
self.account_id = account_id;
|
||||
self.account_secret_seed = secret_seed;
|
||||
}
|
||||
|
||||
fn submit_message(&mut self) {
|
||||
if let Some(network_tx) = &self.network_tx {
|
||||
let str_amount = self.amount.value();
|
||||
let str_amount = if str_amount.starts_with('.') {
|
||||
&format!("0{}", str_amount)[..]
|
||||
} else {
|
||||
str_amount
|
||||
};
|
||||
match str_amount.parse::<f64>() {
|
||||
Ok(value) => {
|
||||
let amount = (value * 1_000_000_000_000_000_000.0) as u128;
|
||||
let log_target = ActionTarget::WalletLog;
|
||||
let _ = if self.is_bonded {
|
||||
network_tx.send(Action::BondValidatorExtraFrom(self.account_secret_seed, amount, log_target))
|
||||
} else {
|
||||
network_tx.send(Action::BondValidatorFrom(self.account_secret_seed, amount, log_target))
|
||||
};
|
||||
if let Some(action_tx) = &self.action_tx {
|
||||
let _ = action_tx.send(Action::ClosePopup);
|
||||
}
|
||||
},
|
||||
Err(err) => self.log_event(
|
||||
format!("invalid amount, error: {err}"), ActionLevel::Error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn enter_char(&mut self, new_char: char) {
|
||||
let is_separator_needed = !self.amount.value().contains('.') && new_char == '.';
|
||||
if new_char.is_digit(10) || is_separator_needed {
|
||||
let _ = self.amount.handle(InputRequest::InsertChar(new_char));
|
||||
}
|
||||
}
|
||||
|
||||
fn delete_char(&mut self) {
|
||||
let _ = self.amount.handle(InputRequest::DeletePrevChar);
|
||||
}
|
||||
|
||||
fn move_cursor_right(&mut self) {
|
||||
let _ = self.amount.handle(InputRequest::GoToNextChar);
|
||||
}
|
||||
|
||||
fn move_cursor_left(&mut self) {
|
||||
let _ = self.amount.handle(InputRequest::GoToPrevChar);
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialComponent for BondPopup {
|
||||
fn set_active(&mut self, current_tab: CurrentTab) {
|
||||
match current_tab {
|
||||
CurrentTab::BondPopup => self.is_active = true,
|
||||
_ => {
|
||||
self.is_active = false;
|
||||
self.amount = Input::new(String::new());
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for BondPopup {
|
||||
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_normal_border_style(style.get("normal_border_style").copied());
|
||||
self.palette.with_normal_title_style(style.get("normal_title_style").copied());
|
||||
self.palette.with_popup_style(style.get("popup_style").copied());
|
||||
self.palette.with_popup_title_style(style.get("popup_title_style").copied());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_key_event(&mut self, key: KeyEvent) -> Result<Option<Action>> {
|
||||
if self.is_active && key.kind == KeyEventKind::Press {
|
||||
match key.code {
|
||||
KeyCode::Enter => self.submit_message(),
|
||||
KeyCode::Char(to_insert) => self.enter_char(to_insert),
|
||||
KeyCode::Backspace => self.delete_char(),
|
||||
KeyCode::Left => self.move_cursor_left(),
|
||||
KeyCode::Right => self.move_cursor_right(),
|
||||
KeyCode::Esc => self.close_popup(),
|
||||
_ => {},
|
||||
};
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn update(&mut self, action: Action) -> Result<Option<Action>> {
|
||||
match action {
|
||||
Action::SetIsBonded(is_bonded, account_id) if self.account_id == account_id =>
|
||||
self.is_bonded = is_bonded,
|
||||
Action::UsedAccount(account_id, secret_seed) => self.update_used_account(account_id, secret_seed),
|
||||
Action::SetMinValidatorBond(minimal_bond) => self.minimal_bond = minimal_bond,
|
||||
_ => {}
|
||||
};
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
|
||||
if self.is_active {
|
||||
let (border_style, border_type) = self.palette.create_popup_style();
|
||||
let input = Paragraph::new(self.amount.value())
|
||||
.block(Block::bordered()
|
||||
.border_style(border_style)
|
||||
.border_type(border_type)
|
||||
.title_style(self.palette.create_popup_title_style())
|
||||
.title_alignment(Alignment::Right)
|
||||
.title(format!("Staking bond amount")));
|
||||
let v = Layout::vertical([Constraint::Max(3)]).flex(Flex::Center);
|
||||
let h = Layout::horizontal([Constraint::Max(50)]).flex(Flex::Center);
|
||||
let [area] = v.areas(area);
|
||||
let [area] = h.areas(area);
|
||||
|
||||
frame.render_widget(Clear, area);
|
||||
frame.render_widget(input, area);
|
||||
frame.set_cursor_position(Position::new(
|
||||
area.x + self.amount.cursor() as u16 + 1,
|
||||
area.y + 1
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
225
src/components/wallet/current_validator_details.rs
Normal file
225
src/components/wallet/current_validator_details.rs
Normal file
@ -0,0 +1,225 @@
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
use color_eyre::Result;
|
||||
use ratatui::{
|
||||
widgets::Clear,
|
||||
layout::{Alignment, Constraint, Rect},
|
||||
text::Text,
|
||||
widgets::{Block, Cell, Row, Table},
|
||||
Frame
|
||||
};
|
||||
|
||||
use super::{Component, PartialComponent, CurrentTab};
|
||||
use crate::{action::Action, config::Config, palette::StylePalette};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CurrentValidatorDetails {
|
||||
network_tx: Option<Sender<Action>>,
|
||||
is_active: bool,
|
||||
palette: StylePalette,
|
||||
choosen: [u8; 32],
|
||||
total_balance: u128,
|
||||
own_balance: u128,
|
||||
active_stake: u128,
|
||||
others_len: usize,
|
||||
commission: f64,
|
||||
points_ratio: f64,
|
||||
latest_era_claim: u32,
|
||||
is_active_validator: bool,
|
||||
is_nomination_disabled: bool,
|
||||
}
|
||||
|
||||
impl Default for CurrentValidatorDetails {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl CurrentValidatorDetails {
|
||||
const TICKER: &str = " CSPR";
|
||||
const DECIMALS: usize = 5;
|
||||
|
||||
pub fn new() -> Self {
|
||||
CurrentValidatorDetails {
|
||||
network_tx: None,
|
||||
is_active: false,
|
||||
choosen: [0u8; 32],
|
||||
total_balance: 0,
|
||||
own_balance: 0,
|
||||
active_stake: 0,
|
||||
others_len: 0,
|
||||
commission: 0.0,
|
||||
points_ratio: 0.0,
|
||||
palette: Default::default(),
|
||||
latest_era_claim: 0,
|
||||
is_active_validator: false,
|
||||
is_nomination_disabled: 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)
|
||||
}
|
||||
|
||||
fn prepare_stake_imbalance(&self) -> String {
|
||||
let (value, prefix) = if self.own_balance <= self.active_stake {
|
||||
(self.active_stake.saturating_sub(self.own_balance), "+")
|
||||
} else {
|
||||
(self.own_balance.saturating_sub(self.active_stake), "-")
|
||||
};
|
||||
format!("{}{}", prefix, self.prepare_u128(value))
|
||||
}
|
||||
|
||||
fn prepare_state_string(&self) -> String {
|
||||
if self.is_active_validator {
|
||||
"active".to_string()
|
||||
} else {
|
||||
"chilling".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn update_commission(&mut self, maybe_commission: Option<u32>, is_disabled: bool) {
|
||||
self.is_nomination_disabled = is_disabled;
|
||||
match maybe_commission {
|
||||
Some(commission) => {
|
||||
self.commission = commission as f64 / 10_000_000.0;
|
||||
self.is_active_validator = true;
|
||||
},
|
||||
None => {
|
||||
self.commission = 0.0;
|
||||
self.is_active_validator = false;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn update_choosen_validator(&mut self, account_id: [u8; 32], individual: u32, total: u32) {
|
||||
self.points_ratio = match total {
|
||||
0 => 0.0,
|
||||
_ => (individual as f64 / total as f64) * 100.0,
|
||||
};
|
||||
|
||||
if self.choosen != account_id {
|
||||
self.choosen = account_id;
|
||||
if let Some(network_tx) = &self.network_tx {
|
||||
let _ = network_tx.send(Action::GetErasStakersOverview(account_id, false));
|
||||
let _ = network_tx.send(Action::GetNominatorsByValidator(account_id, false));
|
||||
let _ = network_tx.send(Action::GetValidatorPrefs(account_id, false));
|
||||
let _ = network_tx.send(Action::GetValidatorLedger(account_id, false));
|
||||
let _ = network_tx.send(Action::GetValidatorLatestClaim(account_id, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialComponent for CurrentValidatorDetails {
|
||||
fn set_active(&mut self, current_tab: CurrentTab) {
|
||||
match current_tab {
|
||||
CurrentTab::CurrentValidatorsPopup => self.is_active = true,
|
||||
_ => self.is_active = false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for CurrentValidatorDetails {
|
||||
fn register_network_handler(&mut self, tx: Sender<Action>) -> Result<()> {
|
||||
self.network_tx = Some(tx);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn register_config_handler(&mut self, config: Config) -> Result<()> {
|
||||
if let Some(style) = config.styles.get(&crate::app::Mode::Wallet) {
|
||||
self.palette.with_normal_style(style.get("normal_style").copied());
|
||||
self.palette.with_hover_style(style.get("hover_style").copied());
|
||||
self.palette.with_normal_border_style(style.get("normal_border_style").copied());
|
||||
self.palette.with_hover_border_style(style.get("hover_border_style").copied());
|
||||
self.palette.with_normal_title_style(style.get("normal_title_style").copied());
|
||||
self.palette.with_hover_title_style(style.get("hover_title_style").copied());
|
||||
self.palette.with_highlight_style(style.get("highlight_style").copied());
|
||||
self.palette.with_scrollbar_style(style.get("scrollbar_style").copied());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update(&mut self, action: Action) -> Result<Option<Action>> {
|
||||
match action {
|
||||
Action::SetChoosenValidator(account_id, individual, total) => self.update_choosen_validator(account_id, individual, total),
|
||||
Action::SetValidatorPrefs(commission, is_disabled, account_id) if self.choosen == account_id => self.update_commission(commission, is_disabled),
|
||||
Action::SetNominatorsByValidator(noms, account_id) if self.choosen == account_id => self.others_len = noms.len(),
|
||||
Action::SetStakedAmountRatio(_, active_stake, account_id) if self.choosen == account_id => self.active_stake = active_stake.unwrap_or_default(),
|
||||
Action::SetValidatorLatestClaim(era_index, account_id) if self.choosen == account_id => self.latest_era_claim = era_index,
|
||||
Action::SetStakedRatio(total, own, account_id) if self.choosen == account_id => {
|
||||
self.total_balance = total;
|
||||
self.own_balance = own;
|
||||
},
|
||||
|
||||
_ => {}
|
||||
};
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
|
||||
if self.is_active {
|
||||
let (border_style, border_type) = self.palette.create_border_style(self.is_active);
|
||||
let [_, place] = super::nominator_layout(area);
|
||||
let table = Table::new(
|
||||
vec![
|
||||
Row::new(vec![
|
||||
Cell::from(Text::from("Forbidden".to_string()).alignment(Alignment::Left)),
|
||||
Cell::from(Text::from(self.is_nomination_disabled.to_string()).alignment(Alignment::Right)),
|
||||
]),
|
||||
Row::new(vec![
|
||||
Cell::from(Text::from("Nominators".to_string()).alignment(Alignment::Left)),
|
||||
Cell::from(Text::from(self.others_len.to_string()).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.total_balance)).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.own_balance)).alignment(Alignment::Right)),
|
||||
]),
|
||||
Row::new(vec![
|
||||
Cell::from(Text::from("Imbalance".to_string()).alignment(Alignment::Left)),
|
||||
Cell::from(Text::from(self.prepare_stake_imbalance()).alignment(Alignment::Right)),
|
||||
]),
|
||||
Row::new(vec![
|
||||
Cell::from(Text::from("Commission".to_string()).alignment(Alignment::Left)),
|
||||
Cell::from(Text::from(format!("{:.4}%", self.commission)).alignment(Alignment::Right)),
|
||||
]),
|
||||
Row::new(vec![
|
||||
Cell::from(Text::from("Points ratio".to_string()).alignment(Alignment::Left)),
|
||||
Cell::from(Text::from(format!("{:.4}%", self.points_ratio)).alignment(Alignment::Right)),
|
||||
]),
|
||||
Row::new(vec![
|
||||
Cell::from(Text::from("In next era".to_string()).alignment(Alignment::Left)),
|
||||
Cell::from(Text::from(self.prepare_state_string()).alignment(Alignment::Right)),
|
||||
]),
|
||||
Row::new(vec![
|
||||
Cell::from(Text::from("Last payout".to_string()).alignment(Alignment::Left)),
|
||||
Cell::from(Text::from(format!("{} days ago", self.latest_era_claim)).alignment(Alignment::Right)),
|
||||
]),
|
||||
],
|
||||
[
|
||||
Constraint::Max(12),
|
||||
Constraint::Min(0),
|
||||
],
|
||||
)
|
||||
.column_spacing(1)
|
||||
.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("Validator details"));
|
||||
|
||||
frame.render_widget(Clear, place);
|
||||
frame.render_widget(table, place);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
513
src/components/wallet/current_validators.rs
Normal file
513
src/components/wallet/current_validators.rs
Normal file
@ -0,0 +1,513 @@
|
||||
use std::fs::File;
|
||||
use std::path::PathBuf;
|
||||
use std::io::{Write, BufRead, BufReader};
|
||||
|
||||
use subxt::ext::sp_core::crypto::{AccountId32, Ss58AddressFormat, Ss58Codec};
|
||||
|
||||
use color_eyre::Result;
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
use ratatui::layout::{Constraint, Margin};
|
||||
use ratatui::style::{Stylize, Modifier};
|
||||
use ratatui::widgets::Clear;
|
||||
use ratatui::{
|
||||
text::{Line, Text},
|
||||
layout::{Alignment, Rect},
|
||||
widgets::{
|
||||
Block, Cell, Row, Table, TableState, Scrollbar, Padding,
|
||||
ScrollbarOrientation, ScrollbarState,
|
||||
},
|
||||
Frame
|
||||
};
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
use super::{PartialComponent, Component, CurrentTab};
|
||||
use crate::types::{ActionLevel, Nominations, ActionTarget, EraRewardPoints};
|
||||
use crate::{
|
||||
action::Action,
|
||||
config::Config,
|
||||
palette::StylePalette,
|
||||
};
|
||||
|
||||
pub struct CurrentValidators {
|
||||
is_active: bool,
|
||||
network_tx: Option<Sender<Action>>,
|
||||
action_tx: Option<UnboundedSender<Action>>,
|
||||
known_validators_file: PathBuf,
|
||||
palette: StylePalette,
|
||||
scroll_state: ScrollbarState,
|
||||
table_state: TableState,
|
||||
individual: Vec<EraRewardPoints>,
|
||||
not_active_nominations: Vec<EraRewardPoints>,
|
||||
known_validators: std::collections::HashMap<[u8; 32], String>,
|
||||
checked_validators: std::collections::HashSet<[u8; 32]>,
|
||||
total_points: u32,
|
||||
era_index: u32,
|
||||
my_stash_id: Option<[u8; 32]>,
|
||||
account_id: [u8; 32],
|
||||
account_secret_seed: [u8; 32],
|
||||
my_nominations: std::collections::HashMap<[u8; 32], Nominations>,
|
||||
filtered_vector: Vec<EraRewardPoints>,
|
||||
}
|
||||
|
||||
impl Default for CurrentValidators {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl CurrentValidators {
|
||||
const KNOWN_VALIDATORS_FILE: &str = "known-validators";
|
||||
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
is_active: false,
|
||||
network_tx: None,
|
||||
action_tx: None,
|
||||
known_validators_file: Default::default(),
|
||||
scroll_state: ScrollbarState::new(0),
|
||||
table_state: TableState::new(),
|
||||
individual: Default::default(),
|
||||
known_validators: Default::default(),
|
||||
checked_validators: Default::default(),
|
||||
total_points: 0,
|
||||
era_index: 0,
|
||||
my_stash_id: None,
|
||||
account_id: [0u8; 32],
|
||||
account_secret_seed: [0u8; 32],
|
||||
palette: StylePalette::default(),
|
||||
my_nominations: Default::default(),
|
||||
not_active_nominations: Default::default(),
|
||||
filtered_vector: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn close_popup(&mut self) {
|
||||
self.is_active = false;
|
||||
if let Some(action_tx) = &self.action_tx {
|
||||
let _ = action_tx.send(Action::ClosePopup);
|
||||
}
|
||||
}
|
||||
|
||||
fn move_selected(&mut self, index: usize) {
|
||||
if self.filtered_vector.len() > 0 {
|
||||
self.table_state.select(Some(index));
|
||||
self.scroll_state = self.scroll_state.position(index);
|
||||
self.update_choosen_details(index);
|
||||
}
|
||||
}
|
||||
|
||||
fn update_used_account(&mut self, account_id: [u8; 32], secret_seed_str: String) {
|
||||
let secret_seed: [u8; 32] = hex::decode(secret_seed_str)
|
||||
.expect("stored seed is valid hex string; qed")
|
||||
.as_slice()
|
||||
.try_into()
|
||||
.expect("stored seed is valid length; qed");
|
||||
self.account_id = account_id;
|
||||
self.account_secret_seed = secret_seed;
|
||||
}
|
||||
|
||||
fn store_nominators(&mut self, nominations: Nominations, account_id: [u8; 32]) {
|
||||
if self.account_id == account_id {
|
||||
self.not_active_nominations.clear();
|
||||
for account in nominations.targets.iter() {
|
||||
if !self.individual.iter().any(|r| r.account_id == *account) {
|
||||
self.not_active_nominations.push(EraRewardPoints {
|
||||
account_id: *account,
|
||||
address: AccountId32::from(*account)
|
||||
.to_ss58check_with_version(Ss58AddressFormat::custom(1996)),
|
||||
points: 0,
|
||||
disabled: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
self.my_nominations.insert(account_id, nominations);
|
||||
}
|
||||
|
||||
fn update_choosen_details(&self, index: usize) {
|
||||
if let Some(action_tx) = &self.action_tx {
|
||||
let (selected_account_id, selected_points) = self.filtered_vector
|
||||
.get(index)
|
||||
.map(|data| (data.account_id, data.points))
|
||||
.unwrap_or_default();
|
||||
|
||||
let _ = action_tx.send(Action::SetChoosenValidator(
|
||||
selected_account_id,
|
||||
selected_points,
|
||||
self.total_points));
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_choosen(&mut self) {
|
||||
self.checked_validators.clear();
|
||||
}
|
||||
|
||||
fn choose_current_nominated(&mut self) {
|
||||
self.clear_choosen();
|
||||
self.checked_validators.extend(self.my_nominations
|
||||
.get(&self.account_id)
|
||||
.map(|nom| nom.targets.clone())
|
||||
.unwrap_or_default());
|
||||
}
|
||||
|
||||
fn choose_all_validators(&mut self) {
|
||||
self.clear_choosen();
|
||||
self.checked_validators.extend(self.individual
|
||||
.iter().map(|ind| ind.account_id));
|
||||
}
|
||||
|
||||
fn swap_choosen_filter(&mut self) {
|
||||
let is_individual = self.filtered_vector.len() == self.individual.len();
|
||||
self.filtered_vector = self.individual
|
||||
.iter()
|
||||
.filter_map(|data| {
|
||||
let is_good = !is_individual || self.checked_validators.contains(&data.account_id);
|
||||
is_good.then(|| data.clone())
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
self.scroll_state = self.scroll_state.content_length(self.filtered_vector.len());
|
||||
if self.filtered_vector.len() == 0 {
|
||||
self.table_state.select(None);
|
||||
self.scroll_state = self.scroll_state.position(0);
|
||||
} else {
|
||||
self.first_row();
|
||||
}
|
||||
}
|
||||
|
||||
fn flip_validator_check(&mut self) {
|
||||
if let Some(index) = self.table_state.selected() {
|
||||
if let Some(indiv) = self.filtered_vector.get(index) {
|
||||
let current_account_id = indiv.account_id;
|
||||
if self.checked_validators.contains(¤t_account_id) {
|
||||
self.checked_validators.remove(¤t_account_id);
|
||||
} else {
|
||||
self.checked_validators.insert(current_account_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn save_validator_name(&mut self, new_name: String) {
|
||||
if let Some(index) = self.table_state.selected() {
|
||||
let account_id = self.filtered_vector[index].account_id;
|
||||
let _ = self.known_validators.insert(account_id, new_name);
|
||||
|
||||
let mut file = File::create(&self.known_validators_file)
|
||||
.expect("file should be accessible; qed");
|
||||
|
||||
for (account_id, name) in self.known_validators.iter() {
|
||||
let seed = hex::encode(account_id);
|
||||
writeln!(file, "{}:0x{}", &name, &seed).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_known_validator_record(&mut self) {
|
||||
if self.table_state.selected().is_some() {
|
||||
if let Some(action_tx) = &self.action_tx {
|
||||
let _ = action_tx.send(Action::RenameKnownValidatorRecord);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read_known_validators(&mut self, file_path: &PathBuf) -> Result<()> {
|
||||
if let Ok(file) = File::open(file_path) {
|
||||
let reader = BufReader::new(file);
|
||||
for line in reader.lines() {
|
||||
let line = line?.replace("\n", "");
|
||||
let line_split_at = line.find(":").unwrap_or(line.len());
|
||||
let (name, seed) = line.split_at(line_split_at);
|
||||
let seed_str = &seed[3..];
|
||||
|
||||
let account_id: [u8; 32] = hex::decode(seed_str)
|
||||
.expect("stored seed is valid hex string; qed")
|
||||
.as_slice()
|
||||
.try_into()
|
||||
.expect("stored seed is valid length; qed");
|
||||
|
||||
let _ = self.known_validators.insert(account_id, name.to_string());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn first_row(&mut self) {
|
||||
if self.filtered_vector.len() > 0 {
|
||||
self.move_selected(0);
|
||||
}
|
||||
}
|
||||
|
||||
fn next_row(&mut self) {
|
||||
let i = match self.table_state.selected() {
|
||||
Some(i) => {
|
||||
if i >= self.filtered_vector.len() - 1 {
|
||||
i
|
||||
} else {
|
||||
i + 1
|
||||
}
|
||||
},
|
||||
None => 0,
|
||||
};
|
||||
self.move_selected(i);
|
||||
}
|
||||
|
||||
fn last_row(&mut self) {
|
||||
if self.filtered_vector.len() > 0 {
|
||||
let last = self.filtered_vector.len() - 1;
|
||||
self.move_selected(last);
|
||||
}
|
||||
}
|
||||
|
||||
fn previous_row(&mut self) {
|
||||
let i = match self.table_state.selected() {
|
||||
Some(i) => {
|
||||
if i == 0 {
|
||||
0
|
||||
} else {
|
||||
i - 1
|
||||
}
|
||||
},
|
||||
None => 0
|
||||
};
|
||||
self.move_selected(i);
|
||||
}
|
||||
|
||||
fn update_era_rewards(
|
||||
&mut self,
|
||||
era_index: u32,
|
||||
total_points: u32,
|
||||
individual: &Vec<EraRewardPoints>,
|
||||
) {
|
||||
let previous_length = self.individual.len();
|
||||
self.individual = individual.to_vec();
|
||||
self.total_points = total_points;
|
||||
self.era_index = era_index;
|
||||
|
||||
if individual.len() > 0 {
|
||||
if let Some(account_id) = self.my_stash_id {
|
||||
if let Some(index) = self.individual
|
||||
.iter()
|
||||
.position(|item| item.account_id == account_id) {
|
||||
self.individual.swap(0, index);
|
||||
}
|
||||
}
|
||||
if previous_length == 0 {
|
||||
self.filtered_vector = self.individual.clone();
|
||||
self.scroll_state = self.scroll_state.content_length(self.individual.len());
|
||||
self.first_row();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn nominate_choosen(&mut self) {
|
||||
if self.my_stash_id.map(|acc| acc == self.account_id).unwrap_or_default() {
|
||||
if let Some(action_tx) = &self.action_tx {
|
||||
let _ = action_tx.send(Action::EventLog(
|
||||
"nomination from stash account will stop node validation, use another account for nomination".to_string(),
|
||||
ActionLevel::Error,
|
||||
ActionTarget::WalletLog));
|
||||
}
|
||||
} else {
|
||||
if let Some(network_tx) = &self.network_tx {
|
||||
let nominate_targets: Vec<[u8; 32]> = self.checked_validators
|
||||
.clone()
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>();
|
||||
let _ = network_tx.send(Action::NominateTargets(
|
||||
self.account_secret_seed, nominate_targets));
|
||||
}
|
||||
}
|
||||
self.close_popup();
|
||||
}
|
||||
|
||||
fn prepare_nomination_line(&self) -> String {
|
||||
let empty_nominations = Nominations::default();
|
||||
let nominations = self.my_nominations
|
||||
.get(&self.account_id)
|
||||
.unwrap_or(&empty_nominations);
|
||||
|
||||
if nominations.targets.len() == 0 {
|
||||
"No nominations found".to_string()
|
||||
} else {
|
||||
let status = if nominations.suppressed {
|
||||
"Suppressed"
|
||||
} else {
|
||||
"Active"
|
||||
};
|
||||
format!("Submitted at era #{} | {} ",
|
||||
nominations.submitted_in,
|
||||
status,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialComponent for CurrentValidators {
|
||||
fn set_active(&mut self, current_tab: CurrentTab) {
|
||||
match current_tab {
|
||||
CurrentTab::CurrentValidatorsPopup => self.is_active = true,
|
||||
_ => self.is_active = false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for CurrentValidators {
|
||||
fn register_action_handler(&mut self, tx: UnboundedSender<Action>) -> Result<()> {
|
||||
self.action_tx = Some(tx);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn register_network_handler(&mut self, tx: Sender<Action>) -> Result<()> {
|
||||
self.network_tx = Some(tx);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn register_config_handler(&mut self, config: Config) -> Result<()> {
|
||||
if let Some(style) = config.styles.get(&crate::app::Mode::Wallet) {
|
||||
self.palette.with_normal_style(style.get("normal_style").copied());
|
||||
self.palette.with_hover_style(style.get("hover_style").copied());
|
||||
self.palette.with_normal_border_style(style.get("normal_border_style").copied());
|
||||
self.palette.with_hover_border_style(style.get("hover_border_style").copied());
|
||||
self.palette.with_normal_title_style(style.get("normal_title_style").copied());
|
||||
self.palette.with_hover_title_style(style.get("hover_title_style").copied());
|
||||
self.palette.with_highlight_style(style.get("highlight_style").copied());
|
||||
self.palette.with_scrollbar_style(style.get("scrollbar_style").copied());
|
||||
}
|
||||
let mut known_validators_file = config.config.data_dir;
|
||||
known_validators_file.push(Self::KNOWN_VALIDATORS_FILE);
|
||||
self.read_known_validators(&known_validators_file)?;
|
||||
self.known_validators_file = known_validators_file;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update(&mut self, action: Action) -> Result<Option<Action>> {
|
||||
match action {
|
||||
Action::UpdateKnownValidator(validator_name) => self.save_validator_name(validator_name),
|
||||
Action::UsedAccount(account_id, secret_seed) => self.update_used_account(account_id, secret_seed),
|
||||
Action::SetNominatorsByAccount(nominations, account_id) => self.store_nominators(nominations, account_id),
|
||||
Action::SetStashAccount(account_id) => self.my_stash_id = Some(account_id),
|
||||
Action::SetCurrentValidatorEraRewards(era_index, total_points, individual) =>
|
||||
self.update_era_rewards(era_index, total_points, &individual),
|
||||
_ => {}
|
||||
};
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn handle_key_event(&mut self, key: KeyEvent) -> Result<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(),
|
||||
KeyCode::Char('R') => self.update_known_validator_record(),
|
||||
|
||||
KeyCode::Char('c') => self.clear_choosen(),
|
||||
KeyCode::Char('m') => self.choose_current_nominated(),
|
||||
KeyCode::Char('a') => self.choose_all_validators(),
|
||||
KeyCode::Char('f') => self.swap_choosen_filter(),
|
||||
|
||||
KeyCode::Char('N') => self.nominate_choosen(),
|
||||
KeyCode::Enter => self.flip_validator_check(),
|
||||
KeyCode::Esc => self.close_popup(),
|
||||
_ => {},
|
||||
};
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
|
||||
if self.is_active {
|
||||
let (border_style, border_type) = self.palette.create_border_style(self.is_active);
|
||||
let [place, _] = super::nominator_layout(area);
|
||||
|
||||
let top_title = format!("Validators {} | Total points: {}", self.individual.len(), self.total_points);
|
||||
let bottom_title = self.prepare_nomination_line();
|
||||
|
||||
let table = Table::new(
|
||||
self.filtered_vector
|
||||
.iter()
|
||||
.chain(&self.not_active_nominations)
|
||||
.enumerate()
|
||||
.map(|(index, info)| {
|
||||
let is_validator_choosen = self.checked_validators.contains(&info.account_id);
|
||||
let is_current_nomination = self.my_nominations
|
||||
.get(&self.account_id)
|
||||
.map(|x| x.targets.contains(&info.account_id))
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut address_text = Text::from(info.address.clone()).alignment(Alignment::Center);
|
||||
let mut points_text = Text::from(info.points.to_string()).alignment(Alignment::Right);
|
||||
|
||||
let (row_style, is_choosen_text) = if is_validator_choosen {
|
||||
address_text = address_text.add_modifier(Modifier::ITALIC);
|
||||
points_text = points_text.add_modifier(Modifier::ITALIC);
|
||||
(self.palette.create_highlight_style(), ">")
|
||||
} else if is_current_nomination {
|
||||
(Default::default(), "*")
|
||||
} else {
|
||||
(Default::default(), "")
|
||||
};
|
||||
|
||||
if info.disabled {
|
||||
address_text = address_text.add_modifier(Modifier::CROSSED_OUT);
|
||||
points_text = points_text.add_modifier(Modifier::CROSSED_OUT);
|
||||
}
|
||||
|
||||
let default_name = if index == 0 && self.my_stash_id
|
||||
.map(|account_id| account_id == info.account_id)
|
||||
.unwrap_or_default() == true {
|
||||
"My stash"
|
||||
} else {
|
||||
"Ghostie"
|
||||
};
|
||||
|
||||
let name = self.known_validators
|
||||
.get(&info.account_id)
|
||||
.cloned()
|
||||
.unwrap_or(default_name.to_string());
|
||||
|
||||
Row::new(vec![
|
||||
Cell::from(Text::from(is_choosen_text).alignment(Alignment::Left)),
|
||||
Cell::from(Text::from(name).alignment(Alignment::Left)),
|
||||
Cell::from(address_text),
|
||||
Cell::from(points_text),
|
||||
]).style(row_style)
|
||||
}),
|
||||
[
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(12),
|
||||
Constraint::Min(0),
|
||||
Constraint::Length(6),
|
||||
],
|
||||
)
|
||||
.style(self.palette.create_basic_style(false))
|
||||
.highlight_style(self.palette.create_basic_style(true))
|
||||
.column_spacing(1)
|
||||
.block(Block::bordered()
|
||||
.border_style(border_style)
|
||||
.border_type(border_type)
|
||||
.padding(Padding::right(2))
|
||||
.title_style(self.palette.create_title_style(false))
|
||||
.title_bottom(Line::from(bottom_title).left_aligned())
|
||||
.title_top(Line::from(top_title).right_aligned()));
|
||||
|
||||
let scrollbar = Scrollbar::default()
|
||||
.orientation(ScrollbarOrientation::VerticalRight)
|
||||
.begin_symbol(None)
|
||||
.end_symbol(None)
|
||||
.style(self.palette.create_scrollbar_style());
|
||||
|
||||
frame.render_widget(Clear, place);
|
||||
frame.render_stateful_widget(table, place, &mut self.table_state);
|
||||
frame.render_stateful_widget(
|
||||
scrollbar,
|
||||
place.inner(Margin { vertical: 1, horizontal: 1 }),
|
||||
&mut self.scroll_state,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@ -13,14 +13,14 @@ use ratatui::{
|
||||
|
||||
use super::{Component, PartialComponent, CurrentTab};
|
||||
use crate::{
|
||||
types::ActionLevel,
|
||||
action::Action,
|
||||
config::Config,
|
||||
palette::StylePalette,
|
||||
types::{ActionLevel, ActionTarget},
|
||||
};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct WalletLog {
|
||||
struct LogDetails {
|
||||
time: chrono::DateTime<chrono::Local>,
|
||||
level: ActionLevel,
|
||||
message: String,
|
||||
@ -31,7 +31,7 @@ pub struct EventLogs {
|
||||
is_active: bool,
|
||||
scroll_state: ScrollbarState,
|
||||
table_state: TableState,
|
||||
logs: std::collections::VecDeque<WalletLog>,
|
||||
logs: std::collections::VecDeque<LogDetails>,
|
||||
palette: StylePalette
|
||||
}
|
||||
|
||||
@ -39,18 +39,15 @@ impl EventLogs {
|
||||
const MAX_LOGS: usize = 50;
|
||||
|
||||
fn add_new_log(&mut self, message: String, level: ActionLevel) {
|
||||
self.logs.push_back(WalletLog {
|
||||
self.logs.push_front(LogDetails {
|
||||
time: chrono::Local::now(),
|
||||
level,
|
||||
message,
|
||||
});
|
||||
|
||||
if self.logs.len() > Self::MAX_LOGS {
|
||||
let _ = self.logs.pop_front();
|
||||
let _ = self.logs.pop_back();
|
||||
}
|
||||
|
||||
self.table_state.select(Some(self.logs.len() - 1));
|
||||
self.scroll_state = self.scroll_state.content_length(self.logs.len());
|
||||
}
|
||||
|
||||
fn first_row(&mut self) {
|
||||
@ -140,7 +137,8 @@ impl Component for EventLogs {
|
||||
|
||||
fn update(&mut self, action: Action) -> Result<Option<Action>> {
|
||||
match action {
|
||||
Action::WalletLog(message, level) => self.add_new_log(message, level),
|
||||
Action::EventLog(message, level, target) if target == ActionTarget::WalletLog =>
|
||||
self.add_new_log(message, level),
|
||||
_ => {}
|
||||
};
|
||||
Ok(None)
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
use color_eyre::Result;
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
use ratatui::{
|
||||
layout::{Constraint, Layout, Rect},
|
||||
layout::{Constraint, Flex, Layout, Rect},
|
||||
Frame,
|
||||
};
|
||||
|
||||
@ -18,6 +18,13 @@ mod accounts;
|
||||
mod overview;
|
||||
mod add_address_book_record;
|
||||
mod rename_address_book_record;
|
||||
mod account_details;
|
||||
mod staking_ledger;
|
||||
mod bond_popup;
|
||||
mod payee_popup;
|
||||
mod current_validators;
|
||||
mod current_validator_details;
|
||||
mod rename_known_validator;
|
||||
|
||||
use balance::Balance;
|
||||
use transfer::Transfer;
|
||||
@ -29,11 +36,18 @@ use accounts::Accounts;
|
||||
use overview::Overview;
|
||||
use add_address_book_record::AddAddressBookRecord;
|
||||
use rename_address_book_record::RenameAddressBookRecord;
|
||||
use account_details::AccountDetails;
|
||||
use staking_ledger::StakingLedger;
|
||||
use bond_popup::BondPopup;
|
||||
use payee_popup::PayeePopup;
|
||||
use current_validators::CurrentValidators;
|
||||
use current_validator_details::CurrentValidatorDetails;
|
||||
use rename_known_validator::RenameKnownValidator;
|
||||
|
||||
use super::Component;
|
||||
use crate::{action::Action, app::Mode, config::Config};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
pub enum CurrentTab {
|
||||
Nothing,
|
||||
Accounts,
|
||||
@ -44,6 +58,10 @@ pub enum CurrentTab {
|
||||
RenameAccount,
|
||||
RenameAddressBookRecord,
|
||||
Transfer,
|
||||
AccountDetails,
|
||||
BondPopup,
|
||||
PayeePopup,
|
||||
CurrentValidatorsPopup,
|
||||
}
|
||||
|
||||
pub trait PartialComponent: Component {
|
||||
@ -53,6 +71,7 @@ pub trait PartialComponent: Component {
|
||||
pub struct Wallet {
|
||||
is_active: bool,
|
||||
current_tab: CurrentTab,
|
||||
previous_tab: CurrentTab,
|
||||
components: Vec<Box<dyn PartialComponent>>,
|
||||
}
|
||||
|
||||
@ -61,10 +80,12 @@ impl Default for Wallet {
|
||||
Self {
|
||||
is_active: false,
|
||||
current_tab: CurrentTab::Accounts,
|
||||
previous_tab: CurrentTab::Accounts,
|
||||
components: vec![
|
||||
Box::new(Overview::default()),
|
||||
Box::new(Accounts::default()),
|
||||
Box::new(Balance::default()),
|
||||
Box::new(StakingLedger::default()),
|
||||
Box::new(AddressBook::default()),
|
||||
Box::new(EventLogs::default()),
|
||||
Box::new(AddAccount::default()),
|
||||
@ -72,6 +93,12 @@ impl Default for Wallet {
|
||||
Box::new(AddAddressBookRecord::default()),
|
||||
Box::new(RenameAddressBookRecord::default()),
|
||||
Box::new(Transfer::default()),
|
||||
Box::new(AccountDetails::default()),
|
||||
Box::new(BondPopup::default()),
|
||||
Box::new(PayeePopup::default()),
|
||||
Box::new(CurrentValidators::default()),
|
||||
Box::new(CurrentValidatorDetails::default()),
|
||||
Box::new(RenameKnownValidator::default()),
|
||||
],
|
||||
}
|
||||
}
|
||||
@ -127,58 +154,47 @@ impl Component for Wallet {
|
||||
CurrentTab::RenameAccount |
|
||||
CurrentTab::RenameAddressBookRecord |
|
||||
CurrentTab::Transfer |
|
||||
CurrentTab::AddAddressBookRecord => match key.code {
|
||||
KeyCode::Esc => {
|
||||
self.current_tab = CurrentTab::Accounts;
|
||||
for component in self.components.iter_mut() {
|
||||
component.set_active(self.current_tab.clone());
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
CurrentTab::AccountDetails |
|
||||
CurrentTab::BondPopup |
|
||||
CurrentTab::PayeePopup |
|
||||
CurrentTab::CurrentValidatorsPopup |
|
||||
CurrentTab::AddAddressBookRecord => {
|
||||
for component in self.components.iter_mut() {
|
||||
component.handle_key_event(key)?;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
_ => 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('W') => {
|
||||
self.previous_tab = self.current_tab;
|
||||
self.current_tab = CurrentTab::AddAccount;
|
||||
for component in self.components.iter_mut() {
|
||||
component.set_active(self.current_tab.clone());
|
||||
}
|
||||
},
|
||||
KeyCode::Char('A') => {
|
||||
self.previous_tab = self.current_tab;
|
||||
self.current_tab = CurrentTab::AddAddressBookRecord;
|
||||
for component in self.components.iter_mut() {
|
||||
component.set_active(self.current_tab.clone());
|
||||
}
|
||||
},
|
||||
KeyCode::Char('T') => {
|
||||
self.previous_tab = self.current_tab;
|
||||
self.current_tab = CurrentTab::Transfer;
|
||||
for component in self.components.iter_mut() {
|
||||
component.set_active(self.current_tab.clone());
|
||||
}
|
||||
},
|
||||
KeyCode::Char('l') | KeyCode::Right => {
|
||||
self.move_right();
|
||||
for component in self.components.iter_mut() {
|
||||
component.set_active(self.current_tab.clone());
|
||||
}
|
||||
KeyCode::Char('B') => {
|
||||
self.previous_tab = self.current_tab;
|
||||
self.current_tab = CurrentTab::BondPopup;
|
||||
},
|
||||
KeyCode::Char('h') | KeyCode::Left => {
|
||||
self.move_left();
|
||||
for component in self.components.iter_mut() {
|
||||
component.set_active(self.current_tab.clone());
|
||||
}
|
||||
KeyCode::Char('O') => {
|
||||
self.previous_tab = self.current_tab;
|
||||
self.current_tab = CurrentTab::PayeePopup;
|
||||
},
|
||||
KeyCode::Char('N') => {
|
||||
self.previous_tab = self.current_tab;
|
||||
self.current_tab = CurrentTab::CurrentValidatorsPopup;
|
||||
},
|
||||
KeyCode::Char('l') | KeyCode::Right => self.move_right(),
|
||||
KeyCode::Char('h') | KeyCode::Left => self.move_left(),
|
||||
_ => {
|
||||
for component in self.components.iter_mut() {
|
||||
component.handle_key_event(key)?;
|
||||
@ -191,15 +207,28 @@ impl Component for Wallet {
|
||||
|
||||
fn update(&mut self, action: Action) -> Result<Option<Action>> {
|
||||
match action {
|
||||
Action::SetActiveScreen(Mode::Wallet) => self.is_active = true,
|
||||
Action::UpdateAccountName(_) | Action::NewAccount(_) =>
|
||||
self.current_tab = CurrentTab::Accounts,
|
||||
Action::UpdateAddressBookRecord(_) | Action::NewAddressBookRecord(_, _) | Action::ClosePopup =>
|
||||
self.current_tab = CurrentTab::AddressBook,
|
||||
Action::RenameAccount(_) => self.current_tab = CurrentTab::RenameAccount,
|
||||
Action::RenameAddressBookRecord(_) =>
|
||||
self.current_tab = CurrentTab::RenameAddressBookRecord,
|
||||
Action::TransferTo(_) => self.current_tab = CurrentTab::Transfer,
|
||||
Action::SetActiveScreen(Mode::Wallet) => {
|
||||
self.is_active = true;
|
||||
self.current_tab = CurrentTab::Accounts;
|
||||
self.previous_tab = CurrentTab::Accounts;
|
||||
},
|
||||
Action::RenameAccount(_) => {
|
||||
self.previous_tab = self.current_tab;
|
||||
self.current_tab = CurrentTab::RenameAccount;
|
||||
},
|
||||
Action::RenameAddressBookRecord(_) => {
|
||||
self.previous_tab = self.current_tab;
|
||||
self.current_tab = CurrentTab::RenameAddressBookRecord;
|
||||
}
|
||||
Action::TransferTo(_) => {
|
||||
self.previous_tab = self.current_tab;
|
||||
self.current_tab = CurrentTab::Transfer;
|
||||
},
|
||||
Action::AccountDetailsOf(_, _) => {
|
||||
self.previous_tab = self.current_tab;
|
||||
self.current_tab = CurrentTab::AccountDetails;
|
||||
},
|
||||
Action::ClosePopup => self.current_tab = self.previous_tab,
|
||||
_ => {}
|
||||
}
|
||||
for component in self.components.iter_mut() {
|
||||
@ -210,7 +239,7 @@ impl Component for Wallet {
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
|
||||
let screen = super::screen_layout(area);
|
||||
let screen = super::layouts::screen_layout(area);
|
||||
for component in self.components.iter_mut() {
|
||||
component.draw(frame, screen)?;
|
||||
}
|
||||
@ -233,11 +262,21 @@ pub fn bars_layout(area: Rect) -> [Rect; 2] {
|
||||
]).areas(place)
|
||||
}
|
||||
|
||||
pub fn account_layout(area: Rect) -> [Rect; 3] {
|
||||
pub fn account_layout(area: Rect) -> [Rect; 4] {
|
||||
let [place, _] = bars_layout(area);
|
||||
Layout::vertical([
|
||||
Constraint::Max(4),
|
||||
Constraint::Min(0),
|
||||
Constraint::Max(7),
|
||||
Constraint::Max(6),
|
||||
]).areas(place)
|
||||
}
|
||||
|
||||
pub fn nominator_layout(area: Rect) -> [Rect; 2] {
|
||||
let v = Layout::vertical([Constraint::Max(11)]).flex(Flex::Center);
|
||||
let [area] = v.areas(area);
|
||||
Layout::horizontal([
|
||||
Constraint::Percentage(60),
|
||||
Constraint::Percentage(40),
|
||||
]).areas(area)
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@ use super::{Component, PartialComponent, CurrentTab};
|
||||
use crate::{
|
||||
action::Action,
|
||||
config::Config,
|
||||
palette::StylePalette,
|
||||
palette::StylePalette, widgets::DotSpinner,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
@ -28,7 +28,8 @@ impl Default for Overview {
|
||||
}
|
||||
|
||||
impl Overview {
|
||||
const DECIMALS_FOR_BALANCE: usize = 6;
|
||||
const TICKER: &str = " CSPR";
|
||||
const DECIMALS: usize = 6;
|
||||
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
@ -39,9 +40,15 @@ impl Overview {
|
||||
}
|
||||
}
|
||||
|
||||
fn prepare_u128(&self, value: u128, after: usize) -> String {
|
||||
let value = value as f64 / 10f64.powi(18);
|
||||
format!("{:.after$}", value)
|
||||
fn prepare_u128(&self, maybe_value: Option<u128>) -> String {
|
||||
match maybe_value {
|
||||
Some(value) => {
|
||||
let value = value as f64 / 10f64.powi(18);
|
||||
let after = Self::DECIMALS;
|
||||
format!("{:.after$}{}", value, Self::TICKER)
|
||||
},
|
||||
None => format!("{}{}", DotSpinner::default().to_string(), Self::TICKER)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -70,14 +77,14 @@ impl Component for Overview {
|
||||
fn update(&mut self, action: Action) -> Result<Option<Action>> {
|
||||
match action {
|
||||
Action::SetExistentialDeposit(ed) => self.existential_balance = Some(ed),
|
||||
Action::SetTotalIssuance(issuance) => self.total_issuance = Some(issuance),
|
||||
Action::SetTotalIssuance(maybe_issuance) => self.total_issuance = maybe_issuance,
|
||||
_ => {}
|
||||
};
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
|
||||
let [place, _, _] = super::account_layout(area);
|
||||
let [place, _, _, _] = super::account_layout(area);
|
||||
let (border_style, border_type) = self.palette
|
||||
.create_border_style(self.is_active);
|
||||
|
||||
@ -85,25 +92,16 @@ impl Component for Overview {
|
||||
[
|
||||
Row::new(vec![
|
||||
Cell::from(Text::from("total supply: ".to_string()).alignment(Alignment::Left)),
|
||||
Cell::from(Text::from(self.prepare_u128(
|
||||
self.total_issuance.unwrap_or_default(),
|
||||
Self::DECIMALS_FOR_BALANCE,
|
||||
)).alignment(Alignment::Center)),
|
||||
Cell::from(Text::from("CSPR".to_string()).alignment(Alignment::Right)),
|
||||
Cell::from(Text::from(self.prepare_u128(self.total_issuance)).alignment(Alignment::Right)),
|
||||
]),
|
||||
Row::new(vec![
|
||||
Cell::from(Text::from("min deposit: ".to_string()).alignment(Alignment::Left)),
|
||||
Cell::from(Text::from(self.prepare_u128(
|
||||
self.existential_balance.unwrap_or_default(),
|
||||
Self::DECIMALS_FOR_BALANCE,
|
||||
)).alignment(Alignment::Center)),
|
||||
Cell::from(Text::from("CSPR".to_string()).alignment(Alignment::Right)),
|
||||
Cell::from(Text::from(self.prepare_u128(self.existential_balance)).alignment(Alignment::Right)),
|
||||
]),
|
||||
],
|
||||
[
|
||||
Constraint::Max(15),
|
||||
Constraint::Min(0),
|
||||
Constraint::Length(5),
|
||||
]
|
||||
)
|
||||
.block(Block::bordered()
|
||||
|
||||
374
src/components/wallet/payee_popup.rs
Normal file
374
src/components/wallet/payee_popup.rs
Normal file
@ -0,0 +1,374 @@
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind};
|
||||
use color_eyre::Result;
|
||||
use ratatui::{
|
||||
layout::{Alignment, Constraint, Flex, Layout, Position, Rect},
|
||||
text::Text,
|
||||
widgets::{Block, Cell, Clear, Paragraph, Row, Table, TableState},
|
||||
Frame,
|
||||
};
|
||||
use subxt::ext::sp_core::crypto::{
|
||||
ByteArray, Ss58Codec, Ss58AddressFormat, AccountId32,
|
||||
};
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
use super::{Component, PartialComponent, CurrentTab};
|
||||
use crate::{
|
||||
action::Action,
|
||||
config::Config,
|
||||
palette::StylePalette,
|
||||
types::{ActionLevel, ActionTarget, RewardDestination},
|
||||
widgets::{Input, InputRequest},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PayeePopup {
|
||||
is_active: bool,
|
||||
action_tx: Option<UnboundedSender<Action>>,
|
||||
network_tx: Option<Sender<Action>>,
|
||||
table_state: TableState,
|
||||
account_secret_seed: [u8; 32],
|
||||
account_id: [u8; 32],
|
||||
proposed_account_id: Option<[u8; 32]>,
|
||||
is_bonded: bool,
|
||||
is_account_chosen: bool,
|
||||
is_input_active: bool,
|
||||
address: Input,
|
||||
possible_payee_options: &'static [(&'static str, &'static str)],
|
||||
current_reward_destination: RewardDestination,
|
||||
palette: StylePalette
|
||||
}
|
||||
|
||||
impl Default for PayeePopup {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl PayeePopup {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
is_active: false,
|
||||
account_secret_seed: [0u8; 32],
|
||||
account_id: [0u8; 32],
|
||||
proposed_account_id: None,
|
||||
action_tx: None,
|
||||
network_tx: None,
|
||||
table_state: TableState::new(),
|
||||
is_bonded: false,
|
||||
is_account_chosen: false,
|
||||
is_input_active: false,
|
||||
address: Input::new(String::new()),
|
||||
current_reward_destination: Default::default(),
|
||||
possible_payee_options: &[
|
||||
("Re-stake", "(pay into the stash account, increasing the amount at stake accordingly)"),
|
||||
("Stake", "(pay into the stash account, not increasing the amount at stake)"),
|
||||
("Account", "(pay into a specified account different from stash)"),
|
||||
("None", "(refuse to receive all rewards from staking)"),
|
||||
],
|
||||
palette: StylePalette::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn close_popup(&mut self) {
|
||||
self.is_active = false;
|
||||
if let Some(action_tx) = &self.action_tx {
|
||||
let _ = action_tx.send(Action::ClosePopup);
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_index_from_destination(&mut self) -> usize {
|
||||
let (index, address) = match self.current_reward_destination {
|
||||
RewardDestination::Staked => (0, Default::default()),
|
||||
RewardDestination::Stash => (1, Default::default()),
|
||||
RewardDestination::Account(account_id) => {
|
||||
let address = AccountId32::from(account_id)
|
||||
.to_ss58check_with_version(Ss58AddressFormat::custom(1996));
|
||||
(2, address)
|
||||
},
|
||||
RewardDestination::None => (3, Default::default()),
|
||||
_ => (0, Default::default()),
|
||||
};
|
||||
self.address = Input::new(address);
|
||||
index
|
||||
}
|
||||
|
||||
fn move_to_row(&mut self, index: usize) {
|
||||
self.table_state.select(Some(index));
|
||||
self.is_account_chosen = index == 2;
|
||||
}
|
||||
|
||||
fn next_row(&mut self) {
|
||||
let i = match self.table_state.selected() {
|
||||
Some(i) => {
|
||||
if i >= self.possible_payee_options.len() - 1 {
|
||||
i
|
||||
} else {
|
||||
i + 1
|
||||
}
|
||||
},
|
||||
None => 0,
|
||||
};
|
||||
self.move_to_row(i);
|
||||
}
|
||||
|
||||
|
||||
fn previous_row(&mut self) {
|
||||
let i = match self.table_state.selected() {
|
||||
Some(i) => {
|
||||
if i == 0 {
|
||||
0
|
||||
} else {
|
||||
i - 1
|
||||
}
|
||||
},
|
||||
None => 0
|
||||
};
|
||||
self.move_to_row(i);
|
||||
}
|
||||
|
||||
fn log_event(&mut self, message: String, level: ActionLevel) {
|
||||
if let Some(action_tx) = &self.action_tx {
|
||||
let _ = action_tx.send(Action::EventLog(
|
||||
message,
|
||||
level,
|
||||
ActionTarget::WalletLog));
|
||||
}
|
||||
}
|
||||
|
||||
fn update_used_account(&mut self, account_id: [u8; 32], secret_seed_str: String) {
|
||||
let secret_seed: [u8; 32] = hex::decode(secret_seed_str)
|
||||
.expect("stored seed is valid hex string; qed")
|
||||
.as_slice()
|
||||
.try_into()
|
||||
.expect("stored seed is valid length; qed");
|
||||
self.account_id = account_id;
|
||||
self.account_secret_seed = secret_seed;
|
||||
}
|
||||
|
||||
fn trigger_address_input(&mut self) {
|
||||
self.is_input_active = !self.is_input_active;
|
||||
}
|
||||
|
||||
fn submit_new_input(&mut self) {
|
||||
match AccountId32::from_ss58check_with_version(self.address.value()) {
|
||||
Ok((account_id, format)) => {
|
||||
if format != Ss58AddressFormat::custom(1996) {
|
||||
self.log_event(
|
||||
format!("provided public address for {} is not part of Casper/Ghost ecosystem", self.address.value()),
|
||||
ActionLevel::Error);
|
||||
}
|
||||
let seed_vec = account_id.to_raw_vec();
|
||||
let mut account_id = [0u8; 32];
|
||||
account_id.copy_from_slice(&seed_vec);
|
||||
|
||||
self.proposed_account_id = Some(account_id);
|
||||
self.submit_new_payee();
|
||||
},
|
||||
_ => {
|
||||
self.log_event(
|
||||
format!("could not create valid account id from {}", self.address.value()),
|
||||
ActionLevel::Error);
|
||||
self.proposed_account_id = None;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
fn submit_new_payee(&mut self) {
|
||||
if let Some(index) = self.table_state.selected() {
|
||||
let new_destination = match index {
|
||||
0 => RewardDestination::Staked,
|
||||
1 => RewardDestination::Stash,
|
||||
2 => {
|
||||
let account_id = self.proposed_account_id
|
||||
.expect("checked before in submit_new_input; qed");
|
||||
RewardDestination::Account(account_id)
|
||||
}
|
||||
3 => RewardDestination::None,
|
||||
_ => RewardDestination::Staked,
|
||||
};
|
||||
|
||||
if !self.is_bonded {
|
||||
self.log_event(
|
||||
"no bond detected, stake minimum bond amount first".to_string(),
|
||||
ActionLevel::Warn);
|
||||
} else if new_destination == self.current_reward_destination {
|
||||
self.log_event(
|
||||
"same destination choosen, no need for transaction".to_string(),
|
||||
ActionLevel::Warn);
|
||||
} else {
|
||||
if let Some(network_tx) = &self.network_tx {
|
||||
let _ = network_tx.send(Action::SetPayee(
|
||||
self.account_secret_seed,
|
||||
new_destination,
|
||||
ActionTarget::WalletLog));
|
||||
}
|
||||
}
|
||||
if let Some(action_tx) = &self.action_tx {
|
||||
let _ = action_tx.send(Action::ClosePopup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn enter_char(&mut self, new_char: char) {
|
||||
let _ = self.address.handle(InputRequest::InsertChar(new_char));
|
||||
}
|
||||
|
||||
fn delete_char(&mut self) {
|
||||
let _ = self.address.handle(InputRequest::DeletePrevChar);
|
||||
}
|
||||
|
||||
fn move_cursor_right(&mut self) {
|
||||
let _ = self.address.handle(InputRequest::GoToNextChar);
|
||||
}
|
||||
|
||||
fn move_cursor_left(&mut self) {
|
||||
let _ = self.address.handle(InputRequest::GoToPrevChar);
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialComponent for PayeePopup {
|
||||
fn set_active(&mut self, current_tab: CurrentTab) {
|
||||
match current_tab {
|
||||
CurrentTab::PayeePopup => self.is_active = true,
|
||||
_ => {
|
||||
self.is_active = false;
|
||||
self.is_account_chosen = false;
|
||||
self.is_input_active = false;
|
||||
|
||||
let index = self.parse_index_from_destination();
|
||||
self.move_to_row(index);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for PayeePopup {
|
||||
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_normal_border_style(style.get("normal_border_style").copied());
|
||||
self.palette.with_normal_title_style(style.get("normal_title_style").copied());
|
||||
self.palette.with_popup_style(style.get("popup_style").copied());
|
||||
self.palette.with_popup_title_style(style.get("popup_title_style").copied());
|
||||
self.palette.with_highlight_style(style.get("highlight_style").copied());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_key_event(&mut self, key: KeyEvent) -> Result<Option<Action>> {
|
||||
if self.is_active && key.kind == KeyEventKind::Press {
|
||||
if self.is_input_active {
|
||||
match key.code {
|
||||
KeyCode::Enter => self.submit_new_input(),
|
||||
KeyCode::Char(to_insert) => self.enter_char(to_insert),
|
||||
KeyCode::Backspace => self.delete_char(),
|
||||
KeyCode::Left => self.move_cursor_left(),
|
||||
KeyCode::Right => self.move_cursor_right(),
|
||||
KeyCode::Esc => self.trigger_address_input(),
|
||||
_ => {},
|
||||
};
|
||||
} else {
|
||||
match key.code {
|
||||
KeyCode::Enter if !self.is_account_chosen => self.submit_new_payee(),
|
||||
KeyCode::Enter if self.is_account_chosen => self.trigger_address_input(),
|
||||
KeyCode::Up | KeyCode::Char('k') => self.previous_row(),
|
||||
KeyCode::Down | KeyCode::Char('j') => self.next_row(),
|
||||
KeyCode::Esc => self.close_popup(),
|
||||
_ => {},
|
||||
};
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn update(&mut self, action: Action) -> Result<Option<Action>> {
|
||||
match action {
|
||||
Action::UsedAccount(account_id, secret_seed) => self.update_used_account(account_id, secret_seed),
|
||||
Action::SetIsBonded(is_bonded, account_id) if self.account_id == account_id =>
|
||||
self.is_bonded = is_bonded,
|
||||
Action::SetStakingPayee(reward_destination, account_id) if self.account_id == account_id => {
|
||||
let destination_changed = self.current_reward_destination != reward_destination;
|
||||
self.current_reward_destination = reward_destination;
|
||||
if destination_changed || self.table_state.selected().is_none() {
|
||||
let index = self.parse_index_from_destination();
|
||||
self.move_to_row(index);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
|
||||
if self.is_active {
|
||||
let (border_style, border_type) = self.palette.create_popup_style();
|
||||
let size = area.as_size();
|
||||
let input_area = Rect::new(size.width / 2, size.height / 2, 51, 3);
|
||||
|
||||
let table = Table::new(
|
||||
self.possible_payee_options
|
||||
.iter()
|
||||
.map(|data| {
|
||||
Row::new(vec![
|
||||
Cell::from(Text::from(data.0.to_string()).alignment(Alignment::Left)),
|
||||
Cell::from(Text::from(data.1.to_string()).alignment(Alignment::Left)),
|
||||
])
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
[Constraint::Length(8), 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_style(self.palette.create_popup_title_style())
|
||||
.title_alignment(Alignment::Right)
|
||||
.title("Select reward destination"));
|
||||
|
||||
let v = Layout::vertical([Constraint::Max(6)]).flex(Flex::Center);
|
||||
let h = Layout::horizontal([Constraint::Max(83)]).flex(Flex::Center);
|
||||
let [area] = v.areas(area);
|
||||
let [area] = h.areas(area);
|
||||
|
||||
frame.render_widget(Clear, area);
|
||||
frame.render_stateful_widget(table, area, &mut self.table_state);
|
||||
|
||||
if self.is_input_active {
|
||||
let input_amount = Paragraph::new(self.address.value())
|
||||
.block(Block::bordered()
|
||||
.border_style(border_style)
|
||||
.border_type(border_type)
|
||||
.title_style(self.palette.create_popup_title_style())
|
||||
.title_alignment(Alignment::Right)
|
||||
.title("Destination account"));
|
||||
|
||||
let v = Layout::vertical([Constraint::Max(8)]).flex(Flex::Center);
|
||||
let h = Layout::horizontal([Constraint::Max(51)]).flex(Flex::Center);
|
||||
let [input_area] = v.areas(input_area);
|
||||
let [input_area] = h.areas(input_area);
|
||||
|
||||
frame.render_widget(Clear, input_area);
|
||||
frame.render_widget(input_amount, input_area);
|
||||
|
||||
frame.set_cursor_position(Position::new(
|
||||
input_area.x + self.address.cursor() as u16 + 1,
|
||||
input_area.y + 1
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@ -40,12 +40,19 @@ impl RenameAccount {
|
||||
palette: StylePalette::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenameAccount {
|
||||
fn close_popup(&mut self) {
|
||||
self.is_active = false;
|
||||
if let Some(action_tx) = &self.action_tx {
|
||||
let _ = action_tx.send(Action::ClosePopup);
|
||||
}
|
||||
}
|
||||
|
||||
fn submit_message(&mut self) {
|
||||
if let Some(action_tx) = &self.action_tx {
|
||||
let _ = action_tx.send(Action::UpdateAccountName(self.name.value().to_string()));
|
||||
let _ = action_tx.send(Action::UpdateAccountName(
|
||||
self.name.value().to_string()));
|
||||
let _ = action_tx.send(Action::ClosePopup);
|
||||
}
|
||||
}
|
||||
|
||||
@ -112,7 +119,7 @@ impl Component for RenameAccount {
|
||||
KeyCode::Backspace => self.delete_char(),
|
||||
KeyCode::Left => self.move_cursor_left(),
|
||||
KeyCode::Right => self.move_cursor_right(),
|
||||
KeyCode::Esc => self.is_active = false,
|
||||
KeyCode::Esc => self.close_popup(),
|
||||
_ => {},
|
||||
};
|
||||
}
|
||||
|
||||
@ -40,13 +40,19 @@ impl RenameAddressBookRecord {
|
||||
palette: StylePalette::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenameAddressBookRecord {
|
||||
fn close_popup(&mut self) {
|
||||
self.is_active = false;
|
||||
if let Some(action_tx) = &self.action_tx {
|
||||
let _ = action_tx.send(Action::ClosePopup);
|
||||
}
|
||||
}
|
||||
|
||||
fn submit_message(&mut self) {
|
||||
if let Some(action_tx) = &self.action_tx {
|
||||
let _ = action_tx.send(Action::UpdateAddressBookRecord(
|
||||
self.name.value().to_string()));
|
||||
let _ = action_tx.send(Action::ClosePopup);
|
||||
}
|
||||
}
|
||||
|
||||
@ -113,7 +119,7 @@ impl Component for RenameAddressBookRecord {
|
||||
KeyCode::Backspace => self.delete_char(),
|
||||
KeyCode::Left => self.move_cursor_left(),
|
||||
KeyCode::Right => self.move_cursor_right(),
|
||||
KeyCode::Esc => self.is_active = false,
|
||||
KeyCode::Esc => self.close_popup(),
|
||||
_ => {},
|
||||
};
|
||||
}
|
||||
|
||||
143
src/components/wallet/rename_known_validator.rs
Normal file
143
src/components/wallet/rename_known_validator.rs
Normal file
@ -0,0 +1,143 @@
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind};
|
||||
use color_eyre::Result;
|
||||
use ratatui::{
|
||||
layout::{Position, Alignment, Constraint, Flex, Layout, Rect},
|
||||
widgets::{Block, Clear, Paragraph},
|
||||
Frame
|
||||
};
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
|
||||
use super::{Component, PartialComponent, CurrentTab};
|
||||
use crate::{
|
||||
widgets::{Input, InputRequest},
|
||||
action::Action,
|
||||
config::Config,
|
||||
palette::StylePalette,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RenameKnownValidator {
|
||||
is_active: bool,
|
||||
action_tx: Option<UnboundedSender<Action>>,
|
||||
name: Input,
|
||||
palette: StylePalette
|
||||
}
|
||||
|
||||
impl Default for RenameKnownValidator {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl RenameKnownValidator {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
is_active: false,
|
||||
action_tx: None,
|
||||
name: Input::new(String::new()),
|
||||
palette: StylePalette::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn close_popup(&mut self) {
|
||||
self.is_active = false;
|
||||
self.name = Input::new(String::new());
|
||||
}
|
||||
|
||||
fn submit_message(&mut self) {
|
||||
if let Some(action_tx) = &self.action_tx {
|
||||
let _ = action_tx.send(Action::UpdateKnownValidator(
|
||||
self.name.value().to_string()));
|
||||
}
|
||||
self.close_popup();
|
||||
}
|
||||
|
||||
fn enter_char(&mut self, new_char: char) {
|
||||
let _ = self.name.handle(InputRequest::InsertChar(new_char));
|
||||
}
|
||||
|
||||
fn delete_char(&mut self) {
|
||||
let _ = self.name.handle(InputRequest::DeletePrevChar);
|
||||
}
|
||||
|
||||
fn move_cursor_right(&mut self) {
|
||||
let _ = self.name.handle(InputRequest::GoToNextChar);
|
||||
}
|
||||
|
||||
fn move_cursor_left(&mut self) {
|
||||
let _ = self.name.handle(InputRequest::GoToPrevChar);
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialComponent for RenameKnownValidator {
|
||||
fn set_active(&mut self, _current_tab: CurrentTab) { }
|
||||
}
|
||||
|
||||
impl Component for RenameKnownValidator {
|
||||
fn register_action_handler(&mut self, tx: UnboundedSender<Action>) -> Result<()> {
|
||||
self.action_tx = Some(tx);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update(&mut self, action: Action) -> Result<Option<Action>> {
|
||||
match action {
|
||||
Action::RenameKnownValidatorRecord => self.is_active = true,
|
||||
_ => {}
|
||||
};
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
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_normal_border_style(style.get("normal_border_style").copied());
|
||||
self.palette.with_normal_title_style(style.get("normal_title_style").copied());
|
||||
self.palette.with_popup_style(style.get("popup_style").copied());
|
||||
self.palette.with_popup_title_style(style.get("popup_title_style").copied());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_key_event(&mut self, key: KeyEvent) -> Result<Option<Action>> {
|
||||
if self.is_active && key.kind == KeyEventKind::Press {
|
||||
match key.code {
|
||||
KeyCode::Enter => self.submit_message(),
|
||||
KeyCode::Char(to_insert) => self.enter_char(to_insert),
|
||||
KeyCode::Backspace => self.delete_char(),
|
||||
KeyCode::Left => self.move_cursor_left(),
|
||||
KeyCode::Right => self.move_cursor_right(),
|
||||
KeyCode::Esc => self.close_popup(),
|
||||
_ => {},
|
||||
};
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
|
||||
if self.is_active {
|
||||
let size = area.as_size();
|
||||
let area = Rect::new(size.width / 2, size.height / 2, 51, 3);
|
||||
let (border_style, border_type) = self.palette.create_popup_style();
|
||||
|
||||
let input = Paragraph::new(self.name.value())
|
||||
.block(Block::bordered()
|
||||
.border_style(border_style)
|
||||
.border_type(border_type)
|
||||
.title_style(self.palette.create_popup_title_style())
|
||||
.title_alignment(Alignment::Right)
|
||||
.title("Know validator name"));
|
||||
let v = Layout::vertical([Constraint::Max(3)]).flex(Flex::Center);
|
||||
let h = Layout::horizontal([Constraint::Max(50)]).flex(Flex::Center);
|
||||
let [area] = v.areas(area);
|
||||
let [area] = h.areas(area);
|
||||
|
||||
frame.render_widget(Clear, area);
|
||||
frame.render_widget(input, area);
|
||||
frame.set_cursor_position(Position::new(
|
||||
area.x + self.name.cursor() as u16 + 1,
|
||||
area.y + 1
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
178
src/components/wallet/staking_ledger.rs
Normal file
178
src/components/wallet/staking_ledger.rs
Normal file
@ -0,0 +1,178 @@
|
||||
use color_eyre::Result;
|
||||
use ratatui::{
|
||||
text::Text,
|
||||
layout::{Alignment, Constraint, Rect},
|
||||
widgets::{Block, Cell, Row, Table},
|
||||
Frame
|
||||
};
|
||||
use subxt::ext::sp_core::crypto::{Ss58Codec, Ss58AddressFormat, AccountId32};
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
use super::{Component, PartialComponent, CurrentTab};
|
||||
use crate::{
|
||||
action::Action, config::Config, palette::StylePalette,
|
||||
types::RewardDestination, widgets::DotSpinner,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct StakingLedger {
|
||||
is_active: bool,
|
||||
is_bonded: bool,
|
||||
account_id: [u8; 32],
|
||||
network_tx: Option<Sender<Action>>,
|
||||
total_staked: Option<u128>,
|
||||
active_staked: Option<u128>,
|
||||
reward_destination: RewardDestination,
|
||||
palette: StylePalette
|
||||
}
|
||||
|
||||
impl Default for StakingLedger {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl StakingLedger {
|
||||
const TICKER: &str = " CSPR";
|
||||
const DECIMALS: usize = 6;
|
||||
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
is_active: false,
|
||||
is_bonded: false,
|
||||
account_id: [0u8; 32],
|
||||
network_tx: None,
|
||||
total_staked: None,
|
||||
active_staked: None,
|
||||
reward_destination: Default::default(),
|
||||
palette: StylePalette::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_used_account_id(&mut self, account_id: [u8; 32]) {
|
||||
self.account_id = account_id;
|
||||
if let Some(network_tx) = &self.network_tx {
|
||||
let _ = network_tx.send(Action::GetValidatorLedger(account_id, false));
|
||||
let _ = network_tx.send(Action::GetStakingPayee(account_id, false));
|
||||
let _ = network_tx.send(Action::GetIsStashBonded(account_id, false));
|
||||
}
|
||||
}
|
||||
|
||||
fn prepare_u128(&self, maybe_value: Option<u128>) -> String {
|
||||
match maybe_value {
|
||||
Some(value) => {
|
||||
let value = value as f64 / 10f64.powi(18);
|
||||
let after = Self::DECIMALS;
|
||||
format!("{:.after$}{}", value, Self::TICKER)
|
||||
},
|
||||
None => format!("{}{}", DotSpinner::default().to_string(), Self::TICKER)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_reward_destination(&self) -> String {
|
||||
match self.reward_destination {
|
||||
RewardDestination::Staked => "re-stake".to_string(),
|
||||
RewardDestination::Stash => "stake".to_string(),
|
||||
RewardDestination::Controller => "controller".to_string(),
|
||||
RewardDestination::None => "none".to_string(),
|
||||
RewardDestination::Account(account_id) => {
|
||||
let address = AccountId32::from(account_id)
|
||||
.to_ss58check_with_version(Ss58AddressFormat::custom(1996));
|
||||
let tail = address.len().saturating_sub(5);
|
||||
format!("{}..{}", &address[..5], &address[tail..])
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn is_bonded_to_string(&self) -> String {
|
||||
if self.is_bonded {
|
||||
"bonded".to_string()
|
||||
} else {
|
||||
"no bond".to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialComponent for StakingLedger {
|
||||
fn set_active(&mut self, current_tab: CurrentTab) {
|
||||
match current_tab {
|
||||
CurrentTab::Accounts => self.is_active = true,
|
||||
_ => self.is_active = false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for StakingLedger {
|
||||
fn register_network_handler(&mut self, tx: Sender<Action>) -> Result<()> {
|
||||
self.network_tx = Some(tx);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn register_config_handler(&mut self, config: Config) -> Result<()> {
|
||||
if let Some(style) = config.styles.get(&crate::app::Mode::Wallet) {
|
||||
self.palette.with_normal_style(style.get("normal_style").copied());
|
||||
self.palette.with_hover_style(style.get("hover_style").copied());
|
||||
self.palette.with_normal_border_style(style.get("normal_border_style").copied());
|
||||
self.palette.with_hover_border_style(style.get("hover_border_style").copied());
|
||||
self.palette.with_normal_title_style(style.get("normal_title_style").copied());
|
||||
self.palette.with_hover_title_style(style.get("hover_title_style").copied());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update(&mut self, action: Action) -> Result<Option<Action>> {
|
||||
match action {
|
||||
Action::UsedAccount(account_id, _) => self.set_used_account_id(account_id),
|
||||
Action::SetIsBonded(is_bonded, account_id) if self.account_id == account_id =>
|
||||
self.is_bonded = is_bonded,
|
||||
Action::SetStakingPayee(reward_destination, account_id) if self.account_id == account_id =>
|
||||
self.reward_destination = reward_destination,
|
||||
Action::SetStakedAmountRatio(total, active, account_id) if self.account_id == account_id => {
|
||||
self.total_staked = total;
|
||||
self.active_staked = active;
|
||||
},
|
||||
_ => {}
|
||||
};
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
|
||||
let [_, _, _, place] = super::account_layout(area);
|
||||
let (border_style, border_type) = self.palette
|
||||
.create_border_style(self.is_active);
|
||||
|
||||
let table = Table::new(
|
||||
[
|
||||
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("destination:".to_string()).alignment(Alignment::Left)),
|
||||
Cell::from(Text::from(self.get_reward_destination()).alignment(Alignment::Right)),
|
||||
]),
|
||||
Row::new(vec![
|
||||
Cell::from(Text::from("total:".to_string()).alignment(Alignment::Left)),
|
||||
Cell::from(Text::from(self.prepare_u128(self.total_staked)).alignment(Alignment::Right))
|
||||
]),
|
||||
Row::new(vec![
|
||||
Cell::from(Text::from("active:".to_string()).alignment(Alignment::Left)),
|
||||
Cell::from(Text::from(self.prepare_u128(self.active_staked)).alignment(Alignment::Right)),
|
||||
]),
|
||||
],
|
||||
[
|
||||
Constraint::Max(12),
|
||||
Constraint::Min(14),
|
||||
]
|
||||
)
|
||||
.block(Block::bordered()
|
||||
.border_style(border_style)
|
||||
.border_type(border_type)
|
||||
.title_alignment(Alignment::Right)
|
||||
.title_style(self.palette.create_title_style(false))
|
||||
.title("Nomination stake"));
|
||||
|
||||
frame.render_widget(table, place);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@ -17,7 +17,7 @@ use crate::{
|
||||
action::Action,
|
||||
config::Config,
|
||||
palette::StylePalette,
|
||||
types::ActionLevel,
|
||||
types::{ActionLevel, ActionTarget},
|
||||
widgets::{Input, InputRequest},
|
||||
};
|
||||
|
||||
@ -59,9 +59,17 @@ impl Transfer {
|
||||
}
|
||||
}
|
||||
|
||||
fn close_popup(&mut self) {
|
||||
self.is_active = false;
|
||||
if let Some(action_tx) = &self.action_tx {
|
||||
let _ = action_tx.send(Action::ClosePopup);
|
||||
}
|
||||
}
|
||||
|
||||
fn log_event(&mut self, message: String, level: ActionLevel) {
|
||||
if let Some(action_tx) = &self.action_tx {
|
||||
let _ = action_tx.send(Action::WalletLog(message, level));
|
||||
let _ = action_tx.send(
|
||||
Action::EventLog(message, level, ActionTarget::WalletLog));
|
||||
}
|
||||
}
|
||||
|
||||
@ -81,7 +89,13 @@ impl Transfer {
|
||||
let mut account_id = [0u8; 32];
|
||||
account_id.copy_from_slice(&seed_vec);
|
||||
|
||||
match self.amount.value().parse::<f64>() {
|
||||
let str_amount = self.amount.value();
|
||||
let str_amount = if str_amount.starts_with('.') {
|
||||
&format!("0{}", str_amount)[..]
|
||||
} else {
|
||||
str_amount
|
||||
};
|
||||
match str_amount.parse::<f64>() {
|
||||
Ok(value) => {
|
||||
let amount = (value * 1_000_000_000_000_000_000.0) as u128;
|
||||
let _ = network_tx.send(Action::TransferBalance(
|
||||
@ -199,7 +213,7 @@ impl Component for Transfer {
|
||||
KeyCode::Backspace => self.delete_char(),
|
||||
KeyCode::Left => self.move_cursor_left(),
|
||||
KeyCode::Right => self.move_cursor_right(),
|
||||
KeyCode::Esc => self.is_active = false,
|
||||
KeyCode::Esc => self.close_popup(),
|
||||
_ => {},
|
||||
};
|
||||
}
|
||||
@ -208,7 +222,7 @@ impl Component for Transfer {
|
||||
|
||||
fn update(&mut self, action: Action) -> Result<Option<Action>> {
|
||||
match action {
|
||||
Action::UsedAccount(seed) => self.sender = seed,
|
||||
Action::UsedAccount(_, seed) => self.sender = seed,
|
||||
Action::TransferTo(who) => {
|
||||
self.receiver = Input::new(who);
|
||||
self.receiver_or_amount = ReceiverOrAmount::Amount;
|
||||
@ -221,8 +235,8 @@ impl Component for Transfer {
|
||||
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
|
||||
if self.is_active {
|
||||
let size = area.as_size();
|
||||
let receiver_area = Rect::new(size.width / 2, size.height / 2, 50, 3);
|
||||
let amount_area = Rect::new(size.width / 2, size.height / 2 + 3, 50, 3);
|
||||
let receiver_area = Rect::new(size.width / 2, size.height / 2, 51, 3);
|
||||
let amount_area = Rect::new(size.width / 2, size.height / 2 + 3, 51, 3);
|
||||
let (border_style, border_type) = self.palette.create_popup_style();
|
||||
|
||||
let input_receiver = Paragraph::new(self.receiver.value())
|
||||
@ -241,8 +255,8 @@ impl Component for Transfer {
|
||||
.title_alignment(Alignment::Right)
|
||||
.title("Amount to send"));
|
||||
|
||||
let v = Layout::vertical([Constraint::Max(3)]).flex(Flex::Center);
|
||||
let h = Layout::horizontal([Constraint::Max(50)]).flex(Flex::Center);
|
||||
let v = Layout::vertical([Constraint::Length(3)]).flex(Flex::Center);
|
||||
let h = Layout::horizontal([Constraint::Length(51)]).flex(Flex::Center);
|
||||
|
||||
let [receiver_area] = v.areas(receiver_area);
|
||||
let [receiver_area] = h.areas(receiver_area);
|
||||
|
||||
@ -2,9 +2,10 @@ use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use subxt::{
|
||||
OnlineClient,
|
||||
backend::{legacy::LegacyRpcMethods, rpc::RpcClient},
|
||||
backend::rpc::RpcClient,
|
||||
};
|
||||
|
||||
mod modes;
|
||||
mod action;
|
||||
mod app;
|
||||
mod cli;
|
||||
@ -54,9 +55,7 @@ async fn main() -> Result<()> {
|
||||
let (action_tx, action_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
|
||||
let rpc_client = RpcClient::from_url(args.rpc_endpoint).await?;
|
||||
let legacy_client_api = LegacyRpcMethods::<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 best_blocks_sub = online_client.blocks().subscribe_best().await?;
|
||||
@ -66,7 +65,6 @@ async fn main() -> Result<()> {
|
||||
let mut network = network::Network::new(
|
||||
cloned_action_tx,
|
||||
online_client,
|
||||
legacy_client_api,
|
||||
rpc_client,
|
||||
);
|
||||
start_tokio_action_loop(sync_io_rx, &mut network);
|
||||
|
||||
69
src/modes.rs
Normal file
69
src/modes.rs
Normal file
@ -0,0 +1,69 @@
|
||||
use ratatui::layout::Constraint;
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum Mode {
|
||||
Menu,
|
||||
Help,
|
||||
Explorer,
|
||||
Wallet,
|
||||
Validator,
|
||||
Empty,
|
||||
}
|
||||
|
||||
impl Default for Mode {
|
||||
fn default() -> Self {
|
||||
Self::Explorer
|
||||
}
|
||||
}
|
||||
|
||||
impl Mode {
|
||||
const MENU_DATA_TITLE: &str = "Help for navigation menu ";
|
||||
const MENU_DATA_HEADERS: &[&str] = &["Hot Key", "Target", "Description"];
|
||||
const MENU_DATA_CONSTRAINTS: &[Constraint] = &[Constraint::Length(10), Constraint::Min(0)];
|
||||
const MENU_DATA_ROWS: &[&[&str]] = &[
|
||||
&["(↑) | k", "Select side menu item above"],
|
||||
&["(↓) | j", "Select side menu item below"],
|
||||
&["(→) | l", "Select next tab in the screen"],
|
||||
&["(←) | h", "Select previous tab in the screen"],
|
||||
&["Enter", "Enter the selected screen"],
|
||||
&["Esc", "Navigate back to the side menu"],
|
||||
];
|
||||
|
||||
const NON_EXISTENT_TITLE: &str = "Non existent";
|
||||
const NON_EXISTENT_DATA_HEADERS: &[&str] = &["Empty header"];
|
||||
const NON_EXISTENT_DATA_CONSTRAINTS: &[Constraint] = &[Constraint::Percentage(100)];
|
||||
const NON_EXISTENT_DATA_ROWS: &[&[&str]] = &[&["Not implemented yet"]];
|
||||
|
||||
pub fn get_help_title(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Menu => &Self::MENU_DATA_TITLE,
|
||||
_ => &Self::NON_EXISTENT_TITLE,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_help_headers(&self) -> &'static [&'static str] {
|
||||
match self {
|
||||
Self::Menu => &Self::MENU_DATA_HEADERS,
|
||||
_ => &Self::NON_EXISTENT_DATA_HEADERS,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_help_data(&self) -> &'static [&'static [&'static str]] {
|
||||
match self {
|
||||
Self::Menu => &Self::MENU_DATA_ROWS,
|
||||
_ => &Self::NON_EXISTENT_DATA_ROWS,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_help_constraints(&self) -> &'static [Constraint] {
|
||||
match self {
|
||||
Self::Menu => &Self::MENU_DATA_CONSTRAINTS,
|
||||
_ => &Self::NON_EXISTENT_DATA_CONSTRAINTS,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_help_text(&self) -> &'static str {
|
||||
"(Esc) close | (↑) move up | (↓) move down"
|
||||
}
|
||||
}
|
||||
@ -1,27 +1,31 @@
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
use color_eyre::Result;
|
||||
use subxt::backend::legacy::rpc_methods::LegacyRpcMethods;
|
||||
use subxt::{backend::{legacy::rpc_methods::SystemHealth, rpc::RpcClient}, rpc_params};
|
||||
use codec::{Encode, Decode};
|
||||
|
||||
use crate::{action::Action, casper::CasperConfig};
|
||||
use crate::{action::Action, network::miscellaneous::get_slow_clap_storage_key, types::{BlockRange, PeerInformation}};
|
||||
|
||||
pub async fn get_node_name(
|
||||
action_tx: &UnboundedSender<Action>,
|
||||
api: &LegacyRpcMethods<CasperConfig>,
|
||||
rpc_client: &RpcClient,
|
||||
) -> Result<()> {
|
||||
let maybe_node_name = api.system_name().await.ok();
|
||||
let maybe_node_name = rpc_client
|
||||
.request("system_name", rpc_params![])
|
||||
.await
|
||||
.ok();
|
||||
action_tx.send(Action::SetNodeName(maybe_node_name))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_system_health(
|
||||
action_tx: &UnboundedSender<Action>,
|
||||
api: &LegacyRpcMethods<CasperConfig>,
|
||||
rpc_client: &RpcClient,
|
||||
) -> Result<()> {
|
||||
let (maybe_peers, is_syncing, should_have_peers) = api
|
||||
.system_health()
|
||||
let (maybe_peers, is_syncing, should_have_peers) = rpc_client
|
||||
.request("system_health", rpc_params![])
|
||||
.await
|
||||
.ok()
|
||||
.map_or((None, false, false), |health| (
|
||||
.map_or((None, false, false), |health: SystemHealth| (
|
||||
Some(health.peers),
|
||||
health.is_syncing,
|
||||
health.should_have_peers,
|
||||
@ -35,10 +39,11 @@ pub async fn get_system_health(
|
||||
|
||||
pub async fn get_genesis_hash(
|
||||
action_tx: &UnboundedSender<Action>,
|
||||
api: &LegacyRpcMethods<CasperConfig>,
|
||||
rpc_client: &RpcClient,
|
||||
) -> Result<()> {
|
||||
let maybe_genesis_hash = api
|
||||
.genesis_hash()
|
||||
let params = rpc_params![0u32];
|
||||
let maybe_genesis_hash = rpc_client
|
||||
.request("chain_getBlockHash", params)
|
||||
.await
|
||||
.ok();
|
||||
action_tx.send(Action::SetGenesisHash(maybe_genesis_hash))?;
|
||||
@ -47,10 +52,10 @@ pub async fn get_genesis_hash(
|
||||
|
||||
pub async fn get_chain_name(
|
||||
action_tx: &UnboundedSender<Action>,
|
||||
api: &LegacyRpcMethods<CasperConfig>,
|
||||
rpc_client: &RpcClient,
|
||||
) -> Result<()> {
|
||||
let maybe_chain_name = api
|
||||
.system_chain()
|
||||
let maybe_chain_name = rpc_client
|
||||
.request("system_chain", rpc_params![])
|
||||
.await
|
||||
.ok();
|
||||
action_tx.send(Action::SetChainName(maybe_chain_name))?;
|
||||
@ -59,12 +64,100 @@ pub async fn get_chain_name(
|
||||
|
||||
pub async fn get_system_version(
|
||||
action_tx: &UnboundedSender<Action>,
|
||||
api: &LegacyRpcMethods<CasperConfig>,
|
||||
rpc_client: &RpcClient,
|
||||
) -> Result<()> {
|
||||
let maybe_system_version = api
|
||||
.system_version()
|
||||
let maybe_system_version = rpc_client
|
||||
.request("system_version", rpc_params![])
|
||||
.await
|
||||
.ok();
|
||||
action_tx.send(Action::SetChainVersion(maybe_system_version))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_pending_extrinsics(
|
||||
action_tx: &UnboundedSender<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(())
|
||||
}
|
||||
|
||||
pub async fn rotate_keys(
|
||||
action_tx: &UnboundedSender<Action>,
|
||||
rpc_client: &RpcClient,
|
||||
) -> Result<()> {
|
||||
let rotated_keys: String = rpc_client
|
||||
.request("author_rotateKeys", rpc_params![])
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
action_tx.send(Action::StoreRotatedKeys(rotated_keys))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_block_range(
|
||||
action_tx: &UnboundedSender<Action>,
|
||||
rpc_client: &RpcClient,
|
||||
chain_id: u64
|
||||
) -> Result<()> {
|
||||
let chain_id_encoded = chain_id.encode();
|
||||
let block_range_key_raw = get_slow_clap_storage_key(b"block-", &chain_id_encoded);
|
||||
let mut block_range_key = String::from("0x");
|
||||
for byte in block_range_key_raw {
|
||||
block_range_key.push_str(&format!("{:02x}", byte));
|
||||
}
|
||||
let block_range: BlockRange = rpc_client
|
||||
.request("offchain_localStorageGet", rpc_params!["PERSISTENT", block_range_key])
|
||||
.await
|
||||
.ok()
|
||||
.map(|hex_string: String| {
|
||||
let bytes = hex::decode(&hex_string[2..]).expect("Invalid hex string");
|
||||
let mut cursor = &bytes[..];
|
||||
let from_block: u64 = u64::decode(&mut cursor).expect("first valid");
|
||||
let to_block: u64 = u64::decode(&mut cursor).expect("second valid");
|
||||
BlockRange { from_block, to_block }
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
action_tx.send(Action::SetBlockRange(chain_id, block_range))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
109
src/network/miscellaneous.rs
Normal file
109
src/network/miscellaneous.rs
Normal file
@ -0,0 +1,109 @@
|
||||
use subxt::ext::sp_runtime::Perbill;
|
||||
|
||||
const SLOW_CLAP_DB_PREFIX: &[u8] = b"slow_clap::";
|
||||
|
||||
// generated outside, based on params
|
||||
// MIN_INFLATION: u32 = 0_006_900;
|
||||
// MAX_INFLATION: u32 = 0_690_000;
|
||||
// IDEAL_STAKE: u32 = 0_690_000;
|
||||
// FALLOFF: u32 = 0_050_000;
|
||||
// MAX_PIECE_COUNT: u32 = 40;
|
||||
const PIECEWISE_LINEAR_POUNTS: [(Perbill, Perbill); 32] = [
|
||||
(Perbill::from_parts(0), Perbill::from_parts(6900000)),
|
||||
(Perbill::from_parts(690000000), Perbill::from_parts(690000000)),
|
||||
(Perbill::from_parts(692740000), Perbill::from_parts(664536000)),
|
||||
(Perbill::from_parts(695588000), Perbill::from_parts(639072000)),
|
||||
(Perbill::from_parts(698554000), Perbill::from_parts(613608000)),
|
||||
(Perbill::from_parts(701647000), Perbill::from_parts(588144000)),
|
||||
(Perbill::from_parts(704879000), Perbill::from_parts(562680000)),
|
||||
(Perbill::from_parts(708262000), Perbill::from_parts(537216000)),
|
||||
(Perbill::from_parts(711811000), Perbill::from_parts(511752000)),
|
||||
(Perbill::from_parts(715545000), Perbill::from_parts(486288000)),
|
||||
(Perbill::from_parts(719482000), Perbill::from_parts(460824000)),
|
||||
(Perbill::from_parts(723646000), Perbill::from_parts(435360000)),
|
||||
(Perbill::from_parts(728066000), Perbill::from_parts(409896000)),
|
||||
(Perbill::from_parts(732774000), Perbill::from_parts(384432000)),
|
||||
(Perbill::from_parts(737811000), Perbill::from_parts(358968000)),
|
||||
(Perbill::from_parts(743227000), Perbill::from_parts(333504000)),
|
||||
(Perbill::from_parts(749083000), Perbill::from_parts(308040000)),
|
||||
(Perbill::from_parts(755456000), Perbill::from_parts(282576000)),
|
||||
(Perbill::from_parts(762447000), Perbill::from_parts(257112000)),
|
||||
(Perbill::from_parts(770189000), Perbill::from_parts(231648000)),
|
||||
(Perbill::from_parts(778863000), Perbill::from_parts(206184000)),
|
||||
(Perbill::from_parts(788725000), Perbill::from_parts(180720000)),
|
||||
(Perbill::from_parts(800151000), Perbill::from_parts(155256000)),
|
||||
(Perbill::from_parts(813735000), Perbill::from_parts(129792000)),
|
||||
(Perbill::from_parts(830484000), Perbill::from_parts(104328000)),
|
||||
(Perbill::from_parts(852337000), Perbill::from_parts(78864000)),
|
||||
(Perbill::from_parts(877801000), Perbill::from_parts(57460000)),
|
||||
(Perbill::from_parts(903265000), Perbill::from_parts(42422000)),
|
||||
(Perbill::from_parts(928728000), Perbill::from_parts(31857000)),
|
||||
(Perbill::from_parts(954189000), Perbill::from_parts(24435000)),
|
||||
(Perbill::from_parts(979651000), Perbill::from_parts(19220000)),
|
||||
(Perbill::from_parts(1000000000), Perbill::from_parts(16291000)),
|
||||
];
|
||||
const MAXIMUM_INFLATION: Perbill = Perbill::from_parts(690000000);
|
||||
|
||||
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 get_slow_clap_storage_key(first: &[u8], second: &[u8]) -> Vec<u8> {
|
||||
let mut key = SLOW_CLAP_DB_PREFIX.to_vec();
|
||||
key.extend(first);
|
||||
key.extend(second);
|
||||
key
|
||||
}
|
||||
@ -1,142 +1,509 @@
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
use color_eyre::Result;
|
||||
use subxt::{
|
||||
backend::{
|
||||
legacy::LegacyRpcMethods,
|
||||
rpc::RpcClient,
|
||||
},
|
||||
tx::{TxProgress, TxStatus},
|
||||
utils::H256,
|
||||
OnlineClient,
|
||||
backend::rpc::RpcClient, tx::{TxProgress, TxStatus}, utils::H256, OnlineClient
|
||||
};
|
||||
|
||||
mod legacy_rpc_calls;
|
||||
mod predefinded_calls;
|
||||
mod predefined_calls;
|
||||
mod predefined_txs;
|
||||
mod subscriptions;
|
||||
mod miscellaneous;
|
||||
mod raw_calls;
|
||||
|
||||
pub use miscellaneous::calculate_for_fraction;
|
||||
|
||||
use crate::{
|
||||
types::ActionLevel,
|
||||
types::{ActionLevel, ActionTarget},
|
||||
action::Action,
|
||||
casper::CasperConfig,
|
||||
};
|
||||
|
||||
pub use subscriptions::{FinalizedSubscription, BestSubscription};
|
||||
|
||||
const GATEKEEPED_CHAIN_IDS: [u64; 1] = [
|
||||
11155111, //Sepolia
|
||||
];
|
||||
|
||||
struct TxToWatch {
|
||||
tx_progress: TxProgress<CasperConfig, OnlineClient<CasperConfig>>,
|
||||
sender: String,
|
||||
target: ActionTarget,
|
||||
}
|
||||
|
||||
pub struct Network {
|
||||
action_tx: UnboundedSender<Action>,
|
||||
online_client_api: OnlineClient<CasperConfig>,
|
||||
legacy_client_api: LegacyRpcMethods<CasperConfig>,
|
||||
rpc_client: RpcClient,
|
||||
best_hash: Option<H256>,
|
||||
finalized_hash: Option<H256>,
|
||||
stash_to_watch: Option<[u8; 32]>,
|
||||
validator_details_to_watch: Option<[u8; 32]>,
|
||||
accounts_to_watch: std::collections::HashSet<[u8; 32]>,
|
||||
transactions_to_watch: Vec<TxProgress<CasperConfig, OnlineClient<CasperConfig>>>,
|
||||
transactions_to_watch: Vec<TxToWatch>,
|
||||
eras_to_watch: std::collections::HashSet<u32>,
|
||||
senders: std::collections::HashMap<String, u32>,
|
||||
}
|
||||
|
||||
impl Network {
|
||||
pub fn new(
|
||||
action_tx: UnboundedSender<Action>,
|
||||
online_client_api: OnlineClient<CasperConfig>,
|
||||
legacy_client_api: LegacyRpcMethods<CasperConfig>,
|
||||
rpc_client: RpcClient,
|
||||
) -> Self {
|
||||
Self {
|
||||
action_tx,
|
||||
online_client_api,
|
||||
legacy_client_api,
|
||||
rpc_client,
|
||||
best_hash: None,
|
||||
finalized_hash: None,
|
||||
stash_to_watch: None,
|
||||
validator_details_to_watch: None,
|
||||
accounts_to_watch: Default::default(),
|
||||
transactions_to_watch: Default::default(),
|
||||
eras_to_watch: Default::default(),
|
||||
senders: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn store_stash_or_validator_if_possible(&mut self, account_id: [u8; 32], is_stash: bool) {
|
||||
if is_stash {
|
||||
match self.stash_to_watch {
|
||||
Some(stash) if stash == account_id => {},
|
||||
_ => self.stash_to_watch = Some(account_id),
|
||||
}
|
||||
} else {
|
||||
match self.validator_details_to_watch {
|
||||
Some(stash) if stash == account_id => {},
|
||||
_ => self.validator_details_to_watch = Some(account_id),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn store_sender_nonce(&mut self, seed: &str, maybe_nonce: Option<u32>) {
|
||||
if let Some(current_nonce) = maybe_nonce {
|
||||
self.senders
|
||||
.entry(seed.to_string())
|
||||
.and_modify(|stored_nonce| {
|
||||
if *stored_nonce < current_nonce {
|
||||
*stored_nonce = current_nonce;
|
||||
}
|
||||
})
|
||||
.or_insert(current_nonce);
|
||||
} else {
|
||||
let _ = self.senders.remove(seed);
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_transaction_and_decrement_nonce(&mut self, index: usize) {
|
||||
let removed = self.transactions_to_watch.remove(index);
|
||||
self.senders
|
||||
.get_mut(&removed.sender)
|
||||
.map(|nonce| *nonce = nonce.saturating_sub(1));
|
||||
}
|
||||
|
||||
pub async fn handle_network_event(&mut self, io_event: Action) -> Result<()> {
|
||||
match io_event {
|
||||
Action::NewBestHash(hash) => {
|
||||
self.best_hash = Some(hash);
|
||||
for account_id in self.accounts_to_watch.iter() {
|
||||
predefinded_calls::get_balance(&self.action_tx, &self.online_client_api, &account_id).await?;
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
Action::NewFinalizedHash(hash) => {
|
||||
self.finalized_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?;
|
||||
predefined_calls::get_account_payee(&self.action_tx, &self.online_client_api, &stash_to_watch).await?;
|
||||
predefined_calls::get_slashing_spans(&self.action_tx, &self.online_client_api, &stash_to_watch).await?;
|
||||
|
||||
for era_index in self.eras_to_watch.iter() {
|
||||
predefined_calls::get_validator_staking_result(&self.action_tx, &self.online_client_api, &stash_to_watch, *era_index).await?;
|
||||
}
|
||||
}
|
||||
if let Some(validator_details_to_watch) = self.validator_details_to_watch {
|
||||
predefined_calls::get_nominators_by_validator(&self.action_tx, &self.online_client_api, &validator_details_to_watch).await?;
|
||||
predefined_calls::get_validator_prefs(&self.action_tx, &self.online_client_api, &validator_details_to_watch).await?;
|
||||
predefined_calls::get_staking_value_ratio(&self.action_tx, &self.online_client_api, &validator_details_to_watch).await?;
|
||||
predefined_calls::get_validator_latest_claim(&self.action_tx, &self.online_client_api, &validator_details_to_watch).await?;
|
||||
predefined_calls::get_account_payee(&self.action_tx, &self.online_client_api, &validator_details_to_watch).await?;
|
||||
predefined_calls::get_validators_ledger(&self.action_tx, &self.online_client_api, &validator_details_to_watch).await?;
|
||||
predefined_calls::get_is_stash_bonded(&self.action_tx, &self.online_client_api, &validator_details_to_watch).await?;
|
||||
predefined_calls::get_nominators_by_account(&self.action_tx, &self.online_client_api, &validator_details_to_watch).await?;
|
||||
}
|
||||
|
||||
for account_id in self.accounts_to_watch.iter() {
|
||||
predefined_calls::get_balance(&self.action_tx, &self.online_client_api, &account_id).await?;
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
Action::CheckPendingTransactions => {
|
||||
let length = self.transactions_to_watch.len();
|
||||
for i in (0..length).rev() {
|
||||
let pending_tx = &mut self.transactions_to_watch[i];
|
||||
let ext_hash = pending_tx.extrinsic_hash();
|
||||
match (*pending_tx).next().await {
|
||||
let ext_hash = pending_tx.tx_progress.extrinsic_hash();
|
||||
let log_target = pending_tx.target.clone();
|
||||
match (*pending_tx).tx_progress.next().await {
|
||||
Some(Ok(status)) => {
|
||||
match status {
|
||||
TxStatus::Validated => self.action_tx.send(Action::WalletLog(format!("transaction {} is part of future queue", ext_hash), ActionLevel::Info))?,
|
||||
TxStatus::Broadcasted { num_peers } => self.action_tx.send(Action::WalletLog(format!("transaction {} has been broardcasted to {} nodes", ext_hash, num_peers), ActionLevel::Info))?,
|
||||
TxStatus::NoLongerInBestBlock => self.action_tx.send(Action::WalletLog(format!("transaction {} is no longer in a best block", ext_hash), ActionLevel::Warn))?,
|
||||
TxStatus::InBestBlock(b) => self.action_tx.send(Action::WalletLog(format!("transaction {} included in the block header {}", b.extrinsic_hash(), b.block_hash()), ActionLevel::Info))?,
|
||||
TxStatus::Validated => self.action_tx.send(Action::EventLog(format!("transaction {} is part of future queue", ext_hash), ActionLevel::Info, log_target))?,
|
||||
TxStatus::Broadcasted { num_peers } => self.action_tx.send(Action::EventLog(format!("transaction {} has been broardcasted to {} nodes", ext_hash, num_peers), ActionLevel::Info, log_target))?,
|
||||
TxStatus::NoLongerInBestBlock => self.action_tx.send(Action::EventLog(format!("transaction {} is no longer in a best block", ext_hash), ActionLevel::Warn, log_target))?,
|
||||
TxStatus::InBestBlock(b) => self.action_tx.send(Action::EventLog(format!("transaction {} included in the block header {}", b.extrinsic_hash(), b.block_hash()), ActionLevel::Info, log_target))?,
|
||||
TxStatus::InFinalizedBlock(b) => {
|
||||
self.action_tx.send(Action::WalletLog(format!("transaction {} has been finalized in block header {}", b.extrinsic_hash(), b.block_hash()), ActionLevel::Info))?;
|
||||
self.action_tx.send(Action::EventLog(format!("transaction {} has been finalized in block header {}", b.extrinsic_hash(), b.block_hash()), ActionLevel::Info, log_target))?;
|
||||
self.transactions_to_watch.remove(i);
|
||||
}
|
||||
TxStatus::Error { message } => {
|
||||
self.action_tx.send(Action::WalletLog(format!("transaction {} error, something get wrong: {message}", ext_hash), ActionLevel::Error))?;
|
||||
self.transactions_to_watch.remove(i);
|
||||
self.action_tx.send(Action::EventLog(format!("transaction {} error, something get wrong: {message}", ext_hash), ActionLevel::Error, log_target))?;
|
||||
self.remove_transaction_and_decrement_nonce(i);
|
||||
}
|
||||
TxStatus::Invalid { message } => {
|
||||
self.action_tx.send(Action::WalletLog(format!("transaction {} invalid: {message}", ext_hash), ActionLevel::Error))?;
|
||||
self.transactions_to_watch.remove(i);
|
||||
self.action_tx.send(Action::EventLog(format!("transaction {} invalid: {message}", ext_hash), ActionLevel::Error, log_target))?;
|
||||
self.remove_transaction_and_decrement_nonce(i);
|
||||
}
|
||||
TxStatus::Dropped { message } => {
|
||||
self.action_tx.send(Action::WalletLog(format!("transaction {} was dropped: {message}", ext_hash), ActionLevel::Error))?;
|
||||
self.transactions_to_watch.remove(i);
|
||||
self.action_tx.send(Action::EventLog(format!("transaction {} was dropped: {message}", ext_hash), ActionLevel::Error, log_target))?;
|
||||
self.remove_transaction_and_decrement_nonce(i);
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
self.action_tx.send(Action::WalletLog(format!("transaction {} was dropped", ext_hash), ActionLevel::Error))?;
|
||||
self.transactions_to_watch.remove(i);
|
||||
self.action_tx.send(Action::EventLog(format!("transaction {} was dropped", ext_hash), ActionLevel::Error, log_target))?;
|
||||
self.remove_transaction_and_decrement_nonce(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
Action::GetSystemHealth => legacy_rpc_calls::get_system_health(&self.action_tx, &self.legacy_client_api).await,
|
||||
Action::GetNodeName => legacy_rpc_calls::get_node_name(&self.action_tx, &self.legacy_client_api).await,
|
||||
Action::GetGenesisHash => legacy_rpc_calls::get_genesis_hash(&self.action_tx, &self.legacy_client_api).await,
|
||||
Action::GetChainName => legacy_rpc_calls::get_chain_name(&self.action_tx, &self.legacy_client_api).await,
|
||||
Action::GetChainVersion => legacy_rpc_calls::get_system_version(&self.action_tx, &self.legacy_client_api).await,
|
||||
Action::GetBlockAuthor(hash, logs) => predefinded_calls::get_block_author(&self.action_tx, &self.online_client_api, &logs, &hash).await,
|
||||
Action::GetActiveEra => predefinded_calls::get_active_era(&self.action_tx, &self.online_client_api).await,
|
||||
Action::GetEpochProgress => predefinded_calls::get_epoch_progress(&self.action_tx, &self.online_client_api).await,
|
||||
Action::GetPendingExtrinsics => predefinded_calls::get_pending_extrinsics(&self.action_tx, &self.rpc_client).await,
|
||||
Action::GetSystemHealth => legacy_rpc_calls::get_system_health(&self.action_tx, &self.rpc_client).await,
|
||||
Action::GetNodeName => legacy_rpc_calls::get_node_name(&self.action_tx, &self.rpc_client).await,
|
||||
Action::GetGenesisHash => legacy_rpc_calls::get_genesis_hash(&self.action_tx, &self.rpc_client).await,
|
||||
Action::GetChainName => legacy_rpc_calls::get_chain_name(&self.action_tx, &self.rpc_client).await,
|
||||
Action::GetChainVersion => legacy_rpc_calls::get_system_version(&self.action_tx, &self.rpc_client).await,
|
||||
Action::GetPendingExtrinsics => legacy_rpc_calls::get_pending_extrinsics(&self.action_tx, &self.rpc_client).await,
|
||||
Action::GetConnectedPeers => legacy_rpc_calls::get_connected_peers(&self.action_tx, &self.rpc_client).await,
|
||||
Action::GetListenAddresses => legacy_rpc_calls::get_listen_addresses(&self.action_tx, &self.rpc_client).await,
|
||||
Action::GetLocalIdentity => legacy_rpc_calls::get_local_identity(&self.action_tx, &self.rpc_client).await,
|
||||
Action::RotateSessionKeys => legacy_rpc_calls::rotate_keys(&self.action_tx, &self.rpc_client).await,
|
||||
Action::GetBlockRange => {
|
||||
for chain_id in GATEKEEPED_CHAIN_IDS {
|
||||
legacy_rpc_calls::get_block_range(&self.action_tx, &self.rpc_client, chain_id).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Action::GetExistentialDeposit => predefinded_calls::get_existential_deposit(&self.action_tx, &self.online_client_api).await,
|
||||
Action::GetTotalIssuance => predefinded_calls::get_total_issuance(&self.action_tx, &self.online_client_api).await,
|
||||
Action::GetBlockAuthor(hash, logs) => predefined_calls::get_block_author(&self.action_tx, &self.online_client_api, &logs, &hash).await,
|
||||
Action::GetActiveEra => predefined_calls::get_active_era(&self.action_tx, &self.online_client_api).await,
|
||||
Action::GetCurrentEra => predefined_calls::get_current_era(&self.action_tx, &self.online_client_api).await,
|
||||
Action::GetEpochProgress => predefined_calls::get_epoch_progress(&self.action_tx, &self.online_client_api).await,
|
||||
Action::GetMinValidatorBond => predefined_calls::get_minimal_validator_bond(&self.action_tx, &self.online_client_api).await,
|
||||
Action::GetGatekeepedNetwork(chain_id) => predefined_calls::get_gatekeeped_network(&self.action_tx, &self.online_client_api, chain_id).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::GetCurrentValidatorEraRewards => predefined_calls::get_current_validator_reward_in_era(&self.action_tx, &self.online_client_api).await,
|
||||
|
||||
Action::SetSender(seed, maybe_nonce) => {
|
||||
self.store_sender_nonce(&seed, maybe_nonce);
|
||||
Ok(())
|
||||
}
|
||||
Action::RemoveEraToWatch(era_index) => {
|
||||
self.eras_to_watch.remove(&era_index);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Action::GetStakingPayee(account_id, is_stash) => {
|
||||
self.store_stash_or_validator_if_possible(account_id, is_stash);
|
||||
predefined_calls::get_account_payee(&self.action_tx, &self.online_client_api, &account_id).await
|
||||
}
|
||||
Action::GetValidatorLatestClaim(account_id, is_stash) => {
|
||||
self.store_stash_or_validator_if_possible(account_id, is_stash);
|
||||
predefined_calls::get_validator_latest_claim(&self.action_tx, &self.online_client_api, &account_id).await
|
||||
}
|
||||
Action::GetSlashingSpans(account_id, is_stash) => {
|
||||
self.store_stash_or_validator_if_possible(account_id, is_stash);
|
||||
predefined_calls::get_slashing_spans(&self.action_tx, &self.online_client_api, &account_id).await
|
||||
}
|
||||
Action::GetValidatorLedger(account_id, is_stash) => {
|
||||
self.store_stash_or_validator_if_possible(account_id, is_stash);
|
||||
predefined_calls::get_validators_ledger(&self.action_tx, &self.online_client_api, &account_id).await
|
||||
}
|
||||
Action::GetIsStashBonded(account_id, is_stash) => {
|
||||
self.store_stash_or_validator_if_possible(account_id, is_stash);
|
||||
predefined_calls::get_is_stash_bonded(&self.action_tx, &self.online_client_api, &account_id).await
|
||||
},
|
||||
Action::GetErasStakersOverview(account_id, is_stash) => {
|
||||
self.store_stash_or_validator_if_possible(account_id, is_stash);
|
||||
predefined_calls::get_staking_value_ratio(&self.action_tx, &self.online_client_api, &account_id).await
|
||||
},
|
||||
Action::GetValidatorPrefs(account_id, is_stash) => {
|
||||
self.store_stash_or_validator_if_possible(account_id, is_stash);
|
||||
predefined_calls::get_validator_prefs(&self.action_tx, &self.online_client_api, &account_id).await
|
||||
},
|
||||
Action::GetNominatorsByValidator(account_id, is_stash) => {
|
||||
self.store_stash_or_validator_if_possible(account_id, is_stash);
|
||||
predefined_calls::get_nominators_by_validator(&self.action_tx, &self.online_client_api, &account_id).await
|
||||
},
|
||||
Action::GetNominatorsByAccount(account_id, is_stash) => {
|
||||
self.store_stash_or_validator_if_possible(account_id, is_stash);
|
||||
predefined_calls::get_nominators_by_account(&self.action_tx, &self.online_client_api, &account_id).await
|
||||
},
|
||||
Action::GetValidatorAllRewards(account_id, is_stash) => {
|
||||
self.store_stash_or_validator_if_possible(account_id, is_stash);
|
||||
predefined_calls::get_validator_staking_results(&self.action_tx, &self.online_client_api, &account_id).await
|
||||
},
|
||||
Action::GetQueuedSessionKeys(account_id, is_stash) => {
|
||||
self.store_stash_or_validator_if_possible(account_id, is_stash);
|
||||
predefined_calls::get_queued_session_keys(&self.action_tx, &self.online_client_api, &self.rpc_client, &account_id).await
|
||||
},
|
||||
Action::GetSessionKeys(account_id, is_stash) => {
|
||||
self.store_stash_or_validator_if_possible(account_id, is_stash);
|
||||
predefined_calls::get_session_keys(&self.action_tx, &self.online_client_api, &self.rpc_client, &account_id).await
|
||||
},
|
||||
Action::BalanceRequest(account_id, remove) => {
|
||||
if remove {
|
||||
let _ = self.accounts_to_watch.remove(&account_id);
|
||||
Ok(())
|
||||
} else {
|
||||
let _ = self.accounts_to_watch.insert(account_id);
|
||||
predefinded_calls::get_balance(&self.action_tx, &self.online_client_api, &account_id).await
|
||||
predefined_calls::get_balance(&self.action_tx, &self.online_client_api, &account_id).await
|
||||
}
|
||||
}
|
||||
Action::TransferBalance(sender, receiver, amount) => {
|
||||
let sender_cloned = sender.clone();
|
||||
let maybe_nonce = self.senders.get_mut(&sender);
|
||||
|
||||
let sender: [u8; 32] = hex::decode(sender)
|
||||
.expect("stored seed is valid hex string; qed")
|
||||
.as_slice()
|
||||
.try_into()
|
||||
.expect("stored seed is valid length; qed");
|
||||
|
||||
if let Ok(tx_progress) = predefinded_calls::transfer_balance(
|
||||
if let Ok(tx_progress) = predefined_txs::transfer_balance(
|
||||
&self.action_tx,
|
||||
&self.online_client_api,
|
||||
&sender,
|
||||
&receiver,
|
||||
&amount,
|
||||
maybe_nonce,
|
||||
).await {
|
||||
self.transactions_to_watch.push(tx_progress);
|
||||
self.transactions_to_watch.push(TxToWatch {
|
||||
tx_progress,
|
||||
sender: sender_cloned,
|
||||
target: ActionTarget::WalletLog,
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Action::BondValidatorExtraFrom(sender, amount, log_target) => {
|
||||
let sender_str = hex::encode(sender);
|
||||
let maybe_nonce = self.senders.get_mut(&sender_str);
|
||||
|
||||
if let Ok(tx_progress) = predefined_txs::bond_extra(
|
||||
&self.action_tx,
|
||||
&self.online_client_api,
|
||||
&sender,
|
||||
&amount,
|
||||
maybe_nonce,
|
||||
log_target,
|
||||
).await {
|
||||
self.transactions_to_watch.push(TxToWatch {
|
||||
tx_progress,
|
||||
sender: sender_str,
|
||||
target: log_target,
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Action::BondValidatorFrom(sender, amount, log_target) => {
|
||||
let sender_str = hex::encode(sender);
|
||||
let maybe_nonce = self.senders.get_mut(&sender_str);
|
||||
if let Ok(tx_progress) = predefined_txs::bond(
|
||||
&self.action_tx,
|
||||
&self.online_client_api,
|
||||
&sender,
|
||||
&amount,
|
||||
maybe_nonce,
|
||||
log_target,
|
||||
).await {
|
||||
self.transactions_to_watch.push(TxToWatch {
|
||||
tx_progress,
|
||||
sender: sender_str,
|
||||
target: log_target,
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Action::PayoutStakers(sender, stash, era_index) => {
|
||||
let sender_str = hex::encode(sender);
|
||||
let maybe_nonce = self.senders.get_mut(&sender_str);
|
||||
if let Ok(tx_progress) = predefined_txs::payout_stakers(
|
||||
&self.action_tx,
|
||||
&self.online_client_api,
|
||||
&sender,
|
||||
&stash,
|
||||
era_index,
|
||||
maybe_nonce,
|
||||
).await {
|
||||
self.eras_to_watch.insert(era_index);
|
||||
self.transactions_to_watch.push(TxToWatch {
|
||||
tx_progress,
|
||||
sender: sender_str,
|
||||
target: ActionTarget::ValidatorLog,
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Action::SetSessionKeys(sender, hashed_keys) => {
|
||||
let sender_str = hex::encode(sender);
|
||||
let maybe_nonce = self.senders.get_mut(&sender_str);
|
||||
if let Ok(tx_progress) = predefined_txs::set_keys(
|
||||
&self.action_tx,
|
||||
&self.online_client_api,
|
||||
&sender,
|
||||
&hashed_keys,
|
||||
maybe_nonce,
|
||||
).await {
|
||||
self.transactions_to_watch.push(TxToWatch {
|
||||
tx_progress,
|
||||
sender: sender_str,
|
||||
target: ActionTarget::ValidatorLog,
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Action::ValidateFrom(sender, percent) => {
|
||||
let sender_str = hex::encode(sender);
|
||||
let maybe_nonce = self.senders.get_mut(&sender_str);
|
||||
if let Ok(tx_progress) = predefined_txs::validate(
|
||||
&self.action_tx,
|
||||
&self.online_client_api,
|
||||
&sender,
|
||||
percent,
|
||||
maybe_nonce,
|
||||
).await {
|
||||
self.transactions_to_watch.push(TxToWatch {
|
||||
tx_progress,
|
||||
sender: sender_str,
|
||||
target: ActionTarget::ValidatorLog,
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Action::ChillFrom(sender) => {
|
||||
let sender_str = hex::encode(sender);
|
||||
let maybe_nonce = self.senders.get_mut(&sender_str);
|
||||
if let Ok(tx_progress) = predefined_txs::chill(
|
||||
&self.action_tx,
|
||||
&self.online_client_api,
|
||||
&sender,
|
||||
maybe_nonce,
|
||||
).await {
|
||||
self.transactions_to_watch.push(TxToWatch {
|
||||
tx_progress,
|
||||
sender: sender_str,
|
||||
target: ActionTarget::ValidatorLog,
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Action::UnbondFrom(sender, amount) => {
|
||||
let sender_str = hex::encode(sender);
|
||||
let maybe_nonce = self.senders.get_mut(&sender_str);
|
||||
if let Ok(tx_progress) = predefined_txs::unbond(
|
||||
&self.action_tx,
|
||||
&self.online_client_api,
|
||||
&sender,
|
||||
&amount,
|
||||
maybe_nonce,
|
||||
).await {
|
||||
self.transactions_to_watch.push(TxToWatch {
|
||||
tx_progress,
|
||||
sender: sender_str,
|
||||
target: ActionTarget::ValidatorLog,
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Action::RebondFrom(sender, amount) => {
|
||||
let sender_str = hex::encode(sender);
|
||||
let maybe_nonce = self.senders.get_mut(&sender_str);
|
||||
if let Ok(tx_progress) = predefined_txs::rebond(
|
||||
&self.action_tx,
|
||||
&self.online_client_api,
|
||||
&sender,
|
||||
&amount,
|
||||
maybe_nonce,
|
||||
).await {
|
||||
self.transactions_to_watch.push(TxToWatch {
|
||||
tx_progress,
|
||||
sender: sender_str,
|
||||
target: ActionTarget::ValidatorLog,
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Action::WithdrawUnbondedFrom(sender, spans) => {
|
||||
let sender_str = hex::encode(sender);
|
||||
let maybe_nonce = self.senders.get_mut(&sender_str);
|
||||
if let Ok(tx_progress) = predefined_txs::withdraw_unbonded(
|
||||
&self.action_tx,
|
||||
&self.online_client_api,
|
||||
&sender,
|
||||
&spans,
|
||||
maybe_nonce,
|
||||
).await {
|
||||
self.transactions_to_watch.push(TxToWatch {
|
||||
tx_progress,
|
||||
sender: sender_str,
|
||||
target: ActionTarget::ValidatorLog,
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Action::SetPayee(sender, reward_destination, log_target) => {
|
||||
let sender_str = hex::encode(sender);
|
||||
let maybe_nonce = self.senders.get_mut(&sender_str);
|
||||
if let Ok(tx_progress) = predefined_txs::set_payee(
|
||||
&self.action_tx,
|
||||
&self.online_client_api,
|
||||
&sender,
|
||||
reward_destination,
|
||||
maybe_nonce,
|
||||
log_target,
|
||||
).await {
|
||||
self.transactions_to_watch.push(TxToWatch {
|
||||
tx_progress,
|
||||
sender: sender_str,
|
||||
target: log_target,
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Action::NominateTargets(sender, nomination_targets) => {
|
||||
let sender_str = hex::encode(sender);
|
||||
let maybe_nonce = self.senders.get_mut(&sender_str);
|
||||
if let Ok(tx_progress) = predefined_txs::nominate(
|
||||
&self.action_tx,
|
||||
&self.online_client_api,
|
||||
&sender,
|
||||
&nomination_targets,
|
||||
maybe_nonce,
|
||||
).await {
|
||||
self.transactions_to_watch.push(TxToWatch {
|
||||
tx_progress,
|
||||
sender: sender_str,
|
||||
target: ActionTarget::WalletLog,
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
690
src/network/predefined_calls.rs
Normal file
690
src/network/predefined_calls.rs
Normal file
@ -0,0 +1,690 @@
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
use color_eyre::Result;
|
||||
use subxt::{
|
||||
backend::rpc::RpcClient,
|
||||
client::OnlineClient,
|
||||
config::substrate::DigestItem,
|
||||
ext::sp_runtime::Saturating,
|
||||
ext::sp_core::crypto::{
|
||||
AccountId32, Ss58AddressFormat, Ss58Codec,
|
||||
},
|
||||
rpc_params,
|
||||
utils::H256,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
action::Action,
|
||||
casper_network::runtime_types::{ghost_networks::NetworkType, pallet_staking::RewardDestination, sp_consensus_slots},
|
||||
types::{EraInfo, EraRewardPoints, Gatekeeper, Nominations, Nominator, SlashingSpan, SessionKeyInfo, SystemAccount, UnlockChunk},
|
||||
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 maybe_total_issuance = super::raw_calls::balances::total_issuance(api, None)
|
||||
.await?;
|
||||
action_tx.send(Action::SetTotalIssuance(maybe_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?
|
||||
.map(|balance| SystemAccount {
|
||||
nonce: balance.nonce,
|
||||
free: balance.data.free,
|
||||
reserved: balance.data.reserved,
|
||||
frozen: balance.data.frozen,
|
||||
}
|
||||
);
|
||||
action_tx.send(Action::BalanceResponse(*account_id, maybe_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 adjusted_issuance = super::raw_calls::networks::bridged_imbalance(api, None)
|
||||
.await?
|
||||
.map(|imbalance| total_issuance
|
||||
.saturating_add(imbalance.bridged_out)
|
||||
.saturating_sub(imbalance.bridged_in)
|
||||
)
|
||||
.unwrap_or_default();
|
||||
|
||||
let accumulated_commission = super::raw_calls::networks::accumulated_commission(api, None)
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
|
||||
let (inflation, fraction) = super::calculate_for_fraction(total_staked, adjusted_issuance);
|
||||
let current_ratio: f64 = 1.0 + accumulated_commission as f64 / adjusted_issuance as f64;
|
||||
let yearly_current_ratio = current_ratio.powf(365.0) - 1.0;
|
||||
|
||||
let expected_inflation = (inflation.deconstruct() as f64) * yearly_current_ratio / 10_000_000.0;
|
||||
let expected_fraction = (fraction.deconstruct() as f64) * yearly_current_ratio / 10_000_000.0;
|
||||
let expected_reminder = expected_inflation - expected_fraction;
|
||||
|
||||
let inflation_str = format!("{:.4}%", expected_inflation);
|
||||
let fraction_str = format!("{:.4}%", expected_fraction);
|
||||
let treasury_str = format!("{:.4}%", expected_reminder);
|
||||
|
||||
action_tx.send(Action::Inflation(inflation_str))?;
|
||||
action_tx.send(Action::Apy(fraction_str))?;
|
||||
action_tx.send(Action::TreasuryApy(treasury_str))?;
|
||||
action_tx.send(Action::NextReward(accumulated_commission))?;
|
||||
|
||||
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 current_era = super::raw_calls::staking::active_era(api, None)
|
||||
.await?
|
||||
.map(|era_info| era_info.index)
|
||||
.unwrap_or(0);
|
||||
let era_depth = super::raw_calls::staking::history_depth(api).unwrap_or(0);
|
||||
|
||||
let start = current_era.saturating_sub(era_depth);
|
||||
for era_index in (start..current_era).rev() {
|
||||
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(())
|
||||
}
|
||||
|
||||
pub async fn get_current_validator_reward_in_era(
|
||||
action_tx: &UnboundedSender<Action>,
|
||||
api: &OnlineClient<CasperConfig>,
|
||||
) -> Result<()> {
|
||||
let era_index = super::raw_calls::staking::active_era(api, None)
|
||||
.await?
|
||||
.map(|era_info| era_info.index)
|
||||
.unwrap_or_default();
|
||||
|
||||
let disabled_validators = super::raw_calls::staking::disabled_validators(api, None)
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
|
||||
let maybe_era_reward_points = super::raw_calls::staking::eras_reward_points(api, None, era_index)
|
||||
.await?;
|
||||
|
||||
let (total_points, individual) = match maybe_era_reward_points {
|
||||
Some(era_reward_points) => {
|
||||
(
|
||||
era_reward_points.total,
|
||||
era_reward_points.individual
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, (account_id, points))| {
|
||||
let address = AccountId32::from(account_id.0)
|
||||
.to_ss58check_with_version(Ss58AddressFormat::custom(1996));
|
||||
EraRewardPoints {
|
||||
address,
|
||||
account_id: account_id.0,
|
||||
points: *points,
|
||||
disabled: disabled_validators.contains(&(index as u32)),
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
},
|
||||
None => (0, Vec::new()),
|
||||
};
|
||||
|
||||
action_tx.send(Action::SetCurrentValidatorEraRewards(
|
||||
era_index, total_points, individual))?;
|
||||
|
||||
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 == 0)
|
||||
.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?;
|
||||
|
||||
match maybe_ledger {
|
||||
Some(ledger) => {
|
||||
let chunks = ledger.unlocking.0
|
||||
.iter()
|
||||
.map(|chunk| UnlockChunk { value: chunk.value, era: chunk.era })
|
||||
.collect::<Vec<_>>();
|
||||
action_tx.send(Action::SetStakedAmountRatio(Some(ledger.total), Some(ledger.active), *account_id))?;
|
||||
action_tx.send(Action::SetValidatorEraUnlocking(chunks, *account_id))?;
|
||||
},
|
||||
None => {
|
||||
action_tx.send(Action::SetStakedAmountRatio(None, None, *account_id))?;
|
||||
action_tx.send(Action::SetValidatorEraUnlocking(Vec::new(), *account_id))?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_nominators_by_account(
|
||||
action_tx: &UnboundedSender<Action>,
|
||||
api: &OnlineClient<CasperConfig>,
|
||||
account_id: &[u8; 32],
|
||||
) -> Result<()> {
|
||||
let nominators = super::raw_calls::staking::nominators(api, None, account_id)
|
||||
.await?
|
||||
.map(|n| Nominations {
|
||||
targets: n.targets
|
||||
.0
|
||||
.into_iter()
|
||||
.map(|account_id_32| account_id_32.0)
|
||||
.collect::<Vec<_>>(),
|
||||
submitted_in: n.submitted_in,
|
||||
suppressed: n.suppressed,
|
||||
})
|
||||
.unwrap_or_default();
|
||||
action_tx.send(Action::SetNominatorsByAccount(nominators, *account_id))?;
|
||||
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_overview = super::raw_calls::staking::eras_stakers_overview(api, None, active_era_index, account_id)
|
||||
.await?;
|
||||
|
||||
let nominators = match maybe_eras_stakers_overview {
|
||||
Some(overview) => {
|
||||
let mut others = Vec::with_capacity(overview.nominator_count as usize);
|
||||
for page in 0..overview.page_count {
|
||||
let page_index = page as u32;
|
||||
let nominators = super::raw_calls::staking::eras_stakers_paged(api, None, active_era_index, page_index, account_id)
|
||||
.await?;
|
||||
others.append(&mut nominators
|
||||
.map(|n| n.others
|
||||
.iter()
|
||||
.map(|info| Nominator {
|
||||
account_id: info.who.0,
|
||||
address: AccountId32::from(info.who.0)
|
||||
.to_ss58check_with_version(Ss58AddressFormat::custom(1996)),
|
||||
value: info.value,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
)
|
||||
.unwrap_or_default()
|
||||
);
|
||||
}
|
||||
others
|
||||
},
|
||||
None => Vec::new(),
|
||||
};
|
||||
|
||||
action_tx.send(Action::SetNominatorsByValidator(nominators, *account_id))?;
|
||||
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::SetIsBonded(is_bonded, *account_id))?;
|
||||
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, *account_id))?;
|
||||
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 (commission, blocked) = match maybe_validator_prefs {
|
||||
Some(prefs) => (Some(prefs.commission.0), prefs.blocked),
|
||||
None => (None, false),
|
||||
};
|
||||
action_tx.send(Action::SetValidatorPrefs(commission, blocked, *account_id))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_minimal_validator_bond(
|
||||
action_tx: &UnboundedSender<Action>,
|
||||
api: &OnlineClient<CasperConfig>,
|
||||
) -> Result<()> {
|
||||
let min_validator_bond = super::raw_calls::staking::min_validator_bond(api, None)
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
action_tx.send(Action::SetMinValidatorBond(min_validator_bond))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_slashing_spans(
|
||||
action_tx: &UnboundedSender<Action>,
|
||||
api: &OnlineClient<CasperConfig>,
|
||||
account_id: &[u8; 32],
|
||||
) -> Result<()> {
|
||||
let slashing_spans_length = super::raw_calls::staking::slashing_spans(api, None, account_id)
|
||||
.await?
|
||||
.map(|spans| {
|
||||
let mut last_start = spans.last_start;
|
||||
let mut index = spans.span_index;
|
||||
let last = SlashingSpan { index, start: last_start, length: None };
|
||||
let prior = spans.prior.iter().cloned().map(move |length| {
|
||||
let start = last_start.saturating_sub(length);
|
||||
last_start = start;
|
||||
index.saturating_reduce(1);
|
||||
|
||||
SlashingSpan { index, start, length: Some(length) }
|
||||
});
|
||||
|
||||
std::iter::once(last).chain(prior).count()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
action_tx.send(Action::SetSlashingSpansLength(slashing_spans_length, *account_id))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_validator_latest_claim(
|
||||
action_tx: &UnboundedSender<Action>,
|
||||
api: &OnlineClient<CasperConfig>,
|
||||
account_id: &[u8; 32],
|
||||
) -> Result<()> {
|
||||
let current_era = super::raw_calls::staking::active_era(api, None)
|
||||
.await?
|
||||
.map(|era_info| era_info.index)
|
||||
.unwrap_or(0);
|
||||
let era_depth = super::raw_calls::staking::history_depth(api).unwrap_or(0);
|
||||
|
||||
let last_era = current_era.saturating_sub(era_depth);
|
||||
let mut claimed_era = current_era;
|
||||
|
||||
while claimed_era > last_era {
|
||||
let is_claimed = super::raw_calls::staking::claimed_rewards(api, None, claimed_era, account_id)
|
||||
.await?
|
||||
.map(|claimed_rewards| claimed_rewards.len() > 0)
|
||||
.unwrap_or_default();
|
||||
|
||||
if is_claimed {
|
||||
break;
|
||||
}
|
||||
|
||||
claimed_era -= 1;
|
||||
}
|
||||
|
||||
action_tx.send(Action::SetValidatorLatestClaim(current_era.saturating_sub(claimed_era), *account_id))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_account_payee(
|
||||
action_tx: &UnboundedSender<Action>,
|
||||
api: &OnlineClient<CasperConfig>,
|
||||
account_id: &[u8; 32],
|
||||
) -> Result<()> {
|
||||
let payee = super::raw_calls::staking::payee(api, None, account_id)
|
||||
.await?
|
||||
.map(|payee| match payee {
|
||||
RewardDestination::Stash => crate::types::RewardDestination::Stash,
|
||||
RewardDestination::Staked => crate::types::RewardDestination::Staked,
|
||||
RewardDestination::Account(account_id_32) => crate::types::RewardDestination::Account(account_id_32.0),
|
||||
RewardDestination::Controller => crate::types::RewardDestination::Controller,
|
||||
RewardDestination::None => crate::types::RewardDestination::None,
|
||||
})
|
||||
.unwrap_or_default();
|
||||
action_tx.send(Action::SetStakingPayee(payee, *account_id))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_gatekeeped_network(
|
||||
action_tx: &UnboundedSender<Action>,
|
||||
api: &OnlineClient<CasperConfig>,
|
||||
chain_id: u64,
|
||||
) -> Result<()> {
|
||||
let gatekeeped_network = super::raw_calls::networks::networks(api, None, chain_id)
|
||||
.await?
|
||||
.map(|network| Gatekeeper {
|
||||
chain_id,
|
||||
chain_name: String::from_utf8_lossy(&network.chain_name)
|
||||
.to_string(),
|
||||
chain_type: match network.network_type {
|
||||
NetworkType::Evm => String::from("EVM"),
|
||||
NetworkType::Utxo => String::from("UTXO"),
|
||||
NetworkType::Undefined => String::from("???"),
|
||||
},
|
||||
gatekeeper: String::from_utf8_lossy(&network.gatekeeper)
|
||||
.to_string(),
|
||||
incoming_fee: network.incoming_fee,
|
||||
outgoing_fee: network.outgoing_fee,
|
||||
})
|
||||
.unwrap_or_default();
|
||||
action_tx.send(Action::SetGatekeepedNetwork(gatekeeped_network))?;
|
||||
Ok(())
|
||||
}
|
||||
329
src/network/predefined_txs.rs
Normal file
329
src/network/predefined_txs.rs
Normal file
@ -0,0 +1,329 @@
|
||||
use color_eyre::Result;
|
||||
use subxt::{
|
||||
ext::sp_core::{sr25519::Pair, Pair as PairT},
|
||||
tx::{PairSigner, TxProgress},
|
||||
OnlineClient,
|
||||
};
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
|
||||
use crate::{
|
||||
action::Action,
|
||||
casper::{CasperConfig, CasperExtrinsicParamsBuilder},
|
||||
casper_network::{self, runtime_types},
|
||||
types::{ActionLevel, ActionTarget, RewardDestination},
|
||||
};
|
||||
|
||||
pub async fn transfer_balance(
|
||||
action_tx: &UnboundedSender<Action>,
|
||||
api: &OnlineClient<CasperConfig>,
|
||||
sender: &[u8; 32],
|
||||
receiver: &[u8; 32],
|
||||
amount: &u128,
|
||||
maybe_nonce: Option<&mut u32>,
|
||||
) -> 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);
|
||||
inner_sign_and_submit_then_watch(
|
||||
action_tx,
|
||||
api,
|
||||
sender,
|
||||
maybe_nonce,
|
||||
Box::new(transfer_tx),
|
||||
"transfer",
|
||||
ActionTarget::WalletLog,
|
||||
).await
|
||||
}
|
||||
|
||||
pub async fn bond_extra(
|
||||
action_tx: &UnboundedSender<Action>,
|
||||
api: &OnlineClient<CasperConfig>,
|
||||
sender: &[u8; 32],
|
||||
amount: &u128,
|
||||
maybe_nonce: Option<&mut u32>,
|
||||
log_target: ActionTarget,
|
||||
) -> Result<TxProgress<CasperConfig, OnlineClient<CasperConfig>>> {
|
||||
let bond_extra_tx = casper_network::tx().staking().bond_extra(*amount);
|
||||
inner_sign_and_submit_then_watch(
|
||||
action_tx,
|
||||
api,
|
||||
sender,
|
||||
maybe_nonce,
|
||||
Box::new(bond_extra_tx),
|
||||
"bond extra",
|
||||
log_target,
|
||||
).await
|
||||
}
|
||||
|
||||
pub async fn bond(
|
||||
action_tx: &UnboundedSender<Action>,
|
||||
api: &OnlineClient<CasperConfig>,
|
||||
sender: &[u8; 32],
|
||||
amount: &u128,
|
||||
maybe_nonce: Option<&mut u32>,
|
||||
log_target: ActionTarget,
|
||||
) -> Result<TxProgress<CasperConfig, OnlineClient<CasperConfig>>> {
|
||||
// auto-stake everything by now
|
||||
let reward_destination = casper_network::runtime_types::pallet_staking::RewardDestination::Staked;
|
||||
let bond_tx = casper_network::tx().staking().bond(*amount, reward_destination);
|
||||
inner_sign_and_submit_then_watch(
|
||||
action_tx,
|
||||
api,
|
||||
sender,
|
||||
maybe_nonce,
|
||||
Box::new(bond_tx),
|
||||
"bond",
|
||||
log_target,
|
||||
).await
|
||||
}
|
||||
|
||||
pub async fn payout_stakers(
|
||||
action_tx: &UnboundedSender<Action>,
|
||||
api: &OnlineClient<CasperConfig>,
|
||||
sender: &[u8; 32],
|
||||
stash: &[u8; 32],
|
||||
era_index: u32,
|
||||
maybe_nonce: Option<&mut u32>,
|
||||
) -> Result<TxProgress<CasperConfig, OnlineClient<CasperConfig>>> {
|
||||
let stash_id = subxt::utils::AccountId32::from(*stash);
|
||||
let payout_stakers_tx = casper_network::tx().staking().payout_stakers(stash_id, era_index);
|
||||
inner_sign_and_submit_then_watch(
|
||||
action_tx,
|
||||
api,
|
||||
sender,
|
||||
maybe_nonce,
|
||||
Box::new(payout_stakers_tx),
|
||||
"payout stakers",
|
||||
ActionTarget::ValidatorLog,
|
||||
).await
|
||||
}
|
||||
|
||||
pub async fn set_keys(
|
||||
action_tx: &UnboundedSender<Action>,
|
||||
api: &OnlineClient<CasperConfig>,
|
||||
sender: &[u8; 32],
|
||||
hashed_keys_str: &String,
|
||||
maybe_nonce: Option<&mut u32>,
|
||||
) -> Result<TxProgress<CasperConfig, OnlineClient<CasperConfig>>> {
|
||||
let (gran_key, babe_key, audi_key, slow_key) = {
|
||||
let s = hashed_keys_str.trim_start_matches("0x");
|
||||
(
|
||||
hex::decode(&s[0..64]).unwrap().as_slice().try_into().unwrap(),
|
||||
hex::decode(&s[64..128]).unwrap().as_slice().try_into().unwrap(),
|
||||
hex::decode(&s[128..192]).unwrap().as_slice().try_into().unwrap(),
|
||||
hex::decode(&s[192..256]).unwrap().as_slice().try_into().unwrap(),
|
||||
)
|
||||
};
|
||||
let session_keys = runtime_types::casper_runtime::opaque::SessionKeys {
|
||||
grandpa: runtime_types::sp_consensus_grandpa::app::Public(gran_key),
|
||||
babe: runtime_types::sp_consensus_babe::app::Public(babe_key),
|
||||
authority_discovery: runtime_types::sp_authority_discovery::app::Public(audi_key),
|
||||
slow_clap: runtime_types::ghost_slow_clap::sr25519::app_sr25519::Public(slow_key),
|
||||
};
|
||||
// it seems like there is no check for the second paramter, that's why
|
||||
// we it can be anything. For example empty vector.
|
||||
let set_keys_tx = casper_network::tx().session().set_keys(session_keys, Vec::new());
|
||||
inner_sign_and_submit_then_watch(
|
||||
action_tx,
|
||||
api,
|
||||
sender,
|
||||
maybe_nonce,
|
||||
Box::new(set_keys_tx),
|
||||
"set keys",
|
||||
ActionTarget::ValidatorLog,
|
||||
).await
|
||||
}
|
||||
|
||||
pub async fn validate(
|
||||
action_tx: &UnboundedSender<Action>,
|
||||
api: &OnlineClient<CasperConfig>,
|
||||
sender: &[u8; 32],
|
||||
percent: u32,
|
||||
maybe_nonce: Option<&mut u32>,
|
||||
) -> Result<TxProgress<CasperConfig, OnlineClient<CasperConfig>>> {
|
||||
let validator_prefs = casper_network::runtime_types::pallet_staking::ValidatorPrefs {
|
||||
commission: runtime_types::sp_arithmetic::per_things::Perbill(percent),
|
||||
blocked: percent >= 1_000_000_000u32,
|
||||
};
|
||||
let validate_tx = casper_network::tx().staking().validate(validator_prefs);
|
||||
inner_sign_and_submit_then_watch(
|
||||
action_tx,
|
||||
api,
|
||||
sender,
|
||||
maybe_nonce,
|
||||
Box::new(validate_tx),
|
||||
"validate",
|
||||
ActionTarget::ValidatorLog,
|
||||
).await
|
||||
}
|
||||
|
||||
pub async fn chill(
|
||||
action_tx: &UnboundedSender<Action>,
|
||||
api: &OnlineClient<CasperConfig>,
|
||||
sender: &[u8; 32],
|
||||
maybe_nonce: Option<&mut u32>,
|
||||
) -> Result<TxProgress<CasperConfig, OnlineClient<CasperConfig>>> {
|
||||
let chill_tx = casper_network::tx().staking().chill();
|
||||
inner_sign_and_submit_then_watch(
|
||||
action_tx,
|
||||
api,
|
||||
sender,
|
||||
maybe_nonce,
|
||||
Box::new(chill_tx),
|
||||
"chill",
|
||||
ActionTarget::ValidatorLog,
|
||||
).await
|
||||
}
|
||||
|
||||
pub async fn unbond(
|
||||
action_tx: &UnboundedSender<Action>,
|
||||
api: &OnlineClient<CasperConfig>,
|
||||
sender: &[u8; 32],
|
||||
amount: &u128,
|
||||
maybe_nonce: Option<&mut u32>,
|
||||
) -> Result<TxProgress<CasperConfig, OnlineClient<CasperConfig>>> {
|
||||
let unbond_tx = casper_network::tx().staking().unbond(*amount);
|
||||
inner_sign_and_submit_then_watch(
|
||||
action_tx,
|
||||
api,
|
||||
sender,
|
||||
maybe_nonce,
|
||||
Box::new(unbond_tx),
|
||||
"unbond",
|
||||
ActionTarget::ValidatorLog,
|
||||
).await
|
||||
}
|
||||
|
||||
pub async fn rebond(
|
||||
action_tx: &UnboundedSender<Action>,
|
||||
api: &OnlineClient<CasperConfig>,
|
||||
sender: &[u8; 32],
|
||||
amount: &u128,
|
||||
maybe_nonce: Option<&mut u32>,
|
||||
) -> Result<TxProgress<CasperConfig, OnlineClient<CasperConfig>>> {
|
||||
let rebond_tx = casper_network::tx().staking().rebond(*amount);
|
||||
inner_sign_and_submit_then_watch(
|
||||
action_tx,
|
||||
api,
|
||||
sender,
|
||||
maybe_nonce,
|
||||
Box::new(rebond_tx),
|
||||
"rebond",
|
||||
ActionTarget::ValidatorLog,
|
||||
).await
|
||||
}
|
||||
|
||||
pub async fn withdraw_unbonded(
|
||||
action_tx: &UnboundedSender<Action>,
|
||||
api: &OnlineClient<CasperConfig>,
|
||||
sender: &[u8; 32],
|
||||
spans: &u32,
|
||||
maybe_nonce: Option<&mut u32>,
|
||||
) -> Result<TxProgress<CasperConfig, OnlineClient<CasperConfig>>> {
|
||||
let withdraw_unbonded_tx = casper_network::tx().staking().withdraw_unbonded(*spans);
|
||||
inner_sign_and_submit_then_watch(
|
||||
action_tx,
|
||||
api,
|
||||
sender,
|
||||
maybe_nonce,
|
||||
Box::new(withdraw_unbonded_tx),
|
||||
"withdraw unbonded",
|
||||
ActionTarget::ValidatorLog,
|
||||
).await
|
||||
}
|
||||
|
||||
pub async fn set_payee(
|
||||
action_tx: &UnboundedSender<Action>,
|
||||
api: &OnlineClient<CasperConfig>,
|
||||
sender: &[u8; 32],
|
||||
reward_destination: RewardDestination,
|
||||
maybe_nonce: Option<&mut u32>,
|
||||
log_target: ActionTarget,
|
||||
) -> Result<TxProgress<CasperConfig, OnlineClient<CasperConfig>>> {
|
||||
let reward_destination = match reward_destination {
|
||||
RewardDestination::Staked => casper_network::runtime_types::pallet_staking::RewardDestination::Staked,
|
||||
RewardDestination::Stash => casper_network::runtime_types::pallet_staking::RewardDestination::Stash,
|
||||
RewardDestination::Controller => casper_network::runtime_types::pallet_staking::RewardDestination::Controller,
|
||||
RewardDestination::None => casper_network::runtime_types::pallet_staking::RewardDestination::None,
|
||||
RewardDestination::Account(account) => {
|
||||
let account_id = subxt::utils::AccountId32::from(account);
|
||||
casper_network::runtime_types::pallet_staking::RewardDestination::Account(account_id)
|
||||
}
|
||||
};
|
||||
let set_payee_tx = casper_network::tx().staking().set_payee(reward_destination);
|
||||
inner_sign_and_submit_then_watch(
|
||||
action_tx,
|
||||
api,
|
||||
sender,
|
||||
maybe_nonce,
|
||||
Box::new(set_payee_tx),
|
||||
"set payee",
|
||||
log_target,
|
||||
).await
|
||||
}
|
||||
|
||||
pub async fn nominate(
|
||||
action_tx: &UnboundedSender<Action>,
|
||||
api: &OnlineClient<CasperConfig>,
|
||||
sender: &[u8; 32],
|
||||
nomination_targets: &Vec<[u8; 32]>,
|
||||
maybe_nonce: Option<&mut u32>,
|
||||
) -> Result<TxProgress<CasperConfig, OnlineClient<CasperConfig>>> {
|
||||
let targets = nomination_targets
|
||||
.iter()
|
||||
.map(|acc| subxt::utils::MultiAddress::Id(subxt::utils::AccountId32::from(*acc)))
|
||||
.collect::<Vec<_>>();
|
||||
let nominate_tx = casper_network::tx().staking().nominate(targets);
|
||||
inner_sign_and_submit_then_watch(
|
||||
action_tx,
|
||||
api,
|
||||
sender,
|
||||
maybe_nonce,
|
||||
Box::new(nominate_tx),
|
||||
"nominate",
|
||||
ActionTarget::WalletLog,
|
||||
).await
|
||||
}
|
||||
|
||||
async fn inner_sign_and_submit_then_watch(
|
||||
action_tx: &UnboundedSender<Action>,
|
||||
api: &OnlineClient<CasperConfig>,
|
||||
sender: &[u8; 32],
|
||||
mut maybe_nonce: Option<&mut u32>,
|
||||
tx_call: Box<dyn subxt::tx::Payload>,
|
||||
tx_name: &str,
|
||||
target: ActionTarget,
|
||||
) -> Result<TxProgress<CasperConfig, OnlineClient<CasperConfig>>> {
|
||||
let signer = PairSigner::<CasperConfig, Pair>::new(Pair::from_seed(sender));
|
||||
let tx_params = match maybe_nonce {
|
||||
Some(ref mut nonce) => {
|
||||
**nonce = nonce.saturating_add(1);
|
||||
CasperExtrinsicParamsBuilder::new()
|
||||
.nonce(nonce.saturating_sub(1) as u64)
|
||||
.build()
|
||||
},
|
||||
None => CasperExtrinsicParamsBuilder::new().build(),
|
||||
};
|
||||
|
||||
match api
|
||||
.tx()
|
||||
.sign_and_submit_then_watch(&tx_call, &signer, tx_params)
|
||||
.await {
|
||||
Ok(tx_progress) => {
|
||||
action_tx.send(Action::EventLog(
|
||||
format!("{tx_name} transaction {} sent", tx_progress.extrinsic_hash()),
|
||||
ActionLevel::Info,
|
||||
target))?;
|
||||
Ok(tx_progress)
|
||||
},
|
||||
Err(err) => {
|
||||
if let Some(ref mut nonce) = maybe_nonce {
|
||||
**nonce = nonce.saturating_sub(1);
|
||||
}
|
||||
action_tx.send(Action::EventLog(
|
||||
format!("error during {tx_name} transaction: {err}"),
|
||||
ActionLevel::Error,
|
||||
target))?;
|
||||
Err(err.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
42
src/network/raw_calls/babe.rs
Normal file
42
src/network/raw_calls/babe.rs
Normal 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)
|
||||
}
|
||||
24
src/network/raw_calls/balances.rs
Normal file
24
src/network/raw_calls/balances.rs
Normal 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)
|
||||
}
|
||||
54
src/network/raw_calls/mod.rs
Normal file
54
src/network/raw_calls/mod.rs
Normal 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 networks;
|
||||
|
||||
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)
|
||||
}
|
||||
41
src/network/raw_calls/networks.rs
Normal file
41
src/network/raw_calls/networks.rs
Normal file
@ -0,0 +1,41 @@
|
||||
use color_eyre::Result;
|
||||
use subxt::{
|
||||
utils::H256,
|
||||
client::OnlineClient,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
casper_network::{
|
||||
self,
|
||||
runtime_types::ghost_networks::{BridgeAdjustment, NetworkData},
|
||||
},
|
||||
CasperConfig,
|
||||
};
|
||||
|
||||
pub async fn bridged_imbalance(
|
||||
online_client: &OnlineClient<CasperConfig>,
|
||||
at_hash: Option<&H256>,
|
||||
) -> Result<Option<BridgeAdjustment<u128>>> {
|
||||
let storage_key = casper_network::storage().ghost_networks().bridged_imbalance();
|
||||
let maybe_bridged_imbalance = super::do_storage_call(online_client, &storage_key, at_hash).await?;
|
||||
Ok(maybe_bridged_imbalance)
|
||||
}
|
||||
|
||||
pub async fn accumulated_commission(
|
||||
online_client: &OnlineClient<CasperConfig>,
|
||||
at_hash: Option<&H256>,
|
||||
) -> Result<Option<u128>> {
|
||||
let storage_key = casper_network::storage().ghost_networks().accumulated_commission();
|
||||
let maybe_accumulated_commission = super::do_storage_call(online_client, &storage_key, at_hash).await?;
|
||||
Ok(maybe_accumulated_commission)
|
||||
}
|
||||
|
||||
pub async fn networks(
|
||||
online_client: &OnlineClient<CasperConfig>,
|
||||
at_hash: Option<&H256>,
|
||||
chain_id: u64,
|
||||
) -> Result<Option<NetworkData>> {
|
||||
let storage_key = casper_network::storage().ghost_networks().networks(chain_id);
|
||||
let maybe_network = super::do_storage_call(online_client, &storage_key, at_hash).await?;
|
||||
Ok(maybe_network)
|
||||
}
|
||||
36
src/network/raw_calls/session.rs
Normal file
36
src/network/raw_calls/session.rs
Normal 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)
|
||||
}
|
||||
227
src/network/raw_calls/staking.rs
Normal file
227
src/network/raw_calls/staking.rs
Normal file
@ -0,0 +1,227 @@
|
||||
use color_eyre::Result;
|
||||
use subxt::{
|
||||
client::OnlineClient,
|
||||
utils::{AccountId32, H256},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
casper_network::{
|
||||
self,
|
||||
runtime_types::{
|
||||
pallet_staking::{
|
||||
slashing::SlashingSpans, ActiveEraInfo, EraRewardPoints,
|
||||
RewardDestination, StakingLedger, ValidatorPrefs, Nominations,
|
||||
},
|
||||
sp_arithmetic::per_things::Perbill,
|
||||
sp_staking::{ExposurePage, 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 nominators(
|
||||
online_client: &OnlineClient<CasperConfig>,
|
||||
at_hash: Option<&H256>,
|
||||
account: &[u8; 32],
|
||||
) -> Result<Option<Nominations>> {
|
||||
let account_id = super::convert_array_to_account_id(account);
|
||||
let storage_key = casper_network::storage().staking().nominators(account_id);
|
||||
let maybe_nominators = super::do_storage_call(online_client, &storage_key, at_hash).await.unwrap();
|
||||
Ok(maybe_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_paged(
|
||||
online_client: &OnlineClient<CasperConfig>,
|
||||
at_hash: Option<&H256>,
|
||||
era_index: u32,
|
||||
page_index: u32,
|
||||
account: &[u8; 32],
|
||||
) -> Result<Option<ExposurePage<AccountId32, u128>>> {
|
||||
let account_id = super::convert_array_to_account_id(account);
|
||||
let storage_key = casper_network::storage().staking().eras_stakers_paged(era_index, account_id, page_index);
|
||||
let maybe_eras_stakers_paged = super::do_storage_call(online_client, &storage_key, at_hash).await?;
|
||||
Ok(maybe_eras_stakers_paged)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
pub async fn disabled_validators(
|
||||
online_client: &OnlineClient<CasperConfig>,
|
||||
at_hash: Option<&H256>,
|
||||
) -> Result<Option<Vec<u32>>> {
|
||||
let storage_key = casper_network::storage().staking().disabled_validators();
|
||||
let maybe_disabled_validators = super::do_storage_call(online_client, &storage_key, at_hash).await?;
|
||||
Ok(maybe_disabled_validators)
|
||||
}
|
||||
|
||||
pub async fn min_validator_bond(
|
||||
online_client: &OnlineClient<CasperConfig>,
|
||||
at_hash: Option<&H256>,
|
||||
) -> Result<Option<u128>> {
|
||||
let storage_key = casper_network::storage().staking().min_validator_bond();
|
||||
let maybe_min_validator_bond = super::do_storage_call(online_client, &storage_key, at_hash).await?;
|
||||
Ok(maybe_min_validator_bond)
|
||||
}
|
||||
|
||||
pub async fn slashing_spans(
|
||||
online_client: &OnlineClient<CasperConfig>,
|
||||
at_hash: Option<&H256>,
|
||||
account: &[u8; 32],
|
||||
) -> Result<Option<SlashingSpans>> {
|
||||
let account_id = super::convert_array_to_account_id(account);
|
||||
let storage_key = casper_network::storage().staking().slashing_spans(account_id);
|
||||
let maybe_slashing_spans = super::do_storage_call(online_client, &storage_key, at_hash).await?;
|
||||
Ok(maybe_slashing_spans)
|
||||
}
|
||||
|
||||
pub async fn payee(
|
||||
online_client: &OnlineClient<CasperConfig>,
|
||||
at_hash: Option<&H256>,
|
||||
account: &[u8; 32],
|
||||
) -> Result<Option<RewardDestination<AccountId32>>> {
|
||||
let account_id = super::convert_array_to_account_id(account);
|
||||
let storage_key = casper_network::storage().staking().payee(account_id);
|
||||
let maybe_payee = super::do_storage_call(online_client, &storage_key, at_hash).await?;
|
||||
Ok(maybe_payee)
|
||||
}
|
||||
|
||||
pub fn history_depth(
|
||||
online_client: &OnlineClient<CasperConfig>,
|
||||
) -> Result<u32> {
|
||||
let constant_query = casper_network::constants().staking().history_depth();
|
||||
let history_depth = super::do_constant_call(online_client, &constant_query)?;
|
||||
Ok(history_depth)
|
||||
}
|
||||
24
src/network/raw_calls/system.rs
Normal file
24
src/network/raw_calls/system.rs
Normal 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
0
src/network/raw_rpc.rs
Normal file
@ -1,6 +1,12 @@
|
||||
use crate::{types::CasperExtrinsicDetails, action::Action, casper::CasperBlock};
|
||||
use crate::{
|
||||
types::CasperExtrinsicDetails,
|
||||
action::Action,
|
||||
casper::CasperBlock,
|
||||
};
|
||||
use color_eyre::Result;
|
||||
|
||||
use super::GATEKEEPED_CHAIN_IDS;
|
||||
|
||||
pub struct FinalizedSubscription {
|
||||
action_tx: tokio::sync::mpsc::UnboundedSender<Action>,
|
||||
network_tx: std::sync::mpsc::Sender<Action>,
|
||||
@ -112,8 +118,18 @@ impl BestSubscription {
|
||||
self.network_tx.send(Action::NewBestHash(block_hash))?;
|
||||
self.network_tx.send(Action::GetBlockAuthor(block_hash, block.header().digest.logs.clone()))?;
|
||||
self.network_tx.send(Action::GetActiveEra)?;
|
||||
self.network_tx.send(Action::GetCurrentEra)?;
|
||||
self.network_tx.send(Action::GetEpochProgress)?;
|
||||
self.network_tx.send(Action::GetTotalIssuance)?;
|
||||
self.network_tx.send(Action::GetValidatorsNumber)?;
|
||||
self.network_tx.send(Action::GetNominatorsNumber)?;
|
||||
self.network_tx.send(Action::GetInflation)?;
|
||||
self.network_tx.send(Action::GetCurrentValidatorEraRewards)?;
|
||||
self.network_tx.send(Action::GetMinValidatorBond)?;
|
||||
|
||||
for chain_id in GATEKEEPED_CHAIN_IDS {
|
||||
self.network_tx.send(Action::GetGatekeepedNetwork(chain_id))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -6,3 +6,11 @@ pub struct EraInfo {
|
||||
pub index: u32,
|
||||
pub start: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, Decode)]
|
||||
pub struct EraRewardPoints {
|
||||
pub address: String,
|
||||
pub account_id: [u8; 32],
|
||||
pub points: u32,
|
||||
pub disabled: bool,
|
||||
}
|
||||
|
||||
@ -8,3 +8,11 @@ pub enum ActionLevel {
|
||||
Warn,
|
||||
Error,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Display, Serialize, Deserialize)]
|
||||
pub enum ActionTarget {
|
||||
#[default]
|
||||
WalletLog,
|
||||
ValidatorLog,
|
||||
NominatorLog,
|
||||
}
|
||||
|
||||
@ -2,8 +2,23 @@ mod era;
|
||||
mod extrinsics;
|
||||
mod log;
|
||||
mod account;
|
||||
mod peer;
|
||||
mod session;
|
||||
mod nominator;
|
||||
mod staking;
|
||||
mod networks;
|
||||
|
||||
pub use extrinsics::CasperExtrinsicDetails;
|
||||
pub use era::EraInfo;
|
||||
pub use era::{EraRewardPoints, EraInfo};
|
||||
pub use log::ActionLevel;
|
||||
pub use log::ActionTarget;
|
||||
pub use account::SystemAccount;
|
||||
pub use peer::PeerInformation;
|
||||
pub use session::SessionKeyInfo;
|
||||
pub use nominator::Nominator;
|
||||
pub use nominator::Nominations;
|
||||
pub use staking::UnlockChunk;
|
||||
pub use staking::RewardDestination;
|
||||
pub use staking::SlashingSpan;
|
||||
pub use networks::Gatekeeper;
|
||||
pub use networks::BlockRange;
|
||||
|
||||
18
src/types/networks.rs
Normal file
18
src/types/networks.rs
Normal file
@ -0,0 +1,18 @@
|
||||
use codec::Decode;
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, Decode)]
|
||||
pub struct Gatekeeper {
|
||||
pub chain_id: u64,
|
||||
pub chain_name: String,
|
||||
pub chain_type: String,
|
||||
pub gatekeeper: String,
|
||||
pub incoming_fee: u32,
|
||||
pub outgoing_fee: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Decode)]
|
||||
pub struct BlockRange {
|
||||
pub from_block: u64,
|
||||
pub to_block: u64,
|
||||
}
|
||||
16
src/types/nominator.rs
Normal file
16
src/types/nominator.rs
Normal file
@ -0,0 +1,16 @@
|
||||
use codec::Decode;
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, Decode)]
|
||||
pub struct Nominator {
|
||||
pub account_id: [u8; 32],
|
||||
pub address: String,
|
||||
pub value: u128,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, Decode)]
|
||||
pub struct Nominations {
|
||||
pub targets: Vec<[u8; 32]>,
|
||||
pub submitted_in: u32,
|
||||
pub suppressed: bool,
|
||||
}
|
||||
12
src/types/peer.rs
Normal file
12
src/types/peer.rs
Normal 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,
|
||||
}
|
||||
10
src/types/points.rs
Normal file
10
src/types/points.rs
Normal file
@ -0,0 +1,10 @@
|
||||
use codec::Decode;
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, Decode)]
|
||||
pub struct EraRewardPoints {
|
||||
pub nonce: u32,
|
||||
pub free: u128,
|
||||
pub reserved: u128,
|
||||
pub frozen: u128,
|
||||
}
|
||||
8
src/types/session.rs
Normal file
8
src/types/session.rs
Normal 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,
|
||||
}
|
||||
26
src/types/staking.rs
Normal file
26
src/types/staking.rs
Normal file
@ -0,0 +1,26 @@
|
||||
use codec::Decode;
|
||||
use strum::Display;
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, Decode)]
|
||||
pub struct UnlockChunk {
|
||||
pub value: u128,
|
||||
pub era: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, Decode)]
|
||||
pub struct SlashingSpan {
|
||||
pub index: u32,
|
||||
pub start: u32,
|
||||
pub length: Option<u32>
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq, Display, Serialize, Deserialize)]
|
||||
pub enum RewardDestination {
|
||||
#[default]
|
||||
None,
|
||||
Staked,
|
||||
Stash,
|
||||
Account([u8; 32]),
|
||||
Controller,
|
||||
}
|
||||
@ -11,4 +11,4 @@ pub use big_text::BigText;
|
||||
pub use big_text::PixelSize;
|
||||
pub use input::{Input, InputRequest};
|
||||
|
||||
const CYCLE: i64 = 1500;
|
||||
const CYCLE: i64 = 1560;
|
||||
|
||||
26
update.sh
Executable file
26
update.sh
Executable file
@ -0,0 +1,26 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "[+] trying to fetch main branch"
|
||||
git fetch origin main
|
||||
|
||||
if [ $(git rev-list --count HEAD..origin/main) -gt 0 ]; then
|
||||
echo "[+] pulling latest version of the main branch"
|
||||
git pull origin main
|
||||
else
|
||||
echo "[+] nothing to update from the remote repository"
|
||||
fi
|
||||
|
||||
echo "[+] build for the new version starts in 3 seconds..."
|
||||
sleep 3
|
||||
cargo build --release
|
||||
|
||||
echo "[+] need to store the 'ghost-eye' binary to '/usr/local/bin/'"
|
||||
sudo cp target/release/ghost-eye /usr/local/bin
|
||||
|
||||
if ! grep -Fxq "alias ge=\"ghost-eye\"" ~/.bashrc; then
|
||||
echo "alias ge=\"ghost-eye\"" >> ~/.bashrc
|
||||
source ~/.bashrc
|
||||
echo "[+] alias 'ge' added, type 'ge' to run ghost-eye"
|
||||
else
|
||||
echo "[+] alias 'ge' already found"
|
||||
fi
|
||||
Loading…
Reference in New Issue
Block a user