fixes for active screen and wallet screen draft added
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
This commit is contained in:
		
							parent
							
								
									405061265b
								
							
						
					
					
						commit
						b3cebfa0a4
					
				| @ -21,13 +21,11 @@ human-panic = "2.0.2" | ||||
| json5 = "0.4.1" | ||||
| lazy_static = "1.5.0" | ||||
| libc = "0.2.159" | ||||
| primitive-types = "0.13.1" | ||||
| ratatui = { version = "0.28.1", features = ["serde", "macros"] } | ||||
| serde = { version = "1.0.210", features = ["derive"] } | ||||
| signal-hook = "0.3.17" | ||||
| sp-core = "34.0.0" | ||||
| strum = { version = "0.26.3", features = ["derive"] } | ||||
| subxt = { version = "0.38.0", features = ["jsonrpsee"] } | ||||
| subxt = { version = "0.38.0", features = ["substrate-compat"] } | ||||
| tokio = { version = "1.40.0", features = ["full"] } | ||||
| tokio-util = "0.7.12" | ||||
| tracing = "0.1.37" | ||||
|  | ||||
| @ -7,7 +7,7 @@ | ||||
|             "hover_border_style": "blue", | ||||
|             "normal_title_style": "blue", | ||||
|             "hover_title_style": "", | ||||
|             "tagged_style": "yellow italic", | ||||
|             "highlight_style": "yellow italic", | ||||
|         }, | ||||
|         "Explorer": { | ||||
|             "normal_style": "", | ||||
| @ -16,17 +16,33 @@ | ||||
|             "hover_border_style": "blue", | ||||
|             "normal_title_style": "blue", | ||||
|             "hover_title_style": "", | ||||
|             "tagged_style": "yellow bold", | ||||
|             "highlight_style": "yellow bold", | ||||
|             "scrollbar_style": "white on blue", | ||||
|         }, | ||||
|         "Wallet": { | ||||
|             "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", | ||||
|         } | ||||
|     }, | ||||
|     "keybindings": { | ||||
|         "Menu": { | ||||
|             "<q>": "Quit", | ||||
|             "<Ctrl-d>": "Quit", | ||||
|             "<Ctrl-c>": "Quit", | ||||
|             "<Ctrl-z>": "Suspend", | ||||
|         }, | ||||
|         "Explorer": { | ||||
|             "<q>": "Quit", | ||||
|             "<Ctrl-d>": "Quit", | ||||
|             "<Ctrl-c>": "Quit", | ||||
|             "<Ctrl-z>": "Suspend", | ||||
|         }, | ||||
|         "ExplorerActive": { | ||||
|         "Wallet": { | ||||
|             "<q>": "Quit", | ||||
|             "<Ctrl-d>": "Quit", | ||||
|             "<Ctrl-c>": "Quit", | ||||
| @ -38,11 +54,5 @@ | ||||
|             "<Ctrl-c>": "Quit", | ||||
|             "<Ctrl-z>": "Suspend", | ||||
|         }, | ||||
|         "EmptyActive": { | ||||
|             "<q>": "Quit", | ||||
|             "<Ctrl-d>": "Quit", | ||||
|             "<Ctrl-c>": "Quit", | ||||
|             "<Ctrl-z>": "Suspend", | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,14 +1,15 @@ | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use strum::Display; | ||||
| use primitive_types::H256; | ||||
| 
 | ||||
| use subxt::utils::H256; | ||||
| use subxt::config::substrate::DigestItem; | ||||
| 
 | ||||
| use crate::{ | ||||
|     CasperAccountId, 
 | ||||
|     types::{EraInfo, CasperExtrinsicDetails}, | ||||
| }; | ||||
| 
 | ||||
| use subxt::utils::AccountId32; | ||||
| 
 | ||||
| #[derive(Debug, Clone, PartialEq, Eq, Display, Serialize, Deserialize)] | ||||
| pub enum Action { | ||||
|     Tick, | ||||
| @ -22,6 +23,10 @@ pub enum Action { | ||||
|     Help, | ||||
| 
 | ||||
|     SetMode(crate::app::Mode), | ||||
|     SetActiveScreen(crate::app::Mode), | ||||
|     UsedExplorerBlock(Option<u32>), | ||||
|     UsedExplorerLog(Option<String>), | ||||
|     UsedAccount(AccountId32), | ||||
| 
 | ||||
|     NewBestBlock(u32), | ||||
|     NewBestHash(H256), | ||||
| @ -31,7 +36,7 @@ pub enum Action { | ||||
|     ExtrinsicsLength(u32, usize), | ||||
| 
 | ||||
|     GetBlockAuthor(H256, Vec<DigestItem>), | ||||
|     SetBlockAuthor(H256, Option<CasperAccountId>), | ||||
|     SetBlockAuthor(H256, String), | ||||
| 
 | ||||
|     GetNodeName, | ||||
|     GetSystemHealth, | ||||
| @ -52,10 +57,16 @@ pub enum Action { | ||||
|     SetChainName(Option<String>), | ||||
|     SetChainVersion(Option<String>), | ||||
| 
 | ||||
|     BestBlockInformation(H256, u32, Vec<CasperExtrinsicDetails>), | ||||
|     FinalizedBlockInformation(H256, u32, Vec<CasperExtrinsicDetails>), | ||||
|     BestBlockInformation(H256, u32), | ||||
|     FinalizedBlockInformation(H256, u32), | ||||
|     ExtrinsicsForBlock(u32, Vec<CasperExtrinsicDetails>), | ||||
|     SetActiveEra(EraInfo), | ||||
|     SetEpochProgress(u64, u64), | ||||
|     SetValidatorsForExplorer(Vec<CasperAccountId>), // TODO: change to BlockAuthor
 | ||||
|     SetPendingExtrinsicsLength(usize), // TODO: rename in oreder to match tx.pool
 | ||||
|     SetPendingExtrinsicsLength(usize), 
 | ||||
| 
 | ||||
|     GetTotalIssuance, | ||||
|     GetExistentialDeposit, | ||||
| 
 | ||||
|     SetExistentialDeposit(u128), | ||||
|     SetTotalIssuance(u128), | ||||
| } | ||||
|  | ||||
							
								
								
									
										20
									
								
								src/app.rs
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								src/app.rs
									
									
									
									
									
								
							| @ -11,14 +11,17 @@ use crate::{ | ||||
|     config::Config, | ||||
|     tui::{Event, Tui}, | ||||
|     components::{ | ||||
|         menu::Menu, version::Version, explorer::Explorer, empty::Empty, | ||||
|         health::Health, fps::FpsCounter, Component}, | ||||
|         menu::Menu, version::Version, explorer::Explorer, wallet::Wallet, 
 | ||||
|         empty::Empty, health::Health, fps::FpsCounter, Component, | ||||
|     }, | ||||
| }; | ||||
| 
 | ||||
| #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] | ||||
| pub enum Mode { | ||||
|     Menu, | ||||
|     Explorer, | ||||
|     Wallet, | ||||
|     WalletActive, | ||||
|     ExplorerActive, | ||||
|     Empty, | ||||
|     EmptyActive, | ||||
| @ -67,6 +70,7 @@ impl App { | ||||
|                 Box::new(Health::default()), | ||||
|                 Box::new(Version::default()), | ||||
|                 Box::new(Explorer::default()), | ||||
|                 Box::new(Wallet::default()), | ||||
|                 Box::new(Empty::default()), | ||||
|             ], | ||||
|             should_quite: false, | ||||
| @ -163,6 +167,7 @@ impl App { | ||||
|         self.network_tx.send(Action::GetGenesisHash)?; | ||||
|         self.network_tx.send(Action::GetChainName)?; | ||||
|         self.network_tx.send(Action::GetChainVersion)?; | ||||
|         self.network_tx.send(Action::GetExistentialDeposit)?; | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
| @ -226,7 +231,7 @@ impl App { | ||||
|             } | ||||
| 
 | ||||
|             match self.mode { | ||||
|                 Mode::Explorer | Mode::ExplorerActive => { | ||||
|                 Mode::Explorer => { | ||||
|                     if let Some(component) = self.components.get_mut(4) { | ||||
|                         if let Err(err) = component.draw(frame, frame.area()) { | ||||
|                             let _ = self | ||||
| @ -235,6 +240,15 @@ impl App { | ||||
|                         } | ||||
|                     } | ||||
|                 }, | ||||
|                 Mode::Wallet => { | ||||
|                     if let Some(component) = self.components.get_mut(5) { | ||||
|                         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()) { | ||||
|  | ||||
| @ -20,7 +20,8 @@ impl Config for CasperConfig { | ||||
|     type AssetId = u32; | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| /// A 32-byte cryptographic identifier. This is a simplified version of
 | ||||
| /// `sp_core::crypto::AccountId32`. 
 | ||||
| pub type CasperAccountId = subxt::utils::AccountId32; | ||||
| pub type CasperBlock = Block<CasperConfig, OnlineClient<CasperConfig>>; | ||||
| 
 | ||||
|  | ||||
| @ -1,4 +1,6 @@ | ||||
| use color_eyre::Result; | ||||
| use tokio::sync::mpsc::UnboundedSender; | ||||
| use crossterm::event::{KeyEvent, KeyCode}; | ||||
| use ratatui::{ | ||||
|     layout::{Alignment, Rect}, | ||||
|     text::Line, | ||||
| @ -14,6 +16,7 @@ use crate::{ | ||||
| #[derive(Debug, Clone, Default)] | ||||
| pub struct Empty { | ||||
|     is_active: bool, | ||||
|     action_tx: Option<UnboundedSender<Action>>, | ||||
| } | ||||
| 
 | ||||
| impl Empty { | ||||
| @ -84,27 +87,33 @@ impl Empty { | ||||
|         ] | ||||
|     } | ||||
| 
 | ||||
|     fn set_active(&mut self) -> Result<()> { | ||||
|         self.is_active = true; | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     fn unset_active(&mut self) -> Result<()> { | ||||
|     fn move_out(&mut self) -> Result<Option<Action>> { | ||||
|         self.is_active = false; | ||||
|         Ok(()) | ||||
|         Ok(Some(Action::SetActiveScreen(Mode::Menu))) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl Component for Empty { | ||||
|     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::SetMode(Mode::EmptyActive) if !self.is_active => self.set_active()?, | ||||
|             Action::SetMode(_) if self.is_active => self.unset_active()?, | ||||
|             Action::SetActiveScreen(Mode::Empty) => self.is_active = true, | ||||
|             _ => {} | ||||
|         }; | ||||
|         Ok(None) | ||||
|     } | ||||
| 
 | ||||
|     fn handle_key_event(&mut self, key: KeyEvent) -> Result<Option<Action>> { | ||||
|         match key.code { | ||||
|             KeyCode::Esc if self.is_active => self.move_out(), 
 | ||||
|             _ => Ok(None), | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { | ||||
|         let screen = super::screen_layout(area); | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										296
									
								
								src/components/explorer/block_explorer.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										296
									
								
								src/components/explorer/block_explorer.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,296 @@ | ||||
| use color_eyre::Result; | ||||
| use crossterm::event::{KeyCode, KeyEvent}; | ||||
| use ratatui::layout::{Constraint, Margin}; | ||||
| use ratatui::widgets::{Padding, Scrollbar, ScrollbarOrientation}; | ||||
| use ratatui::{ | ||||
|     text::Text, | ||||
|     layout::{Alignment, Rect}, 
 | ||||
|     widgets::{Block, ScrollbarState, Cell, Row, Table, TableState}, 
 | ||||
|     Frame | ||||
| }; | ||||
| use subxt::utils::H256; | ||||
| use tokio::sync::mpsc::UnboundedSender; | ||||
| 
 | ||||
| use super::{Component, PartialComponent, CurrentTab}; | ||||
| use crate::{ | ||||
|     action::Action, 
 | ||||
|     config::Config, 
 | ||||
|     palette::StylePalette, 
 | ||||
| }; | ||||
| 
 | ||||
| #[derive(Debug, Default)] | ||||
| struct BlockInfo { | ||||
|     block_number: u32, | ||||
|     finalized: bool, | ||||
| } | ||||
| 
 | ||||
| pub struct BlockExplorer { | ||||
|     is_active: bool, | ||||
|     action_tx: Option<UnboundedSender<Action>>, | ||||
|     blocks: std::collections::VecDeque<BlockInfo>, | ||||
|     block_headers: std::collections::HashMap<u32, H256>, | ||||
|     block_authors: std::collections::HashMap<H256, String>, | ||||
|     scroll_state: ScrollbarState, | ||||
|     table_state: TableState, | ||||
|     palette: StylePalette, | ||||
| } | ||||
| 
 | ||||
| impl Default for BlockExplorer { | ||||
|     fn default() -> Self { | ||||
|         Self::new() | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl BlockExplorer { | ||||
|     const MAX_BLOCKS: usize = 50; | ||||
| 
 | ||||
|     pub fn new() -> Self { | ||||
|         Self { | ||||
|             is_active: false, | ||||
|             blocks: Default::default(), | ||||
|             action_tx: None, | ||||
|             block_authors: Default::default(), | ||||
|             block_headers: Default::default(), | ||||
|             scroll_state: ScrollbarState::new(0), | ||||
|             table_state: TableState::new(), | ||||
|             palette: StylePalette::default(), | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fn update_latest_block_info( | ||||
|         &mut self, | ||||
|         hash: H256, | ||||
|         block_number: u32, | ||||
|     ) { | ||||
|         let front_block_number = self.blocks | ||||
|             .front() | ||||
|             .map(|block| block.block_number) | ||||
|             .unwrap_or_default(); | ||||
| 
 | ||||
|         if front_block_number < block_number { | ||||
|             self.blocks.push_front(BlockInfo { | ||||
|                 block_number, | ||||
|                 finalized: false, | ||||
|             }); | ||||
| 
 | ||||
|             self.block_headers.insert(block_number, hash); | ||||
|             if self.blocks.len() > Self::MAX_BLOCKS { | ||||
|                 if let Some(block) = self.blocks.pop_back() { | ||||
|                     if let Some(hash) = self.block_headers.remove(&block.block_number) { | ||||
|                         self.block_authors.remove(&hash); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             self.scroll_state = self.scroll_state.content_length(self.blocks.len()); | ||||
|             if self.table_state.selected().is_some() { | ||||
|                 self.next_row(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fn update_finalized_block_info( | ||||
|         &mut self, | ||||
|         header: H256, | ||||
|         block_number: u32, | ||||
|     ) { | ||||
|         for idx in 0..self.blocks.len() { | ||||
|             if self.blocks[idx].finalized { break; } | ||||
|             else if self.blocks[idx].block_number > block_number { continue; } | ||||
|             else { 
 | ||||
|                 self.block_headers.insert(block_number, header); | ||||
|                 self.blocks[idx].finalized = true; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fn send_used_explorer_block(&mut self, index: usize) { | ||||
|         if let Some(action_tx) = &self.action_tx { | ||||
|             let maybe_block_number = self.blocks.get(index).map(|info| info.block_number); | ||||
|             let _ = action_tx.send(Action::UsedExplorerBlock(maybe_block_number)); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fn first_row(&mut self) { | ||||
|         if self.blocks.len() > 0 { | ||||
|             self.table_state.select(Some(0)); | ||||
|             self.scroll_state = self.scroll_state.position(0); | ||||
|             self.send_used_explorer_block(0); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fn next_row(&mut self) { | ||||
|         let i = match self.table_state.selected() { | ||||
|             Some(i) => { | ||||
|                 if i >= self.blocks.len() - 1 { | ||||
|                     i | ||||
|                 } else { | ||||
|                     i + 1 | ||||
|                 } | ||||
|             }, | ||||
|             None => 0, | ||||
|         }; | ||||
|         self.table_state.select(Some(i)); | ||||
|         self.scroll_state = self.scroll_state.position(i); | ||||
|         self.send_used_explorer_block(i); | ||||
|     } | ||||
| 
 | ||||
|     fn last_row(&mut self) { | ||||
|         if self.blocks.len() > 0 { | ||||
|             let last = self.blocks.len() - 1; | ||||
|             self.table_state.select(Some(last)); | ||||
|             self.scroll_state = self.scroll_state.position(last); | ||||
|             self.send_used_explorer_block(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); | ||||
|         self.send_used_explorer_block(i); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl PartialComponent for BlockExplorer { | ||||
|     fn set_active(&mut self, current_tab: CurrentTab) { | ||||
|         match current_tab { | ||||
|             CurrentTab::Blocks => self.is_active = true, | ||||
|             CurrentTab::Extrinsics => self.is_active = false, | ||||
|             CurrentTab::Nothing => { | ||||
|                 self.is_active = false; | ||||
|                 self.table_state.select(None); | ||||
|                 self.scroll_state = self.scroll_state.position(0); | ||||
|                 self.send_used_explorer_block(usize::MAX); | ||||
|             }, | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl Component for BlockExplorer { | ||||
|     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::Explorer) { | ||||
|             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::BestBlockInformation(header, block_number) => self.update_latest_block_info(header, block_number), | ||||
|             Action::FinalizedBlockInformation(header, block_number) => self.update_finalized_block_info(header, block_number), | ||||
|             Action::SetBlockAuthor(header, author) => { | ||||
|                 let _ = self.block_authors.insert(header, author); | ||||
|             }, | ||||
|             _ => {}, | ||||
|         }; | ||||
|         Ok(None) | ||||
|     } | ||||
| 
 | ||||
|     fn handle_key_event(&mut self, key: KeyEvent) -> Result<Option<Action>> { | ||||
|         match key.code { | ||||
|             KeyCode::Char('k') | KeyCode::Up if self.is_active => self.previous_row(), | ||||
|             KeyCode::Char('j') | KeyCode::Down if self.is_active => self.next_row(), | ||||
|             KeyCode::Char('K') if self.is_active => self.first_row(), | ||||
|             KeyCode::Char('J') if self.is_active => self.last_row(), | ||||
|             _ => {}, | ||||
|         }; | ||||
| 
 | ||||
|         Ok(None) | ||||
|     } | ||||
| 
 | ||||
|     fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { | ||||
|         let [place, _] = super::explorer_scrollbars_layout(area); | ||||
| 
 | ||||
|         let (border_style, border_type) = self.palette.create_border_style(self.is_active); | ||||
|         let default_hash = H256::repeat_byte(69u8); | ||||
|         let default_author = "...".to_string(); | ||||
|         let rows = self.blocks | ||||
|             .iter() | ||||
|             .map(|info| { | ||||
|                 let header = self.block_headers | ||||
|                     .get(&info.block_number) | ||||
|                     .unwrap_or(&default_hash); | ||||
|                 let author = self.block_authors | ||||
|                     .get(&header) | ||||
|                     .unwrap_or(&default_author); | ||||
| 
 | ||||
|                 if info.finalized { | ||||
|                     Row::new(vec![ | ||||
|                         Cell::from(Text::from(info.block_number.to_string()).alignment(Alignment::Left)), | ||||
|                         Cell::from(Text::from(header.to_string()).alignment(Alignment::Center)), | ||||
|                         Cell::from(Text::from(author.clone()).alignment(Alignment::Right)), | ||||
|                     ]).style(self.palette.create_highlight_style()) | ||||
|                 } else { | ||||
|                     Row::new(vec![ | ||||
|                         Cell::from(Text::from(info.block_number.to_string()).alignment(Alignment::Left)), | ||||
|                         Cell::from(Text::from(header.to_string()).alignment(Alignment::Center)), | ||||
|                         Cell::from(Text::from(author.clone()).alignment(Alignment::Right)), | ||||
|                     ]) | ||||
|                 } | ||||
|             }) | ||||
|             .collect::<Vec<_>>(); | ||||
| 
 | ||||
|         let max_block_number_length = self.blocks | ||||
|             .front() | ||||
|             .map(|block| block.block_number) | ||||
|             .unwrap_or_default() | ||||
|             .checked_ilog10() | ||||
|             .unwrap_or(0) as u16 + 1; | ||||
| 
 | ||||
|         let table = Table::new( | ||||
|             rows, | ||||
|             [ | ||||
|                 Constraint::Max(max_block_number_length + 2), | ||||
|                 Constraint::Min(15), | ||||
|                 Constraint::Min(0), | ||||
|             ], | ||||
|         ) | ||||
|         .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("Blocks")); | ||||
| 
 | ||||
|         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(()) | ||||
|     } | ||||
| } | ||||
| @ -7,7 +7,7 @@ use ratatui::{ | ||||
|     Frame | ||||
| }; | ||||
| 
 | ||||
| use super::Component; | ||||
| use super::{Component, PartialComponent, CurrentTab}; | ||||
| use crate::{ | ||||
|     config::Config, | ||||
|     widgets::{BigText, PixelSize}, 
 | ||||
| @ -46,6 +46,10 @@ impl BlockTicker { | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl PartialComponent for BlockTicker { | ||||
|     fn set_active(&mut self, _current_tab: CurrentTab) {} | ||||
| } | ||||
| 
 | ||||
| impl Component for BlockTicker { | ||||
|     fn register_config_handler(&mut self, config: Config) -> Result<()> { | ||||
|         if let Some(style) = config.styles.get(&crate::app::Mode::Explorer) { | ||||
| @ -102,7 +106,7 @@ impl Component for BlockTicker { | ||||
|             let big_text = BigText::builder() | ||||
|                 .centered() | ||||
|                 .pixel_size(PixelSize::Quadrant) | ||||
|                 .style(self.palette.create_text_style(false)) | ||||
|                 .style(self.palette.create_basic_style(false)) | ||||
|                 .lines(vec![ | ||||
|                     text.into(), | ||||
|                 ]) | ||||
|  | ||||
| @ -6,7 +6,7 @@ use ratatui::{ | ||||
|     Frame, | ||||
| }; | ||||
| 
 | ||||
| use super::Component; | ||||
| use super::{Component, PartialComponent, CurrentTab}; | ||||
| use crate::{ | ||||
|     config::Config, | ||||
|     action::Action, 
 | ||||
| @ -34,6 +34,10 @@ impl CurrentEpoch { | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl PartialComponent for CurrentEpoch { | ||||
|     fn set_active(&mut self, _current_tab: CurrentTab) {} | ||||
| } | ||||
| 
 | ||||
| impl Component for CurrentEpoch { | ||||
|     fn register_config_handler(&mut self, config: Config) -> Result<()> { | ||||
|         if let Some(style) = config.styles.get(&crate::app::Mode::Explorer) { | ||||
| @ -97,7 +101,7 @@ impl Component for CurrentEpoch { | ||||
|             let big_text = BigText::builder() | ||||
|                 .centered() | ||||
|                 .pixel_size(PixelSize::Quadrant) | ||||
|                 .style(self.palette.create_text_style(false)) | ||||
|                 .style(self.palette.create_basic_style(false)) | ||||
|                 .lines(vec![ | ||||
|                     text.into(), | ||||
|                 ]) | ||||
|  | ||||
| @ -6,7 +6,7 @@ use ratatui::{ | ||||
|     Frame, | ||||
| }; | ||||
| 
 | ||||
| use super::Component; | ||||
| use super::{Component, PartialComponent, CurrentTab}; | ||||
| use crate::{ | ||||
|     config::Config, | ||||
|     action::Action, 
 | ||||
| @ -35,6 +35,10 @@ impl CurrentEra { | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl PartialComponent for CurrentEra { | ||||
|     fn set_active(&mut self, _current_tab: CurrentTab) {} | ||||
| } | ||||
| 
 | ||||
| impl Component for CurrentEra { | ||||
|     fn register_config_handler(&mut self, config: Config) -> Result<()> { | ||||
|         if let Some(style) = config.styles.get(&crate::app::Mode::Explorer) { | ||||
| @ -111,7 +115,7 @@ impl Component for CurrentEra { | ||||
|             let big_text = BigText::builder() | ||||
|                 .centered() | ||||
|                 .pixel_size(PixelSize::Quadrant) | ||||
|                 .style(self.palette.create_text_style(false)) | ||||
|                 .style(self.palette.create_basic_style(false)) | ||||
|                 .lines(vec![ | ||||
|                     text.into(), | ||||
|                 ]) | ||||
|  | ||||
| @ -1,7 +1,6 @@ | ||||
| use std::collections::{HashMap, VecDeque}; | ||||
| use color_eyre::Result; | ||||
| use crossterm::event::{KeyCode, KeyEvent}; | ||||
| use primitive_types::H256; | ||||
| use ratatui::{ | ||||
|     layout::{Alignment, Rect}, 
 | ||||
|     prelude::*, 
 | ||||
| @ -9,7 +8,8 @@ use ratatui::{ | ||||
|     widgets::{Block, BorderType, Paragraph}, | ||||
|     Frame | ||||
| }; | ||||
| use sp_core::crypto::{AccountId32, Ss58Codec, Ss58AddressFormat}; | ||||
| use subxt::ext::sp_core::crypto::{Ss58Codec, Ss58AddressFormat}; | ||||
| use subxt::utils::H256; | ||||
| use codec::Decode; | ||||
| 
 | ||||
| use super::Component; | ||||
| @ -126,9 +126,10 @@ impl ExplorerBlocks { | ||||
|             .authors | ||||
|             .get(&hash) | ||||
|             .map_or(String::from("..."), |author| { | ||||
|                 let extended_author = AccountId32::decode(&mut author.as_ref()) | ||||
|                 let extended_author = CasperAccountId::decode(&mut author.as_ref()) | ||||
|                     .expect("author should be valid AccountId32; qed"); | ||||
|                 extended_author.to_ss58check_with_version(Ss58AddressFormat::custom(1996)) | ||||
|                 let account_id = subxt::ext::sp_core::crypto::AccountId32::from(extended_author.0); | ||||
|                 account_id.to_ss58check_with_version(Ss58AddressFormat::custom(1996)) | ||||
|             }); | ||||
| 
 | ||||
|         if free_space < Self::LENGTH_OF_BLOCK_HASH + Self::LENGTH_OF_ADDRESS { | ||||
| @ -177,7 +178,7 @@ impl ExplorerBlocks { | ||||
| 
 | ||||
|         let normal_style = self.palette.create_text_style(false); | ||||
|         let active_style = self.palette.create_text_style(true); | ||||
|         let finalized_style = self.palette.create_tagged_style(); | ||||
|         let finalized_style = self.palette.create_highlight_style(); | ||||
| 
 | ||||
|         for (idx, current_block_info) in self.blocks.iter().skip(start_index).enumerate() { | ||||
|             if idx == total_length { break; } | ||||
| @ -459,7 +460,7 @@ impl Component for ExplorerBlocks { | ||||
|             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_tagged_style(style.get("tagged_style").copied()); | ||||
|             self.palette.with_highlight_style(style.get("highlight_style").copied()); | ||||
|         } | ||||
|         Ok(()) | ||||
|     } | ||||
| @ -499,7 +500,6 @@ impl Component for ExplorerBlocks { | ||||
| 
 | ||||
|         let (border_style_block, border_type_block) = self.palette.create_border_style(self.is_active && self.used_paragraph_index == 0); | ||||
|         let (border_style_extrinsics, border_type_extrinsics) = self.palette.create_border_style(self.is_active && self.used_paragraph_index == 1); | ||||
|         // TODO: never used, revisit
 | ||||
|         let (border_style_event, border_type_event) = self.palette.create_border_style(self.is_active && self.used_paragraph_index == 2); | ||||
| 
 | ||||
|         frame.render_widget(self.prepare_blocks_paragraph(blocks_place, border_style_block, border_type_block), blocks_place); | ||||
|  | ||||
							
								
								
									
										280
									
								
								src/components/explorer/extrinsic_explorer.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										280
									
								
								src/components/explorer/extrinsic_explorer.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,280 @@ | ||||
| use std::collections::{HashMap, VecDeque}; | ||||
| use std::usize; | ||||
| use color_eyre::Result; | ||||
| use crossterm::event::{KeyCode, KeyEvent}; | ||||
| use ratatui::layout::{Constraint, Margin}; | ||||
| use ratatui::widgets::{Padding, Scrollbar, ScrollbarOrientation}; | ||||
| use ratatui::{ | ||||
|     text::Text, | ||||
|     layout::{Alignment, Rect}, 
 | ||||
|     widgets::{Block, ScrollbarState, Cell, Row, Table, TableState}, 
 | ||||
|     Frame | ||||
| }; | ||||
| use tokio::sync::mpsc::UnboundedSender; | ||||
| 
 | ||||
| use super::{Component, CurrentTab, PartialComponent}; | ||||
| use crate::{ | ||||
|     types::CasperExtrinsicDetails, | ||||
|     action::Action, 
 | ||||
|     config::Config, 
 | ||||
|     palette::StylePalette, 
 | ||||
| }; | ||||
| 
 | ||||
| pub struct ExtrinsicExplorer { | ||||
|     is_active: bool, | ||||
|     action_tx: Option<UnboundedSender<Action>>, | ||||
|     extrinsics: HashMap<u32, Vec<CasperExtrinsicDetails>>, | ||||
|     current_extrinsics: Option<Vec<CasperExtrinsicDetails>>, | ||||
|     block_numbers: VecDeque<u32>, | ||||
|     scroll_state: ScrollbarState, | ||||
|     table_state: TableState, | ||||
|     palette: StylePalette, | ||||
| } | ||||
| 
 | ||||
| impl Default for ExtrinsicExplorer { | ||||
|     fn default() -> Self { | ||||
|         Self::new() | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl ExtrinsicExplorer { | ||||
|     const MAX_BLOCKS: usize = 50; | ||||
| 
 | ||||
|     pub fn new() -> Self { | ||||
|         Self { | ||||
|             is_active: false, | ||||
|             action_tx: None, | ||||
|             current_extrinsics: None, | ||||
|             extrinsics: Default::default(), | ||||
|             block_numbers: Default::default(), | ||||
|             scroll_state: ScrollbarState::new(0), | ||||
|             table_state: TableState::new(), | ||||
|             palette: StylePalette::default(), | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fn update_extrinsics_for_header( | ||||
|         &mut self, | ||||
|         block_number: u32, | ||||
|         extrinsics: Vec<CasperExtrinsicDetails>, | ||||
|     ) { | ||||
|         self.extrinsics.insert(block_number, extrinsics); | ||||
|         self.block_numbers.push_front(block_number); | ||||
| 
 | ||||
|         if self.block_numbers.len() > Self::MAX_BLOCKS { | ||||
|             if let Some(remove_block_number) = self.block_numbers.pop_back() { | ||||
|                 let _ = self.extrinsics.remove(&remove_block_number); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fn send_used_explorer_log(&mut self, index: usize) { | ||||
|         if let Some(action_tx) = &self.action_tx { | ||||
|             let maybe_log = self.current_extrinsics | ||||
|                 .as_ref() | ||||
|                 .map(|ext| { | ||||
|                     ext.get(index).map(|ext| { | ||||
|                         hex::encode(&ext.field_bytes.clone()) | ||||
|                     }) | ||||
|                 }) | ||||
|                 .flatten(); | ||||
|             let _ = action_tx.send(Action::UsedExplorerLog(maybe_log.clone())); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fn first_row(&mut self) { | ||||
|         match &self.current_extrinsics { | ||||
|             Some(exts) if exts.len() > 0 => self.table_state.select(Some(0)), | ||||
|             _ => self.table_state.select(None), | ||||
|         } | ||||
|         self.scroll_state = self.scroll_state.position(0); | ||||
|         self.send_used_explorer_log(0); | ||||
|     } | ||||
| 
 | ||||
|     fn next_row(&mut self) { | ||||
|         match &self.current_extrinsics  { | ||||
|             Some(exts) => { | ||||
|                 let i = match self.table_state.selected() { | ||||
|                     Some(i) => { | ||||
|                         if i >= exts.len() - 1 { | ||||
|                             i | ||||
|                         } else { | ||||
|                             i + 1 | ||||
|                         } | ||||
|                     }, | ||||
|                     None => 0, | ||||
|                 }; | ||||
|                 self.table_state.select(Some(i)); | ||||
|                 self.scroll_state = self.scroll_state.position(i); | ||||
|                 self.send_used_explorer_log(i); | ||||
|             }, | ||||
|             None => { | ||||
|                 self.table_state.select(None); | ||||
|                 self.scroll_state = self.scroll_state.position(0); | ||||
|                 self.send_used_explorer_log(0); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fn last_row(&mut self) { | ||||
|         match &self.current_extrinsics { | ||||
|             Some(exts) => { | ||||
|                 let last = exts.len().saturating_sub(1); | ||||
|                 self.table_state.select(Some(last)); | ||||
|                 self.scroll_state = self.scroll_state.position(last); | ||||
|                 self.send_used_explorer_log(last); | ||||
|             }, | ||||
|             None => { | ||||
|                 self.table_state.select(None); | ||||
|                 self.scroll_state = self.scroll_state.position(0); | ||||
|                 self.send_used_explorer_log(0); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fn previous_row(&mut self) { | ||||
|         match &self.current_extrinsics { | ||||
|             Some(_) => { | ||||
|                 let i = self.table_state | ||||
|                     .selected() | ||||
|                     .map(|i| i.saturating_sub(1)) | ||||
|                     .unwrap_or_default(); | ||||
|                 self.table_state.select(Some(i)); | ||||
|                 self.scroll_state = self.scroll_state.position(i); | ||||
|                 self.send_used_explorer_log(i); | ||||
|             }, | ||||
|             None => { | ||||
|                 self.table_state.select(None); | ||||
|                 self.scroll_state = self.scroll_state.position(0); | ||||
|                 self.send_used_explorer_log(0); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl PartialComponent for ExtrinsicExplorer { | ||||
|     fn set_active(&mut self, current_tab: CurrentTab) { | ||||
|         if current_tab == CurrentTab::Extrinsics { | ||||
|             self.is_active = true; | ||||
|         } else { | ||||
|             self.is_active = false; | ||||
|             self.table_state.select(None); | ||||
|             self.scroll_state = self.scroll_state.position(0); | ||||
|             self.send_used_explorer_log(usize::MAX); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl Component for ExtrinsicExplorer { | ||||
|     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::Explorer) { | ||||
|             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::UsedExplorerBlock(maybe_block_number) => { | ||||
|                 let block_number = maybe_block_number.unwrap_or_default(); | ||||
|                 if let Some(exts) = self.extrinsics.get(&block_number) { | ||||
|                     self.current_extrinsics = Some(exts.to_vec()); | ||||
|                     self.scroll_state = self.scroll_state.content_length(exts.len()); | ||||
|                 } else { | ||||
|                     self.current_extrinsics = None; | ||||
|                     self.scroll_state = self.scroll_state.content_length(0); | ||||
|                 } | ||||
|             }, | ||||
|             Action::ExtrinsicsForBlock(block_number, extrinsics) => 
 | ||||
|                 self.update_extrinsics_for_header(block_number, extrinsics), | ||||
|             _ => {}, | ||||
|         }; | ||||
|         Ok(None) | ||||
|     } | ||||
| 
 | ||||
|     fn handle_key_event(&mut self, key: KeyEvent) -> Result<Option<Action>> { | ||||
|         match key.code { | ||||
|             KeyCode::Char('k') | KeyCode::Up if self.is_active => self.previous_row(), | ||||
|             KeyCode::Char('j') | KeyCode::Down if self.is_active => self.next_row(), | ||||
|             KeyCode::Char('K') if self.is_active => self.first_row(), | ||||
|             KeyCode::Char('J') if self.is_active => self.last_row(), | ||||
|             _ => {}, | ||||
|         }; | ||||
| 
 | ||||
|         Ok(None) | ||||
|     } | ||||
| 
 | ||||
|     fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { | ||||
|         let [_, place] = super::explorer_scrollbars_layout(area); | ||||
| 
 | ||||
|         let (border_style, border_type) = self.palette.create_border_style(self.is_active); | ||||
| 
 | ||||
|         let mut longest_pallet_name_length = 0; | ||||
|         let mut longest_variant_name_length = 0; | ||||
| 
 | ||||
|         let rows = match &self.current_extrinsics { | ||||
|             Some(exts) => exts | ||||
|                 .iter() | ||||
|                 .enumerate() | ||||
|                 .map(|(idx, ext)| { | ||||
|                     longest_pallet_name_length = longest_pallet_name_length.max(ext.pallet_name.len()); | ||||
|                     longest_variant_name_length = longest_variant_name_length.max(ext.variant_name.len()); | ||||
|                     Row::new(vec![ | ||||
|                         Cell::from(Text::from(idx.to_string()).alignment(Alignment::Left)), | ||||
|                         Cell::from(Text::from(ext.pallet_name.clone()).alignment(Alignment::Right)), | ||||
|                         Cell::from(Text::from(ext.variant_name.clone()).alignment(Alignment::Left)), 
 | ||||
|                         Cell::from(Text::from(ext.hash.to_string()).alignment(Alignment::Right)), | ||||
|                     ]) | ||||
|                 }) | ||||
|                 .collect::<Vec<_>>(), | ||||
|             None => Vec::new(), | ||||
|         }; | ||||
| 
 | ||||
|         let table = Table::new( | ||||
|             rows, | ||||
|             [ | ||||
|                 Constraint::Max(2), | ||||
|                 Constraint::Min(longest_pallet_name_length as u16 + 2), | ||||
|                 Constraint::Min(longest_variant_name_length as u16 + 2), | ||||
|                 Constraint::Min(0), | ||||
|             ], | ||||
|         ) | ||||
|         .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("Extrinsics")); | ||||
| 
 | ||||
|         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(()) | ||||
|     } | ||||
| } | ||||
| @ -7,7 +7,7 @@ use ratatui::{ | ||||
|     Frame, | ||||
| }; | ||||
| 
 | ||||
| use super::Component; | ||||
| use super::{Component, PartialComponent, CurrentTab}; | ||||
| use crate::{palette::StylePalette, config::Config, action::Action}; | ||||
| 
 | ||||
| #[derive(Debug, Default)] | ||||
| @ -70,6 +70,10 @@ impl ExtrinsicsChart { | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl PartialComponent for ExtrinsicsChart { | ||||
|     fn set_active(&mut self, _current_tab: CurrentTab) {} | ||||
| } | ||||
| 
 | ||||
| impl Component for ExtrinsicsChart { | ||||
|     fn register_config_handler(&mut self, config: Config) -> Result<()> { | ||||
|         if let Some(style) = config.styles.get(&crate::app::Mode::Explorer) { | ||||
|  | ||||
| @ -5,7 +5,7 @@ use ratatui::{ | ||||
|     Frame, | ||||
| }; | ||||
| 
 | ||||
| use super::Component; | ||||
| use super::{Component, PartialComponent, CurrentTab}; | ||||
| use crate::{ | ||||
|     config::Config, 
 | ||||
|     action::Action, 
 | ||||
| @ -26,6 +26,10 @@ impl FinalizedBlock { | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl PartialComponent for FinalizedBlock { | ||||
|     fn set_active(&mut self, _current_tab: CurrentTab) {} | ||||
| } | ||||
| 
 | ||||
| impl Component for FinalizedBlock { | ||||
|     fn register_config_handler(&mut self, config: Config) -> Result<()> { | ||||
|         if let Some(style) = config.styles.get(&crate::app::Mode::Explorer) { | ||||
| @ -73,7 +77,7 @@ impl Component for FinalizedBlock { | ||||
|             let big_text = BigText::builder() | ||||
|                 .centered() | ||||
|                 .pixel_size(PixelSize::Quadrant) | ||||
|                 .style(self.palette.create_text_style(false)) | ||||
|                 .style(self.palette.create_basic_style(false)) | ||||
|                 .lines(vec![ | ||||
|                     text.into(), | ||||
|                 ]) | ||||
|  | ||||
| @ -5,7 +5,7 @@ use ratatui::{ | ||||
|     Frame, | ||||
| }; | ||||
| 
 | ||||
| use super::Component; | ||||
| use super::{Component, PartialComponent, CurrentTab}; | ||||
| use crate::{ | ||||
|     config::Config, 
 | ||||
|     action::Action, 
 | ||||
| @ -26,6 +26,10 @@ impl LatestBlock { | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl PartialComponent for LatestBlock { | ||||
|     fn set_active(&mut self, _current_tab: CurrentTab) {} | ||||
| } | ||||
| 
 | ||||
| impl Component for LatestBlock { | ||||
|     fn register_config_handler(&mut self, config: Config) -> Result<()> { | ||||
|         if let Some(style) = config.styles.get(&crate::app::Mode::Explorer) { | ||||
| @ -73,7 +77,7 @@ impl Component for LatestBlock { | ||||
|             let big_text = BigText::builder() | ||||
|                 .centered() | ||||
|                 .pixel_size(PixelSize::Quadrant) | ||||
|                 .style(self.palette.create_text_style(false)) | ||||
|                 .style(self.palette.create_basic_style(false)) | ||||
|                 .lines(vec![ | ||||
|                     text.into(), | ||||
|                 ]) | ||||
|  | ||||
							
								
								
									
										83
									
								
								src/components/explorer/log_explorer.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								src/components/explorer/log_explorer.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,83 @@ | ||||
| use color_eyre::Result; | ||||
| use ratatui::{ | ||||
|     layout::{Alignment, Rect}, 
 | ||||
|     text::Line, 
 | ||||
|     widgets::{Block, Paragraph, Wrap}, 
 | ||||
|     Frame | ||||
| }; | ||||
| 
 | ||||
| use super::{Component, CurrentTab, PartialComponent}; | ||||
| use crate::{ | ||||
|     action::Action, 
 | ||||
|     config::Config, 
 | ||||
|     palette::StylePalette, 
 | ||||
| }; | ||||
| 
 | ||||
| pub struct LogExplorer { | ||||
|     is_active: bool, | ||||
|     maybe_log: Option<String>, | ||||
|     palette: StylePalette, | ||||
| } | ||||
| 
 | ||||
| impl Default for LogExplorer { | ||||
|     fn default() -> Self { | ||||
|         Self::new() | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl LogExplorer { | ||||
|     pub fn new() -> Self { | ||||
|         Self { | ||||
|             is_active: false, | ||||
|             maybe_log: None, | ||||
|             palette: StylePalette::default(), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl PartialComponent for LogExplorer { | ||||
|     fn set_active(&mut self, _current_tab: CurrentTab) {} | ||||
| } | ||||
| 
 | ||||
| impl Component for LogExplorer { | ||||
|     fn register_config_handler(&mut self, config: Config) -> Result<()> { | ||||
|         if let Some(style) = config.styles.get(&crate::app::Mode::Explorer) { | ||||
|             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()); | ||||
|         } | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     fn update(&mut self, action: Action) -> Result<Option<Action>> { | ||||
|         match action { | ||||
|             Action::UsedExplorerLog(maybe_log) => self.maybe_log = maybe_log, | ||||
|             _ => {}, | ||||
|         }; | ||||
|         Ok(None) | ||||
|     } | ||||
| 
 | ||||
|     fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { | ||||
|         let [_, _, place] = super::explorer_layout(area); | ||||
| 
 | ||||
|         let (border_style, border_type) = self.palette.create_border_style(self.is_active); | ||||
| 
 | ||||
|         let line = match &self.maybe_log { | ||||
|             Some(log) => Line::from(hex::encode(log)), | ||||
|             None => Line::from(""), | ||||
|         }; | ||||
| 
 | ||||
|         let paragraph = Paragraph::new(line) | ||||
|             .block(Block::bordered() | ||||
|                 .border_style(border_style) | ||||
|                 .border_type(border_type) | ||||
|                 .title_alignment(Alignment::Right) | ||||
|                 .title_style(self.palette.create_title_style(false)) | ||||
|                 .title("Logs")) | ||||
|             .alignment(Alignment::Center) | ||||
|             .wrap(Wrap { trim: true }); | ||||
| 
 | ||||
|         frame.render_widget(paragraph, place); | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
| @ -1,12 +1,13 @@ | ||||
| use color_eyre::Result; | ||||
| use crossterm::event::KeyEvent; | ||||
| use crossterm::event::{KeyCode, KeyEvent}; | ||||
| use ratatui::{ | ||||
|     layout::{Constraint, Flex, Layout, Rect}, | ||||
|     Frame, | ||||
| }; | ||||
| use tokio::sync::mpsc::UnboundedSender; | ||||
| 
 | ||||
| use super::Component; | ||||
| use crate::{config::Config, action::Action}; | ||||
| use crate::{action::Action, app::Mode, config::Config}; | ||||
| 
 | ||||
| mod latest_block; | ||||
| mod finalized_block; | ||||
| @ -14,7 +15,9 @@ mod block_ticker; | ||||
| mod current_era; | ||||
| mod current_epoch; | ||||
| mod extrinsics_chart; | ||||
| mod explorer_blocks; | ||||
| mod block_explorer; | ||||
| mod extrinsic_explorer; | ||||
| mod log_explorer; | ||||
| 
 | ||||
| use latest_block::LatestBlock; | ||||
| use finalized_block::FinalizedBlock; | ||||
| @ -22,15 +25,32 @@ use block_ticker::BlockTicker; | ||||
| use current_era::CurrentEra; | ||||
| use current_epoch::CurrentEpoch; | ||||
| use extrinsics_chart::ExtrinsicsChart; | ||||
| use explorer_blocks::ExplorerBlocks; | ||||
| use block_explorer::BlockExplorer; | ||||
| use extrinsic_explorer::ExtrinsicExplorer; | ||||
| use log_explorer::LogExplorer; | ||||
| 
 | ||||
| #[derive(Debug, Clone, PartialEq)] | ||||
| pub enum CurrentTab { | ||||
|     Nothing, | ||||
|     Blocks, | ||||
|     Extrinsics, | ||||
| } | ||||
| 
 | ||||
| pub trait PartialComponent: Component { | ||||
|     fn set_active(&mut self, current_tab: CurrentTab); | ||||
| } | ||||
| 
 | ||||
| pub struct Explorer { | ||||
|     components: Vec<Box<dyn Component>> | ||||
|     is_active: bool, | ||||
|     current_tab: CurrentTab, | ||||
|     components: Vec<Box<dyn PartialComponent>> | ||||
| } | ||||
| 
 | ||||
| impl Default for Explorer { | ||||
|     fn default() -> Self { | ||||
|         Self { | ||||
|             is_active: false, | ||||
|             current_tab: CurrentTab::Nothing, | ||||
|             components: vec![ | ||||
|                 Box::new(BlockTicker::default()), | ||||
|                 Box::new(LatestBlock::default()), | ||||
| @ -38,13 +58,42 @@ impl Default for Explorer { | ||||
|                 Box::new(CurrentEra::default()), | ||||
|                 Box::new(CurrentEpoch::default()), | ||||
|                 Box::new(ExtrinsicsChart::default()), | ||||
|                 Box::new(ExplorerBlocks::default()), | ||||
|                 Box::new(BlockExplorer::default()), | ||||
|                 Box::new(ExtrinsicExplorer::default()), | ||||
|                 Box::new(LogExplorer::default()), | ||||
|             ] | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl Explorer { | ||||
|     fn move_left(&mut self) { | ||||
|         if let CurrentTab::Extrinsics = self.current_tab { | ||||
|             self.current_tab = CurrentTab::Blocks; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fn move_right(&mut self) { | ||||
|         match self.current_tab { | ||||
|             CurrentTab::Nothing => self.current_tab = CurrentTab::Blocks, | ||||
|             CurrentTab::Blocks => self.current_tab = CurrentTab::Extrinsics, | ||||
|             _ => {} | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl PartialComponent for Explorer { | ||||
|     fn set_active(&mut self, _current_tab: CurrentTab) {} | ||||
| } | ||||
| 
 | ||||
| impl Component for Explorer { | ||||
|     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<()> { | ||||
|         if let Some(_) = config.styles.get(&crate::app::Mode::Explorer) { | ||||
|             for component in self.components.iter_mut() { | ||||
| @ -55,13 +104,46 @@ impl Component for Explorer { | ||||
|     } | ||||
| 
 | ||||
|     fn handle_key_event(&mut self, key: KeyEvent) -> Result<Option<Action>> { | ||||
|         if self.is_active { | ||||
|             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::Enter | 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()); | ||||
|                     } | ||||
|                 }, | ||||
|                 _ => { | ||||
|                     for component in self.components.iter_mut() { | ||||
|                         component.handle_key_event(key)?; | ||||
|                     } | ||||
|                 }, | ||||
|             } | ||||
|         } 
 | ||||
|         Ok(None) | ||||
|     } | ||||
| 
 | ||||
|     fn update(&mut self, action: Action) -> Result<Option<Action>> { | ||||
|         if let Action::SetActiveScreen(Mode::Explorer) = action { | ||||
|             self.is_active = true; | ||||
|             self.current_tab = CurrentTab::Blocks; | ||||
|             for component in self.components.iter_mut() { | ||||
|                 component.set_active(self.current_tab.clone()); | ||||
|             } | ||||
|         } | ||||
|         for component in self.components.iter_mut() { | ||||
|             component.update(action.clone())?; | ||||
|         } | ||||
|  | ||||
| @ -41,7 +41,7 @@ impl FpsCounter { | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fn app_tick(&mut self) -> Result<()> { | ||||
|     fn app_tick(&mut self) { | ||||
|         self.tick_count += 1; | ||||
|         let now = Instant::now(); | ||||
|         let elapsed = (now - self.last_tick_update).as_secs_f64(); | ||||
| @ -50,10 +50,9 @@ impl FpsCounter { | ||||
|             self.last_tick_update = now; | ||||
|             self.tick_count = 0; | ||||
|         } | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     fn render_tick(&mut self) -> Result<()> { | ||||
|     fn render_tick(&mut self) { | ||||
|         self.frame_count += 1; | ||||
|         let now = Instant::now(); | ||||
|         let elapsed = (now - self.last_frame_update).as_secs_f64(); | ||||
| @ -62,15 +61,14 @@ impl FpsCounter { | ||||
|             self.last_frame_update = now; | ||||
|             self.frame_count = 0; | ||||
|         } | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl Component for FpsCounter { | ||||
|     fn update(&mut self, action: Action) -> Result<Option<Action>> { | ||||
|         match action { | ||||
|             Action::Tick => self.app_tick()?, | ||||
|             Action::Render => self.render_tick()?, | ||||
|             Action::Tick => self.app_tick(), | ||||
|             Action::Render => self.render_tick(), | ||||
|             _ => {} | ||||
|         }; | ||||
|         Ok(None) | ||||
|  | ||||
| @ -39,28 +39,6 @@ impl Health { | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fn set_sync_state( | ||||
|         &mut self, 
 | ||||
|         peers: Option<usize>, 
 | ||||
|         is_syncing: bool, 
 | ||||
|         should_have_peers: bool, | ||||
|     ) -> Result<()> { | ||||
|         self.peers = peers; | ||||
|         self.is_syncing = is_syncing; | ||||
|         self.should_have_peers = should_have_peers; | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     fn set_tx_pool_length(&mut self, tx_pool_length: usize) -> Result<()> { | ||||
|         self.tx_pool_length = tx_pool_length; | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     fn set_node_name(&mut self, name: Option<String>) -> Result<()> { | ||||
|         self.name = name; | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     pub fn is_syncing_as_string(&self) -> String { | ||||
|         if self.is_syncing { 
 | ||||
|             format!("syncing {}", VerticalBlocks::default().to_string()) | ||||
| @ -90,10 +68,12 @@ impl Component for Health { | ||||
|     fn update(&mut self, action: Action) -> Result<Option<Action>> { | ||||
|         match action { | ||||
|             Action::SetSystemHealth(peers, is_syncing, should_have_peers) => { | ||||
|                 self.set_sync_state(peers, is_syncing, should_have_peers)? | ||||
|                 self.peers = peers; | ||||
|                 self.is_syncing = is_syncing; | ||||
|                 self.should_have_peers = should_have_peers; | ||||
|             }, | ||||
|             Action::SetNodeName(name) => self.set_node_name(name)?, | ||||
|             Action::SetPendingExtrinsicsLength(length) => self.set_tx_pool_length(length)?, | ||||
|             Action::SetNodeName(name) => self.name = name, | ||||
|             Action::SetPendingExtrinsicsLength(length) => self.tx_pool_length = length, | ||||
|             _ => {} | ||||
|         }; | ||||
|         Ok(None) | ||||
|  | ||||
| @ -9,8 +9,8 @@ use crate::{config::Config, action::Action, app::Mode}; | ||||
| 
 | ||||
| pub struct Menu { | ||||
|     command_tx: Option<UnboundedSender<Action>>, | ||||
|     list_state: ListState, | ||||
|     items: Vec<String>, | ||||
|     current_item_index: usize, | ||||
|     is_active: bool, | ||||
|     palette: StylePalette, | ||||
| } | ||||
| @ -23,8 +23,9 @@ impl Default for Menu { | ||||
| 
 | ||||
| impl Menu { | ||||
|     pub fn new() -> Self { | ||||
|         Self { | ||||
|         let mut new_list = Self { | ||||
|             command_tx: None, | ||||
|             list_state: ListState::default(), | ||||
|             items: vec![ | ||||
|                 String::from("Explorer"), | ||||
|                 String::from("Wallet"), | ||||
| @ -33,66 +34,62 @@ impl Menu { | ||||
|                 String::from("Governance"), | ||||
|                 String::from("Operations"), | ||||
|             ], | ||||
|             current_item_index: Default::default(), | ||||
|             is_active: true, | ||||
|             palette: StylePalette::default(), | ||||
|             is_active: true, | ||||
| 
 | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fn move_current_up(&mut self) -> Result<()> { | ||||
|         self.current_item_index = self | ||||
|             .current_item_index | ||||
|             .saturating_sub(1); | ||||
| 
 | ||||
|         if let Some(command_tx) = &self.command_tx { | ||||
|             match self.current_item_index { | ||||
|                 0 => command_tx.send(Action::SetMode(Mode::Explorer))?, | ||||
|                 _ => command_tx.send(Action::SetMode(Mode::Empty))?, | ||||
|             } | ||||
|         }; | ||||
|         Ok(()) | ||||
|         new_list.list_state.select(Some(0)); | ||||
|         new_list | ||||
|     } | ||||
| 
 | ||||
|     fn move_current_down(&mut self) -> Result<()> { | ||||
|         let new_current = self.current_item_index + 1; | ||||
|         if new_current < self.items.len() { | ||||
|             self.current_item_index = new_current; | ||||
|         } | ||||
| 
 | ||||
|         if let Some(command_tx) = &self.command_tx { | ||||
|             match self.current_item_index { | ||||
|                 0 => command_tx.send(Action::SetMode(Mode::Explorer))?, | ||||
|                 _ => command_tx.send(Action::SetMode(Mode::Empty))?, | ||||
|     fn next_row(&mut self) -> Result<Option<Action>> { | ||||
|         let i = match self.list_state.selected() { | ||||
|             Some(i) => { | ||||
|                 if i >= self.items.len() - 1 { | ||||
|                     i | ||||
|                 } else { | ||||
|                     i + 1 | ||||
|                 } | ||||
|             }, | ||||
|             None => 0, | ||||
|         }; | ||||
|         Ok(()) | ||||
|         self.list_state.select(Some(i)); | ||||
|         match i { | ||||
|             0 => Ok(Some(Action::SetMode(Mode::Explorer))), | ||||
|             1 => Ok(Some(Action::SetMode(Mode::Wallet))), | ||||
|             _ => Ok(Some(Action::SetMode(Mode::Empty))), | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fn set_active(&mut self) -> Result<()> { | ||||
|         self.is_active = true; | ||||
|         if let Some(command_tx) = &self.command_tx { | ||||
|             match self.current_item_index { | ||||
|                 0 => command_tx.send(Action::SetMode(Mode::Explorer))?, | ||||
|                 _ => command_tx.send(Action::SetMode(Mode::Empty))?, | ||||
|     fn previous_row(&mut self) -> Result<Option<Action>> { | ||||
|         let i = match self.list_state.selected() { | ||||
|             Some(i) => { | ||||
|                 if i == 0 { | ||||
|                     0 | ||||
|                 } else { | ||||
|                     i - 1 | ||||
|                 } | ||||
|             }, | ||||
|             None => 0 | ||||
|         }; | ||||
|         Ok(()) | ||||
|         self.list_state.select(Some(i)); | ||||
|         match i { | ||||
|             0 => Ok(Some(Action::SetMode(Mode::Explorer))), | ||||
|             1 => Ok(Some(Action::SetMode(Mode::Wallet))), | ||||
|             _ => Ok(Some(Action::SetMode(Mode::Empty))), | ||||
|         } | ||||
| 
 | ||||
|     fn unset_active(&mut self) -> Result<()> { | ||||
|         self.is_active = false; | ||||
|         if let Some(command_tx) = &self.command_tx { | ||||
|             match self.current_item_index { | ||||
|                 0 => command_tx.send(Action::SetMode(Mode::ExplorerActive))?, | ||||
|                 _ => command_tx.send(Action::SetMode(Mode::EmptyActive))?, | ||||
|             } | ||||
|         }; | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl Component for Menu { | ||||
|     fn update(&mut self, action: Action) -> Result<Option<Action>> { | ||||
|         match action { | ||||
|             Action::SetActiveScreen(Mode::Menu) => self.is_active = true, | ||||
|             _ => {} | ||||
|         }; | ||||
|         Ok(None) | ||||
|     } | ||||
| 
 | ||||
|     fn register_action_handler(&mut self, tx: UnboundedSender<Action>) -> Result<()> { | ||||
|         self.command_tx = Some(tx); | ||||
|         Ok(()) | ||||
| @ -104,46 +101,48 @@ impl Component for Menu { | ||||
|             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()); | ||||
|         } | ||||
|         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.move_current_up()?, 
 | ||||
|            KeyCode::Down | KeyCode::Char('j') if self.is_active => self.move_current_down()?, 
 | ||||
|            KeyCode::Enter | KeyCode::Char('l') | KeyCode::Right if self.is_active => self.unset_active()?, | ||||
|            KeyCode::Esc if !self.is_active => self.set_active()?, | ||||
|            _ => {}, | ||||
|         }; | ||||
|         Ok(None) | ||||
|             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::Enter | KeyCode::Char('l') | KeyCode::Right if self.is_active => { | ||||
|                 self.is_active = false; | ||||
|                 match self.list_state.selected() { | ||||
|                     Some(0) => Ok(Some(Action::SetActiveScreen(Mode::Explorer))), | ||||
|                     Some(1) => Ok(Some(Action::SetActiveScreen(Mode::Wallet))), | ||||
|                     _ => Ok(Some(Action::SetActiveScreen(Mode::Empty))), | ||||
|                 } | ||||
|             }, | ||||
|             _ => Ok(None), | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { | ||||
|         let [menu, _] = super::menu_layout(area); | ||||
|         let width = menu.as_size().width as usize; | ||||
| 
 | ||||
|         let mut text = vec![]; | ||||
|         for (position, item) in self.items.iter().enumerate() { | ||||
|             let active = position == self.current_item_index; | ||||
|             let line = Line::raw(format!("{:^width$}", item)) | ||||
|                 .style(self.palette.create_text_style(active)) | ||||
|                 .centered(); | ||||
|             text.push(line); | ||||
|         } | ||||
| 
 | ||||
|         let (color, border_type) = self.palette.create_border_style(self.is_active); | ||||
|         let block = Block::bordered() | ||||
|             .border_style(color) | ||||
|             .border_type(border_type); | ||||
| 
 | ||||
|         let paragraph = Paragraph::new(text) | ||||
|         let list = List::default() | ||||
|             .items(self.items | ||||
|                 .iter() | ||||
|                 .map(|item| ListItem::new( | ||||
|                     Line::raw(item.as_str()).alignment(Alignment::Center) | ||||
|                 )) | ||||
|                 .collect::<Vec<_>>() | ||||
|             ) | ||||
|             .block(block) | ||||
|             .alignment(Alignment::Center); | ||||
|             .style(self.palette.create_basic_style(false)) | ||||
|             .highlight_style(self.palette.create_highlight_style()); | ||||
| 
 | ||||
|         frame.render_widget(paragraph, menu); | ||||
|         frame.render_stateful_widget(list, menu, &mut self.list_state); | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -13,6 +13,7 @@ pub mod health; | ||||
| pub mod menu; | ||||
| pub mod version; | ||||
| pub mod explorer; | ||||
| pub mod wallet; | ||||
| pub mod empty; | ||||
| 
 | ||||
| pub trait Component { | ||||
|  | ||||
| @ -5,11 +5,14 @@ use ratatui::{ | ||||
|     widgets::{Block, Paragraph, Wrap}, | ||||
|     Frame, | ||||
| }; | ||||
| use primitive_types::H256; | ||||
| use subxt::utils::H256; | ||||
| 
 | ||||
| use super::Component; | ||||
| use crate::{ | ||||
|     action::Action, palette::StylePalette, widgets::OghamCenter | ||||
|     config::Config, | ||||
|     action::Action, 
 | ||||
|     palette::StylePalette, 
 | ||||
|     widgets::OghamCenter, | ||||
| }; | ||||
| 
 | ||||
| #[derive(Debug, Clone, Default)] | ||||
| @ -21,21 +24,6 @@ pub struct Version { | ||||
| } | ||||
| 
 | ||||
| impl Version { | ||||
|     fn set_chain_name(&mut self, chain_name: Option<String>) -> Result<()> { | ||||
|         self.chain_name = chain_name; | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     fn set_node_version(&mut self, node_version: Option<String>) -> Result<()> { | ||||
|         self.node_version = node_version; | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     fn set_genesis_hash(&mut self, genesis_hash: Option<H256>) -> Result<()> { | ||||
|         self.genesis_hash = genesis_hash; | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     fn prepared_genesis_hash(&self) -> String { | ||||
|         match self.genesis_hash { | ||||
|             Some(genesis_hash) => genesis_hash.to_string(), | ||||
| @ -45,20 +33,28 @@ impl Version { | ||||
| } | ||||
| 
 | ||||
| impl Component for Version { | ||||
|     fn register_config_handler(&mut self, config: Config) -> Result<()> { | ||||
|         if let Some(style) = config.styles.get(&crate::app::Mode::Menu) { | ||||
|             self.palette.with_normal_style(style.get("normal_style").copied()); | ||||
|             self.palette.with_normal_border_style(style.get("normal_border_style").copied()); | ||||
|         } | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     fn update(&mut self, action: Action) -> Result<Option<Action>> { | ||||
|         match action { | ||||
|             Action::SetChainName(maybe_name) => self.set_chain_name(maybe_name)?, | ||||
|             Action::SetChainVersion(version) => self.set_node_version(version)?, | ||||
|             Action::SetGenesisHash(maybe_genesis) => self.set_genesis_hash(maybe_genesis)?, | ||||
|             Action::SetChainName(maybe_name) => self.chain_name = maybe_name, | ||||
|             Action::SetChainVersion(version) => self.node_version = version, | ||||
|             Action::SetGenesisHash(maybe_genesis) => self.genesis_hash = maybe_genesis, | ||||
|             _ => {} | ||||
|         }; | ||||
|         } | ||||
|         Ok(None) | ||||
|     } | ||||
| 
 | ||||
|     fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { | ||||
|         let [_, version] = super::menu_layout(area); | ||||
| 
 | ||||
|         let text_style = self.palette.create_text_style(false); | ||||
|         let text_style = self.palette.create_basic_style(false); | ||||
|         let (border_style, border_type) = self.palette.create_border_style(false); | ||||
|         let text = vec![ | ||||
|             Line::styled(self.chain_name.clone().unwrap_or(OghamCenter::default().to_string()), text_style), | ||||
|  | ||||
							
								
								
									
										278
									
								
								src/components/wallet/accounts.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										278
									
								
								src/components/wallet/accounts.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,278 @@ | ||||
| use std::path::PathBuf; | ||||
| use std::fs::File; | ||||
| use std::io::{Write, BufRead, BufReader}; | ||||
| 
 | ||||
| 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 subxt::{ | ||||
|     tx::PairSigner, | ||||
|     ext::sp_core::{ | ||||
|         Pair as PairT, 
 | ||||
|         sr25519::Pair, | ||||
|         crypto::{Ss58Codec, Ss58AddressFormat, AccountId32}, | ||||
|     }, | ||||
| }; | ||||
| use tokio::sync::mpsc::UnboundedSender; | ||||
| 
 | ||||
| use super::{PartialComponent, Component, CurrentTab}; | ||||
| use crate::casper::CasperConfig; | ||||
| use crate::{ | ||||
|     action::Action, 
 | ||||
|     config::Config, 
 | ||||
|     palette::StylePalette, 
 | ||||
| }; | ||||
| 
 | ||||
| pub struct Accounts { | ||||
|     is_active: bool, | ||||
|     action_tx: Option<UnboundedSender<Action>>, | ||||
|     palette: StylePalette, | ||||
|     scroll_state: ScrollbarState, | ||||
|     table_state: TableState, | ||||
|     wallet_keys: Vec<(String, String, PairSigner<CasperConfig, Pair>)>, | ||||
| } | ||||
| 
 | ||||
| impl Default for Accounts { | ||||
|     fn default() -> Self { | ||||
|         Self::new() | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl Accounts { | ||||
|     pub fn new() -> Self { | ||||
|         Self { | ||||
|             is_active: false, | ||||
|             action_tx: None, | ||||
|             scroll_state: ScrollbarState::new(0), | ||||
|             table_state: TableState::new(), | ||||
|             wallet_keys: Vec::new(), | ||||
|             palette: StylePalette::default(), | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fn read_or_create(&mut self, file_path: &PathBuf) -> Result<()> { | ||||
|         assert!(self.wallet_keys.len() == 0, "wallet_keys already exists"); | ||||
|         match File::open(file_path) { | ||||
|             Ok(file) => { | ||||
|                 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 (wallet_name, wallet_key) = line.split_at(line_split_at); | ||||
|                     let wallet_key = &wallet_key[3..]; | ||||
| 
 | ||||
|                     let seed: [u8; 32] = hex::decode(wallet_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 pair_signer = PairSigner::<CasperConfig, Pair>::new(pair); | ||||
|                     let address = AccountId32::from(seed.clone()) | ||||
|                         .to_ss58check_with_version(Ss58AddressFormat::custom(1996)); | ||||
| 
 | ||||
|                     self.wallet_keys.push((wallet_name.to_string(), address, pair_signer)); | ||||
|                 } | ||||
|             }, | ||||
|             Err(_) => { | ||||
|                 let (pair, seed) = match std::fs::read_to_string("/etc/ghost/wallet-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); | ||||
|                         (pair, seed) | ||||
|                     } | ||||
|                     Err(_) => { | ||||
|                         let (pair, seed) = Pair::generate(); | ||||
|                         (pair, seed) | ||||
|                     } | ||||
|                 }; | ||||
| 
 | ||||
|                 let secret_seed = hex::encode(seed); | ||||
|                 let pair_signer = PairSigner::<CasperConfig, Pair>::new(pair); | ||||
| 
 | ||||
|                 let mut new_file = File::create(file_path)?; | ||||
|                 writeln!(new_file, "ghostie:0x{}", &secret_seed)?; | ||||
| 
 | ||||
|                 let address = AccountId32::from(seed.clone()) | ||||
|                     .to_ss58check_with_version(Ss58AddressFormat::custom(1996)); | ||||
| 
 | ||||
|                 self.wallet_keys.push(("ghostie".to_string(), address, pair_signer)); | ||||
|             } | ||||
|         }; | ||||
|         self.table_state.select(Some(0)); | ||||
|         self.scroll_state = self.scroll_state.content_length(self.wallet_keys.len()); | ||||
|         self.send_wallet_change(0); | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     fn send_wallet_change(&mut self, index: usize) { | ||||
|         if let Some(action_tx) = &self.action_tx { | ||||
|             let (_, _, pair) = &self.wallet_keys[index]; | ||||
|             let _ = action_tx.send(Action::UsedAccount(pair.account_id().clone())); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fn first_row(&mut self) { | ||||
|         if self.wallet_keys.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.wallet_keys.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.wallet_keys.len() > 0 { | ||||
|             let last = self.wallet_keys.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 Accounts { | ||||
|     fn set_active(&mut self, current_tab: CurrentTab) { | ||||
|         match current_tab { | ||||
|             CurrentTab::Accounts => self.is_active = true, | ||||
|             _ => { | ||||
|                 self.is_active = false; | ||||
|                 self.table_state.select(None); | ||||
|                 self.scroll_state = self.scroll_state.position(0); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl Component for Accounts { | ||||
|     // TODO network_tx is needed here NOT action_tx
 | ||||
|     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::Explorer) { | ||||
|             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 wallet_keys_file = config.config.data_dir; | ||||
|         wallet_keys_file.push("wallet-keys"); | ||||
|         self.read_or_create(&wallet_keys_file)?; | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     fn update(&mut self, action: Action) -> Result<Option<Action>> { | ||||
|         match action { | ||||
|             _ => {} | ||||
|         }; | ||||
|         Ok(None) | ||||
|     } | ||||
| 
 | ||||
|     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('K') if self.is_active => self.first_row(), | ||||
|            KeyCode::Char('J') if self.is_active => self.last_row(), | ||||
|            // TODO: swap on alt+j or G/gg to bottom and up
 | ||||
|            _ => {}, | ||||
|         }; | ||||
|         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( | ||||
|             self.wallet_keys | ||||
|                 .iter() | ||||
|                 .map(|info| Row::new(vec![ | ||||
|                         Cell::from(Text::from(info.0.clone()).alignment(Alignment::Left)), | ||||
|                         Cell::from(Text::from(info.1.clone()).alignment(Alignment::Center)), 
 | ||||
|                         Cell::from(Text::from("31 CSPR".to_string()).alignment(Alignment::Right)), | ||||
|                 ])), | ||||
|             [ | ||||
|                 Constraint::Min(0), | ||||
|                 Constraint::Min(15), | ||||
|                 Constraint::Min(10), | ||||
|             ], | ||||
|         ) | ||||
|         .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 Accounts")); | ||||
| 
 | ||||
|         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(()) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										82
									
								
								src/components/wallet/add.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								src/components/wallet/add.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,82 @@ | ||||
| use crossterm::event::{KeyEvent, KeyCode}; | ||||
| use color_eyre::Result; | ||||
| use ratatui::{ | ||||
|     layout::{Constraint, Flex, Layout, Rect}, 
 | ||||
|     widgets::{Block, Clear}, 
 | ||||
|     Frame | ||||
| }; | ||||
| 
 | ||||
| use super::{Component, PartialComponent, CurrentTab}; | ||||
| use crate::{ | ||||
|     action::Action, 
 | ||||
|     config::Config, 
 | ||||
|     palette::StylePalette, 
 | ||||
| }; | ||||
| 
 | ||||
| #[derive(Debug)] | ||||
| pub struct AddAddress { | ||||
|     is_shown: bool, | ||||
|     palette: StylePalette | ||||
| } | ||||
| 
 | ||||
| impl Default for AddAddress { | ||||
|     fn default() -> Self { | ||||
|         Self::new() | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl AddAddress { | ||||
|     pub fn new() -> Self { | ||||
|         Self { | ||||
|             is_shown: false, | ||||
|             palette: StylePalette::default(), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl PartialComponent for AddAddress { | ||||
|     fn set_active(&mut self, _current_tab: CurrentTab) {} | ||||
| } | ||||
| 
 | ||||
| impl Component for AddAddress { | ||||
|     fn register_config_handler(&mut self, config: Config) -> Result<()> { | ||||
|         if let Some(style) = config.styles.get(&crate::app::Mode::Explorer) { | ||||
|             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 handle_key_event(&mut self, key: KeyEvent) -> Result<Option<Action>> { | ||||
|         match key.code { | ||||
|             KeyCode::Char('a') => self.is_shown = true, 
 | ||||
|             KeyCode::Char(' ') => self.is_shown = false, 
 | ||||
|             _ => {}, | ||||
|         }; | ||||
|         Ok(None) | ||||
|     } | ||||
| 
 | ||||
|     fn update(&mut self, action: Action) -> Result<Option<Action>> { | ||||
|         match action { | ||||
|             _ => {} | ||||
|         }; | ||||
|         Ok(None) | ||||
|     } | ||||
| 
 | ||||
|     fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { | ||||
|         if self.is_shown { | ||||
|             let block = Block::bordered().title("Transfer"); | ||||
|             let v = Layout::vertical([Constraint::Min(55)]).flex(Flex::Center); | ||||
|             let h = Layout::horizontal([Constraint::Min(10)]).flex(Flex::Center); | ||||
|             let [area] = v.areas(area); | ||||
|             let [area] = h.areas(area); | ||||
|             frame.render_widget(Clear, area); | ||||
|             frame.render_widget(block, area); | ||||
|         } | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										273
									
								
								src/components/wallet/address_book.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										273
									
								
								src/components/wallet/address_book.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,273 @@ | ||||
| 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::text::Text; | ||||
| use ratatui::widgets::{Cell, Padding, Scrollbar, ScrollbarOrientation}; | ||||
| use ratatui::{ | ||||
|     layout::{Alignment, Rect}, 
 | ||||
|     widgets::{Block, ScrollbarState, Row, Table, TableState}, 
 | ||||
|     Frame | ||||
| }; | ||||
| use subxt::ext::sp_core::crypto::{Ss58Codec, Ss58AddressFormat, AccountId32}; | ||||
| 
 | ||||
| use super::{Component, PartialComponent, CurrentTab}; | ||||
| use crate::{ | ||||
|     action::Action, 
 | ||||
|     config::Config, 
 | ||||
|     palette::StylePalette, 
 | ||||
| }; | ||||
| 
 | ||||
| pub struct AddressBook { | ||||
|     is_active: bool, | ||||
|     address_book: Vec<(String, String, AccountId32)>, | ||||
|     scroll_state: ScrollbarState, | ||||
|     table_state: TableState, | ||||
|     palette: StylePalette, | ||||
| } | ||||
| 
 | ||||
| impl Default for AddressBook { | ||||
|     fn default() -> Self { | ||||
|         Self::new() | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl AddressBook { | ||||
|     pub fn new() -> Self { | ||||
|         Self { | ||||
|             is_active: false, | ||||
|             address_book: Vec::new(), | ||||
|             scroll_state: ScrollbarState::new(0), | ||||
|             table_state: TableState::new(), | ||||
|             palette: StylePalette::default(), | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fn read_or_create(&mut self, file_path: &PathBuf) -> Result<()> { | ||||
|         assert!(self.address_book.len() == 0, "address_book already exists"); | ||||
|         match File::open(file_path) { | ||||
|             Ok(file) => { | ||||
|                 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 = &seed[3..]; | ||||
| 
 | ||||
|                     let seed: [u8; 32] = hex::decode(seed) | ||||
|                         .expect("stored seed is valid hex string; qed") | ||||
|                         .as_slice() | ||||
|                         .try_into() | ||||
|                         .expect("stored seed is valid length; qed"); | ||||
| 
 | ||||
|                     let account_id = AccountId32::from(seed); | ||||
|                     let address = AccountId32::from(seed.clone()) | ||||
|                         .to_ss58check_with_version(Ss58AddressFormat::custom(1996)); | ||||
|                     self.address_book.push((name.to_string(), address, account_id)); | ||||
|                 } | ||||
|             }, | ||||
|             Err(_) => { | ||||
|                 let chad_boyz = vec![ | ||||
|                     ("Pierre",   "328d3b7c3046ef7700937d99fb2e98ce2591682c2b5dcf3f562e4da157650237"), | ||||
|                     ("Ghost_7",  "3666e4e19f87bb8680495f31864ce1f1c69d4178002cc01911aef2cc7313f203"), | ||||
|                     ("Neptune1", "ac871e8bab00dd56ba3a1c0bd289357203dcaf10010b0b04ad7472870cd22a3c"), | ||||
|                     ("Neptune2", "425ccd7bda4f5c76788ba23bc0381d7a2e496179c93301208c57501c80a4232a"), | ||||
|                     ("Doctor K", "927a98dcf8f721103005f168476c24b91d7d10d580f457006a908e10e62c7729"), | ||||
|                     ("Starman",  "ac9e227e30a63ce6eeb55cfbb1fb832aa7e1d3fad2bcb3f663de4a91d744fd50"), | ||||
|                     ("Kitsune1", "46c78fcacffd80abc9cca4917ef8369a37e21a1691ca11e7a3b53f80be745313"), | ||||
|                     ("Scientio", "fa5e5a295ec74c3dda81118d9240db1552b28f831838465ae0712e97e78a6728"), | ||||
|                     ("Kitsune2", "4078ddb1ba1388f768fe6aa40ba9124a72692ecbcc83dc088fa86c735e4dc128"), | ||||
|                     ("Proxmio",  "5e1456904c40192cd3a18183df7dffea90d97739830a902cabb702ecdae4f649"), | ||||
|                 ]; | ||||
| 
 | ||||
|                 let mut new_file = File::create(file_path)?; | ||||
|                 chad_boyz | ||||
|                     .iter() | ||||
|                     .for_each(|chad_info| { | ||||
|                         writeln!(new_file, "{}:0x{}", chad_info.0, chad_info.1) | ||||
|                             .expect("should write to address book; qed"); | ||||
|                         let chad_account_id: [u8; 32] = hex::decode(&chad_info.1[..]) | ||||
|                             .expect("stored seed is valid hex string; qed") | ||||
|                             .as_slice() | ||||
|                             .try_into() | ||||
|                             .expect("stored seed is valid length; qed"); | ||||
|                         let chad_account_id = AccountId32::from(chad_account_id); | ||||
|                         let address = AccountId32::from(chad_account_id.clone()) | ||||
|                             .to_ss58check_with_version(Ss58AddressFormat::custom(1996)); | ||||
|                         self.address_book.push((chad_info.0.to_string(), address, chad_account_id)); | ||||
|                     }); | ||||
|             } | ||||
|         }; | ||||
|         self.scroll_state = self.scroll_state.content_length(self.address_book.len()); | ||||
| 
 | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     fn swap_up(&mut self) { | ||||
|         if let Some(src_index) = self.table_state.selected() { | ||||
|             let dst_index = src_index.saturating_sub(1); | ||||
|             if src_index > dst_index { | ||||
|                 self.address_book.swap(src_index, dst_index); | ||||
|                 self.previous_row(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fn swap_down(&mut self) { | ||||
|         if let Some(src_index) = self.table_state.selected() { | ||||
|             let dst_index = src_index + 1; | ||||
|             if dst_index < self.address_book.len() { | ||||
|                 self.address_book.swap(src_index, dst_index); | ||||
|                 self.next_row(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fn delete_row(&mut self) { | ||||
|         if let Some(index) = self.table_state.selected() { | ||||
|             let _ = self.address_book.remove(index); | ||||
|             self.previous_row(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fn first_row(&mut self) { | ||||
|         if self.address_book.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.address_book.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.address_book.len() > 0 { | ||||
|             let last = self.address_book.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 AddressBook { | ||||
|     fn set_active(&mut self, current_tab: CurrentTab) { | ||||
|         match current_tab { | ||||
|             CurrentTab::AddressBook => self.is_active = true, | ||||
|             _ => { | ||||
|                 self.is_active = false; | ||||
|                 self.table_state.select(None); | ||||
|                 self.scroll_state = self.scroll_state.position(0); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl Component for AddressBook { | ||||
|     fn register_config_handler(&mut self, config: Config) -> Result<()> { | ||||
|         if let Some(style) = config.styles.get(&crate::app::Mode::Explorer) { | ||||
|             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 address_book_file = config.config.data_dir; | ||||
|         address_book_file.push("address-book"); | ||||
|         self.read_or_create(&address_book_file)?; | ||||
|         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(), 
 | ||||
|            // TODO: swap on alt+j or G/gg to bottom and up
 | ||||
|            KeyCode::Char('g') if self.is_active => self.first_row(), | ||||
|            KeyCode::Char('G') if self.is_active => self.last_row(), | ||||
|            KeyCode::Char('K') if self.is_active => self.swap_up(), | ||||
|            KeyCode::Char('J') if self.is_active => self.swap_down(), | ||||
|            KeyCode::Char('d') if self.is_active => self.delete_row(), | ||||
|            _ => {}, | ||||
|         }; | ||||
|         Ok(None) | ||||
|     } | ||||
| 
 | ||||
|     fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { | ||||
|         let [_, place] = super::bars_layout(area); | ||||
|         let (border_style, border_type) = self.palette.create_border_style(self.is_active); | ||||
| 
 | ||||
|         let table = Table::new( | ||||
|             self.address_book | ||||
|                 .iter() | ||||
|                 .map(|info| Row::new(vec![ | ||||
|                         Cell::from(Text::from(info.0.clone()).alignment(Alignment::Left)), 
 | ||||
|                         Cell::from(Text::from(info.1.clone()).alignment(Alignment::Center)), 
 | ||||
|                         Cell::from(Text::from("31 CSPR".to_string()).alignment(Alignment::Right)), | ||||
|                 ])), | ||||
|             [ | ||||
|                 Constraint::Min(0), | ||||
|                 Constraint::Min(13), | ||||
|                 Constraint::Min(10), | ||||
|             ], | ||||
|         ) | ||||
|         .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("Address Book")); | ||||
| 
 | ||||
|         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(()) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										137
									
								
								src/components/wallet/balance.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										137
									
								
								src/components/wallet/balance.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,137 @@ | ||||
| use color_eyre::Result; | ||||
| use ratatui::{ | ||||
|     text::Text, | ||||
|     layout::{Alignment, Constraint, Rect}, 
 | ||||
|     widgets::{Block, Cell, Row, Table}, 
 | ||||
|     Frame | ||||
| }; | ||||
| 
 | ||||
| use super::{Component, PartialComponent, CurrentTab}; | ||||
| use crate::{ | ||||
|     action::Action, 
 | ||||
|     config::Config, 
 | ||||
|     palette::StylePalette, 
 | ||||
| }; | ||||
| 
 | ||||
| #[derive(Debug)] | ||||
| pub struct Balance { | ||||
|     is_active: bool, | ||||
|     total_balance: Option<u128>, | ||||
|     transferable_balance: Option<u128>, | ||||
|     locked_balance: Option<u128>, | ||||
|     bonded_balance: Option<u128>, | ||||
|     palette: StylePalette | ||||
| } | ||||
| 
 | ||||
| impl Default for Balance { | ||||
|     fn default() -> Self { | ||||
|         Self::new() | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl Balance { | ||||
|     const DECIMALS_FOR_BALANCE: usize = 5; | ||||
| 
 | ||||
|     pub fn new() -> Self { | ||||
|         Self { | ||||
|             is_active: false, | ||||
|             total_balance: None, | ||||
|             transferable_balance: None, | ||||
|             locked_balance: None, | ||||
|             bonded_balance: None, | ||||
|             palette: StylePalette::default(), | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fn prepare_u128(&self, value: u128, after: usize) -> String { | ||||
|         let value = value as f64 / 10f64.powi(18); | ||||
|         format!("{:.after$}", value) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl PartialComponent for Balance { | ||||
|     fn set_active(&mut self, current_tab: CurrentTab) { | ||||
|         match current_tab { | ||||
|             CurrentTab::Accounts => self.is_active = true, | ||||
|             _ => self.is_active = false, | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl Component for Balance { | ||||
|     fn register_config_handler(&mut self, config: Config) -> Result<()> { | ||||
|         if let Some(style) = config.styles.get(&crate::app::Mode::Explorer) { | ||||
|             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 { | ||||
|             _ => {} | ||||
|         }; | ||||
|         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("account: ".to_string()).alignment(Alignment::Left)), | ||||
|                     Cell::from(Text::from(self.prepare_u128( | ||||
|                                 self.total_balance.unwrap_or_default(), 
 | ||||
|                                 Self::DECIMALS_FOR_BALANCE, | ||||
|                                 )).alignment(Alignment::Center)), | ||||
|                     Cell::from(Text::from("CSPR".to_string()).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.unwrap_or_default(), 
 | ||||
|                                 Self::DECIMALS_FOR_BALANCE, | ||||
|                                 )).alignment(Alignment::Center)), | ||||
|                     Cell::from(Text::from("CSPR".to_string()).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.unwrap_or_default(), 
 | ||||
|                                 Self::DECIMALS_FOR_BALANCE, | ||||
|                                 )).alignment(Alignment::Center)), | ||||
|                     Cell::from(Text::from("CSPR".to_string()).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.unwrap_or_default(), 
 | ||||
|                                 Self::DECIMALS_FOR_BALANCE, | ||||
|                                 )).alignment(Alignment::Center)), | ||||
|                     Cell::from(Text::from("CSPR".to_string()).alignment(Alignment::Right)), | ||||
|                 ]), | ||||
|             ], | ||||
|             [ | ||||
|                 Constraint::Max(10), | ||||
|                 Constraint::Min(0), | ||||
|                 Constraint::Length(5), | ||||
|             ] | ||||
|         ) | ||||
|         .block(Block::bordered() | ||||
|             .border_style(border_style) | ||||
|             .border_type(border_type) | ||||
|             .title_alignment(Alignment::Right) | ||||
|             .title_style(self.palette.create_title_style(false)) | ||||
|             .title("Balance")); | ||||
| 
 | ||||
|         frame.render_widget(table, place); | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										75
									
								
								src/components/wallet/event_logs.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								src/components/wallet/event_logs.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,75 @@ | ||||
| use color_eyre::Result; | ||||
| use ratatui::{ | ||||
|     layout::{Alignment, Rect}, 
 | ||||
|     widgets::{Block, Paragraph, Wrap}, 
 | ||||
|     Frame | ||||
| }; | ||||
| 
 | ||||
| use super::{Component, PartialComponent, CurrentTab}; | ||||
| use crate::{ | ||||
|     action::Action, 
 | ||||
|     config::Config, 
 | ||||
|     palette::StylePalette, 
 | ||||
| }; | ||||
| 
 | ||||
| #[derive(Debug)] | ||||
| pub struct EventLogs { | ||||
|     palette: StylePalette | ||||
| } | ||||
| 
 | ||||
| impl Default for EventLogs { | ||||
|     fn default() -> Self { | ||||
|         Self::new() | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl EventLogs { | ||||
|     pub fn new() -> Self { | ||||
|         Self { | ||||
|             palette: StylePalette::default(), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl PartialComponent for EventLogs { | ||||
|     fn set_active(&mut self, _current_tab: CurrentTab) {} | ||||
| } | ||||
| 
 | ||||
| impl Component for EventLogs { | ||||
|     fn register_config_handler(&mut self, config: Config) -> Result<()> { | ||||
|         if let Some(style) = config.styles.get(&crate::app::Mode::Explorer) { | ||||
|             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 { | ||||
|             _ => {} | ||||
|         }; | ||||
|         Ok(None) | ||||
|     } | ||||
| 
 | ||||
|     fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { | ||||
|         let [_, place] = super::wallet_layout(area); | ||||
| 
 | ||||
|         let (border_style, border_type) = self.palette.create_border_style(false); | ||||
|         let paragraph = Paragraph::new("latest logs") | ||||
|             .block(Block::bordered() | ||||
|                 .border_style(border_style) | ||||
|                 .border_type(border_type) | ||||
|                 .title_alignment(Alignment::Right) | ||||
|                 .title_style(self.palette.create_title_style(false)) | ||||
|                 .title("Logs")) | ||||
|             .alignment(Alignment::Center) | ||||
|             .wrap(Wrap { trim: true }); | ||||
| 
 | ||||
|         frame.render_widget(paragraph, place); | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										178
									
								
								src/components/wallet/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										178
									
								
								src/components/wallet/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,178 @@ | ||||
| use color_eyre::Result; | ||||
| use crossterm::event::{KeyCode, KeyEvent}; | ||||
| use ratatui::{ | ||||
|     layout::{Constraint, Layout, Rect}, | ||||
|     Frame, | ||||
| }; | ||||
| 
 | ||||
| mod balance; | ||||
| mod transfer; | ||||
| mod address_book; | ||||
| mod add; | ||||
| mod event_logs; | ||||
| mod accounts; | ||||
| mod overview; | ||||
| 
 | ||||
| use balance::Balance; | ||||
| use tokio::sync::mpsc::UnboundedSender; | ||||
| use transfer::Transfer; | ||||
| use address_book::AddressBook; | ||||
| use add::AddAddress; | ||||
| use event_logs::EventLogs; | ||||
| use accounts::Accounts; | ||||
| use overview::Overview; | ||||
| 
 | ||||
| use super::Component; | ||||
| use crate::{action::Action, app::Mode, config::Config}; | ||||
| 
 | ||||
| #[derive(Debug, Clone, PartialEq)] | ||||
| pub enum CurrentTab { | ||||
|     Nothing, | ||||
|     Accounts, | ||||
|     AddressBook, | ||||
| } | ||||
| 
 | ||||
| pub trait PartialComponent: Component { | ||||
|     fn set_active(&mut self, current_tab: CurrentTab); | ||||
| } | ||||
| 
 | ||||
| pub struct Wallet { | ||||
|     is_active: bool, | ||||
|     current_tab: CurrentTab, | ||||
|     components: Vec<Box<dyn PartialComponent>>, | ||||
| } | ||||
| 
 | ||||
| impl Default for Wallet { | ||||
|     fn default() -> Self { | ||||
|         Self { | ||||
|             is_active: false, | ||||
|             current_tab: CurrentTab::Accounts, | ||||
|             components: vec![ | ||||
|                 Box::new(Overview::default()), | ||||
|                 Box::new(Accounts::default()), | ||||
|                 Box::new(Balance::default()), | ||||
|                 Box::new(AddressBook::default()), | ||||
|                 Box::new(EventLogs::default()), | ||||
|                 Box::new(Transfer::default()), | ||||
|                 Box::new(AddAddress::default()), | ||||
|             ], | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl Wallet { | ||||
|     fn move_left(&mut self) { | ||||
|         if let CurrentTab::AddressBook = self.current_tab { | ||||
|             self.current_tab = CurrentTab::Accounts; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fn move_right(&mut self) { | ||||
|         match self.current_tab { | ||||
|             CurrentTab::Nothing => self.current_tab = CurrentTab::Accounts, | ||||
|             CurrentTab::Accounts => self.current_tab = CurrentTab::AddressBook, | ||||
|             _ => {} | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl PartialComponent for Wallet { | ||||
|     fn set_active(&mut self, _current_tab: CurrentTab) {} | ||||
| } | ||||
| 
 | ||||
| impl Component for Wallet { | ||||
|     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<()> { | ||||
|         if let Some(_) = config.styles.get(&crate::app::Mode::Explorer) { | ||||
|             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 { | ||||
|             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::Enter | 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()); | ||||
|                     } | ||||
|                 }, | ||||
|                 _ => { | ||||
|                     for component in self.components.iter_mut() { | ||||
|                         component.handle_key_event(key)?; | ||||
|                     } | ||||
|                 }, | ||||
|             } | ||||
|         } 
 | ||||
|         Ok(None) | ||||
|     } | ||||
| 
 | ||||
|     fn update(&mut self, action: Action) -> Result<Option<Action>> { | ||||
|         if let Action::SetActiveScreen(Mode::Wallet) = action { | ||||
|             self.is_active = true; | ||||
|             self.current_tab = CurrentTab::Accounts; | ||||
|             for component in self.components.iter_mut() { | ||||
|                 component.set_active(self.current_tab.clone()); | ||||
|             } | ||||
|         } | ||||
|         for component in self.components.iter_mut() { | ||||
|             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 wallet_layout(area: Rect) -> [Rect; 2] { | ||||
|     Layout::vertical([ | ||||
|         Constraint::Percentage(75), | ||||
|         Constraint::Percentage(25), | ||||
|     ]).areas(area) | ||||
| } | ||||
| 
 | ||||
| pub fn bars_layout(area: Rect) -> [Rect; 2] { | ||||
|     let [place, _] = wallet_layout(area); | ||||
|     Layout::horizontal([ | ||||
|         Constraint::Percentage(50), | ||||
|         Constraint::Percentage(50), | ||||
|     ]).areas(place) | ||||
| } | ||||
| 
 | ||||
| pub fn account_layout(area: Rect) -> [Rect; 3] { | ||||
|     let [place, _] = bars_layout(area); | ||||
|     Layout::vertical([ | ||||
|         Constraint::Max(4), | ||||
|         Constraint::Min(0), | ||||
|         Constraint::Max(6), | ||||
|     ]).areas(place) | ||||
| } | ||||
							
								
								
									
										119
									
								
								src/components/wallet/overview.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								src/components/wallet/overview.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,119 @@ | ||||
| use color_eyre::Result; | ||||
| use ratatui::{ | ||||
|     text::Text, | ||||
|     layout::{Alignment, Constraint, Rect}, 
 | ||||
|     widgets::{Block, Cell, Row, Table}, 
 | ||||
|     Frame | ||||
| }; | ||||
| 
 | ||||
| use super::{Component, PartialComponent, CurrentTab}; | ||||
| use crate::{ | ||||
|     action::Action, 
 | ||||
|     config::Config, 
 | ||||
|     palette::StylePalette, 
 | ||||
| }; | ||||
| 
 | ||||
| #[derive(Debug)] | ||||
| pub struct Overview { | ||||
|     is_active: bool, | ||||
|     existential_balance: Option<u128>, | ||||
|     total_issuance: Option<u128>, | ||||
|     palette: StylePalette | ||||
| } | ||||
| 
 | ||||
| impl Default for Overview { | ||||
|     fn default() -> Self { | ||||
|         Self::new() | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl Overview { | ||||
|     const DECIMALS_FOR_BALANCE: usize = 5; | ||||
| 
 | ||||
|     pub fn new() -> Self { | ||||
|         Self { | ||||
|             is_active: false, | ||||
|             existential_balance: None, | ||||
|             total_issuance: None, | ||||
|             palette: StylePalette::default(), | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fn prepare_u128(&self, value: u128, after: usize) -> String { | ||||
|         let value = value as f64 / 10f64.powi(18); | ||||
|         format!("{:.after$}", value) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl PartialComponent for Overview { | ||||
|     fn set_active(&mut self, current_tab: CurrentTab) { | ||||
|         match current_tab { | ||||
|             CurrentTab::Accounts => self.is_active = true, | ||||
|             _ => self.is_active = false, | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl Component for Overview { | ||||
|     fn register_config_handler(&mut self, config: Config) -> Result<()> { | ||||
|         if let Some(style) = config.styles.get(&crate::app::Mode::Explorer) { | ||||
|             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::SetExistentialDeposit(ed) => self.existential_balance = Some(ed), | ||||
|             Action::SetTotalIssuance(issuance) => self.total_issuance = Some(issuance), | ||||
|             _ => {} | ||||
|         }; | ||||
|         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("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)), | ||||
|                 ]), | ||||
|                 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)), | ||||
|                 ]), | ||||
|             ], | ||||
|             [ | ||||
|                 Constraint::Max(15), | ||||
|                 Constraint::Min(0), | ||||
|                 Constraint::Length(5), | ||||
|             ] | ||||
|         ) | ||||
|         .block(Block::bordered() | ||||
|             .border_style(border_style) | ||||
|             .border_type(border_type) | ||||
|             .title_alignment(Alignment::Right) | ||||
|             .title_style(self.palette.create_title_style(false)) | ||||
|             .title("Overview")); | ||||
| 
 | ||||
|         frame.render_widget(table, place); | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										83
									
								
								src/components/wallet/transfer.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								src/components/wallet/transfer.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,83 @@ | ||||
| use color_eyre::Result; | ||||
| use crossterm::event::{KeyEvent, KeyCode}; | ||||
| use ratatui::{ | ||||
|     layout::{Constraint, Flex, Layout, Rect}, 
 | ||||
|     widgets::{Block, Clear}, 
 | ||||
|     Frame, | ||||
|     prelude::Stylize, | ||||
| }; | ||||
| 
 | ||||
| use super::{Component, PartialComponent, CurrentTab}; | ||||
| use crate::{ | ||||
|     action::Action, 
 | ||||
|     config::Config, 
 | ||||
|     palette::StylePalette, 
 | ||||
| }; | ||||
| 
 | ||||
| #[derive(Debug)] | ||||
| pub struct Transfer { | ||||
|     palette: StylePalette, | ||||
|     is_shown: bool, | ||||
| } | ||||
| 
 | ||||
| impl Default for Transfer { | ||||
|     fn default() -> Self { | ||||
|         Self::new() | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl Transfer { | ||||
|     pub fn new() -> Self { | ||||
|         Self { | ||||
|             is_shown: false, | ||||
|             palette: StylePalette::default(), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl PartialComponent for Transfer { | ||||
|     fn set_active(&mut self, _current_tab: CurrentTab) {} | ||||
| } | ||||
| 
 | ||||
| impl Component for Transfer { | ||||
|     fn register_config_handler(&mut self, config: Config) -> Result<()> { | ||||
|         if let Some(style) = config.styles.get(&crate::app::Mode::Explorer) { | ||||
|             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 handle_key_event(&mut self, key: KeyEvent) -> Result<Option<Action>> { | ||||
|         match key.code { | ||||
|             KeyCode::Char('t') => self.is_shown = true, 
 | ||||
|             KeyCode::Char(' ') => self.is_shown = false, 
 | ||||
|             _ => {}, | ||||
|         }; | ||||
|         Ok(None) | ||||
|     } | ||||
| 
 | ||||
|     fn update(&mut self, action: Action) -> Result<Option<Action>> { | ||||
|         match action { | ||||
|             _ => {} | ||||
|         }; | ||||
|         Ok(None) | ||||
|     } | ||||
| 
 | ||||
|     fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { | ||||
|         if self.is_shown { | ||||
|             let block = Block::bordered().on_red().title("Transfer"); | ||||
|             let v = Layout::vertical([Constraint::Max(10)]).flex(Flex::Center); | ||||
|             let h = Layout::horizontal([Constraint::Max(55)]).flex(Flex::Center); | ||||
|             let [area] = v.areas(area); | ||||
|             let [area] = h.areas(area); | ||||
|             frame.render_widget(Clear, area); | ||||
|             frame.render_widget(block, area); | ||||
|         } | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
| @ -17,13 +17,13 @@ const CONFIG: &str = include_str!("../config/config.json5"); | ||||
| pub struct AppConfig { | ||||
|     #[serde(default)] | ||||
|     pub data_dir: PathBuf, | ||||
|     #[allow(unused)] | ||||
|     #[serde(default)] | ||||
|     pub config_dir: PathBuf, | ||||
| } | ||||
| 
 | ||||
| #[derive(Clone, Debug, Default, Deserialize)] | ||||
| pub struct Config { | ||||
|     #[allow(unused)] | ||||
|     #[serde(default, flatten)] | ||||
|     pub config: AppConfig, | ||||
|     #[serde(default)] | ||||
|  | ||||
| @ -65,6 +65,10 @@ impl Network { | ||||
|             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::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::UsedAccount(account_id) => predefinded_calls::get_balance(&self.action_tx, &self.online_client_api, account_id).await,
 | ||||
|             _ => Ok(()) | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @ -1,7 +1,8 @@ | ||||
| use primitive_types::H256; | ||||
| use tokio::sync::mpsc::UnboundedSender; | ||||
| use color_eyre::Result; | ||||
| use subxt::{ | ||||
|     ext::sp_core::crypto::{AccountId32, Ss58Codec, Ss58AddressFormat}, | ||||
|     utils::H256, | ||||
|     backend::rpc::RpcClient, | ||||
|     client::OnlineClient, | ||||
|     config::substrate::DigestItem, | ||||
| @ -10,9 +11,12 @@ use subxt::{ | ||||
| 
 | ||||
| use crate::{ | ||||
|     action::Action, 
 | ||||
|     casper_network::{ | ||||
|         self, 
 | ||||
|         runtime_types::sp_consensus_slots, | ||||
|     }, 
 | ||||
|     types::EraInfo, 
 | ||||
|     casper_network::{self, runtime_types::sp_consensus_slots}, | ||||
|     CasperConfig, | ||||
|     CasperAccountId, CasperConfig | ||||
| }; | ||||
| 
 | ||||
| pub async fn get_block_author( | ||||
| @ -39,7 +43,17 @@ pub async fn get_block_author( | ||||
|         _ => None, | ||||
|     }; | ||||
| 
 | ||||
|     action_tx.send(Action::SetBlockAuthor(*at_hash, maybe_author.cloned()))?; | ||||
|     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(()) | ||||
| } | ||||
| @ -108,3 +122,44 @@ pub async fn get_pending_extrinsics( | ||||
|     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: subxt::utils::AccountId32,
 | ||||
| //) -> Result<()> {
 | ||||
| //    let storage_key = casper_network::storage().system().account(&account_id);
 | ||||
| //    let balance = api.storage()
 | ||||
| //        .at_latest()
 | ||||
| //        .await?
 | ||||
| //        .fetch(&storage_key)
 | ||||
| //        .await?;
 | ||||
| //
 | ||||
| //    action_tx.send(Action::SetTotalIssuance(total_issuance))?;
 | ||||
| //    Ok(())
 | ||||
| //}
 | ||||
|  | ||||
| @ -46,8 +46,8 @@ impl FinalizedSubscription { | ||||
|                 )); | ||||
|             } | ||||
| 
 | ||||
|             self.action_tx.send(Action::FinalizedBlockInformation( | ||||
|                     block_hash, block_number, extrinsic_details))?; | ||||
|             self.action_tx.send(Action::FinalizedBlockInformation(block_hash, block_number))?; | ||||
|             self.action_tx.send(Action::ExtrinsicsForBlock(block_number, extrinsic_details))?; | ||||
|             self.action_tx.send(Action::NewFinalizedHash(block_hash))?; | ||||
|             self.action_tx.send(Action::NewFinalizedBlock(block_number))?; | ||||
| 
 | ||||
| @ -103,8 +103,8 @@ impl BestSubscription { | ||||
|                 )); | ||||
|             } | ||||
| 
 | ||||
|             self.action_tx.send(Action::BestBlockInformation( | ||||
|                     block_hash, block_number, extrinsic_details))?; | ||||
|             self.action_tx.send(Action::BestBlockInformation(block_hash, block_number))?; | ||||
|             self.action_tx.send(Action::ExtrinsicsForBlock(block_number, extrinsic_details))?; | ||||
|             self.action_tx.send(Action::NewBestHash(block_hash))?; | ||||
|             self.action_tx.send(Action::BestBlockUpdated(block_number))?; | ||||
|             self.action_tx.send(Action::NewBestBlock(block_number))?; | ||||
| @ -113,6 +113,7 @@ impl BestSubscription { | ||||
|             self.network_tx.send(Action::GetBlockAuthor(block_hash, block.header().digest.logs.clone()))?; | ||||
|             self.network_tx.send(Action::GetActiveEra)?; | ||||
|             self.network_tx.send(Action::GetEpochProgress)?; | ||||
|             self.network_tx.send(Action::GetTotalIssuance)?; | ||||
|         } | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
| @ -12,7 +12,8 @@ pub struct StylePalette { | ||||
|     normal_title_style: Option<Style>, | ||||
|     hover_title_style: Option<Style>, | ||||
| 
 | ||||
|     tagged_style: Option<Style>, | ||||
|     highlight_style: Option<Style>, | ||||
|     scrollbar_style: Option<Style>, | ||||
| 
 | ||||
|     normal_border_type: BorderType, | ||||
|     hover_border_type: BorderType, | ||||
| @ -33,7 +34,8 @@ impl StylePalette { | ||||
|             hover_border_style: None, | ||||
|             normal_title_style: None, | ||||
|             hover_title_style: None, | ||||
|             tagged_style: None, | ||||
|             highlight_style: None, | ||||
|             scrollbar_style: None, | ||||
| 
 | ||||
|             normal_border_type: BorderType::Plain, | ||||
|             hover_border_type: BorderType::Double, | ||||
| @ -64,15 +66,23 @@ impl StylePalette { | ||||
|         self.hover_title_style = hover_title_style; | ||||
|     } | ||||
| 
 | ||||
|     pub fn with_tagged_style(&mut self, tagged_style: Option<Style>) { | ||||
|         self.tagged_style = tagged_style; | ||||
|     pub fn with_highlight_style(&mut self, highlight_style: Option<Style>) { | ||||
|         self.highlight_style = highlight_style; | ||||
|     } | ||||
| 
 | ||||
|     pub fn create_tagged_style(&self) -> Style { | ||||
|         self.tagged_style.unwrap_or_default() | ||||
|     pub fn with_scrollbar_style(&mut self, scrollbar_style: Option<Style>) { | ||||
|         self.scrollbar_style = scrollbar_style; | ||||
|     } | ||||
| 
 | ||||
|     pub fn create_text_style(&mut self, active: bool) -> Style { | ||||
|     pub fn create_scrollbar_style(&mut self) -> Style { | ||||
|         self.scrollbar_style.unwrap_or_default() | ||||
|     } | ||||
| 
 | ||||
|     pub fn create_highlight_style(&self) -> Style { | ||||
|         self.highlight_style.unwrap_or_default() | ||||
|     } | ||||
| 
 | ||||
|     pub fn create_basic_style(&mut self, active: bool) -> Style { | ||||
|         if active { 
 | ||||
|             self.hover_style.unwrap_or_default() | ||||
|         } else { 
 | ||||
|  | ||||
							
								
								
									
										67
									
								
								src/types/local.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								src/types/local.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,67 @@ | ||||
| use std::path::PathBuf; | ||||
| use std::fs::File; | ||||
| use std::io::{Write, BufRead, BufReader}; | ||||
| 
 | ||||
| use color_eyre::Result; | ||||
| 
 | ||||
| #[derive(Debug)] | ||||
| pub struct LocalStorage<ValueType> { | ||||
|     file_path: PathBuf, | ||||
|     values: Vec<(String, ValueType)>, | ||||
| } | ||||
| 
 | ||||
| impl<ValueType: std::fmt::Display> LocalStorage<ValueType> { | ||||
|     pub fn new(file_path: PathBuf) -> Self { | ||||
|         Self { file_path, values: Vec::new() } | ||||
|     } | ||||
| 
 | ||||
|     fn read_or_create<F1, F2>( | ||||
|         &mut self, 
 | ||||
|         make_values: F1, 
 | ||||
|         fallback_on_error: F2, | ||||
|     ) -> Result<()> | ||||
|     where | ||||
|         F1: Fn(&[u8; 32]) -> ValueType, | ||||
|         F2: Fn(&PathBuf) -> Vec<(String, ValueType)>, | ||||
|     { | ||||
|         assert!(self.values.len() == 0, "values already imported from local storage"); | ||||
|         match File::open(&self.file_path) { | ||||
|             Ok(file) => { | ||||
|                 let reader = BufReader::new(file); | ||||
| 
 | ||||
|                 for line in reader.lines() { | ||||
|                     let line = line?; | ||||
| 
 | ||||
|                     let line_split_at = line | ||||
|                         .find(":") | ||||
|                         .unwrap_or(line.len() - 1); | ||||
| 
 | ||||
|                     let (name, value) = line.split_at(line_split_at); | ||||
|                     let value = &value[3..value.len() - 1]; | ||||
| 
 | ||||
|                     let value: [u8; 32] = hex::decode(value) | ||||
|                         .expect("stored seed is valid hex string; qed") | ||||
|                         .as_slice() | ||||
|                         .try_into() | ||||
|                         .expect("stored seed is valid length; qed"); | ||||
| 
 | ||||
|                     self.values.push((name.to_string(), make_values(&value))); | ||||
|                 } | ||||
|             }, | ||||
|             Err(_) => self.values = fallback_on_error(&self.file_path), | ||||
|         }; | ||||
|         self.write_to_file()?; | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     fn write_to_file(&self) -> Result<()> { | ||||
|         let mut file = File::create(&self.file_path)?; | ||||
|         self.values | ||||
|             .iter() | ||||
|             .map(|value| { | ||||
|                 writeln!(file, "{}:{}", value.0, value.1) | ||||
|                     .expect("should write in new file; qed"); | ||||
|             }); | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user