Compare commits

...

77 Commits
v0.2.6 ... main

Author SHA1 Message Date
9e7cdffd29
minor navigation fixes for validator tab
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-03-12 15:36:47 +03:00
ac67bb4310
added ability to change reward destination from validator tab
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-03-12 15:16:08 +03:00
756a1089f6
list of nominators targets ported to wallet tab
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-03-02 17:47:23 +03:00
edd1b6d616
payee change added from wallet tab
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-03-02 15:30:06 +03:00
13295288ce
ability to bond as nominator from wallet tab
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-03-01 15:44:27 +03:00
105ca64dc6
make targets to be choosen
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-02-28 14:30:59 +03:00
2d01ef73c7
more in-depth validator staking status
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-02-27 21:31:49 +03:00
39818f848e
more detailed description of the xclip missing error
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-02-26 19:49:42 +03:00
48eb85efa3
fix for withdrawals table, many thanks to @neptune
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-02-26 15:43:03 +03:00
de1732372e
nomination staking info aded to wallet tab
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-02-25 18:49:29 +03:00
814854b286
close popup after chill execution
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-02-25 17:23:28 +03:00
f8b4215546
reload keys and push own wallet to address book
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-02-22 15:58:48 +03:00
bb5a0e8eba
fix for the staker status
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-02-22 14:54:06 +03:00
ee5640349a
migrate to active_era for staking activity
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-02-21 14:05:17 +03:00
3557e5fa4e
correct reverse indexing in BTreeMap
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-02-21 13:30:07 +03:00
3284d5d07e
iterate payouts in reverse order
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-02-20 20:58:34 +03:00
d42bb97d36
homepage and repository are added to the manifest file
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-02-18 17:59:33 +03:00
c4f0f6f35c
validator status detection draft added
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-02-18 17:54:20 +03:00
e16d319b72
remove listen address from a default flow, navigation through hotkey
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-02-18 17:17:27 +03:00
2635792dab
minimization of rpc calls based on the responses
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-02-18 17:10:04 +03:00
33027f96d5
last payout for the nominator tab added
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-02-18 16:28:48 +03:00
9dad47210b
default select for the nominator tab
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-02-18 15:28:29 +03:00
22db763d31
correct representation of points_ratio
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-02-18 14:29:25 +03:00
5e90456fdf
own stash identification fix
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-02-17 17:31:37 +03:00
6e7104f229
prettify popup for session keys rotation
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-02-17 15:35:39 +03:00
9d346172b6
remove redundant selectability for session keys in validators tab
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-02-17 15:14:22 +03:00
08e0f3d576
remove unnecessary historical raw calls
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-02-17 15:03:50 +03:00
4fc26712a5
interpret empty leading zero for float as valid value
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-02-17 14:58:02 +03:00
d53e0242fb
fix Perbill representation for validator's commission
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-02-17 14:36:14 +03:00
309c97d60e
correct history depth for historical payout rewards
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-02-17 14:12:31 +03:00
76d87aecbf
make ledger responses depending on account_id
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-02-17 13:06:11 +03:00
2afc38068a
additional validator info in nominator tab
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-02-16 16:23:00 +03:00
fafc5bd2c2
show spinner for non-validator stash
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-02-16 14:26:19 +03:00
90c07aa339
decrement nonce on transaction error
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-02-16 13:55:03 +03:00
c7cdafe3b6
validator details from nominator page added
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-02-14 16:39:27 +03:00
a2584f8212
scroll bars added to validator and nominator tabs
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-02-13 14:48:57 +03:00
5d73989a43
bump version
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-02-11 16:19:37 +03:00
91fb7380d6
multiple transactions from stash account
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-02-11 16:18:27 +03:00
10b8337f8d
withdraw unbonded functionality added
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-02-11 16:09:34 +03:00
0cb651728e
rebond of unlocked functionality added
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-02-10 14:33:39 +03:00
adf1d94cbe
cleanup in transaction section
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-02-07 17:23:44 +03:00
0777e6ebf0
unbond functionality added
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-02-07 15:16:50 +03:00
b1ff74c637
validator chill ability added
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-02-07 14:45:41 +03:00
3e22e933ab
small fixes for the update, run it with source update.sh
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-02-07 14:06:14 +03:00
38e95f58ed
automation of updates and aliasing
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-02-06 17:16:32 +03:00
26b8700455
validate action implemented
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-02-06 16:28:47 +03:00
8555ddceec
bump version and small cleanup
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-02-05 15:46:57 +03:00
5b0ef1c4b7
adding session keys finalized
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-02-05 15:45:42 +03:00
7e9e6ac329
draft for the session key rotation
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-02-04 18:45:18 +03:00
4f8c8f5262
ability to claim staking rewards based on era index added
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-02-04 15:54:10 +03:00
afd11aa294
bond and bond_extra transaction added
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-02-03 17:31:03 +03:00
853bdcd76e
additional pop-up for balance detalization in address book
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-02-02 16:41:08 +03:00
9b1136e4ca
bump version
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-01-24 16:01:04 +03:00
507c03959d
decrement stored nonce if tx failed for any reason
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-01-24 16:00:36 +03:00
8f63c7483d
add parameter target to the log event on each page
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-01-24 15:55:47 +03:00
493a4db663
bump version
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-01-23 21:59:01 +03:00
9e045ca6cc
make two decimals for reward_details
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-01-23 21:58:23 +03:00
a365acc2a1
update points for piecewise linear, based on the casper v0.0.3 curve
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-01-23 21:55:07 +03:00
44bd27e8f0
fix for the reward history range
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-01-23 21:53:09 +03:00
87eca056a3
usage of active era for reward points and bump version
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-01-23 17:25:41 +03:00
ba79a4cba8
current validators table in nominators tab added
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-01-23 17:06:27 +03:00
80fd07bcfe
fix extra movement to the right
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-01-22 16:41:22 +03:00
b922aa75f7
naive implementation of sending multiple transactions
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-01-22 15:32:58 +03:00
8b302a0814
bump the version
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-01-21 15:33:45 +03:00
25d0a37aa8
forget to add
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-01-21 15:33:12 +03:00
5323363b35
small fixes to make code more readable
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-01-21 15:27:49 +03:00
9ebb1d9543
bump version based on all small fixes
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-01-20 16:47:13 +03:00
6347474633
bug with index out-of-range in widget fixed with help of lowest common multiple, thanks to @ogenkidu and @metanomad
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-01-20 16:00:58 +03:00
f9233c6291
dot spinner for all non-existent balances
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-01-20 14:57:18 +03:00
9e77dd816d
make account-driven calls dependent on the finalized block but not latest, thanks to @doctor_k
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-01-20 13:52:05 +03:00
ddc8eb062a
make the transacation logs independent from the block finalization or production, thanks to @doctor_k
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-01-20 13:50:01 +03:00
3662f9c666
newly generated keys representation fix, thanks to @sargio, @ASB_clown and @metanomad
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-01-20 13:39:38 +03:00
3b7aaa7fd2
bump version
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-01-20 13:16:04 +03:00
2e0205a581
sketch for the validators page
Signed-off-by: Uncle Stinky <uncle.stinky@ghostchain.io>
2025-01-19 18:04:02 +03:00
85e44f0bb8
additional spaces needed, because borders are symbols too
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2024-12-25 14:25:09 +03:00
cbf3aa95a5
the worst fix ever, disable 'q' for Quit
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2024-12-25 14:23:37 +03:00
10a9ca166c
inverse the logs queue, ascending order for events
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2024-12-25 13:18:08 +03:00
72 changed files with 8375 additions and 514 deletions

View File

@ -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.52"
edition = "2021"
homepage = "https://git.ghostchain.io/ghostchain"
repository = "https://git.ghostchain.io/ghostchain/ghost-eye"
[dependencies]
better-panic = "0.3.0"

