added ability to change reward destination from validator tab
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
This commit is contained in:
parent
756a1089f6
commit
ac67bb4310
@ -2,7 +2,7 @@
|
|||||||
name = "ghost-eye"
|
name = "ghost-eye"
|
||||||
authors = ["str3tch <stretch@ghostchain.io>"]
|
authors = ["str3tch <stretch@ghostchain.io>"]
|
||||||
description = "Application for interacting with Casper/Ghost nodes that are exposing RPC only to the localhost"
|
description = "Application for interacting with Casper/Ghost nodes that are exposing RPC only to the localhost"
|
||||||
version = "0.3.50"
|
version = "0.3.51"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
homepage = "https://git.ghostchain.io/ghostchain"
|
homepage = "https://git.ghostchain.io/ghostchain"
|
||||||
repository = "https://git.ghostchain.io/ghostchain/ghost-eye"
|
repository = "https://git.ghostchain.io/ghostchain/ghost-eye"
|
||||||
|
@ -29,6 +29,7 @@ mod chill_popup;
|
|||||||
mod unbond_popup;
|
mod unbond_popup;
|
||||||
mod rebond_popup;
|
mod rebond_popup;
|
||||||
mod withdraw_popup;
|
mod withdraw_popup;
|
||||||
|
mod payee_popup;
|
||||||
|
|
||||||
use stash_details::StashDetails;
|
use stash_details::StashDetails;
|
||||||
use staking_details::StakingDetails;
|
use staking_details::StakingDetails;
|
||||||
@ -48,6 +49,7 @@ use chill_popup::ChillPopup;
|
|||||||
use unbond_popup::UnbondPopup;
|
use unbond_popup::UnbondPopup;
|
||||||
use rebond_popup::RebondPopup;
|
use rebond_popup::RebondPopup;
|
||||||
use withdraw_popup::WithdrawPopup;
|
use withdraw_popup::WithdrawPopup;
|
||||||
|
use payee_popup::PayeePopup;
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||||
pub enum CurrentTab {
|
pub enum CurrentTab {
|
||||||
@ -66,6 +68,7 @@ pub enum CurrentTab {
|
|||||||
UnbondPopup,
|
UnbondPopup,
|
||||||
RebondPopup,
|
RebondPopup,
|
||||||
WithdrawPopup,
|
WithdrawPopup,
|
||||||
|
PayeePopup,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait PartialComponent: Component {
|
pub trait PartialComponent: Component {
|
||||||
@ -104,6 +107,7 @@ impl Default for Validator {
|
|||||||
Box::new(UnbondPopup::default()),
|
Box::new(UnbondPopup::default()),
|
||||||
Box::new(RebondPopup::default()),
|
Box::new(RebondPopup::default()),
|
||||||
Box::new(WithdrawPopup::default()),
|
Box::new(WithdrawPopup::default()),
|
||||||
|
Box::new(PayeePopup::default()),
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -257,6 +261,13 @@ impl Component for Validator {
|
|||||||
component.set_active(self.current_tab.clone());
|
component.set_active(self.current_tab.clone());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
KeyCode::Char('I') => {
|
||||||
|
self.previous_tab = self.current_tab;
|
||||||
|
self.current_tab = CurrentTab::PayeePopup;
|
||||||
|
for component in self.components.iter_mut() {
|
||||||
|
component.set_active(self.current_tab.clone());
|
||||||
|
}
|
||||||
|
},
|
||||||
_ => {
|
_ => {
|
||||||
for component in self.components.iter_mut() {
|
for component in self.components.iter_mut() {
|
||||||
component.handle_key_event(key)?;
|
component.handle_key_event(key)?;
|
||||||
|
365
src/components/validator/payee_popup.rs
Normal file
365
src/components/validator/payee_popup.rs
Normal file
@ -0,0 +1,365 @@
|
|||||||
|
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind};
|
||||||
|
use color_eyre::Result;
|
||||||
|
use ratatui::{
|
||||||
|
layout::{Alignment, Constraint, Flex, Layout, Position, Rect},
|
||||||
|
text::Text,
|
||||||
|
widgets::{Block, Cell, Clear, Paragraph, Row, Table, TableState},
|
||||||
|
Frame,
|
||||||
|
};
|
||||||
|
use subxt::ext::sp_core::crypto::{
|
||||||
|
ByteArray, Ss58Codec, Ss58AddressFormat, AccountId32,
|
||||||
|
};
|
||||||
|
use tokio::sync::mpsc::UnboundedSender;
|
||||||
|
use std::sync::mpsc::Sender;
|
||||||
|
|
||||||
|
use super::{Component, PartialComponent, CurrentTab};
|
||||||
|
use crate::{
|
||||||
|
action::Action,
|
||||||
|
config::Config,
|
||||||
|
palette::StylePalette,
|
||||||
|
types::{ActionLevel, ActionTarget, RewardDestination},
|
||||||
|
widgets::{Input, InputRequest},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct PayeePopup {
|
||||||
|
is_active: bool,
|
||||||
|
action_tx: Option<UnboundedSender<Action>>,
|
||||||
|
network_tx: Option<Sender<Action>>,
|
||||||
|
table_state: TableState,
|
||||||
|
stash_secret_seed: [u8; 32],
|
||||||
|
stash_account_id: [u8; 32],
|
||||||
|
proposed_account_id: Option<[u8; 32]>,
|
||||||
|
is_bonded: bool,
|
||||||
|
is_account_chosen: bool,
|
||||||
|
is_input_active: bool,
|
||||||
|
address: Input,
|
||||||
|
possible_payee_options: &'static [(&'static str, &'static str)],
|
||||||
|
current_reward_destination: RewardDestination,
|
||||||
|
palette: StylePalette
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for PayeePopup {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PayeePopup {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
is_active: false,
|
||||||
|
stash_secret_seed: [0u8; 32],
|
||||||
|
stash_account_id: [0u8; 32],
|
||||||
|
proposed_account_id: None,
|
||||||
|
action_tx: None,
|
||||||
|
network_tx: None,
|
||||||
|
table_state: TableState::new(),
|
||||||
|
is_bonded: false,
|
||||||
|
is_account_chosen: false,
|
||||||
|
is_input_active: false,
|
||||||
|
address: Input::new(String::new()),
|
||||||
|
current_reward_destination: Default::default(),
|
||||||
|
possible_payee_options: &[
|
||||||
|
("Re-stake", "(pay into the stash account, increasing the amount at stake accordingly)"),
|
||||||
|
("Stake", "(pay into the stash account, not increasing the amount at stake)"),
|
||||||
|
("Account", "(pay into a specified account different from stash)"),
|
||||||
|
("None", "(refuse to receive all rewards from staking)"),
|
||||||
|
],
|
||||||
|
palette: StylePalette::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn close_popup(&mut self) {
|
||||||
|
self.is_active = false;
|
||||||
|
if let Some(action_tx) = &self.action_tx {
|
||||||
|
let _ = action_tx.send(Action::ClosePopup);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_index_from_destination(&mut self) -> usize {
|
||||||
|
let (index, address) = match self.current_reward_destination {
|
||||||
|
RewardDestination::Staked => (0, Default::default()),
|
||||||
|
RewardDestination::Stash => (1, Default::default()),
|
||||||
|
RewardDestination::Account(account_id) => {
|
||||||
|
let address = AccountId32::from(account_id)
|
||||||
|
.to_ss58check_with_version(Ss58AddressFormat::custom(1996));
|
||||||
|
(2, address)
|
||||||
|
},
|
||||||
|
RewardDestination::None => (3, Default::default()),
|
||||||
|
_ => (0, Default::default()),
|
||||||
|
};
|
||||||
|
self.address = Input::new(address);
|
||||||
|
index
|
||||||
|
}
|
||||||
|
|
||||||
|
fn move_to_row(&mut self, index: usize) {
|
||||||
|
self.table_state.select(Some(index));
|
||||||
|
self.is_account_chosen = index == 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_row(&mut self) {
|
||||||
|
let i = match self.table_state.selected() {
|
||||||
|
Some(i) => {
|
||||||
|
if i >= self.possible_payee_options.len() - 1 {
|
||||||
|
i
|
||||||
|
} else {
|
||||||
|
i + 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => 0,
|
||||||
|
};
|
||||||
|
self.move_to_row(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fn previous_row(&mut self) {
|
||||||
|
let i = match self.table_state.selected() {
|
||||||
|
Some(i) => {
|
||||||
|
if i == 0 {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
i - 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => 0
|
||||||
|
};
|
||||||
|
self.move_to_row(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn log_event(&mut self, message: String, level: ActionLevel) {
|
||||||
|
if let Some(action_tx) = &self.action_tx {
|
||||||
|
let _ = action_tx.send(Action::EventLog(
|
||||||
|
message,
|
||||||
|
level,
|
||||||
|
ActionTarget::ValidatorLog));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn trigger_address_input(&mut self) {
|
||||||
|
self.is_input_active = !self.is_input_active;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn submit_new_input(&mut self) {
|
||||||
|
match AccountId32::from_ss58check_with_version(self.address.value()) {
|
||||||
|
Ok((account_id, format)) => {
|
||||||
|
if format != Ss58AddressFormat::custom(1996) {
|
||||||
|
self.log_event(
|
||||||
|
format!("provided public address for {} is not part of Casper/Ghost ecosystem", self.address.value()),
|
||||||
|
ActionLevel::Error);
|
||||||
|
}
|
||||||
|
let seed_vec = account_id.to_raw_vec();
|
||||||
|
let mut account_id = [0u8; 32];
|
||||||
|
account_id.copy_from_slice(&seed_vec);
|
||||||
|
|
||||||
|
self.proposed_account_id = Some(account_id);
|
||||||
|
self.submit_new_payee();
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
self.log_event(
|
||||||
|
format!("could not create valid account id from {}", self.address.value()),
|
||||||
|
ActionLevel::Error);
|
||||||
|
self.proposed_account_id = None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fn submit_new_payee(&mut self) {
|
||||||
|
if let Some(index) = self.table_state.selected() {
|
||||||
|
let new_destination = match index {
|
||||||
|
0 => RewardDestination::Staked,
|
||||||
|
1 => RewardDestination::Stash,
|
||||||
|
2 => {
|
||||||
|
let account_id = self.proposed_account_id
|
||||||
|
.expect("checked before in submit_new_input; qed");
|
||||||
|
RewardDestination::Account(account_id)
|
||||||
|
}
|
||||||
|
3 => RewardDestination::None,
|
||||||
|
_ => RewardDestination::Staked,
|
||||||
|
};
|
||||||
|
|
||||||
|
if !self.is_bonded {
|
||||||
|
self.log_event(
|
||||||
|
"no bond detected, stake minimum bond amount first".to_string(),
|
||||||
|
ActionLevel::Warn);
|
||||||
|
} else if new_destination == self.current_reward_destination {
|
||||||
|
self.log_event(
|
||||||
|
"same destination choosen, no need for transaction".to_string(),
|
||||||
|
ActionLevel::Warn);
|
||||||
|
} else {
|
||||||
|
if let Some(network_tx) = &self.network_tx {
|
||||||
|
let _ = network_tx.send(Action::SetPayee(
|
||||||
|
self.stash_secret_seed,
|
||||||
|
new_destination,
|
||||||
|
ActionTarget::ValidatorLog));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(action_tx) = &self.action_tx {
|
||||||
|
let _ = action_tx.send(Action::ClosePopup);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn enter_char(&mut self, new_char: char) {
|
||||||
|
let _ = self.address.handle(InputRequest::InsertChar(new_char));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_char(&mut self) {
|
||||||
|
let _ = self.address.handle(InputRequest::DeletePrevChar);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn move_cursor_right(&mut self) {
|
||||||
|
let _ = self.address.handle(InputRequest::GoToNextChar);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn move_cursor_left(&mut self) {
|
||||||
|
let _ = self.address.handle(InputRequest::GoToPrevChar);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialComponent for PayeePopup {
|
||||||
|
fn set_active(&mut self, current_tab: CurrentTab) {
|
||||||
|
match current_tab {
|
||||||
|
CurrentTab::PayeePopup => self.is_active = true,
|
||||||
|
_ => {
|
||||||
|
self.is_active = false;
|
||||||
|
self.is_account_chosen = false;
|
||||||
|
self.is_input_active = false;
|
||||||
|
|
||||||
|
let index = self.parse_index_from_destination();
|
||||||
|
self.move_to_row(index);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for PayeePopup {
|
||||||
|
fn register_network_handler(&mut self, tx: Sender<Action>) -> Result<()> {
|
||||||
|
self.network_tx = Some(tx);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn register_action_handler(&mut self, tx: UnboundedSender<Action>) -> Result<()> {
|
||||||
|
self.action_tx = Some(tx);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn register_config_handler(&mut self, config: Config) -> Result<()> {
|
||||||
|
if let Some(style) = config.styles.get(&crate::app::Mode::Wallet) {
|
||||||
|
self.palette.with_normal_style(style.get("normal_style").copied());
|
||||||
|
self.palette.with_normal_border_style(style.get("normal_border_style").copied());
|
||||||
|
self.palette.with_normal_title_style(style.get("normal_title_style").copied());
|
||||||
|
self.palette.with_popup_style(style.get("popup_style").copied());
|
||||||
|
self.palette.with_popup_title_style(style.get("popup_title_style").copied());
|
||||||
|
self.palette.with_highlight_style(style.get("highlight_style").copied());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_key_event(&mut self, key: KeyEvent) -> Result<Option<Action>> {
|
||||||
|
if self.is_active && key.kind == KeyEventKind::Press {
|
||||||
|
if self.is_input_active {
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Enter => self.submit_new_input(),
|
||||||
|
KeyCode::Char(to_insert) => self.enter_char(to_insert),
|
||||||
|
KeyCode::Backspace => self.delete_char(),
|
||||||
|
KeyCode::Left => self.move_cursor_left(),
|
||||||
|
KeyCode::Right => self.move_cursor_right(),
|
||||||
|
KeyCode::Esc => self.trigger_address_input(),
|
||||||
|
_ => {},
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Enter if !self.is_account_chosen => self.submit_new_payee(),
|
||||||
|
KeyCode::Enter if self.is_account_chosen => self.trigger_address_input(),
|
||||||
|
KeyCode::Up | KeyCode::Char('k') => self.previous_row(),
|
||||||
|
KeyCode::Down | KeyCode::Char('j') => self.next_row(),
|
||||||
|
KeyCode::Esc => self.close_popup(),
|
||||||
|
_ => {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, action: Action) -> Result<Option<Action>> {
|
||||||
|
match action {
|
||||||
|
Action::SetStashAccount(account_id) => self.stash_account_id = account_id,
|
||||||
|
Action::SetStashSecret(secret_seed) => self.stash_secret_seed = secret_seed,
|
||||||
|
Action::SetIsBonded(is_bonded, account_id) if self.stash_account_id == account_id =>
|
||||||
|
self.is_bonded = is_bonded,
|
||||||
|
Action::SetStakingPayee(reward_destination, account_id) if self.stash_account_id == account_id => {
|
||||||
|
let destination_changed = self.current_reward_destination != reward_destination;
|
||||||
|
self.current_reward_destination = reward_destination;
|
||||||
|
if destination_changed || self.table_state.selected().is_none() {
|
||||||
|
let index = self.parse_index_from_destination();
|
||||||
|
self.move_to_row(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
|
||||||
|
if self.is_active {
|
||||||
|
let (border_style, border_type) = self.palette.create_popup_style();
|
||||||
|
let size = area.as_size();
|
||||||
|
let input_area = Rect::new(size.width / 2, size.height / 2, 51, 3);
|
||||||
|
|
||||||
|
let table = Table::new(
|
||||||
|
self.possible_payee_options
|
||||||
|
.iter()
|
||||||
|
.map(|data| {
|
||||||
|
Row::new(vec![
|
||||||
|
Cell::from(Text::from(data.0.to_string()).alignment(Alignment::Left)),
|
||||||
|
Cell::from(Text::from(data.1.to_string()).alignment(Alignment::Left)),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
[Constraint::Length(8), Constraint::Min(0)]
|
||||||
|
)
|
||||||
|
.highlight_style(self.palette.create_highlight_style())
|
||||||
|
.column_spacing(1)
|
||||||
|
.block(Block::bordered()
|
||||||
|
.border_style(border_style)
|
||||||
|
.border_type(border_type)
|
||||||
|
.title_style(self.palette.create_popup_title_style())
|
||||||
|
.title_alignment(Alignment::Right)
|
||||||
|
.title("Select reward destination"));
|
||||||
|
|
||||||
|
let v = Layout::vertical([Constraint::Max(6)]).flex(Flex::Center);
|
||||||
|
let h = Layout::horizontal([Constraint::Max(83)]).flex(Flex::Center);
|
||||||
|
let [area] = v.areas(area);
|
||||||
|
let [area] = h.areas(area);
|
||||||
|
|
||||||
|
frame.render_widget(Clear, area);
|
||||||
|
frame.render_stateful_widget(table, area, &mut self.table_state);
|
||||||
|
|
||||||
|
if self.is_input_active {
|
||||||
|
let input_amount = Paragraph::new(self.address.value())
|
||||||
|
.block(Block::bordered()
|
||||||
|
.border_style(border_style)
|
||||||
|
.border_type(border_type)
|
||||||
|
.title_style(self.palette.create_popup_title_style())
|
||||||
|
.title_alignment(Alignment::Right)
|
||||||
|
.title("Destination account"));
|
||||||
|
|
||||||
|
let v = Layout::vertical([Constraint::Max(8)]).flex(Flex::Center);
|
||||||
|
let h = Layout::horizontal([Constraint::Max(51)]).flex(Flex::Center);
|
||||||
|
let [input_area] = v.areas(input_area);
|
||||||
|
let [input_area] = h.areas(input_area);
|
||||||
|
|
||||||
|
frame.render_widget(Clear, input_area);
|
||||||
|
frame.render_widget(input_amount, input_area);
|
||||||
|
|
||||||
|
frame.set_cursor_position(Position::new(
|
||||||
|
input_area.x + self.address.cursor() as u16 + 1,
|
||||||
|
input_area.y + 1
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user