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" | json5 = "0.4.1" | ||||||
| lazy_static = "1.5.0" | lazy_static = "1.5.0" | ||||||
| libc = "0.2.159" | libc = "0.2.159" | ||||||
| primitive-types = "0.13.1" |  | ||||||
| ratatui = { version = "0.28.1", features = ["serde", "macros"] } | ratatui = { version = "0.28.1", features = ["serde", "macros"] } | ||||||
| serde = { version = "1.0.210", features = ["derive"] } | serde = { version = "1.0.210", features = ["derive"] } | ||||||
| signal-hook = "0.3.17" | signal-hook = "0.3.17" | ||||||
| sp-core = "34.0.0" |  | ||||||
| strum = { version = "0.26.3", features = ["derive"] } | 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 = { version = "1.40.0", features = ["full"] } | ||||||
| tokio-util = "0.7.12" | tokio-util = "0.7.12" | ||||||
| tracing = "0.1.37" | tracing = "0.1.37" | ||||||
|  | |||||||
| @ -7,7 +7,7 @@ | |||||||
|             "hover_border_style": "blue", |             "hover_border_style": "blue", | ||||||
|             "normal_title_style": "blue", |             "normal_title_style": "blue", | ||||||
|             "hover_title_style": "", |             "hover_title_style": "", | ||||||
|             "tagged_style": "yellow italic", |             "highlight_style": "yellow italic", | ||||||
|         }, |         }, | ||||||
|         "Explorer": { |         "Explorer": { | ||||||
|             "normal_style": "", |             "normal_style": "", | ||||||
| @ -16,17 +16,33 @@ | |||||||
|             "hover_border_style": "blue", |             "hover_border_style": "blue", | ||||||
|             "normal_title_style": "blue", |             "normal_title_style": "blue", | ||||||
|             "hover_title_style": "", |             "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": { |     "keybindings": { | ||||||
|  |         "Menu": { | ||||||
|  |             "<q>": "Quit", | ||||||
|  |             "<Ctrl-d>": "Quit", | ||||||
|  |             "<Ctrl-c>": "Quit", | ||||||
|  |             "<Ctrl-z>": "Suspend", | ||||||
|  |         }, | ||||||
|         "Explorer": { |         "Explorer": { | ||||||
|             "<q>": "Quit", |             "<q>": "Quit", | ||||||
|             "<Ctrl-d>": "Quit", |             "<Ctrl-d>": "Quit", | ||||||
|             "<Ctrl-c>": "Quit", |             "<Ctrl-c>": "Quit", | ||||||
|             "<Ctrl-z>": "Suspend", |             "<Ctrl-z>": "Suspend", | ||||||
|         }, |         }, | ||||||
|         "ExplorerActive": { |         "Wallet": { | ||||||
|             "<q>": "Quit", |             "<q>": "Quit", | ||||||
|             "<Ctrl-d>": "Quit", |             "<Ctrl-d>": "Quit", | ||||||
|             "<Ctrl-c>": "Quit", |             "<Ctrl-c>": "Quit", | ||||||
| @ -38,11 +54,5 @@ | |||||||
|             "<Ctrl-c>": "Quit", |             "<Ctrl-c>": "Quit", | ||||||
|             "<Ctrl-z>": "Suspend", |             "<Ctrl-z>": "Suspend", | ||||||
|         }, |         }, | ||||||
|         "EmptyActive": { |  | ||||||
|             "<q>": "Quit", |  | ||||||
|             "<Ctrl-d>": "Quit", |  | ||||||
|             "<Ctrl-c>": "Quit", |  | ||||||
|             "<Ctrl-z>": "Suspend", |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,14 +1,15 @@ | |||||||
| use serde::{Deserialize, Serialize}; | use serde::{Deserialize, Serialize}; | ||||||
| use strum::Display; | use strum::Display; | ||||||
| use primitive_types::H256; |  | ||||||
| 
 | 
 | ||||||
|  | use subxt::utils::H256; | ||||||
| use subxt::config::substrate::DigestItem; | use subxt::config::substrate::DigestItem; | ||||||
| 
 | 
 | ||||||
| use crate::{ | use crate::{ | ||||||
|     CasperAccountId, 
 |  | ||||||
|     types::{EraInfo, CasperExtrinsicDetails}, |     types::{EraInfo, CasperExtrinsicDetails}, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | use subxt::utils::AccountId32; | ||||||
|  | 
 | ||||||
| #[derive(Debug, Clone, PartialEq, Eq, Display, Serialize, Deserialize)] | #[derive(Debug, Clone, PartialEq, Eq, Display, Serialize, Deserialize)] | ||||||
| pub enum Action { | pub enum Action { | ||||||
|     Tick, |     Tick, | ||||||
| @ -22,6 +23,10 @@ pub enum Action { | |||||||
|     Help, |     Help, | ||||||
| 
 | 
 | ||||||
|     SetMode(crate::app::Mode), |     SetMode(crate::app::Mode), | ||||||
|  |     SetActiveScreen(crate::app::Mode), | ||||||
|  |     UsedExplorerBlock(Option<u32>), | ||||||
|  |     UsedExplorerLog(Option<String>), | ||||||
|  |     UsedAccount(AccountId32), | ||||||
| 
 | 
 | ||||||
|     NewBestBlock(u32), |     NewBestBlock(u32), | ||||||
|     NewBestHash(H256), |     NewBestHash(H256), | ||||||
| @ -31,7 +36,7 @@ pub enum Action { | |||||||
|     ExtrinsicsLength(u32, usize), |     ExtrinsicsLength(u32, usize), | ||||||
| 
 | 
 | ||||||
|     GetBlockAuthor(H256, Vec<DigestItem>), |     GetBlockAuthor(H256, Vec<DigestItem>), | ||||||
|     SetBlockAuthor(H256, Option<CasperAccountId>), |     SetBlockAuthor(H256, String), | ||||||
| 
 | 
 | ||||||
|     GetNodeName, |     GetNodeName, | ||||||
|     GetSystemHealth, |     GetSystemHealth, | ||||||
| @ -52,10 +57,16 @@ pub enum Action { | |||||||
|     SetChainName(Option<String>), |     SetChainName(Option<String>), | ||||||
|     SetChainVersion(Option<String>), |     SetChainVersion(Option<String>), | ||||||
| 
 | 
 | ||||||
|     BestBlockInformation(H256, u32, Vec<CasperExtrinsicDetails>), |     BestBlockInformation(H256, u32), | ||||||
|     FinalizedBlockInformation(H256, u32, Vec<CasperExtrinsicDetails>), |     FinalizedBlockInformation(H256, u32), | ||||||
|  |     ExtrinsicsForBlock(u32, Vec<CasperExtrinsicDetails>), | ||||||
|     SetActiveEra(EraInfo), |     SetActiveEra(EraInfo), | ||||||
|     SetEpochProgress(u64, u64), |     SetEpochProgress(u64, u64), | ||||||
|     SetValidatorsForExplorer(Vec<CasperAccountId>), // TODO: change to BlockAuthor
 |     SetPendingExtrinsicsLength(usize), 
 | ||||||
|     SetPendingExtrinsicsLength(usize), // TODO: rename in oreder to match tx.pool
 | 
 | ||||||
|  |     GetTotalIssuance, | ||||||
|  |     GetExistentialDeposit, | ||||||
|  | 
 | ||||||
|  |     SetExistentialDeposit(u128), | ||||||
|  |     SetTotalIssuance(u128), | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										20
									
								
								src/app.rs
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								src/app.rs
									
									
									
									
									
								
							| @ -11,14 +11,17 @@ use crate::{ | |||||||
|     config::Config, |     config::Config, | ||||||
|     tui::{Event, Tui}, |     tui::{Event, Tui}, | ||||||
|     components::{ |     components::{ | ||||||
|         menu::Menu, version::Version, explorer::Explorer, empty::Empty, |         menu::Menu, version::Version, explorer::Explorer, wallet::Wallet, 
 | ||||||
|         health::Health, fps::FpsCounter, Component}, |         empty::Empty, health::Health, fps::FpsCounter, Component, | ||||||
|  |     }, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] | #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] | ||||||
| pub enum Mode { | pub enum Mode { | ||||||
|     Menu, |     Menu, | ||||||
|     Explorer, |     Explorer, | ||||||
|  |     Wallet, | ||||||
|  |     WalletActive, | ||||||
|     ExplorerActive, |     ExplorerActive, | ||||||
|     Empty, |     Empty, | ||||||
|     EmptyActive, |     EmptyActive, | ||||||
| @ -67,6 +70,7 @@ impl App { | |||||||
|                 Box::new(Health::default()), |                 Box::new(Health::default()), | ||||||
|                 Box::new(Version::default()), |                 Box::new(Version::default()), | ||||||
|                 Box::new(Explorer::default()), |                 Box::new(Explorer::default()), | ||||||
|  |                 Box::new(Wallet::default()), | ||||||
|                 Box::new(Empty::default()), |                 Box::new(Empty::default()), | ||||||
|             ], |             ], | ||||||
|             should_quite: false, |             should_quite: false, | ||||||
| @ -163,6 +167,7 @@ impl App { | |||||||
|         self.network_tx.send(Action::GetGenesisHash)?; |         self.network_tx.send(Action::GetGenesisHash)?; | ||||||
|         self.network_tx.send(Action::GetChainName)?; |         self.network_tx.send(Action::GetChainName)?; | ||||||
|         self.network_tx.send(Action::GetChainVersion)?; |         self.network_tx.send(Action::GetChainVersion)?; | ||||||
|  |         self.network_tx.send(Action::GetExistentialDeposit)?; | ||||||
|         Ok(()) |         Ok(()) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -226,7 +231,7 @@ impl App { | |||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             match self.mode { |             match self.mode { | ||||||
|                 Mode::Explorer | Mode::ExplorerActive => { |                 Mode::Explorer => { | ||||||
|                     if let Some(component) = self.components.get_mut(4) { |                     if let Some(component) = self.components.get_mut(4) { | ||||||
|                         if let Err(err) = component.draw(frame, frame.area()) { |                         if let Err(err) = component.draw(frame, frame.area()) { | ||||||
|                             let _ = self |                             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 Some(component) = self.components.last_mut() { | ||||||
|                         if let Err(err) = component.draw(frame, frame.area()) { |                         if let Err(err) = component.draw(frame, frame.area()) { | ||||||
|  | |||||||
| @ -20,7 +20,8 @@ impl Config for CasperConfig { | |||||||
|     type AssetId = u32; |     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 CasperAccountId = subxt::utils::AccountId32; | ||||||
| pub type CasperBlock = Block<CasperConfig, OnlineClient<CasperConfig>>; | pub type CasperBlock = Block<CasperConfig, OnlineClient<CasperConfig>>; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,4 +1,6 @@ | |||||||
| use color_eyre::Result; | use color_eyre::Result; | ||||||
|  | use tokio::sync::mpsc::UnboundedSender; | ||||||
|  | use crossterm::event::{KeyEvent, KeyCode}; | ||||||
| use ratatui::{ | use ratatui::{ | ||||||
|     layout::{Alignment, Rect}, |     layout::{Alignment, Rect}, | ||||||
|     text::Line, |     text::Line, | ||||||
| @ -14,6 +16,7 @@ use crate::{ | |||||||
| #[derive(Debug, Clone, Default)] | #[derive(Debug, Clone, Default)] | ||||||
| pub struct Empty { | pub struct Empty { | ||||||
|     is_active: bool, |     is_active: bool, | ||||||
|  |     action_tx: Option<UnboundedSender<Action>>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl Empty { | impl Empty { | ||||||
| @ -84,27 +87,33 @@ impl Empty { | |||||||
|         ] |         ] | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fn set_active(&mut self) -> Result<()> { |     fn move_out(&mut self) -> Result<Option<Action>> { | ||||||
|         self.is_active = true; |  | ||||||
|         Ok(()) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     fn unset_active(&mut self) -> Result<()> { |  | ||||||
|         self.is_active = false; |         self.is_active = false; | ||||||
|         Ok(()) |         Ok(Some(Action::SetActiveScreen(Mode::Menu))) | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl Component for Empty { | 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>> { |     fn update(&mut self, action: Action) -> Result<Option<Action>> { | ||||||
|         match action { |         match action { | ||||||
|             Action::SetMode(Mode::EmptyActive) if !self.is_active => self.set_active()?, |             Action::SetActiveScreen(Mode::Empty) => self.is_active = true, | ||||||
|             Action::SetMode(_) if self.is_active => self.unset_active()?, |  | ||||||
|             _ => {} |             _ => {} | ||||||
|         }; |         }; | ||||||
|         Ok(None) |         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<()> { |     fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { | ||||||
|         let screen = super::screen_layout(area); |         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 |     Frame | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| use super::Component; | use super::{Component, PartialComponent, CurrentTab}; | ||||||
| use crate::{ | use crate::{ | ||||||
|     config::Config, |     config::Config, | ||||||
|     widgets::{BigText, PixelSize}, 
 |     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 { | impl Component for BlockTicker { | ||||||
|     fn register_config_handler(&mut self, config: Config) -> Result<()> { |     fn register_config_handler(&mut self, config: Config) -> Result<()> { | ||||||
|         if let Some(style) = config.styles.get(&crate::app::Mode::Explorer) { |         if let Some(style) = config.styles.get(&crate::app::Mode::Explorer) { | ||||||
| @ -102,7 +106,7 @@ impl Component for BlockTicker { | |||||||
|             let big_text = BigText::builder() |             let big_text = BigText::builder() | ||||||
|                 .centered() |                 .centered() | ||||||
|                 .pixel_size(PixelSize::Quadrant) |                 .pixel_size(PixelSize::Quadrant) | ||||||
|                 .style(self.palette.create_text_style(false)) |                 .style(self.palette.create_basic_style(false)) | ||||||
|                 .lines(vec![ |                 .lines(vec![ | ||||||
|                     text.into(), |                     text.into(), | ||||||
|                 ]) |                 ]) | ||||||
|  | |||||||
| @ -6,7 +6,7 @@ use ratatui::{ | |||||||
|     Frame, |     Frame, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| use super::Component; | use super::{Component, PartialComponent, CurrentTab}; | ||||||
| use crate::{ | use crate::{ | ||||||
|     config::Config, |     config::Config, | ||||||
|     action::Action, 
 |     action::Action, 
 | ||||||
| @ -34,6 +34,10 @@ impl CurrentEpoch { | |||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | impl PartialComponent for CurrentEpoch { | ||||||
|  |     fn set_active(&mut self, _current_tab: CurrentTab) {} | ||||||
|  | } | ||||||
|  | 
 | ||||||
| impl Component for CurrentEpoch { | impl Component for CurrentEpoch { | ||||||
|     fn register_config_handler(&mut self, config: Config) -> Result<()> { |     fn register_config_handler(&mut self, config: Config) -> Result<()> { | ||||||
|         if let Some(style) = config.styles.get(&crate::app::Mode::Explorer) { |         if let Some(style) = config.styles.get(&crate::app::Mode::Explorer) { | ||||||
| @ -97,7 +101,7 @@ impl Component for CurrentEpoch { | |||||||
|             let big_text = BigText::builder() |             let big_text = BigText::builder() | ||||||
|                 .centered() |                 .centered() | ||||||
|                 .pixel_size(PixelSize::Quadrant) |                 .pixel_size(PixelSize::Quadrant) | ||||||
|                 .style(self.palette.create_text_style(false)) |                 .style(self.palette.create_basic_style(false)) | ||||||
|                 .lines(vec![ |                 .lines(vec![ | ||||||
|                     text.into(), |                     text.into(), | ||||||
|                 ]) |                 ]) | ||||||
|  | |||||||
| @ -6,7 +6,7 @@ use ratatui::{ | |||||||
|     Frame, |     Frame, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| use super::Component; | use super::{Component, PartialComponent, CurrentTab}; | ||||||
| use crate::{ | use crate::{ | ||||||
|     config::Config, |     config::Config, | ||||||
|     action::Action, 
 |     action::Action, 
 | ||||||
| @ -35,6 +35,10 @@ impl CurrentEra { | |||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | impl PartialComponent for CurrentEra { | ||||||
|  |     fn set_active(&mut self, _current_tab: CurrentTab) {} | ||||||
|  | } | ||||||
|  | 
 | ||||||
| impl Component for CurrentEra { | impl Component for CurrentEra { | ||||||
|     fn register_config_handler(&mut self, config: Config) -> Result<()> { |     fn register_config_handler(&mut self, config: Config) -> Result<()> { | ||||||
|         if let Some(style) = config.styles.get(&crate::app::Mode::Explorer) { |         if let Some(style) = config.styles.get(&crate::app::Mode::Explorer) { | ||||||
| @ -111,7 +115,7 @@ impl Component for CurrentEra { | |||||||
|             let big_text = BigText::builder() |             let big_text = BigText::builder() | ||||||
|                 .centered() |                 .centered() | ||||||
|                 .pixel_size(PixelSize::Quadrant) |                 .pixel_size(PixelSize::Quadrant) | ||||||
|                 .style(self.palette.create_text_style(false)) |                 .style(self.palette.create_basic_style(false)) | ||||||
|                 .lines(vec![ |                 .lines(vec![ | ||||||
|                     text.into(), |                     text.into(), | ||||||
|                 ]) |                 ]) | ||||||
|  | |||||||
| @ -1,7 +1,6 @@ | |||||||
| use std::collections::{HashMap, VecDeque}; | use std::collections::{HashMap, VecDeque}; | ||||||
| use color_eyre::Result; | use color_eyre::Result; | ||||||
| use crossterm::event::{KeyCode, KeyEvent}; | use crossterm::event::{KeyCode, KeyEvent}; | ||||||
| use primitive_types::H256; |  | ||||||
| use ratatui::{ | use ratatui::{ | ||||||
|     layout::{Alignment, Rect}, 
 |     layout::{Alignment, Rect}, 
 | ||||||
|     prelude::*, 
 |     prelude::*, 
 | ||||||
| @ -9,7 +8,8 @@ use ratatui::{ | |||||||
|     widgets::{Block, BorderType, Paragraph}, |     widgets::{Block, BorderType, Paragraph}, | ||||||
|     Frame |     Frame | ||||||
| }; | }; | ||||||
| use sp_core::crypto::{AccountId32, Ss58Codec, Ss58AddressFormat}; | use subxt::ext::sp_core::crypto::{Ss58Codec, Ss58AddressFormat}; | ||||||
|  | use subxt::utils::H256; | ||||||
| use codec::Decode; | use codec::Decode; | ||||||
| 
 | 
 | ||||||
| use super::Component; | use super::Component; | ||||||
| @ -126,9 +126,10 @@ impl ExplorerBlocks { | |||||||
|             .authors |             .authors | ||||||
|             .get(&hash) |             .get(&hash) | ||||||
|             .map_or(String::from("..."), |author| { |             .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"); |                     .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 { |         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 normal_style = self.palette.create_text_style(false); | ||||||
|         let active_style = self.palette.create_text_style(true); |         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() { |         for (idx, current_block_info) in self.blocks.iter().skip(start_index).enumerate() { | ||||||
|             if idx == total_length { break; } |             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_hover_border_style(style.get("hover_border_style").copied()); | ||||||
|             self.palette.with_normal_title_style(style.get("normal_title_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_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(()) |         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_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); |         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); |         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); |         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, |     Frame, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| use super::Component; | use super::{Component, PartialComponent, CurrentTab}; | ||||||
| use crate::{palette::StylePalette, config::Config, action::Action}; | use crate::{palette::StylePalette, config::Config, action::Action}; | ||||||
| 
 | 
 | ||||||
| #[derive(Debug, Default)] | #[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 { | impl Component for ExtrinsicsChart { | ||||||
|     fn register_config_handler(&mut self, config: Config) -> Result<()> { |     fn register_config_handler(&mut self, config: Config) -> Result<()> { | ||||||
|         if let Some(style) = config.styles.get(&crate::app::Mode::Explorer) { |         if let Some(style) = config.styles.get(&crate::app::Mode::Explorer) { | ||||||
|  | |||||||
| @ -5,7 +5,7 @@ use ratatui::{ | |||||||
|     Frame, |     Frame, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| use super::Component; | use super::{Component, PartialComponent, CurrentTab}; | ||||||
| use crate::{ | use crate::{ | ||||||
|     config::Config, 
 |     config::Config, 
 | ||||||
|     action::Action, 
 |     action::Action, 
 | ||||||
| @ -26,6 +26,10 @@ impl FinalizedBlock { | |||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | impl PartialComponent for FinalizedBlock { | ||||||
|  |     fn set_active(&mut self, _current_tab: CurrentTab) {} | ||||||
|  | } | ||||||
|  | 
 | ||||||
| impl Component for FinalizedBlock { | impl Component for FinalizedBlock { | ||||||
|     fn register_config_handler(&mut self, config: Config) -> Result<()> { |     fn register_config_handler(&mut self, config: Config) -> Result<()> { | ||||||
|         if let Some(style) = config.styles.get(&crate::app::Mode::Explorer) { |         if let Some(style) = config.styles.get(&crate::app::Mode::Explorer) { | ||||||
| @ -73,7 +77,7 @@ impl Component for FinalizedBlock { | |||||||
|             let big_text = BigText::builder() |             let big_text = BigText::builder() | ||||||
|                 .centered() |                 .centered() | ||||||
|                 .pixel_size(PixelSize::Quadrant) |                 .pixel_size(PixelSize::Quadrant) | ||||||
|                 .style(self.palette.create_text_style(false)) |                 .style(self.palette.create_basic_style(false)) | ||||||
|                 .lines(vec![ |                 .lines(vec![ | ||||||
|                     text.into(), |                     text.into(), | ||||||
|                 ]) |                 ]) | ||||||
|  | |||||||
| @ -5,7 +5,7 @@ use ratatui::{ | |||||||
|     Frame, |     Frame, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| use super::Component; | use super::{Component, PartialComponent, CurrentTab}; | ||||||
| use crate::{ | use crate::{ | ||||||
|     config::Config, 
 |     config::Config, 
 | ||||||
|     action::Action, 
 |     action::Action, 
 | ||||||
| @ -26,6 +26,10 @@ impl LatestBlock { | |||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | impl PartialComponent for LatestBlock { | ||||||
|  |     fn set_active(&mut self, _current_tab: CurrentTab) {} | ||||||
|  | } | ||||||
|  | 
 | ||||||
| impl Component for LatestBlock { | impl Component for LatestBlock { | ||||||
|     fn register_config_handler(&mut self, config: Config) -> Result<()> { |     fn register_config_handler(&mut self, config: Config) -> Result<()> { | ||||||
|         if let Some(style) = config.styles.get(&crate::app::Mode::Explorer) { |         if let Some(style) = config.styles.get(&crate::app::Mode::Explorer) { | ||||||
| @ -73,7 +77,7 @@ impl Component for LatestBlock { | |||||||
|             let big_text = BigText::builder() |             let big_text = BigText::builder() | ||||||
|                 .centered() |                 .centered() | ||||||
|                 .pixel_size(PixelSize::Quadrant) |                 .pixel_size(PixelSize::Quadrant) | ||||||
|                 .style(self.palette.create_text_style(false)) |                 .style(self.palette.create_basic_style(false)) | ||||||
|                 .lines(vec![ |                 .lines(vec![ | ||||||
|                     text.into(), |                     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 color_eyre::Result; | ||||||
| use crossterm::event::KeyEvent; | use crossterm::event::{KeyCode, KeyEvent}; | ||||||
| use ratatui::{ | use ratatui::{ | ||||||
|     layout::{Constraint, Flex, Layout, Rect}, |     layout::{Constraint, Flex, Layout, Rect}, | ||||||
|     Frame, |     Frame, | ||||||
| }; | }; | ||||||
|  | use tokio::sync::mpsc::UnboundedSender; | ||||||
| 
 | 
 | ||||||
| use super::Component; | use super::Component; | ||||||
| use crate::{config::Config, action::Action}; | use crate::{action::Action, app::Mode, config::Config}; | ||||||
| 
 | 
 | ||||||
| mod latest_block; | mod latest_block; | ||||||
| mod finalized_block; | mod finalized_block; | ||||||
| @ -14,7 +15,9 @@ mod block_ticker; | |||||||
| mod current_era; | mod current_era; | ||||||
| mod current_epoch; | mod current_epoch; | ||||||
| mod extrinsics_chart; | mod extrinsics_chart; | ||||||
| mod explorer_blocks; | mod block_explorer; | ||||||
|  | mod extrinsic_explorer; | ||||||
|  | mod log_explorer; | ||||||
| 
 | 
 | ||||||
| use latest_block::LatestBlock; | use latest_block::LatestBlock; | ||||||
| use finalized_block::FinalizedBlock; | use finalized_block::FinalizedBlock; | ||||||
| @ -22,15 +25,32 @@ use block_ticker::BlockTicker; | |||||||
| use current_era::CurrentEra; | use current_era::CurrentEra; | ||||||
| use current_epoch::CurrentEpoch; | use current_epoch::CurrentEpoch; | ||||||
| use extrinsics_chart::ExtrinsicsChart; | 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 { | pub struct Explorer { | ||||||
|     components: Vec<Box<dyn Component>> |     is_active: bool, | ||||||
|  |     current_tab: CurrentTab, | ||||||
|  |     components: Vec<Box<dyn PartialComponent>> | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl Default for Explorer { | impl Default for Explorer { | ||||||
|     fn default() -> Self { |     fn default() -> Self { | ||||||
|         Self { |         Self { | ||||||
|  |             is_active: false, | ||||||
|  |             current_tab: CurrentTab::Nothing, | ||||||
|             components: vec![ |             components: vec![ | ||||||
|                 Box::new(BlockTicker::default()), |                 Box::new(BlockTicker::default()), | ||||||
|                 Box::new(LatestBlock::default()), |                 Box::new(LatestBlock::default()), | ||||||
| @ -38,13 +58,42 @@ impl Default for Explorer { | |||||||
|                 Box::new(CurrentEra::default()), |                 Box::new(CurrentEra::default()), | ||||||
|                 Box::new(CurrentEpoch::default()), |                 Box::new(CurrentEpoch::default()), | ||||||
|                 Box::new(ExtrinsicsChart::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 { | 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<()> { |     fn register_config_handler(&mut self, config: Config) -> Result<()> { | ||||||
|         if let Some(_) = config.styles.get(&crate::app::Mode::Explorer) { |         if let Some(_) = config.styles.get(&crate::app::Mode::Explorer) { | ||||||
|             for component in self.components.iter_mut() { |             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>> { |     fn handle_key_event(&mut self, key: KeyEvent) -> Result<Option<Action>> { | ||||||
|         for component in self.components.iter_mut() { |         if self.is_active { | ||||||
|             component.handle_key_event(key)?; |             match key.code { | ||||||
|         } |                 KeyCode::Esc => { | ||||||
|  |                     self.is_active = false; | ||||||
|  |                     self.current_tab = CurrentTab::Nothing; | ||||||
|  |                     for component in self.components.iter_mut() { | ||||||
|  |                         component.set_active(self.current_tab.clone()); | ||||||
|  |                     } | ||||||
|  |                     return Ok(Some(Action::SetActiveScreen(Mode::Menu))); | ||||||
|  |                 }, | ||||||
|  |                 KeyCode::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) |         Ok(None) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fn update(&mut self, action: Action) -> Result<Option<Action>> { |     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() { |         for component in self.components.iter_mut() { | ||||||
|             component.update(action.clone())?; |             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; |         self.tick_count += 1; | ||||||
|         let now = Instant::now(); |         let now = Instant::now(); | ||||||
|         let elapsed = (now - self.last_tick_update).as_secs_f64(); |         let elapsed = (now - self.last_tick_update).as_secs_f64(); | ||||||
| @ -50,10 +50,9 @@ impl FpsCounter { | |||||||
|             self.last_tick_update = now; |             self.last_tick_update = now; | ||||||
|             self.tick_count = 0; |             self.tick_count = 0; | ||||||
|         } |         } | ||||||
|         Ok(()) |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fn render_tick(&mut self) -> Result<()> { |     fn render_tick(&mut self) { | ||||||
|         self.frame_count += 1; |         self.frame_count += 1; | ||||||
|         let now = Instant::now(); |         let now = Instant::now(); | ||||||
|         let elapsed = (now - self.last_frame_update).as_secs_f64(); |         let elapsed = (now - self.last_frame_update).as_secs_f64(); | ||||||
| @ -62,15 +61,14 @@ impl FpsCounter { | |||||||
|             self.last_frame_update = now; |             self.last_frame_update = now; | ||||||
|             self.frame_count = 0; |             self.frame_count = 0; | ||||||
|         } |         } | ||||||
|         Ok(()) |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl Component for FpsCounter { | impl Component for FpsCounter { | ||||||
|     fn update(&mut self, action: Action) -> Result<Option<Action>> { |     fn update(&mut self, action: Action) -> Result<Option<Action>> { | ||||||
|         match action { |         match action { | ||||||
|             Action::Tick => self.app_tick()?, |             Action::Tick => self.app_tick(), | ||||||
|             Action::Render => self.render_tick()?, |             Action::Render => self.render_tick(), | ||||||
|             _ => {} |             _ => {} | ||||||
|         }; |         }; | ||||||
|         Ok(None) |         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 { |     pub fn is_syncing_as_string(&self) -> String { | ||||||
|         if self.is_syncing { 
 |         if self.is_syncing { 
 | ||||||
|             format!("syncing {}", VerticalBlocks::default().to_string()) |             format!("syncing {}", VerticalBlocks::default().to_string()) | ||||||
| @ -90,10 +68,12 @@ impl Component for Health { | |||||||
|     fn update(&mut self, action: Action) -> Result<Option<Action>> { |     fn update(&mut self, action: Action) -> Result<Option<Action>> { | ||||||
|         match action { |         match action { | ||||||
|             Action::SetSystemHealth(peers, is_syncing, should_have_peers) => { |             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::SetNodeName(name) => self.name = name, | ||||||
|             Action::SetPendingExtrinsicsLength(length) => self.set_tx_pool_length(length)?, |             Action::SetPendingExtrinsicsLength(length) => self.tx_pool_length = length, | ||||||
|             _ => {} |             _ => {} | ||||||
|         }; |         }; | ||||||
|         Ok(None) |         Ok(None) | ||||||
|  | |||||||
| @ -9,8 +9,8 @@ use crate::{config::Config, action::Action, app::Mode}; | |||||||
| 
 | 
 | ||||||
| pub struct Menu { | pub struct Menu { | ||||||
|     command_tx: Option<UnboundedSender<Action>>, |     command_tx: Option<UnboundedSender<Action>>, | ||||||
|  |     list_state: ListState, | ||||||
|     items: Vec<String>, |     items: Vec<String>, | ||||||
|     current_item_index: usize, |  | ||||||
|     is_active: bool, |     is_active: bool, | ||||||
|     palette: StylePalette, |     palette: StylePalette, | ||||||
| } | } | ||||||
| @ -23,8 +23,9 @@ impl Default for Menu { | |||||||
| 
 | 
 | ||||||
| impl Menu { | impl Menu { | ||||||
|     pub fn new() -> Self { |     pub fn new() -> Self { | ||||||
|         Self { |         let mut new_list = Self { | ||||||
|             command_tx: None, |             command_tx: None, | ||||||
|  |             list_state: ListState::default(), | ||||||
|             items: vec![ |             items: vec![ | ||||||
|                 String::from("Explorer"), |                 String::from("Explorer"), | ||||||
|                 String::from("Wallet"), |                 String::from("Wallet"), | ||||||
| @ -33,66 +34,62 @@ impl Menu { | |||||||
|                 String::from("Governance"), |                 String::from("Governance"), | ||||||
|                 String::from("Operations"), |                 String::from("Operations"), | ||||||
|             ], |             ], | ||||||
|             current_item_index: Default::default(), |  | ||||||
|             is_active: true, |  | ||||||
|             palette: StylePalette::default(), |             palette: StylePalette::default(), | ||||||
|  |             is_active: true, | ||||||
| 
 | 
 | ||||||
|  |         }; | ||||||
|  |         new_list.list_state.select(Some(0)); | ||||||
|  |         new_list | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     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, | ||||||
|  |         }; | ||||||
|  |         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 move_current_up(&mut self) -> Result<()> { |     fn previous_row(&mut self) -> Result<Option<Action>> { | ||||||
|         self.current_item_index = self |         let i = match self.list_state.selected() { | ||||||
|             .current_item_index |             Some(i) => { | ||||||
|             .saturating_sub(1); |                 if i == 0 { | ||||||
| 
 |                     0 | ||||||
|         if let Some(command_tx) = &self.command_tx { |                 } else { | ||||||
|             match self.current_item_index { |                     i - 1 | ||||||
|                 0 => command_tx.send(Action::SetMode(Mode::Explorer))?, |                 } | ||||||
|                 _ => command_tx.send(Action::SetMode(Mode::Empty))?, |             }, | ||||||
|             } |             None => 0 | ||||||
|         }; |         }; | ||||||
|         Ok(()) |         self.list_state.select(Some(i)); | ||||||
|     } |         match i { | ||||||
| 
 |             0 => Ok(Some(Action::SetMode(Mode::Explorer))), | ||||||
|     fn move_current_down(&mut self) -> Result<()> { |             1 => Ok(Some(Action::SetMode(Mode::Wallet))), | ||||||
|         let new_current = self.current_item_index + 1; |             _ => Ok(Some(Action::SetMode(Mode::Empty))), | ||||||
|         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))?, |  | ||||||
|             } |  | ||||||
|         }; |  | ||||||
|         Ok(()) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     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))?, |  | ||||||
|             } |  | ||||||
|         }; |  | ||||||
|         Ok(()) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     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 { | 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<()> { |     fn register_action_handler(&mut self, tx: UnboundedSender<Action>) -> Result<()> { | ||||||
|         self.command_tx = Some(tx); |         self.command_tx = Some(tx); | ||||||
|         Ok(()) |         Ok(()) | ||||||
| @ -104,46 +101,48 @@ impl Component for Menu { | |||||||
|             self.palette.with_hover_style(style.get("hover_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_normal_border_style(style.get("normal_border_style").copied()); | ||||||
|             self.palette.with_hover_border_style(style.get("hover_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_highlight_style(style.get("highlight_style").copied()); | ||||||
|             self.palette.with_hover_title_style(style.get("hover_title_style").copied()); |  | ||||||
|         } |         } | ||||||
|         Ok(()) |         Ok(()) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fn handle_key_event(&mut self, key: KeyEvent) -> Result<Option<Action>> { |     fn handle_key_event(&mut self, key: KeyEvent) -> Result<Option<Action>> { | ||||||
|         match key.code { |         match key.code { | ||||||
|            KeyCode::Up | KeyCode::Char('k') if self.is_active => self.move_current_up()?, 
 |             KeyCode::Up | KeyCode::Char('k') if self.is_active => self.previous_row(), 
 | ||||||
|            KeyCode::Down | KeyCode::Char('j') if self.is_active => self.move_current_down()?, 
 |             KeyCode::Down | KeyCode::Char('j') if self.is_active => self.next_row(), 
 | ||||||
|            KeyCode::Enter | KeyCode::Char('l') | KeyCode::Right if self.is_active => self.unset_active()?, |             KeyCode::Enter | KeyCode::Char('l') | KeyCode::Right if self.is_active => { | ||||||
|            KeyCode::Esc if !self.is_active => self.set_active()?, |                 self.is_active = false; | ||||||
|            _ => {}, |                 match self.list_state.selected() { | ||||||
|         }; |                     Some(0) => Ok(Some(Action::SetActiveScreen(Mode::Explorer))), | ||||||
|         Ok(None) |                     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<()> { |     fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { | ||||||
|         let [menu, _] = super::menu_layout(area); |         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 (color, border_type) = self.palette.create_border_style(self.is_active); | ||||||
|         let block = Block::bordered() |         let block = Block::bordered() | ||||||
|             .border_style(color) |             .border_style(color) | ||||||
|             .border_type(border_type); |             .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) |             .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(()) |         Ok(()) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -13,6 +13,7 @@ pub mod health; | |||||||
| pub mod menu; | pub mod menu; | ||||||
| pub mod version; | pub mod version; | ||||||
| pub mod explorer; | pub mod explorer; | ||||||
|  | pub mod wallet; | ||||||
| pub mod empty; | pub mod empty; | ||||||
| 
 | 
 | ||||||
| pub trait Component { | pub trait Component { | ||||||
|  | |||||||
| @ -5,11 +5,14 @@ use ratatui::{ | |||||||
|     widgets::{Block, Paragraph, Wrap}, |     widgets::{Block, Paragraph, Wrap}, | ||||||
|     Frame, |     Frame, | ||||||
| }; | }; | ||||||
| use primitive_types::H256; | use subxt::utils::H256; | ||||||
| 
 | 
 | ||||||
| use super::Component; | use super::Component; | ||||||
| use crate::{ | use crate::{ | ||||||
|     action::Action, palette::StylePalette, widgets::OghamCenter |     config::Config, | ||||||
|  |     action::Action, 
 | ||||||
|  |     palette::StylePalette, 
 | ||||||
|  |     widgets::OghamCenter, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| #[derive(Debug, Clone, Default)] | #[derive(Debug, Clone, Default)] | ||||||
| @ -21,21 +24,6 @@ pub struct Version { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl 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 { |     fn prepared_genesis_hash(&self) -> String { | ||||||
|         match self.genesis_hash { |         match self.genesis_hash { | ||||||
|             Some(genesis_hash) => genesis_hash.to_string(), |             Some(genesis_hash) => genesis_hash.to_string(), | ||||||
| @ -45,20 +33,28 @@ impl Version { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl Component for 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>> { |     fn update(&mut self, action: Action) -> Result<Option<Action>> { | ||||||
|         match action { |         match action { | ||||||
|             Action::SetChainName(maybe_name) => self.set_chain_name(maybe_name)?, |             Action::SetChainName(maybe_name) => self.chain_name = maybe_name, | ||||||
|             Action::SetChainVersion(version) => self.set_node_version(version)?, |             Action::SetChainVersion(version) => self.node_version = version, | ||||||
|             Action::SetGenesisHash(maybe_genesis) => self.set_genesis_hash(maybe_genesis)?, |             Action::SetGenesisHash(maybe_genesis) => self.genesis_hash = maybe_genesis, | ||||||
|             _ => {} |             _ => {} | ||||||
|         }; |         } | ||||||
|         Ok(None) |         Ok(None) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { |     fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { | ||||||
|         let [_, version] = super::menu_layout(area); |         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 (border_style, border_type) = self.palette.create_border_style(false); | ||||||
|         let text = vec![ |         let text = vec![ | ||||||
|             Line::styled(self.chain_name.clone().unwrap_or(OghamCenter::default().to_string()), text_style), |             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 { | pub struct AppConfig { | ||||||
|     #[serde(default)] |     #[serde(default)] | ||||||
|     pub data_dir: PathBuf, |     pub data_dir: PathBuf, | ||||||
|  |     #[allow(unused)] | ||||||
|     #[serde(default)] |     #[serde(default)] | ||||||
|     pub config_dir: PathBuf, |     pub config_dir: PathBuf, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[derive(Clone, Debug, Default, Deserialize)] | #[derive(Clone, Debug, Default, Deserialize)] | ||||||
| pub struct Config { | pub struct Config { | ||||||
|     #[allow(unused)] |  | ||||||
|     #[serde(default, flatten)] |     #[serde(default, flatten)] | ||||||
|     pub config: AppConfig, |     pub config: AppConfig, | ||||||
|     #[serde(default)] |     #[serde(default)] | ||||||
|  | |||||||
| @ -65,6 +65,10 @@ impl Network { | |||||||
|             Action::GetActiveEra => predefinded_calls::get_active_era(&self.action_tx, &self.online_client_api).await, |             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::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::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(()) |             _ => Ok(()) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -1,7 +1,8 @@ | |||||||
| use primitive_types::H256; |  | ||||||
| use tokio::sync::mpsc::UnboundedSender; | use tokio::sync::mpsc::UnboundedSender; | ||||||
| use color_eyre::Result; | use color_eyre::Result; | ||||||
| use subxt::{ | use subxt::{ | ||||||
|  |     ext::sp_core::crypto::{AccountId32, Ss58Codec, Ss58AddressFormat}, | ||||||
|  |     utils::H256, | ||||||
|     backend::rpc::RpcClient, |     backend::rpc::RpcClient, | ||||||
|     client::OnlineClient, |     client::OnlineClient, | ||||||
|     config::substrate::DigestItem, |     config::substrate::DigestItem, | ||||||
| @ -10,9 +11,12 @@ use subxt::{ | |||||||
| 
 | 
 | ||||||
| use crate::{ | use crate::{ | ||||||
|     action::Action, 
 |     action::Action, 
 | ||||||
|     types::EraInfo, |     casper_network::{ | ||||||
|     casper_network::{self, runtime_types::sp_consensus_slots}, |         self, 
 | ||||||
|     CasperConfig, |         runtime_types::sp_consensus_slots, | ||||||
|  |     }, 
 | ||||||
|  |     types::EraInfo, 
 | ||||||
|  |     CasperAccountId, CasperConfig | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| pub async fn get_block_author( | pub async fn get_block_author( | ||||||
| @ -39,7 +43,17 @@ pub async fn get_block_author( | |||||||
|         _ => None, |         _ => 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(()) |     Ok(()) | ||||||
| } | } | ||||||
| @ -108,3 +122,44 @@ pub async fn get_pending_extrinsics( | |||||||
|     Ok(()) |     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( |             self.action_tx.send(Action::FinalizedBlockInformation(block_hash, block_number))?; | ||||||
|                     block_hash, block_number, extrinsic_details))?; |             self.action_tx.send(Action::ExtrinsicsForBlock(block_number, extrinsic_details))?; | ||||||
|             self.action_tx.send(Action::NewFinalizedHash(block_hash))?; |             self.action_tx.send(Action::NewFinalizedHash(block_hash))?; | ||||||
|             self.action_tx.send(Action::NewFinalizedBlock(block_number))?; |             self.action_tx.send(Action::NewFinalizedBlock(block_number))?; | ||||||
| 
 | 
 | ||||||
| @ -103,8 +103,8 @@ impl BestSubscription { | |||||||
|                 )); |                 )); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             self.action_tx.send(Action::BestBlockInformation( |             self.action_tx.send(Action::BestBlockInformation(block_hash, block_number))?; | ||||||
|                     block_hash, block_number, extrinsic_details))?; |             self.action_tx.send(Action::ExtrinsicsForBlock(block_number, extrinsic_details))?; | ||||||
|             self.action_tx.send(Action::NewBestHash(block_hash))?; |             self.action_tx.send(Action::NewBestHash(block_hash))?; | ||||||
|             self.action_tx.send(Action::BestBlockUpdated(block_number))?; |             self.action_tx.send(Action::BestBlockUpdated(block_number))?; | ||||||
|             self.action_tx.send(Action::NewBestBlock(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::GetBlockAuthor(block_hash, block.header().digest.logs.clone()))?; | ||||||
|             self.network_tx.send(Action::GetActiveEra)?; |             self.network_tx.send(Action::GetActiveEra)?; | ||||||
|             self.network_tx.send(Action::GetEpochProgress)?; |             self.network_tx.send(Action::GetEpochProgress)?; | ||||||
|  |             self.network_tx.send(Action::GetTotalIssuance)?; | ||||||
|         } |         } | ||||||
|         Ok(()) |         Ok(()) | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -12,7 +12,8 @@ pub struct StylePalette { | |||||||
|     normal_title_style: Option<Style>, |     normal_title_style: Option<Style>, | ||||||
|     hover_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, |     normal_border_type: BorderType, | ||||||
|     hover_border_type: BorderType, |     hover_border_type: BorderType, | ||||||
| @ -33,7 +34,8 @@ impl StylePalette { | |||||||
|             hover_border_style: None, |             hover_border_style: None, | ||||||
|             normal_title_style: None, |             normal_title_style: None, | ||||||
|             hover_title_style: None, |             hover_title_style: None, | ||||||
|             tagged_style: None, |             highlight_style: None, | ||||||
|  |             scrollbar_style: None, | ||||||
| 
 | 
 | ||||||
|             normal_border_type: BorderType::Plain, |             normal_border_type: BorderType::Plain, | ||||||
|             hover_border_type: BorderType::Double, |             hover_border_type: BorderType::Double, | ||||||
| @ -64,15 +66,23 @@ impl StylePalette { | |||||||
|         self.hover_title_style = hover_title_style; |         self.hover_title_style = hover_title_style; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     pub fn with_tagged_style(&mut self, tagged_style: Option<Style>) { |     pub fn with_highlight_style(&mut self, highlight_style: Option<Style>) { | ||||||
|         self.tagged_style = tagged_style; |         self.highlight_style = highlight_style; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     pub fn create_tagged_style(&self) -> Style { |     pub fn with_scrollbar_style(&mut self, scrollbar_style: Option<Style>) { | ||||||
|         self.tagged_style.unwrap_or_default() |         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 { 
 |         if active { 
 | ||||||
|             self.hover_style.unwrap_or_default() |             self.hover_style.unwrap_or_default() | ||||||
|         } else { 
 |         } 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