View File

@ -29,7 +29,18 @@
"highlight_style": "yellow bold",
"popup_style": "blue",
"popup_title_style": "blue",
}
},
"Validator": {
"normal_style": "",
"hover_style": "bold yellow italic on blue",
"normal_border_style": "blue",
"hover_border_style": "blue",
"normal_title_style": "blue",
"hover_title_style": "",
"highlight_style": "yellow bold",
"popup_style": "blue",
"popup_title_style": "blue",
},
},
"keybindings": {
"Menu": {
@ -45,6 +56,11 @@
"<Ctrl-z>": "Suspend",
},
"Wallet": {
"<Ctrl-d>": "Quit",
"<Ctrl-c>": "Quit",
"<Ctrl-z>": "Suspend",
},
"Validator": {
"<q>": "Quit",
"<Ctrl-d>": "Quit",
"<Ctrl-c>": "Quit",

View File

@ -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, PeerInformation, SessionKeyInfo, UnlockChunk, SystemAccount,
RewardDestination,
};
#[derive(Debug, Clone, PartialEq, Eq, Display, Serialize, Deserialize)]
@ -19,29 +21,49 @@ 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, bool),
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),
EventLog(String, ActionLevel, ActionTarget),
NewBestBlock(u32),
NewBestHash(H256),
@ -49,6 +71,10 @@ pub enum Action {
NewFinalizedHash(H256),
BestBlockUpdated(u32),
ExtrinsicsLength(u32, usize),
ValidatorsNumber(u32),
NominatorsNumber(u32),
Inflation(String),
Apy(String),
GetBlockAuthor(H256, Vec<DigestItem>),
SetBlockAuthor(H256, String),
@ -59,29 +85,71 @@ 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),
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>),
SetStashAccount([u8; 32]),
SetStashSecret([u8; 32]),
SetChoosenValidator([u8; 32], u32, u32),
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]),
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,
SetExistentialDeposit(u128),
SetTotalIssuance(u128),
SetMinValidatorBond(u128),
SetTotalIssuance(Option<u128>),
}

View File

@ -12,7 +12,9 @@ use crate::{
tui::{Event, Tui},
components::{
menu::Menu, version::Version, explorer::Explorer, wallet::Wallet,
empty::Empty, health::Health, fps::FpsCounter, Component,
validator::Validator, empty::Empty,
health::Health, fps::FpsCounter,
Component,
},
};
@ -21,10 +23,8 @@ pub enum Mode {
Menu,
Explorer,
Wallet,
WalletActive,
ExplorerActive,
Validator,
Empty,
EmptyActive,
}
impl Default for Mode {
@ -71,6 +71,7 @@ impl App {
Box::new(Version::default()),
Box::new(Explorer::default()),
Box::new(Wallet::default()),
Box::new(Validator::default()),
Box::new(Empty::default()),
],
should_quite: false,
@ -161,6 +162,8 @@ 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)?;
Ok(())
}
@ -171,6 +174,8 @@ impl App {
self.network_tx.send(Action::GetChainName)?;
self.network_tx.send(Action::GetChainVersion)?;
self.network_tx.send(Action::GetExistentialDeposit)?;
self.network_tx.send(Action::GetLocalIdentity)?;
self.network_tx.send(Action::GetListenAddresses)?;
Ok(())
}
@ -252,6 +257,15 @@ impl App {
}
}
},
Mode::Validator => {
if let Some(component) = self.components.get_mut(6) {
if let Err(err) = component.draw(frame, frame.area()) {
let _ = self
.action_tx
.send(Action::Error(format!("failed to draw: {:?}", err)));
}
}
},
_ => {
if let Some(component) = self.components.last_mut() {
if let Err(err) = component.draw(frame, frame.area()) {

View File

@ -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>;

View File

@ -78,7 +78,7 @@ impl Component for FpsCounter {
let [_, place] = super::header_layout(area);
let message = format!(
"{:.2} ticks/sec | {:.2} FPS",
" {:.2} ticks/sec | {:.2} FPS",
self.ticks_per_second,
self.frames_per_second
);

View File

@ -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,6 +76,8 @@ impl Component for Health {
},
Action::SetNodeName(name) => self.name = name,
Action::SetPendingExtrinsicsLength(length) => self.tx_pool_length = length,
Action::NominatorsNumber(number) => self.nominators_count = number,
Action::ValidatorsNumber(number) => self.validators_count = number,
_ => {}
};
Ok(None)
@ -82,11 +86,13 @@ impl Component for Health {
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
let [place, _] = super::header_layout(area);
let message = format!("{:^12} | tx.pool: {:^3} | peers: {:^3} | {:^9}",
let message = format!("{:^12} | tx.pool: {:^3} | peers: {:^3} | {:^9} | validators {:^4} | nominators {:^4} |",
self.name_as_string(),
self.tx_pool_length,
self.peers_as_string(),
self.is_syncing_as_string());
self.is_syncing_as_string(),
self.validators_count,
self.nominators_count);
let span = Span::styled(message, Style::new().dim());
let paragraph = Paragraph::new(span).left_aligned();

View File

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

View File

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

View 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(())
}
}

View 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(())
}
}

View 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(())
}
}

View File

@ -0,0 +1,295 @@
use std::collections::BTreeMap;
use std::sync::mpsc::Sender;
use color_eyre::Result;
use crossterm::event::{KeyCode, KeyEvent};
use ratatui::layout::{Constraint, Margin};
use ratatui::style::Modifier;
use ratatui::{
prelude::Stylize,
text::Text,
layout::{Alignment, Rect},
widgets::{
Block, Cell, Row, Table, TableState, Scrollbar, Padding,
ScrollbarOrientation, ScrollbarState,
},
Frame
};
use tokio::sync::mpsc::UnboundedSender;
use super::{PartialComponent, Component, CurrentTab};
use crate::{
action::Action,
config::Config,
palette::StylePalette,
};
struct EraStakingInfo {
reward: u128,
slash: u128,
is_claimed: bool,
}
pub struct History {
is_active: bool,
network_tx: Option<Sender<Action>>,
action_tx: Option<UnboundedSender<Action>>,
palette: StylePalette,
scroll_state: ScrollbarState,
table_state: TableState,
rewards: BTreeMap<u32, EraStakingInfo>,
}
impl Default for History {
fn default() -> Self {
Self::new()
}
}
impl History {
const TICKER: &str = " CSPR";
const DECIMALS: usize = 5;
pub fn new() -> Self {
Self {
is_active: false,
network_tx: None,
action_tx: None,
scroll_state: ScrollbarState::new(0),
table_state: TableState::new(),
rewards: BTreeMap::new(),
palette: StylePalette::default(),
}
}
fn 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 _ = action_tx.send(Action::PayoutValidatorPopup(*era_index, is_claimed));
}
}
}
}
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 {
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.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::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),
])
}),
[
Constraint::Length(4),
Constraint::Fill(1),
Constraint::Fill(1),
],
)
.highlight_style(self.palette.create_highlight_style())
.column_spacing(1)
.block(Block::bordered()
.border_style(border_style)
.border_type(border_type)
.padding(Padding::right(2))
.title_alignment(Alignment::Right)
.title_style(self.palette.create_title_style(false))
.title("Staking history"));
let scrollbar = Scrollbar::default()
.orientation(ScrollbarOrientation::VerticalRight)
.begin_symbol(None)
.end_symbol(None)
.style(self.palette.create_scrollbar_style());
frame.render_stateful_widget(table, place, &mut self.table_state);
frame.render_stateful_widget(
scrollbar,
place.inner(Margin { vertical: 1, horizontal: 1 }),
&mut self.scroll_state,
);
Ok(())
}
}

