use color_eyre::Result; use crossterm::event::{KeyCode, KeyEvent, KeyEventKind}; use ratatui::{ layout::{Alignment, Constraint, Flex, Layout, Position, Rect}, widgets::{Block, Clear, Paragraph}, Frame, }; use tokio::sync::mpsc::UnboundedSender; use std::sync::mpsc::Sender; use subxt::ext::sp_core::crypto::{ ByteArray, Ss58Codec, Ss58AddressFormat, AccountId32, }; use super::{Component, PartialComponent, CurrentTab}; use crate::{ action::Action, config::Config, palette::StylePalette, types::{ActionLevel, ActionTarget}, widgets::{Input, InputRequest}, }; #[derive(Debug)] enum ReceiverOrAmount { Receiver, Amount, } #[derive(Debug)] pub struct Transfer { network_tx: Option>, action_tx: Option>, receiver_or_amount: ReceiverOrAmount, sender: String, receiver: Input, amount: Input, palette: StylePalette, is_active: bool, } impl Default for Transfer { fn default() -> Self { Self::new() } } impl Transfer { pub fn new() -> Self { Self { sender: Default::default(), network_tx: None, action_tx: None, receiver_or_amount: ReceiverOrAmount::Receiver, receiver: Input::new(String::new()), amount: Input::new(String::new()), palette: StylePalette::default(), is_active: false, } } 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::WalletLog)); } } fn submit_message(&mut self) { match self.receiver_or_amount { ReceiverOrAmount::Receiver => self.receiver_or_amount = ReceiverOrAmount::Amount, ReceiverOrAmount::Amount => if let Some(network_tx) = &self.network_tx { match AccountId32::from_ss58check_with_version(self.receiver.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.receiver.value()), ActionLevel::Error); }, Ok((account_id, format)) if format == Ss58AddressFormat::custom(1996) => { let seed_vec = account_id.to_raw_vec(); let mut account_id = [0u8; 32]; account_id.copy_from_slice(&seed_vec); 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::() { Ok(value) => { let amount = (value * 1_000_000_000_000_000_000.0) as u128; let _ = network_tx.send(Action::TransferBalance( self.sender.clone(), account_id, 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), } }, _ => self.log_event( format!("could not create valid account id from {}", self.receiver.value()), ActionLevel::Error), } } } } fn enter_char(&mut self, new_char: char) { match self.receiver_or_amount { ReceiverOrAmount::Receiver => { let _ = self.receiver.handle(InputRequest::InsertChar(new_char)); }, ReceiverOrAmount::Amount => { 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) { match self.receiver_or_amount { ReceiverOrAmount::Receiver => { let _ = self.receiver.handle(InputRequest::DeletePrevChar); }, ReceiverOrAmount::Amount => { let _ = self.amount.handle(InputRequest::DeletePrevChar); } } } fn move_cursor_right(&mut self) { match self.receiver_or_amount { ReceiverOrAmount::Receiver => { let _ = self.receiver.handle(InputRequest::GoToNextChar); }, ReceiverOrAmount::Amount => { let _ = self.amount.handle(InputRequest::GoToNextChar); } } } fn move_cursor_left(&mut self) { match self.receiver_or_amount { ReceiverOrAmount::Receiver => { let _ = self.receiver.handle(InputRequest::GoToPrevChar); }, ReceiverOrAmount::Amount => { let _ = self.amount.handle(InputRequest::GoToPrevChar); } } } } impl PartialComponent for Transfer { fn set_active(&mut self, current_tab: CurrentTab) { match current_tab { CurrentTab::Transfer => self.is_active = true, _ => { if self.is_active { self.is_active = false; self.receiver = Input::new(String::new()); self.amount = Input::new(String::new()); self.receiver_or_amount = ReceiverOrAmount::Receiver; } }, }; } } impl Component for Transfer { 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::Up => self.receiver_or_amount = ReceiverOrAmount::Receiver, KeyCode::Down => self.receiver_or_amount = ReceiverOrAmount::Amount, 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> { match action { Action::UsedAccount(_, seed) => self.sender = seed, Action::TransferTo(who) => { self.receiver = Input::new(who); self.receiver_or_amount = ReceiverOrAmount::Amount; } _ => {} }; Ok(None) } 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, 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()) .block(Block::bordered() .border_style(border_style) .border_type(border_type) .title_style(self.palette.create_popup_title_style()) .title_alignment(Alignment::Right) .title("Receiver")); let input_amount = 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("Amount to send")); 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); let [amount_area] = v.areas(amount_area); let [amount_area] = h.areas(amount_area); frame.render_widget(Clear, receiver_area); frame.render_widget(Clear, amount_area); frame.render_widget(input_receiver, receiver_area); frame.render_widget(input_amount, amount_area); match self.receiver_or_amount { ReceiverOrAmount::Receiver => { frame.set_cursor_position(Position::new( receiver_area.x + self.receiver.cursor() as u16 + 1, receiver_area.y + 1 )); }, ReceiverOrAmount::Amount => { frame.set_cursor_position(Position::new( amount_area.x + self.amount.cursor() as u16 + 1, amount_area.y + 1 )); } } } Ok(()) } }