From edd1b6d616779e9ef7027c367fdd7bb96e48ea1a Mon Sep 17 00:00:00 2001 From: Uncle Stretch Date: Sun, 2 Mar 2025 15:30:06 +0300 Subject: [PATCH] payee change added from wallet tab Signed-off-by: Uncle Stretch --- Cargo.toml | 2 +- src/action.rs | 7 +- .../nominator/current_validators.rs | 4 +- src/components/validator/staking_details.rs | 25 +- src/components/validator/stash_info.rs | 1 + src/components/wallet/add_account.rs | 15 +- .../wallet/add_address_book_record.rs | 12 +- src/components/wallet/bond_popup.rs | 12 +- src/components/wallet/mod.rs | 62 +-- src/components/wallet/payee_popup.rs | 374 ++++++++++++++++++ src/components/wallet/rename_account.rs | 15 +- .../wallet/rename_address_book_record.rs | 12 +- src/components/wallet/staking_ledger.rs | 42 +- src/components/wallet/transfer.rs | 9 +- src/network/mod.rs | 25 ++ src/network/predefined_calls.rs | 23 +- src/network/predefined_txs.rs | 32 +- src/network/raw_calls/staking.rs | 14 +- src/types/staking.rs | 4 +- 19 files changed, 605 insertions(+), 85 deletions(-) create mode 100644 src/components/wallet/payee_popup.rs diff --git a/Cargo.toml b/Cargo.toml index 09f2ef8..025c654 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.48" +version = "0.3.49" edition = "2021" homepage = "https://git.ghostchain.io/ghostchain" repository = "https://git.ghostchain.io/ghostchain/ghost-eye" diff --git a/src/action.rs b/src/action.rs index dba2947..5300c44 100644 --- a/src/action.rs +++ b/src/action.rs @@ -5,7 +5,9 @@ use subxt::utils::H256; use subxt::config::substrate::DigestItem; use crate::types::{ - ActionLevel, ActionTarget, CasperExtrinsicDetails, EraInfo, EraRewardPoints, Nominator, PeerInformation, SessionKeyInfo, UnlockChunk, SystemAccount + ActionLevel, ActionTarget, CasperExtrinsicDetails, EraInfo, EraRewardPoints, + Nominator, PeerInformation, SessionKeyInfo, UnlockChunk, SystemAccount, + RewardDestination, }; #[derive(Debug, Clone, PartialEq, Eq, Display, Serialize, Deserialize)] @@ -53,6 +55,7 @@ pub enum Action { TransferBalance(String, [u8; 32], u128), 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), @@ -104,6 +107,7 @@ pub enum Action { GetValidatorPrefs([u8; 32], bool), GetSlashingSpans([u8; 32], bool), GetValidatorLatestClaim([u8; 32], bool), + GetStakingPayee([u8; 32], bool), GetValidatorIsDisabled([u8; 32], bool), GetCurrentValidatorEraRewards, @@ -134,6 +138,7 @@ pub enum Action { SetValidatorEraSlash(u32, u128), SetValidatorEraUnlocking(Vec, [u8; 32]), SetValidatorLatestClaim(u32, [u8; 32]), + SetStakingPayee(RewardDestination, [u8; 32]), SetIsBonded(bool, [u8; 32]), SetStakedAmountRatio(Option, Option, [u8; 32]), SetStakedRatio(u128, u128, [u8; 32]), diff --git a/src/components/nominator/current_validators.rs b/src/components/nominator/current_validators.rs index 4834c73..6275734 100644 --- a/src/components/nominator/current_validators.rs +++ b/src/components/nominator/current_validators.rs @@ -207,7 +207,9 @@ impl CurrentValidators { .position(|item| item.account_id == account_id) { self.individual.swap(0, index); } - let current_index = self.table_state.selected().unwrap_or_default(); + let current_index = self.table_state + .selected() + .unwrap_or_default(); self.table_state.select(Some(current_index)); self.update_choosen_details(current_index); } diff --git a/src/components/validator/staking_details.rs b/src/components/validator/staking_details.rs index 6854386..e0bf108 100644 --- a/src/components/validator/staking_details.rs +++ b/src/components/validator/staking_details.rs @@ -6,8 +6,10 @@ use ratatui::{ 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, @@ -19,6 +21,7 @@ pub struct StakingDetails { staked_own: u128, staked_total: u128, stash: [u8; 32], + reward_destination: RewardDestination, } impl Default for StakingDetails { @@ -37,6 +40,7 @@ impl StakingDetails { staked_own: 0, staked_total: 0, stash: [0u8; 32], + reward_destination: Default::default(), } } @@ -45,6 +49,22 @@ impl StakingDetails { 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 { @@ -69,6 +89,8 @@ impl Component for StakingDetails { fn update(&mut self, action: Action) -> Result> { 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; @@ -82,6 +104,7 @@ impl Component for StakingDetails { 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![ @@ -109,7 +132,7 @@ impl Component for StakingDetails { .border_type(border_type) .title_alignment(Alignment::Right) .title_style(self.palette.create_title_style(false)) - .title("Staking details")); + .title(title)); frame.render_widget(table, place); diff --git a/src/components/validator/stash_info.rs b/src/components/validator/stash_info.rs index 0f6dbf5..039fb71 100644 --- a/src/components/validator/stash_info.rs +++ b/src/components/validator/stash_info.rs @@ -160,6 +160,7 @@ impl StashInfo { 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)); } } diff --git a/src/components/wallet/add_account.rs b/src/components/wallet/add_account.rs index af607f2..135f94a 100644 --- a/src/components/wallet/add_account.rs +++ b/src/components/wallet/add_account.rs @@ -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(), _ => {}, }; } diff --git a/src/components/wallet/add_address_book_record.rs b/src/components/wallet/add_address_book_record.rs index 16a1981..f3bf8b0 100644 --- a/src/components/wallet/add_address_book_record.rs +++ b/src/components/wallet/add_address_book_record.rs @@ -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(), _ => {}, }; } diff --git a/src/components/wallet/bond_popup.rs b/src/components/wallet/bond_popup.rs index 310f80f..8e81e02 100644 --- a/src/components/wallet/bond_popup.rs +++ b/src/components/wallet/bond_popup.rs @@ -50,9 +50,7 @@ impl BondPopup { palette: StylePalette::default(), } } -} -impl BondPopup { fn log_event(&mut self, message: String, level: ActionLevel) { if let Some(action_tx) = &self.action_tx { let _ = action_tx.send( @@ -60,6 +58,13 @@ impl BondPopup { } } + 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") @@ -150,6 +155,7 @@ impl Component for BondPopup { } Ok(()) } + fn handle_key_event(&mut self, key: KeyEvent) -> Result> { if self.is_active && key.kind == KeyEventKind::Press { match key.code { @@ -158,7 +164,7 @@ impl Component for BondPopup { 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(), _ => {}, }; } diff --git a/src/components/wallet/mod.rs b/src/components/wallet/mod.rs index c3ab839..ac004d2 100644 --- a/src/components/wallet/mod.rs +++ b/src/components/wallet/mod.rs @@ -21,6 +21,7 @@ mod rename_address_book_record; mod details; mod staking_ledger; mod bond_popup; +mod payee_popup; use balance::Balance; use transfer::Transfer; @@ -35,6 +36,7 @@ use rename_address_book_record::RenameAddressBookRecord; use details::AccountDetails; use staking_ledger::StakingLedger; use bond_popup::BondPopup; +use payee_popup::PayeePopup; use super::Component; use crate::{action::Action, app::Mode, config::Config}; @@ -52,6 +54,7 @@ pub enum CurrentTab { Transfer, AccountDetails, BondPopup, + PayeePopup, } pub trait PartialComponent: Component { @@ -85,6 +88,7 @@ impl Default for Wallet { Box::new(Transfer::default()), Box::new(AccountDetails::default()), Box::new(BondPopup::default()), + Box::new(PayeePopup::default()), ], } } @@ -142,68 +146,40 @@ impl Component for Wallet { CurrentTab::Transfer | CurrentTab::AccountDetails | CurrentTab::BondPopup | - CurrentTab::AddAddressBookRecord => match key.code { - KeyCode::Esc => { - self.current_tab = self.previous_tab; - for component in self.components.iter_mut() { - component.set_active(self.current_tab.clone()); - } - }, - _ => { + CurrentTab::PayeePopup | + 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('B') => { self.previous_tab = self.current_tab; self.current_tab = CurrentTab::BondPopup; - 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('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('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)?; @@ -221,14 +197,6 @@ impl Component for Wallet { self.current_tab = CurrentTab::Accounts; self.previous_tab = CurrentTab::Accounts; }, - Action::UpdateAccountName(_) | Action::NewAccount(_) => { - self.previous_tab = self.current_tab; - self.current_tab = CurrentTab::Accounts; - }, - Action::UpdateAddressBookRecord(_) | Action::NewAddressBookRecord(_, _) => { - self.previous_tab = self.current_tab; - self.current_tab = CurrentTab::AddressBook; - }, Action::RenameAccount(_) => { self.previous_tab = self.current_tab; self.current_tab = CurrentTab::RenameAccount; @@ -245,9 +213,7 @@ impl Component for Wallet { self.previous_tab = self.current_tab; self.current_tab = CurrentTab::AccountDetails; }, - Action::ClosePopup => { - self.current_tab = self.previous_tab; - } + Action::ClosePopup => self.current_tab = self.previous_tab, _ => {} } for component in self.components.iter_mut() { @@ -287,6 +253,6 @@ pub fn account_layout(area: Rect) -> [Rect; 4] { Constraint::Max(4), Constraint::Min(0), Constraint::Max(7), - Constraint::Max(5), + Constraint::Max(6), ]).areas(place) } diff --git a/src/components/wallet/payee_popup.rs b/src/components/wallet/payee_popup.rs new file mode 100644 index 0000000..5dd7aa9 --- /dev/null +++ b/src/components/wallet/payee_popup.rs @@ -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>, + network_tx: Option>, + 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) -> 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()); + self.palette.with_highlight_style(style.get("highlight_style").copied()); + } + Ok(()) + } + + fn handle_key_event(&mut self, key: KeyEvent) -> Result> { + 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> { + 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::>(), + [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(()) + } +} diff --git a/src/components/wallet/rename_account.rs b/src/components/wallet/rename_account.rs index 912c34e..a821092 100644 --- a/src/components/wallet/rename_account.rs +++ b/src/components/wallet/rename_account.rs @@ -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(), _ => {}, }; } diff --git a/src/components/wallet/rename_address_book_record.rs b/src/components/wallet/rename_address_book_record.rs index 286deef..8d7295a 100644 --- a/src/components/wallet/rename_address_book_record.rs +++ b/src/components/wallet/rename_address_book_record.rs @@ -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(), _ => {}, }; } diff --git a/src/components/wallet/staking_ledger.rs b/src/components/wallet/staking_ledger.rs index 7e0fe48..c287798 100644 --- a/src/components/wallet/staking_ledger.rs +++ b/src/components/wallet/staking_ledger.rs @@ -5,14 +5,13 @@ use ratatui::{ 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::{ - widgets::DotSpinner, - action::Action, - config::Config, - palette::StylePalette, + action::Action, config::Config, palette::StylePalette, + types::RewardDestination, widgets::DotSpinner, }; #[derive(Debug)] @@ -23,6 +22,7 @@ pub struct StakingLedger { network_tx: Option>, total_staked: Option, active_staked: Option, + reward_destination: RewardDestination, palette: StylePalette } @@ -44,6 +44,7 @@ impl StakingLedger { network_tx: None, total_staked: None, active_staked: None, + reward_destination: Default::default(), palette: StylePalette::default(), } } @@ -52,6 +53,7 @@ impl StakingLedger { 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)); } } @@ -67,6 +69,21 @@ impl StakingLedger { } } + 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() @@ -106,7 +123,10 @@ impl Component for StakingLedger { fn update(&mut self, action: Action) -> Result> { 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::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; @@ -124,20 +144,24 @@ impl Component for StakingLedger { let table = Table::new( [ Row::new(vec![ - Cell::from(Text::from("Bond ready: ".to_string()).alignment(Alignment::Left)), + 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("total: ".to_string()).alignment(Alignment::Left)), + 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("active:".to_string()).alignment(Alignment::Left)), Cell::from(Text::from(self.prepare_u128(self.active_staked)).alignment(Alignment::Right)), ]), ], [ - Constraint::Max(10), + Constraint::Max(12), Constraint::Min(14), ] ) diff --git a/src/components/wallet/transfer.rs b/src/components/wallet/transfer.rs index 60f5b82..086c1a4 100644 --- a/src/components/wallet/transfer.rs +++ b/src/components/wallet/transfer.rs @@ -59,6 +59,13 @@ 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( @@ -206,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(), _ => {}, }; } diff --git a/src/network/mod.rs b/src/network/mod.rs index bcac64d..e2a8814 100644 --- a/src/network/mod.rs +++ b/src/network/mod.rs @@ -117,6 +117,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_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() { @@ -128,6 +129,7 @@ impl Network { 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?; } @@ -208,6 +210,10 @@ impl Network { 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 @@ -449,6 +455,25 @@ impl Network { } 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(()) + } _ => Ok(()) } } diff --git a/src/network/predefined_calls.rs b/src/network/predefined_calls.rs index 720a4f9..64bdf12 100644 --- a/src/network/predefined_calls.rs +++ b/src/network/predefined_calls.rs @@ -13,8 +13,8 @@ use subxt::{ use crate::{ action::Action, - casper_network::runtime_types::sp_consensus_slots, - types::{EraInfo, EraRewardPoints, Nominator, SessionKeyInfo, UnlockChunk, SystemAccount}, + casper_network::runtime_types::{pallet_staking::RewardDestination, sp_consensus_slots}, + types::{EraInfo, EraRewardPoints, Nominator, SessionKeyInfo, SystemAccount, UnlockChunk}, CasperAccountId, CasperConfig }; @@ -581,3 +581,22 @@ pub async fn get_validator_latest_claim( Ok(()) } + +pub async fn get_account_payee( + action_tx: &UnboundedSender, + api: &OnlineClient, + 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(()) +} diff --git a/src/network/predefined_txs.rs b/src/network/predefined_txs.rs index ffb8643..1a84224 100644 --- a/src/network/predefined_txs.rs +++ b/src/network/predefined_txs.rs @@ -10,7 +10,7 @@ use crate::{ action::Action, casper::{CasperConfig, CasperExtrinsicParamsBuilder}, casper_network::{self, runtime_types}, - types::{ActionLevel, ActionTarget}, + types::{ActionLevel, ActionTarget, RewardDestination}, }; pub async fn transfer_balance( @@ -231,6 +231,36 @@ pub async fn withdraw_unbonded( ).await } +pub async fn set_payee( + action_tx: &UnboundedSender, + api: &OnlineClient, + sender: &[u8; 32], + reward_destination: RewardDestination, + maybe_nonce: Option<&mut u32>, + log_target: ActionTarget, +) -> Result>> { + 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, api: &OnlineClient, diff --git a/src/network/raw_calls/staking.rs b/src/network/raw_calls/staking.rs index bf10d8d..95b3fe0 100644 --- a/src/network/raw_calls/staking.rs +++ b/src/network/raw_calls/staking.rs @@ -9,8 +9,7 @@ use crate::{ self, runtime_types::{ pallet_staking::{ - slashing::SlashingSpans, ActiveEraInfo, EraRewardPoints, - StakingLedger, ValidatorPrefs, + slashing::SlashingSpans, ActiveEraInfo, EraRewardPoints, RewardDestination, StakingLedger, ValidatorPrefs }, sp_arithmetic::per_things::Perbill, sp_staking::{Exposure, PagedExposureMetadata}, @@ -195,6 +194,17 @@ pub async fn slashing_spans( Ok(maybe_slashing_spans) } +pub async fn payee( + 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().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, ) -> Result { diff --git a/src/types/staking.rs b/src/types/staking.rs index 167b9ca..00b5c4b 100644 --- a/src/types/staking.rs +++ b/src/types/staking.rs @@ -11,7 +11,9 @@ pub struct UnlockChunk { #[derive(Default, Debug, Clone, PartialEq, Eq, Display, Serialize, Deserialize)] pub enum RewardDestination { #[default] + None, Staked, Stash, - Account(String), + Account([u8; 32]), + Controller, }