View File

@ -0,0 +1,183 @@
use color_eyre::Result;
use crossterm::event::{KeyCode, KeyEvent};
use ratatui::layout::Margin;
use ratatui::widgets::ListItem;
use ratatui::{
layout::{Alignment, Rect},
widgets::{
Block, List, ListState, Scrollbar,
ScrollbarOrientation, ScrollbarState,
},
Frame
};
use tokio::sync::mpsc::UnboundedSender;
use super::{PartialComponent, Component, CurrentTab};
use crate::{
action::Action,
config::Config,
palette::StylePalette,
};
pub struct ListenAddresses {
is_active: bool,
action_tx: Option<UnboundedSender<Action>>,
palette: StylePalette,
scroll_state: ScrollbarState,
list_state: ListState,
listen_addresses: Vec<String>,
local_identity: String,
}
impl Default for ListenAddresses {
fn default() -> Self {
Self::new()
}
}
impl ListenAddresses {
pub fn new() -> Self {
Self {
is_active: false,
action_tx: None,
scroll_state: ScrollbarState::new(0),
list_state: ListState::default(),
palette: StylePalette::default(),
listen_addresses: Vec::new(),
local_identity: String::new(),
}
}
fn first_row(&mut self) {
if self.listen_addresses.len() > 0 {
self.list_state.select(Some(0));
self.scroll_state = self.scroll_state.position(0);
}
}
fn next_row(&mut self) {
let i = match self.list_state.selected() {
Some(i) => {
if i >= self.listen_addresses.len() - 1 {
i
} else {
i + 1
}
},
None => 0,
};
self.list_state.select(Some(i));
self.scroll_state = self.scroll_state.position(i);
}
fn last_row(&mut self) {
if self.listen_addresses.len() > 0 {
let last = self.listen_addresses.len() - 1;
self.list_state.select(Some(last));
self.scroll_state = self.scroll_state.position(last);
}
}
fn previous_row(&mut self) {
let i = match self.list_state.selected() {
Some(i) => {
if i == 0 {
0
} else {
i - 1
}
},
None => 0
};
self.list_state.select(Some(i));
self.scroll_state = self.scroll_state.position(i);
}
}
impl PartialComponent for ListenAddresses {
fn set_active(&mut self, current_tab: CurrentTab) {
match current_tab {
CurrentTab::ListenAddresses => self.is_active = true,
_ => {
self.is_active = false;
self.list_state.select(None);
self.scroll_state = self.scroll_state.position(0);
}
}
}
}
impl Component for ListenAddresses {
fn register_action_handler(&mut self, tx: UnboundedSender<Action>) -> Result<()> {
self.action_tx = Some(tx);
Ok(())
}
fn register_config_handler(&mut self, config: Config) -> Result<()> {
if let Some(style) = config.styles.get(&crate::app::Mode::Wallet) {
self.palette.with_normal_style(style.get("normal_style").copied());
self.palette.with_hover_style(style.get("hover_style").copied());
self.palette.with_normal_border_style(style.get("normal_border_style").copied());
self.palette.with_hover_border_style(style.get("hover_border_style").copied());
self.palette.with_normal_title_style(style.get("normal_title_style").copied());
self.palette.with_hover_title_style(style.get("hover_title_style").copied());
self.palette.with_highlight_style(style.get("highlight_style").copied());
self.palette.with_scrollbar_style(style.get("scrollbar_style").copied());
}
Ok(())
}
fn update(&mut self, action: Action) -> Result<Option<Action>> {
match action {
Action::SetListenAddresses(addresses) => self.listen_addresses = addresses,
Action::SetLocalIdentity(identity) => self.local_identity = identity,
_ => {}
};
Ok(None)
}
fn handle_key_event(&mut self, key: KeyEvent) -> Result<Option<Action>> {
if self.is_active {
match key.code {
KeyCode::Up | KeyCode::Char('k') => self.previous_row(),
KeyCode::Down | KeyCode::Char('j') => self.next_row(),
KeyCode::Char('g') => self.first_row(),
KeyCode::Char('G') => self.last_row(),
_ => {},
};
}
Ok(None)
}
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
let [_, place] = super::validator_session_and_listen_layout(area);
let (border_style, border_type) = self.palette.create_border_style(self.is_active);
let list = List::new(
self.listen_addresses
.iter()
.map(|addr| ListItem::new(addr.clone()))
)
.highlight_style(self.palette.create_highlight_style())
.block(Block::bordered()
.border_style(border_style)
.border_type(border_type)
.title_alignment(Alignment::Right)
.title_style(self.palette.create_title_style(false))
.title(self.local_identity.clone()));
let scrollbar = Scrollbar::default()
.orientation(ScrollbarOrientation::VerticalRight)
.begin_symbol(None)
.end_symbol(None)
.style(self.palette.create_scrollbar_style());
frame.render_stateful_widget(list, place, &mut self.list_state);
frame.render_stateful_widget(
scrollbar,
place.inner(Margin { vertical: 1, horizontal: 1 }),
&mut self.scroll_state,
);
Ok(())
}
}

View File

@ -0,0 +1,307 @@
use color_eyre::Result;
use crossterm::event::{KeyCode, KeyEvent};
use ratatui::{
layout::{Constraint, Layout, Rect},
Frame,
};
use std::sync::mpsc::Sender;
use tokio::sync::mpsc::UnboundedSender;
use super::Component;
use crate::{action::Action, app::Mode, config::Config};
mod event_log;
mod peers;
mod stash_info;
mod nominators;
mod listen_addresses;
mod history;
mod withdrawals;
mod stash_details;
mod staking_details;
mod reward_details;
mod bond_popup;
mod payout_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 nominators::NominatorsByValidator;
use history::History;
use withdrawals::Withdrawals;
use bond_popup::BondPopup;
use payout_popup::PayoutPopup;
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,
NominatorsByValidator,
History,
Withdrawals,
Peers,
EventLogs,
BondPopup,
PayoutPopup,
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(History::default()),
Box::new(Withdrawals::default()),
Box::new(Peers::default()),
Box::new(ListenAddresses::default()),
Box::new(EventLogs::default()),
Box::new(BondPopup::default()),
Box::new(PayoutPopup::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::ListenAddresses => self.current_tab = CurrentTab::NominatorsByValidator,
_ => {}
}
}
fn move_right(&mut self) {
match self.current_tab {
CurrentTab::ListenAddresses => 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 => {
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::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::screen_layout(area);
for component in self.components.iter_mut() {
component.draw(frame, screen)?;
}
Ok(())
}
}
pub fn validator_layout(area: Rect) -> [Rect; 4] {
Layout::vertical([
Constraint::Length(16),
Constraint::Fill(1),
Constraint::Fill(1),
Constraint::Percentage(25),
]).areas(area)
}
pub fn validator_details_layout(area: Rect) -> [Rect; 2] {
let [place, _, _, _] = validator_layout(area);
Layout::horizontal([
Constraint::Length(31),
Constraint::Fill(1),
]).areas(place)
}
pub fn validator_session_and_listen_layout(area: Rect) -> [Rect; 2] {
let [_, place] = validator_details_layout(area);
Layout::vertical([
Constraint::Length(6),
Constraint::Fill(1),
]).areas(place)
}
pub fn validator_statistics_layout(area: Rect) -> [Rect; 3] {
let [_, place, _, _] = validator_layout(area);
Layout::horizontal([
Constraint::Percentage(30),
Constraint::Percentage(40),
Constraint::Percentage(30),
]).areas(place)
}
pub fn validator_balance_layout(area: Rect) -> [Rect; 3] {
let [place, _] = validator_details_layout(area);
Layout::vertical([
Constraint::Length(6),
Constraint::Length(5),
Constraint::Length(5),
]).areas(place)
}

View File

@ -0,0 +1,215 @@
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() {
if let Some(_) = self.table_state.selected() {
self.last_row();
}
}
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.who.clone()).alignment(Alignment::Left)),
Cell::from(Text::from(self.prepare_u128(info.value)).alignment(Alignment::Right)),
])
}),
[
Constraint::Min(0),
Constraint::Min(11),
],
)
.highlight_style(self.palette.create_highlight_style())
.column_spacing(1)
.block(Block::bordered()
.border_style(border_style)
.border_type(border_type)
.padding(Padding::right(2))
.title_alignment(Alignment::Right)
.title_style(self.palette.create_title_style(false))
.title("My Nominators"));
let scrollbar = Scrollbar::default()
.orientation(ScrollbarOrientation::VerticalRight)
.begin_symbol(None)
.end_symbol(None)
.style(self.palette.create_scrollbar_style());
frame.render_stateful_widget(table, place, &mut self.table_state);
frame.render_stateful_widget(
scrollbar,
place.inner(Margin { vertical: 1, horizontal: 1 }),
&mut self.scroll_state,
);
Ok(())
}
}

