diff --git a/Cargo.toml b/Cargo.toml index cf175ea..459410e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "ghost-eye" authors = ["str3tch "] description = "Application for interacting with Casper/Ghost nodes that are exposing RPC only to the localhost" -version = "0.3.21" +version = "0.3.22" edition = "2021" [dependencies] diff --git a/src/action.rs b/src/action.rs index 5334622..51a8452 100644 --- a/src/action.rs +++ b/src/action.rs @@ -60,6 +60,7 @@ pub enum Action { ChillFrom([u8; 32]), UnbondFrom([u8; 32], u128), RebondFrom([u8; 32], u128), + WithdrawUnbondedFrom([u8; 32], u32), EventLog(String, ActionLevel, ActionTarget), NewBestBlock(u32), @@ -102,6 +103,7 @@ pub enum Action { GetIsStashBonded([u8; 32]), GetErasStakersOverview([u8; 32]), GetValidatorPrefs([u8; 32]), + GetSlashingSpans([u8; 32]), GetCurrentValidatorEraRewards, SetNodeName(Option), @@ -111,6 +113,8 @@ pub enum Action { SetChainVersion(Option), SetStashAccount([u8; 32]), SetStashSecret([u8; 32]), + SetSlashingSpansLength(usize), + SetUnlockingIsEmpty(bool), BestBlockInformation(H256, u32), FinalizedBlockInformation(H256, u32), diff --git a/src/components/validator/mod.rs b/src/components/validator/mod.rs index 5f87e3f..3f9e088 100644 --- a/src/components/validator/mod.rs +++ b/src/components/validator/mod.rs @@ -28,6 +28,7 @@ mod validate_popup; mod chill_popup; mod unbond_popup; mod rebond_popup; +mod withdraw_popup; use stash_details::StashDetails; use staking_details::StakingDetails; @@ -46,6 +47,7 @@ use validate_popup::ValidatePopup; use chill_popup::ChillPopup; use unbond_popup::UnbondPopup; use rebond_popup::RebondPopup; +use withdraw_popup::WithdrawPopup; #[derive(Debug, Copy, Clone, PartialEq)] pub enum CurrentTab { @@ -64,6 +66,7 @@ pub enum CurrentTab { ChillPopup, UnbondPopup, RebondPopup, + WithdrawPopup, } pub trait PartialComponent: Component { @@ -101,6 +104,7 @@ impl Default for Validator { Box::new(ChillPopup::default()), Box::new(UnbondPopup::default()), Box::new(RebondPopup::default()), + Box::new(WithdrawPopup::default()), ], } } @@ -165,6 +169,7 @@ impl Component for Validator { CurrentTab::ChillPopup | CurrentTab::UnbondPopup | CurrentTab::RebondPopup | + CurrentTab::WithdrawPopup | CurrentTab::PayoutPopup => match key.code { KeyCode::Esc => { self.current_tab = self.previous_tab; @@ -234,13 +239,20 @@ impl Component for Validator { component.set_active(self.current_tab.clone()); } }, - KeyCode::Char('D') => { + KeyCode::Char('E') => { self.previous_tab = self.current_tab; self.current_tab = CurrentTab::RebondPopup; for component in self.components.iter_mut() { component.set_active(self.current_tab.clone()); } }, + KeyCode::Char('W') => { + self.previous_tab = self.current_tab; + self.current_tab = CurrentTab::WithdrawPopup; + for component in self.components.iter_mut() { + component.set_active(self.current_tab.clone()); + } + }, _ => { for component in self.components.iter_mut() { component.handle_key_event(key)?; diff --git a/src/components/validator/stash_info.rs b/src/components/validator/stash_info.rs index 95379d0..3f750da 100644 --- a/src/components/validator/stash_info.rs +++ b/src/components/validator/stash_info.rs @@ -211,6 +211,7 @@ impl StashInfo { let _ = network_tx.send(Action::GetQueuedSessionKeys(account_id)); let _ = network_tx.send(Action::GetSessionKeys(account_id)); let _ = network_tx.send(Action::GetValidatorAllRewards(account_id)); + let _ = network_tx.send(Action::GetSlashingSpans(account_id)); } } diff --git a/src/components/validator/withdraw_popup.rs b/src/components/validator/withdraw_popup.rs new file mode 100644 index 0000000..237306a --- /dev/null +++ b/src/components/validator/withdraw_popup.rs @@ -0,0 +1,156 @@ +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>, + network_tx: Option>, + 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, + 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 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() { + 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::EventLog( + "Current stash account will be killed during this transaction".to_string(), + ActionLevel::Warn, + ActionTarget::ValidatorLog)); + 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) -> Result<()> { + self.network_tx = Some(tx); + Ok(()) + } + + fn register_action_handler(&mut self, tx: UnboundedSender) -> Result<()> { + self.action_tx = Some(tx); + Ok(()) + } + + fn register_config_handler(&mut self, config: Config) -> Result<()> { + if let Some(style) = config.styles.get(&crate::app::Mode::Wallet) { + self.palette.with_normal_style(style.get("normal_style").copied()); + self.palette.with_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> { + if self.is_active && key.kind == KeyEventKind::Press { + match key.code { + KeyCode::Enter => self.proceed(), + KeyCode::Esc => self.is_active = false, + _ => {}, + }; + } + Ok(None) + } + + fn update(&mut self, action: Action) -> Result> { + match action { + Action::SetStashSecret(secret_seed) => self.secret_seed = secret_seed, + Action::SetSlashingSpansLength(length) => self.slashing_spans_length = length as u32, + Action::SetStakedAmountRatio(_, active_balance) => self.active_balance = active_balance, + Action::SetUnlockingIsEmpty(is_empty) => self.unlocking_is_empty = is_empty, + Action::SetExistentialDeposit(existential_deposit) => self.existential_deposit = existential_deposit, + _ => {} + }; + 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(()) + } +} diff --git a/src/network/mod.rs b/src/network/mod.rs index b55c8f4..7713628 100644 --- a/src/network/mod.rs +++ b/src/network/mod.rs @@ -108,6 +108,7 @@ impl Network { 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_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?; @@ -190,6 +191,10 @@ impl Network { Ok(()) } + Action::GetSlashingSpans(stash) => { + self.store_stash_if_possible(stash); + predefined_calls::get_slashing_spans(&self.action_tx, &self.online_client_api, &stash).await + } Action::GetValidatorLedger(stash) => { self.store_stash_if_possible(stash); predefined_calls::get_validators_ledger(&self.action_tx, &self.online_client_api, &stash).await @@ -402,6 +407,24 @@ impl Network { } 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(()) + } _ => Ok(()) } } diff --git a/src/network/predefined_calls.rs b/src/network/predefined_calls.rs index e191e9c..d4fb164 100644 --- a/src/network/predefined_calls.rs +++ b/src/network/predefined_calls.rs @@ -435,6 +435,7 @@ pub async fn get_validators_ledger( if let Some(ledger) = maybe_ledger { action_tx.send(Action::SetStakedAmountRatio(ledger.total, ledger.active))?; + action_tx.send(Action::SetUnlockingIsEmpty(ledger.unlocking.0.is_empty()))?; for chunk in ledger.unlocking.0.iter() { action_tx.send(Action::SetValidatorEraUnlocking(chunk.era, chunk.value))?; } @@ -532,3 +533,16 @@ pub async fn get_minimal_validator_bond( action_tx.send(Action::SetMinValidatorBond(min_validator_bond))?; Ok(()) } + +pub async fn get_slashing_spans( + action_tx: &UnboundedSender, + api: &OnlineClient, + 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))?; + Ok(()) +} diff --git a/src/network/predefined_txs.rs b/src/network/predefined_txs.rs index f0fa0e4..cf856e4 100644 --- a/src/network/predefined_txs.rs +++ b/src/network/predefined_txs.rs @@ -210,6 +210,25 @@ pub async fn rebond( ).await } +pub async fn withdraw_unbonded( + action_tx: &UnboundedSender, + api: &OnlineClient, + sender: &[u8; 32], + spans: &u32, + maybe_nonce: Option<&mut u32>, +) -> Result>> { + 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 +} + async fn inner_sign_and_submit_then_watch( action_tx: &UnboundedSender, api: &OnlineClient, diff --git a/src/network/raw_calls/staking.rs b/src/network/raw_calls/staking.rs index 3eba416..6a62e88 100644 --- a/src/network/raw_calls/staking.rs +++ b/src/network/raw_calls/staking.rs @@ -8,7 +8,10 @@ use crate::{ casper_network::{ self, runtime_types::{ - pallet_staking::{ActiveEraInfo, EraRewardPoints, StakingLedger, ValidatorPrefs}, + pallet_staking::{ + slashing::SlashingSpans, ActiveEraInfo, EraRewardPoints, + StakingLedger, ValidatorPrefs, + }, sp_arithmetic::per_things::Perbill, sp_staking::{Exposure, PagedExposureMetadata}, }, @@ -180,3 +183,14 @@ pub async fn 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, + at_hash: Option<&H256>, + account: &[u8; 32], +) -> Result> { + let account_id = super::convert_array_to_account_id(account); + let storage_key = casper_network::storage().staking().slashing_spans(account_id); + let maybe_slashing_spans = super::do_storage_call(online_client, &storage_key, at_hash).await?; + Ok(maybe_slashing_spans) +}