View File

@ -0,0 +1,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(())
}
}

View File

@ -0,0 +1,157 @@
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 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,
is_claimed: bool,
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,
is_claimed: false,
action_tx: None,
network_tx: None,
palette: StylePalette::default(),
}
}
fn store_era_to_claim(&mut self, era_index: u32, is_claimed: bool) {
self.is_claimed = is_claimed;
self.era_index = era_index;
}
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 self.is_claimed {
if let Some(action_tx) = &self.action_tx {
let _ = action_tx.send(Action::EventLog(
format!("staking rewards for era index #{} already claimed", self.era_index),
ActionLevel::Warn,
ActionTarget::ValidatorLog));
}
} else {
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, is_claimed) =>
self.store_era_to_claim(era_index, is_claimed),
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(())
}
}

View File

@ -0,0 +1,198 @@
use color_eyre::Result;
use crossterm::event::{KeyCode, KeyEvent};
use ratatui::layout::{Constraint, Margin};
use ratatui::{
text::Text,
layout::{Alignment, Rect},
widgets::{
Block, Cell, Row, Table, TableState, Scrollbar, Padding,
ScrollbarOrientation, ScrollbarState,
},
Frame
};
use super::{PartialComponent, Component, CurrentTab};
use crate::types::PeerInformation;
use crate::{
action::Action,
config::Config,
palette::StylePalette,
};
pub struct Peers {
is_active: bool,
palette: StylePalette,
scroll_state: ScrollbarState,
table_state: TableState,
peers: Vec<PeerInformation>,
}
impl Default for Peers {
fn default() -> Self {
Self::new()
}
}
impl Peers {
pub fn new() -> Self {
Self {
is_active: false,
scroll_state: ScrollbarState::new(0),
table_state: TableState::new(),
peers: Vec::new(),
palette: StylePalette::default(),
}
}
fn update_peers(&mut self, peers: Vec<PeerInformation>) {
if self.peers.len() > peers.len() {
if let Some(_) = self.table_state.selected() {
self.last_row();
}
}
self.peers = peers;
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(())
}
}

View 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(())
}
}

View File

@ -0,0 +1,170 @@
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,
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%"),
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::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("Current APY".to_string()).alignment(Alignment::Left)),
Cell::from(Text::from(self.apy.clone()).alignment(Alignment::Right)),
]),
Row::new(vec![
Cell::from(Text::from("Inflation".to_string()).alignment(Alignment::Left)),
Cell::from(Text::from(self.inflation.clone()).alignment(Alignment::Right)),
]),
],
[
Constraint::Min(11),
Constraint::Min(0),
],
)
.highlight_style(self.palette.create_highlight_style())
.column_spacing(1)
.block(Block::bordered()
.border_style(border_style)
.border_type(border_type)
.title_alignment(Alignment::Right)
.title_style(self.palette.create_title_style(false))
.title(staking_status));
frame.render_widget(table, place);
Ok(())
}
}

View 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(())
}
}

View File

@ -0,0 +1,141 @@
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,
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,
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::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)),
]),
],
[
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(())
}
}

View 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(())
}
}

View 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(())
}
}

View 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(())
}
}

View 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(())
}
}

View 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(())
}
}

View File

@ -0,0 +1,233 @@
use color_eyre::Result;
use crossterm::event::{KeyCode, KeyEvent};
use ratatui::layout::{Constraint, Margin};
use ratatui::style::{Modifier, Stylize};
use ratatui::{
text::Text,
layout::{Alignment, Rect},
widgets::{
Block, Cell, Row, Table, TableState, Scrollbar,
ScrollbarOrientation, ScrollbarState,
},
Frame
};
use super::{PartialComponent, Component, CurrentTab};
use crate::types::UnlockChunk;
use crate::{
action::Action,
config::Config,
palette::StylePalette,
};
pub struct Withdrawals {
is_active: bool,
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,
scroll_state: ScrollbarState::new(0),
table_state: TableState::new(),
palette: StylePalette::default(),
unlockings: Vec::new(),
stash_account: [0u8; 32],
current_era: 0,
}
}
fn first_row(&mut self) {
if self.unlockings.len() > 0 {
self.table_state.select(Some(0));
self.scroll_state = self.scroll_state.position(0);
}
}
fn next_row(&mut self) {
let i = match self.table_state.selected() {
Some(i) => {
if i >= self.unlockings.len() - 1 {
i
} else {
i + 1
}
},
None => 0,
};
self.table_state.select(Some(i));
self.scroll_state = self.scroll_state.position(i);
}
fn last_row(&mut self) {
if self.unlockings.len() > 0 {
let last = self.unlockings.len() - 1;
self.table_state.select(Some(last));
self.scroll_state = self.scroll_state.position(last);
}
}
fn previous_row(&mut self) {
let i = match self.table_state.selected() {
Some(i) => {
if i == 0 {
0
} else {
i - 1
}
},
None => 0
};
self.table_state.select(Some(i));
self.scroll_state = self.scroll_state.position(i);
}
fn add_new_unlocking(&mut self, 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_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(),
_ => {},
};
}
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(())
}
}

View 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(())
}
}

View File

@ -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,32 @@ 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()));
}
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 +147,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 +157,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 +171,6 @@ impl Accounts {
address,
account_id,
seed: secret_seed,
pair_signer,
});
self.last_row();
self.save_to_file();
@ -242,7 +254,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 +261,6 @@ impl Accounts {
account_id,
address,
seed: wallet_key.to_string(),
pair_signer,
});
}
self.log_event(format!("read {} wallets from disk",
@ -286,9 +296,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,7 +308,6 @@ impl Accounts {
address,
account_id,
seed: secret_seed,
pair_signer,
});
}
};
@ -310,6 +318,69 @@ impl Accounts {
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 +434,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 +489,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 +520,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 +529,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 +538,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)),
])
}),
[

View File

@ -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(),
_ => {},
};
}

View File

@ -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);

View File

@ -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)),
])
}),
[

View File

@ -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)),
]),
],
[

View 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(())
}
}

View File

@ -0,0 +1,223 @@
use std::sync::mpsc::Sender;
use color_eyre::Result;
use ratatui::{
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(table, place);
}
Ok(())
}
}

View File

@ -0,0 +1,389 @@
use std::fs::File;
use std::path::PathBuf;
use std::io::{Write, BufRead, BufReader};
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::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::EraRewardPoints;
use crate::{
action::Action,
config::Config,
palette::StylePalette,
};
pub struct CurrentValidators {
is_active: bool,
action_tx: Option<UnboundedSender<Action>>,
known_validators_file: PathBuf,
palette: StylePalette,
scroll_state: ScrollbarState,
table_state: TableState,
individual: 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],
}
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,
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(),
}
}
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.individual.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 update_choosen_details(&self, index: usize) {
if let Some(action_tx) = &self.action_tx {
let (selected_account_id, selected_points) = self.individual
.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 flip_validator_check(&mut self) {
if let Some(index) = self.table_state.selected() {
if let Some(indiv) = self.individual.get(index) {
let current_account_id = indiv.account_id;
if self.checked_validators.contains(&current_account_id) {
self.checked_validators.remove(&current_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.individual[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.individual.len() > 0 {
self.move_selected(0);
}
}
fn next_row(&mut self) {
let i = match self.table_state.selected() {
Some(i) => {
if i >= self.individual.len() - 1 {
i
} else {
i + 1
}
},
None => 0,
};
self.move_selected(i);
}
fn last_row(&mut self) {
if self.individual.len() > 0 {
let last = self.individual.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>,
) {
self.individual = individual.to_vec();
self.total_points = total_points;
self.era_index = era_index;
if let Some(account_id) = self.my_stash_id {
if self.individual.len() > 1 {
if let Some(index) = self.individual
.iter()
.position(|item| item.account_id == account_id) {
self.individual.swap(0, index);
}
}
}
self.scroll_state = self.scroll_state.content_length(self.individual.len());
}
}
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_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::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::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 table = Table::new(
self.individual
.iter()
.enumerate()
.map(|(index, info)| {
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 is_choosen_text = if self.checked_validators.contains(&info.account_id) {
">"
} else {
""
};
if info.disabled {
address_text = address_text.add_modifier(Modifier::CROSSED_OUT);
points_text = points_text.add_modifier(Modifier::CROSSED_OUT);
}
let mut current_validator_is_my_stash = false;
if index == 0 {
if let Some(account_id) = self.my_stash_id {
current_validator_is_my_stash = account_id == info.account_id;
}
};
if current_validator_is_my_stash {
let name = self.known_validators
.get(&info.account_id)
.cloned()
.unwrap_or("My stash".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(self.palette.create_highlight_style())
} else {
let name = self.known_validators
.get(&info.account_id)
.cloned()
.unwrap_or("Ghostie".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),
])
}
}),
[
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_alignment(Alignment::Right)
.title_style(self.palette.create_title_style(false))
.title(format!("Validators {} | Total points: {}", self.individual.len(), self.total_points)));
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(())
}
}

View File

@ -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)

View File

@ -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('I') => {
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() {
@ -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)
}

View File

@ -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()

View 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(())
}
}

View File

@ -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(),
_ => {},
};
}

View File

@ -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(),
_ => {},
};
}

View 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(())
}
}

View 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(())
}
}

View File

@ -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);

View File

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

View File

@ -1,27 +1,30 @@
use tokio::sync::mpsc::UnboundedSender;
use color_eyre::Result;
use subxt::backend::legacy::rpc_methods::LegacyRpcMethods;
use subxt::{backend::{legacy::rpc_methods::SystemHealth, rpc::RpcClient}, rpc_params};
use crate::{action::Action, casper::CasperConfig};
use crate::{action::Action, types::PeerInformation};
pub async fn get_node_name(
action_tx: &UnboundedSender<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 +38,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 +51,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 +63,72 @@ 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(())
}

View File

@ -0,0 +1,114 @@
use subxt::ext::sp_runtime::Perbill;
// 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 prepare_perbill_fraction_string(value: Perbill) -> String {
let d = value.deconstruct();
let mut m = 10_000_000;
let units = d / m;
let rest = d % m;
for _ in 0..2 {
m /= 10;
}
format!("{}.{:02}%", units, rest / m)
}

View File

@ -1,142 +1,476 @@
use tokio::sync::mpsc::UnboundedSender;
use color_eyre::Result;
use subxt::{
backend::{
legacy::LegacyRpcMethods,
rpc::RpcClient,
},
backend::rpc::RpcClient,
tx::{TxProgress, TxStatus},
utils::H256,
OnlineClient,
};
mod legacy_rpc_calls;
mod predefinded_calls;
mod predefined_calls;
mod predefined_txs;
mod subscriptions;
mod miscellaneous;
mod raw_calls;
pub use miscellaneous::{prepare_perbill_fraction_string, calculate_for_fraction};
use crate::{
types::ActionLevel,
types::{ActionLevel, ActionTarget},
action::Action,
casper::CasperConfig,
};
pub use subscriptions::{FinalizedSubscription, BestSubscription};
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?;
}
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::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::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::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(())
}

View File

@ -1,221 +0,0 @@
use tokio::sync::mpsc::UnboundedSender;
use color_eyre::Result;
use subxt::{
backend::rpc::RpcClient,
client::OnlineClient,
config::substrate::DigestItem,
ext::sp_core::{
crypto::{AccountId32, Ss58AddressFormat, Ss58Codec},
Pair as PairT,
sr25519::Pair,
},
rpc_params,
tx::{PairSigner, TxProgress},
utils::H256,
};
use crate::{
action::Action,
casper_network::{
self,
runtime_types::sp_consensus_slots,
},
types::{SystemAccount, EraInfo, ActionLevel},
CasperAccountId, CasperConfig
};
pub async fn get_block_author(
action_tx: &UnboundedSender<Action>,
api: &OnlineClient<CasperConfig>,
logs: &Vec<DigestItem>,
at_hash: &H256,
) -> Result<()> {
use codec::Decode;
use crate::casper_network::runtime_types::sp_consensus_babe::digests::PreDigest;
let storage_key = casper_network::storage().session().validators();
let validators = api.storage().at(*at_hash).fetch(&storage_key).await?.unwrap_or_default();
let maybe_author = match logs.iter().find(|item| matches!(item, DigestItem::PreRuntime(..))) {
Some(DigestItem::PreRuntime(engine, data)) if *engine == [b'B', b'A', b'B', b'E'] => {
match PreDigest::decode(&mut &data[..]) {
Ok(PreDigest::Primary(primary)) => validators.get(primary.authority_index as usize),
Ok(PreDigest::SecondaryPlain(secondary)) => validators.get(secondary.authority_index as usize),
Ok(PreDigest::SecondaryVRF(secondary)) => validators.get(secondary.authority_index as usize),
_ => None,
}
},
_ => None,
};
let validator = match maybe_author {
Some(author) => {
let extended_author = CasperAccountId::decode(&mut author.as_ref())
.expect("author should be valid AccountId32; qed");
let account_id = AccountId32::from(extended_author.0);
account_id.to_ss58check_with_version(Ss58AddressFormat::custom(1996))
},
None => "...".to_string(),
};
action_tx.send(Action::SetBlockAuthor(*at_hash, validator))?;
Ok(())
}
pub async fn get_active_era(
action_tx: &UnboundedSender<Action>,
api: &OnlineClient<CasperConfig>,
) -> Result<()> {
let storage_key = casper_network::storage().staking().active_era();
if let Some(active_era) = api.storage().at_latest().await?.fetch(&storage_key).await? {
action_tx.send(Action::SetActiveEra(EraInfo {
index: active_era.index,
start: active_era.start,
}))?;
}
Ok(())
}
pub async fn get_epoch_progress(
action_tx: &UnboundedSender<Action>,
api: &OnlineClient<CasperConfig>,
) -> Result<()> {
let storage_key = casper_network::storage().babe().current_slot();
let current_slot = api.storage()
.at_latest()
.await?
.fetch(&storage_key)
.await?
.unwrap_or(sp_consensus_slots::Slot(0u64));
let storage_key = casper_network::storage().babe().epoch_index();
let epoch_index = api.storage()
.at_latest()
.await?
.fetch(&storage_key)
.await?
.unwrap_or_default();
let storage_key = casper_network::storage().babe().genesis_slot();
let genesis_slot = api.storage()
.at_latest()
.await?
.fetch(&storage_key)
.await?
.unwrap_or(sp_consensus_slots::Slot(0u64));
let constant_query = casper_network::constants().babe().epoch_duration();
let epoch_duration = api.constants().at(&constant_query)?;
let epoch_start_slot = epoch_index * epoch_duration + genesis_slot.0;
let progress = current_slot.0.saturating_sub(epoch_start_slot);
action_tx.send(Action::SetEpochProgress(epoch_index, progress))?;
Ok(())
}
pub async fn get_pending_extrinsics(
action_tx: &UnboundedSender<Action>,
rpc_client: &RpcClient,
) -> Result<()> {
let pending_extrinsics: Vec<String> = rpc_client
.request("author_pendingExtrinsics", rpc_params![])
.await?;
action_tx.send(Action::SetPendingExtrinsicsLength(pending_extrinsics.len()))?;
Ok(())
}
pub async fn get_total_issuance(
action_tx: &UnboundedSender<Action>,
api: &OnlineClient<CasperConfig>,
) -> Result<()> {
let storage_key = casper_network::storage().balances().total_issuance();
let total_issuance = api.storage()
.at_latest()
.await?
.fetch(&storage_key)
.await?
.unwrap_or_default();
action_tx.send(Action::SetTotalIssuance(total_issuance))?;
Ok(())
}
pub async fn get_existential_deposit(
action_tx: &UnboundedSender<Action>,
api: &OnlineClient<CasperConfig>,
) -> Result<()> {
let constant_query = casper_network::constants().balances().existential_deposit();
let existential_deposit = api.constants().at(&constant_query)?;
action_tx.send(Action::SetExistentialDeposit(existential_deposit))?;
Ok(())
}
pub async fn get_balance(
action_tx: &UnboundedSender<Action>,
api: &OnlineClient<CasperConfig>,
account_id: &[u8; 32],
) -> Result<()> {
let account_id_converted = subxt::utils::AccountId32::from(*account_id);
let storage_key = casper_network::storage().system().account(account_id_converted);
let maybe_balance = api
.storage()
.at_latest()
.await?
.fetch(&storage_key)
.await?;
let balance = match maybe_balance {
Some(balance) => {
SystemAccount {
nonce: balance.nonce,
free: balance.data.free,
reserved: balance.data.reserved,
frozen: balance.data.frozen,
}
},
None => SystemAccount::default(),
};
action_tx.send(Action::BalanceResponse(*account_id, balance))?;
Ok(())
}
pub async fn transfer_balance(
action_tx: &UnboundedSender<Action>,
api: &OnlineClient<CasperConfig>,
sender: &[u8; 32],
receiver: &[u8; 32],
amount: &u128,
) -> Result<TxProgress<CasperConfig, OnlineClient<CasperConfig>>> {
let receiver_id = subxt::utils::MultiAddress::Id(
subxt::utils::AccountId32::from(*receiver)
);
let transfer_tx = casper_network::tx()
.balances()
.transfer_allow_death(receiver_id, *amount);
let pair = Pair::from_seed(sender);
let signer = PairSigner::<CasperConfig, Pair>::new(pair);
match api
.tx()
.sign_and_submit_then_watch_default(&transfer_tx, &signer)
.await {
Ok(tx_progress) => {
action_tx.send(Action::WalletLog(
format!("transfer transaction {} sent", tx_progress.extrinsic_hash()),
ActionLevel::Info))?;
Ok(tx_progress)
},
Err(err) => {
action_tx.send(Action::WalletLog(
format!("error during transfer: {err}"), ActionLevel::Error))?;
Err(err.into())
}
}
}

View File

@ -0,0 +1,602 @@
use tokio::sync::mpsc::UnboundedSender;
use color_eyre::Result;
use subxt::{
backend::rpc::RpcClient,
client::OnlineClient,
config::substrate::DigestItem,
ext::sp_core::crypto::{
AccountId32, Ss58AddressFormat, Ss58Codec,
},
rpc_params,
utils::H256,
};
use crate::{
action::Action,
casper_network::runtime_types::{pallet_staking::RewardDestination, sp_consensus_slots},
types::{EraInfo, EraRewardPoints, Nominator, 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 (inflation, fraction) = super::calculate_for_fraction(total_staked, total_issuance);
let inflation_str = super::prepare_perbill_fraction_string(inflation);
let fraction_str = super::prepare_perbill_fraction_string(fraction);
action_tx.send(Action::Inflation(inflation_str))?;
action_tx.send(Action::Apy(fraction_str))?;
Ok(())
}
pub async fn get_session_keys(
action_tx: &UnboundedSender<Action>,
api: &OnlineClient<CasperConfig>,
rpc_client: &RpcClient,
account_id: &[u8; 32],
) -> Result<()> {
let maybe_session_keys = super::raw_calls::session::next_keys(api, None, account_id).await?;
let (gran_key, babe_key, audi_key, slow_key) = match maybe_session_keys {
Some(session_keys) => {
let gran_key = format!("0x{}", hex::encode(session_keys.grandpa.0));
let babe_key = format!("0x{}", hex::encode(session_keys.babe.0));
let audi_key = format!("0x{}", hex::encode(session_keys.authority_discovery.0));
let slow_key = format!("0x{}", hex::encode(session_keys.slow_clap.0));
(gran_key, babe_key, audi_key, slow_key)
},
None => (String::new(), String::new(), String::new(), String::new()),
};
check_author_has_key(rpc_client, action_tx, &gran_key, "gran").await?;
check_author_has_key(rpc_client, action_tx, &babe_key, "babe").await?;
check_author_has_key(rpc_client, action_tx, &audi_key, "audi").await?;
check_author_has_key(rpc_client, action_tx, &slow_key, "slow").await?;
Ok(())
}
pub async fn get_queued_session_keys(
action_tx: &UnboundedSender<Action>,
api: &OnlineClient<CasperConfig>,
rpc_client: &RpcClient,
account_id: &[u8; 32],
) -> Result<()> {
let account = super::raw_calls::convert_array_to_account_id(account_id);
let maybe_queued_keys = super::raw_calls::session::queued_keys(api, None).await?;
let (gran_key, babe_key, audi_key, slow_key) = match maybe_queued_keys {
Some(session_keys) => {
match session_keys.iter().find(|tuple| tuple.0 == account) {
Some(keys) => {
let session_keys = &keys.1;
let gran_key = format!("0x{}", hex::encode(session_keys.grandpa.0));
let babe_key = format!("0x{}", hex::encode(session_keys.babe.0));
let audi_key = format!("0x{}", hex::encode(session_keys.authority_discovery.0));
let slow_key = format!("0x{}", hex::encode(session_keys.slow_clap.0));
(gran_key, babe_key, audi_key, slow_key)
},
None => (String::new(), String::new(), String::new(), String::new()),
}
},
None => (String::new(), String::new(), String::new(), String::new()),
};
check_author_has_key(rpc_client, action_tx, &gran_key, "q_gran").await?;
check_author_has_key(rpc_client, action_tx, &babe_key, "q_babe").await?;
check_author_has_key(rpc_client, action_tx, &audi_key, "q_audi").await?;
check_author_has_key(rpc_client, action_tx, &slow_key, "q_slow").await?;
Ok(())
}
async fn check_author_has_key(
rpc_client: &RpcClient,
action_tx: &UnboundedSender<Action>,
key: &str,
name: &str,
) -> Result<()> {
let params_name = if name.starts_with("q_") {
&name[2..]
} else {
name
};
let is_stored: bool = rpc_client
.request("author_hasKey", rpc_params![key, params_name])
.await?;
let session_key_info = SessionKeyInfo {
key: key.to_string(),
is_stored
};
action_tx.send(Action::SetSessionKey(name.to_string(), session_key_info))?;
Ok(())
}
pub async fn get_validator_staking_results(
action_tx: &UnboundedSender<Action>,
api: &OnlineClient<CasperConfig>,
account_id: &[u8; 32],
) -> Result<()> {
let 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() {
if get_validator_staking_result(action_tx, api, account_id, era_index).await? {
break;
}
}
Ok(())
}
pub async fn get_validator_staking_result(
action_tx: &UnboundedSender<Action>,
api: &OnlineClient<CasperConfig>,
account_id: &[u8; 32],
era_index: u32,
) -> Result<bool> {
let no_more_rewards = 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(no_more_rewards)
}
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<bool> {
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,
};
let no_more_rewards = if my_reward > 0 {
action_tx.send(Action::SetValidatorEraReward(era_index, my_reward))?;
false
} else {
true
};
Ok(no_more_rewards)
}
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_validator(
action_tx: &UnboundedSender<Action>,
api: &OnlineClient<CasperConfig>,
account_id: &[u8; 32],
) -> Result<()> {
let active_era_index = super::raw_calls::staking::active_era(api, None)
.await?
.map(|era_info| era_info.index)
.unwrap_or_default();
let maybe_eras_stakers = super::raw_calls::staking::eras_stakers(api, None, active_era_index, account_id)
.await?;
let nominators = match maybe_eras_stakers {
Some(eras_stakers) => eras_stakers
.others
.iter()
.map(|info| {
Nominator {
who: AccountId32::from(info.who.0)
.to_ss58check_with_version(Ss58AddressFormat::custom(1996)),
value: info.value,
}
})
.collect::<Vec<_>>(),
None => Vec::new(),
};
action_tx.send(Action::SetNominatorsByValidator(nominators, *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| spans.prior.len())
.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(())
}

View File

@ -0,0 +1,306 @@
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
}
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())
}
}
}

View File

@ -0,0 +1,42 @@
use color_eyre::Result;
use subxt::{
utils::H256,
client::OnlineClient,
};
use crate::{casper_network::{self, runtime_types::sp_consensus_slots}, CasperConfig};
pub async fn current_slot(
online_client: &OnlineClient<CasperConfig>,
at_hash: Option<&H256>,
) -> Result<Option<sp_consensus_slots::Slot>> {
let storage_key = casper_network::storage().babe().current_slot();
let maybe_current_slot = super::do_storage_call(online_client, &storage_key, at_hash).await?;
Ok(maybe_current_slot)
}
pub async fn epoch_index(
online_client: &OnlineClient<CasperConfig>,
at_hash: Option<&H256>,
) -> Result<Option<u64>> {
let storage_key = casper_network::storage().babe().epoch_index();
let maybe_epoch_index = super::do_storage_call(online_client, &storage_key, at_hash).await?;
Ok(maybe_epoch_index)
}
pub async fn genesis_slot(
online_client: &OnlineClient<CasperConfig>,
at_hash: Option<&H256>,
) -> Result<Option<sp_consensus_slots::Slot>> {
let storage_key = casper_network::storage().babe().genesis_slot();
let maybe_genesis_slot = super::do_storage_call(online_client, &storage_key, at_hash).await?;
Ok(maybe_genesis_slot)
}
pub fn epoch_duration(
online_client: &OnlineClient<CasperConfig>,
) -> Result<u64> {
let constant_query = casper_network::constants().babe().epoch_duration();
let epoch_duration = super::do_constant_call(online_client, &constant_query)?;
Ok(epoch_duration)
}

View File

@ -0,0 +1,24 @@
use color_eyre::Result;
use subxt::{
utils::H256,
client::OnlineClient,
};
use crate::{CasperConfig, casper_network};
pub async fn total_issuance(
online_client: &OnlineClient<CasperConfig>,
at_hash: Option<&H256>,
) -> Result<Option<u128>> {
let storage_key = casper_network::storage().balances().total_issuance();
let maybe_total_issuance = super::do_storage_call(online_client, &storage_key, at_hash).await?;
Ok(maybe_total_issuance)
}
pub fn existential_deposit(
online_client: &OnlineClient<CasperConfig>,
) -> Result<u128> {
let constant_query = casper_network::constants().balances().existential_deposit();
let existential_deposit = super::do_constant_call(online_client, &constant_query)?;
Ok(existential_deposit)
}

View File

@ -0,0 +1,53 @@
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 async fn do_storage_call<'address, Addr>(
online_client: &OnlineClient<CasperConfig>,
storage_key: &'address Addr,
maybe_at_hash: Option<&H256>,
) -> Result<Option<Addr::Target>, subxt::Error>
where
Addr: subxt::storage::Address<IsFetchable = Yes> + 'address,
{
let at_hash = match maybe_at_hash {
Some(at_hash) => BlockRef::from_hash(*at_hash),
None => online_client
.backend()
.latest_finalized_block_ref()
.await?,
};
online_client
.storage()
.at(at_hash)
.fetch(storage_key)
.await
}
pub fn do_constant_call<'address, Addr>(
online_client: &OnlineClient<CasperConfig>,
constant_query: &'address Addr,
) -> Result<Addr::Target, subxt::Error>
where
Addr: subxt::constants::Address + 'address
{
let constant_client = online_client.constants();
constant_client.validate(constant_query).expect("constant query should be correct; qed");
constant_client.at(constant_query)
}
pub fn convert_array_to_account_id(who: &[u8; 32]) -> AccountId32 {
AccountId32::from(*who)
}

View File

@ -0,0 +1,36 @@
use color_eyre::Result;
use subxt::{
utils::{AccountId32, H256},
client::OnlineClient,
};
use crate::{casper_network::{self, runtime_types::casper_runtime::opaque}, CasperConfig};
pub async fn validators(
online_client: &OnlineClient<CasperConfig>,
at_hash: Option<&H256>,
) -> Result<Option<Vec<AccountId32>>> {
let storage_key = casper_network::storage().session().validators();
let maybe_validators = super::do_storage_call(online_client, &storage_key, at_hash).await?;
Ok(maybe_validators)
}
pub async fn next_keys(
online_client: &OnlineClient<CasperConfig>,
at_hash: Option<&H256>,
account: &[u8; 32],
) -> Result<Option<opaque::SessionKeys>> {
let account_id = super::convert_array_to_account_id(account);
let storage_key = casper_network::storage().session().next_keys(account_id);
let maybe_next_keys = super::do_storage_call(online_client, &storage_key, at_hash).await?;
Ok(maybe_next_keys)
}
pub async fn queued_keys(
online_client: &OnlineClient<CasperConfig>,
at_hash: Option<&H256>,
) -> Result<Option<Vec<(AccountId32, opaque::SessionKeys)>>> {
let storage_key = casper_network::storage().session().queued_keys();
let maybe_queued_keys = super::do_storage_call(online_client, &storage_key, at_hash).await?;
Ok(maybe_queued_keys)
}

View File

@ -0,0 +1,214 @@
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
},
sp_arithmetic::per_things::Perbill,
sp_staking::{Exposure, PagedExposureMetadata},
},
},
CasperConfig,
};
pub async fn current_era(
online_client: &OnlineClient<CasperConfig>,
at_hash: Option<&H256>,
) -> Result<Option<u32>> {
let storage_key = casper_network::storage().staking().current_era();
let maybe_current_era = super::do_storage_call(online_client, &storage_key, at_hash).await?;
Ok(maybe_current_era)
}
pub async fn active_era(
online_client: &OnlineClient<CasperConfig>,
at_hash: Option<&H256>,
) -> Result<Option<ActiveEraInfo>> {
let storage_key = casper_network::storage().staking().active_era();
let maybe_active_era = super::do_storage_call(online_client, &storage_key, at_hash).await?;
Ok(maybe_active_era)
}
pub async fn counter_for_validators(
online_client: &OnlineClient<CasperConfig>,
at_hash: Option<&H256>,
) -> Result<Option<u32>> {
let storage_key = casper_network::storage().staking().counter_for_validators();
let maybe_counter_for_validators = super::do_storage_call(online_client, &storage_key, at_hash).await?;
Ok(maybe_counter_for_validators)
}
pub async fn counter_for_nominators(
online_client: &OnlineClient<CasperConfig>,
at_hash: Option<&H256>,
) -> Result<Option<u32>> {
let storage_key = casper_network::storage().staking().counter_for_nominators();
let maybe_counter_for_nominators = super::do_storage_call(online_client, &storage_key, at_hash).await?;
Ok(maybe_counter_for_nominators)
}
pub async fn eras_total_stake(
online_client: &OnlineClient<CasperConfig>,
at_hash: Option<&H256>,
era_index: u32,
) -> Result<Option<u128>> {
let storage_key = casper_network::storage().staking().eras_total_stake(era_index);
let maybe_eras_total_stake = super::do_storage_call(online_client, &storage_key, at_hash).await?;
Ok(maybe_eras_total_stake)
}
pub async fn eras_validator_reward(
online_client: &OnlineClient<CasperConfig>,
at_hash: Option<&H256>,
era_index: u32,
) -> Result<Option<u128>> {
let storage_key = casper_network::storage().staking().eras_validator_reward(era_index);
let maybe_eras_validator_reward = super::do_storage_call(online_client, &storage_key, at_hash).await?;
Ok(maybe_eras_validator_reward)
}
pub async fn eras_reward_points(
online_client: &OnlineClient<CasperConfig>,
at_hash: Option<&H256>,
era_index: u32,
) -> Result<Option<EraRewardPoints<AccountId32>>> {
let storage_key = casper_network::storage().staking().eras_reward_points(era_index);
let maybe_eras_reward_points = super::do_storage_call(online_client, &storage_key, at_hash).await?;
Ok(maybe_eras_reward_points)
}
pub async fn claimed_rewards(
online_client: &OnlineClient<CasperConfig>,
at_hash: Option<&H256>,
era_index: u32,
account: &[u8; 32],
) -> Result<Option<Vec<u32>>> {
let account_id = super::convert_array_to_account_id(account);
let storage_key = casper_network::storage().staking().claimed_rewards(era_index, account_id);
let maybe_claimed_rewards = super::do_storage_call(online_client, &storage_key, at_hash).await?;
Ok(maybe_claimed_rewards)
}
pub async fn validator_slash_in_era(
online_client: &OnlineClient<CasperConfig>,
at_hash: Option<&H256>,
era_index: u32,
account: &[u8; 32],
) -> Result<Option<(Perbill, u128)>> {
let account_id = super::convert_array_to_account_id(account);
let storage_key = casper_network::storage().staking().validator_slash_in_era(era_index, account_id);
let maybe_validator_slash_in_era = super::do_storage_call(online_client, &storage_key, at_hash).await?;
Ok(maybe_validator_slash_in_era)
}
pub async fn ledger(
online_client: &OnlineClient<CasperConfig>,
at_hash: Option<&H256>,
account: &[u8; 32],
) -> Result<Option<StakingLedger>> {
let account_id = super::convert_array_to_account_id(account);
let storage_key = casper_network::storage().staking().ledger(account_id);
let maybe_ledger = super::do_storage_call(online_client, &storage_key, at_hash).await?;
Ok(maybe_ledger)
}
pub async fn eras_stakers(
online_client: &OnlineClient<CasperConfig>,
at_hash: Option<&H256>,
era_index: u32,
account: &[u8; 32],
) -> Result<Option<Exposure<AccountId32, u128>>> {
let account_id = super::convert_array_to_account_id(account);
let storage_key = casper_network::storage().staking().eras_stakers(era_index, account_id);
let maybe_eras_stakers = super::do_storage_call(online_client, &storage_key, at_hash).await?;
Ok(maybe_eras_stakers)
}
pub async fn bonded(
online_client: &OnlineClient<CasperConfig>,
at_hash: Option<&H256>,
account: &[u8; 32],
) -> Result<Option<AccountId32>> {
let account_id = super::convert_array_to_account_id(account);
let storage_key = casper_network::storage().staking().bonded(account_id);
let maybe_bonded = super::do_storage_call(online_client, &storage_key, at_hash).await?;
Ok(maybe_bonded)
}
pub async fn eras_stakers_overview(
online_client: &OnlineClient<CasperConfig>,
at_hash: Option<&H256>,
era_index: u32,
account: &[u8; 32],
) -> Result<Option<PagedExposureMetadata<u128>>> {
let account_id = super::convert_array_to_account_id(account);
let storage_key = casper_network::storage().staking().eras_stakers_overview(era_index, account_id);
let maybe_eras_stakers_overview = super::do_storage_call(online_client, &storage_key, at_hash).await?;
Ok(maybe_eras_stakers_overview)
}
pub async fn validators(
online_client: &OnlineClient<CasperConfig>,
at_hash: Option<&H256>,
account: &[u8; 32],
) -> Result<Option<ValidatorPrefs>> {
let account_id = super::convert_array_to_account_id(account);
let storage_key = casper_network::storage().staking().validators(account_id);
let maybe_validators = super::do_storage_call(online_client, &storage_key, at_hash).await?;
Ok(maybe_validators)
}
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)
}

View File

@ -0,0 +1,24 @@
use color_eyre::Result;
use subxt::{
utils::H256,
client::OnlineClient,
};
use crate::{
casper_network::{
self,
runtime_types::{frame_system::AccountInfo, pallet_balances::types::AccountData},
},
CasperConfig,
};
pub async fn balance(
online_client: &OnlineClient<CasperConfig>,
at_hash: Option<&H256>,
account: &[u8; 32],
) -> Result<Option<AccountInfo<u32, AccountData<u128>>>> {
let account_id = super::convert_array_to_account_id(account);
let storage_key = casper_network::storage().system().account(account_id);
let maybe_balance = super::do_storage_call(online_client, &storage_key, at_hash).await?;
Ok(maybe_balance)
}

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

View File

@ -112,8 +112,14 @@ 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)?;
}
Ok(())
}

View File

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

View File

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

View File

@ -2,8 +2,18 @@ mod era;
mod extrinsics;
mod log;
mod account;
mod peer;
mod session;
mod nominator;
mod staking;
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 staking::UnlockChunk;
pub use staking::RewardDestination;

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

@ -0,0 +1,8 @@
use codec::Decode;
use serde::{Serialize, Deserialize};
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, Decode)]
pub struct Nominator {
pub who: String,
pub value: u128,
}

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

@ -0,0 +1,12 @@
use subxt::utils::H256;
use codec::Decode;
use serde::{Serialize, Deserialize};
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, Decode)]
#[serde(rename_all = "camelCase")]
pub struct PeerInformation {
pub peer_id: String,
pub roles: String,
pub best_hash: H256,
pub best_number: u32,
}

10
src/types/points.rs Normal file
View 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
View File

@ -0,0 +1,8 @@
use codec::Decode;
use serde::{Serialize, Deserialize};
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, Decode)]
pub struct SessionKeyInfo {
pub key: String,
pub is_stored: bool,
}

19
src/types/staking.rs Normal file
View File

@ -0,0 +1,19 @@
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(Default, Debug, Clone, PartialEq, Eq, Display, Serialize, Deserialize)]
pub enum RewardDestination {
#[default]
None,
Staked,
Stash,
Account([u8; 32]),
Controller,
}

View File

@ -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
View 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