diff --git a/Cargo.toml b/Cargo.toml index 43d1c9a..53ff195 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,13 +21,11 @@ human-panic = "2.0.2" json5 = "0.4.1" lazy_static = "1.5.0" libc = "0.2.159" -primitive-types = "0.13.1" ratatui = { version = "0.28.1", features = ["serde", "macros"] } serde = { version = "1.0.210", features = ["derive"] } signal-hook = "0.3.17" -sp-core = "34.0.0" strum = { version = "0.26.3", features = ["derive"] } -subxt = { version = "0.38.0", features = ["jsonrpsee"] } +subxt = { version = "0.38.0", features = ["substrate-compat"] } tokio = { version = "1.40.0", features = ["full"] } tokio-util = "0.7.12" tracing = "0.1.37" diff --git a/config/config.json5 b/config/config.json5 index 7a7a29f..a79952e 100644 --- a/config/config.json5 +++ b/config/config.json5 @@ -7,7 +7,7 @@ "hover_border_style": "blue", "normal_title_style": "blue", "hover_title_style": "", - "tagged_style": "yellow italic", + "highlight_style": "yellow italic", }, "Explorer": { "normal_style": "", @@ -16,17 +16,33 @@ "hover_border_style": "blue", "normal_title_style": "blue", "hover_title_style": "", - "tagged_style": "yellow bold", + "highlight_style": "yellow bold", + "scrollbar_style": "white on blue", + }, + "Wallet": { + "normal_style": "", + "hover_style": "bold yellow italic on blue", + "normal_border_style": "blue", + "hover_border_style": "blue", + "normal_title_style": "blue", + "hover_title_style": "", + "highlight_style": "yellow bold", } }, "keybindings": { + "Menu": { + "": "Quit", + "": "Quit", + "": "Quit", + "": "Suspend", + }, "Explorer": { "": "Quit", "": "Quit", "": "Quit", "": "Suspend", }, - "ExplorerActive": { + "Wallet": { "": "Quit", "": "Quit", "": "Quit", @@ -38,11 +54,5 @@ "": "Quit", "": "Suspend", }, - "EmptyActive": { - "": "Quit", - "": "Quit", - "": "Quit", - "": "Suspend", - } } } diff --git a/src/action.rs b/src/action.rs index 4f8832f..d58d5e9 100644 --- a/src/action.rs +++ b/src/action.rs @@ -1,14 +1,15 @@ use serde::{Deserialize, Serialize}; use strum::Display; -use primitive_types::H256; +use subxt::utils::H256; use subxt::config::substrate::DigestItem; use crate::{ - CasperAccountId, types::{EraInfo, CasperExtrinsicDetails}, }; +use subxt::utils::AccountId32; + #[derive(Debug, Clone, PartialEq, Eq, Display, Serialize, Deserialize)] pub enum Action { Tick, @@ -22,6 +23,10 @@ pub enum Action { Help, SetMode(crate::app::Mode), + SetActiveScreen(crate::app::Mode), + UsedExplorerBlock(Option), + UsedExplorerLog(Option), + UsedAccount(AccountId32), NewBestBlock(u32), NewBestHash(H256), @@ -31,7 +36,7 @@ pub enum Action { ExtrinsicsLength(u32, usize), GetBlockAuthor(H256, Vec), - SetBlockAuthor(H256, Option), + SetBlockAuthor(H256, String), GetNodeName, GetSystemHealth, @@ -52,10 +57,16 @@ pub enum Action { SetChainName(Option), SetChainVersion(Option), - BestBlockInformation(H256, u32, Vec), - FinalizedBlockInformation(H256, u32, Vec), + BestBlockInformation(H256, u32), + FinalizedBlockInformation(H256, u32), + ExtrinsicsForBlock(u32, Vec), SetActiveEra(EraInfo), SetEpochProgress(u64, u64), - SetValidatorsForExplorer(Vec), // TODO: change to BlockAuthor - SetPendingExtrinsicsLength(usize), // TODO: rename in oreder to match tx.pool + SetPendingExtrinsicsLength(usize), + + GetTotalIssuance, + GetExistentialDeposit, + + SetExistentialDeposit(u128), + SetTotalIssuance(u128), } diff --git a/src/app.rs b/src/app.rs index 7245baf..0622298 100644 --- a/src/app.rs +++ b/src/app.rs @@ -11,14 +11,17 @@ use crate::{ config::Config, tui::{Event, Tui}, components::{ - menu::Menu, version::Version, explorer::Explorer, empty::Empty, - health::Health, fps::FpsCounter, Component}, + menu::Menu, version::Version, explorer::Explorer, wallet::Wallet, + empty::Empty, health::Health, fps::FpsCounter, Component, + }, }; #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum Mode { Menu, Explorer, + Wallet, + WalletActive, ExplorerActive, Empty, EmptyActive, @@ -67,6 +70,7 @@ impl App { Box::new(Health::default()), Box::new(Version::default()), Box::new(Explorer::default()), + Box::new(Wallet::default()), Box::new(Empty::default()), ], should_quite: false, @@ -163,6 +167,7 @@ impl App { self.network_tx.send(Action::GetGenesisHash)?; self.network_tx.send(Action::GetChainName)?; self.network_tx.send(Action::GetChainVersion)?; + self.network_tx.send(Action::GetExistentialDeposit)?; Ok(()) } @@ -226,7 +231,7 @@ impl App { } match self.mode { - Mode::Explorer | Mode::ExplorerActive => { + Mode::Explorer => { if let Some(component) = self.components.get_mut(4) { if let Err(err) = component.draw(frame, frame.area()) { let _ = self @@ -235,6 +240,15 @@ impl App { } } }, + Mode::Wallet => { + if let Some(component) = self.components.get_mut(5) { + if let Err(err) = component.draw(frame, frame.area()) { + let _ = self + .action_tx + .send(Action::Error(format!("failed to draw: {:?}", err))); + } + } + }, _ => { if let Some(component) = self.components.last_mut() { if let Err(err) = component.draw(frame, frame.area()) { diff --git a/src/casper.rs b/src/casper.rs index d5922a3..5d9f682 100644 --- a/src/casper.rs +++ b/src/casper.rs @@ -20,7 +20,8 @@ impl Config for CasperConfig { type AssetId = u32; } - +/// A 32-byte cryptographic identifier. This is a simplified version of +/// `sp_core::crypto::AccountId32`. pub type CasperAccountId = subxt::utils::AccountId32; pub type CasperBlock = Block>; diff --git a/src/components/empty.rs b/src/components/empty.rs index e54dfa4..745c4a2 100644 --- a/src/components/empty.rs +++ b/src/components/empty.rs @@ -1,4 +1,6 @@ use color_eyre::Result; +use tokio::sync::mpsc::UnboundedSender; +use crossterm::event::{KeyEvent, KeyCode}; use ratatui::{ layout::{Alignment, Rect}, text::Line, @@ -14,6 +16,7 @@ use crate::{ #[derive(Debug, Clone, Default)] pub struct Empty { is_active: bool, + action_tx: Option>, } impl Empty { @@ -84,27 +87,33 @@ impl Empty { ] } - fn set_active(&mut self) -> Result<()> { - self.is_active = true; - Ok(()) - } - - fn unset_active(&mut self) -> Result<()> { + fn move_out(&mut self) -> Result> { self.is_active = false; - Ok(()) + Ok(Some(Action::SetActiveScreen(Mode::Menu))) } } impl Component for Empty { + fn register_action_handler(&mut self, tx: UnboundedSender) -> Result<()> { + self.action_tx = Some(tx); + Ok(()) + } + fn update(&mut self, action: Action) -> Result> { match action { - Action::SetMode(Mode::EmptyActive) if !self.is_active => self.set_active()?, - Action::SetMode(_) if self.is_active => self.unset_active()?, + Action::SetActiveScreen(Mode::Empty) => self.is_active = true, _ => {} }; Ok(None) } + fn handle_key_event(&mut self, key: KeyEvent) -> Result> { + match key.code { + KeyCode::Esc if self.is_active => self.move_out(), + _ => Ok(None), + } + } + fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { let screen = super::screen_layout(area); diff --git a/src/components/explorer/block_explorer.rs b/src/components/explorer/block_explorer.rs new file mode 100644 index 0000000..8ebc897 --- /dev/null +++ b/src/components/explorer/block_explorer.rs @@ -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>, + blocks: std::collections::VecDeque, + block_headers: std::collections::HashMap, + block_authors: std::collections::HashMap, + 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) -> 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> { + 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> { + 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::>(); + + 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(()) + } +} diff --git a/src/components/explorer/block_ticker.rs b/src/components/explorer/block_ticker.rs index c38f588..21ff9c0 100644 --- a/src/components/explorer/block_ticker.rs +++ b/src/components/explorer/block_ticker.rs @@ -7,7 +7,7 @@ use ratatui::{ Frame }; -use super::Component; +use super::{Component, PartialComponent, CurrentTab}; use crate::{ config::Config, widgets::{BigText, PixelSize}, @@ -46,6 +46,10 @@ impl BlockTicker { } } +impl PartialComponent for BlockTicker { + fn set_active(&mut self, _current_tab: CurrentTab) {} +} + impl Component for BlockTicker { fn register_config_handler(&mut self, config: Config) -> Result<()> { if let Some(style) = config.styles.get(&crate::app::Mode::Explorer) { @@ -102,7 +106,7 @@ impl Component for BlockTicker { let big_text = BigText::builder() .centered() .pixel_size(PixelSize::Quadrant) - .style(self.palette.create_text_style(false)) + .style(self.palette.create_basic_style(false)) .lines(vec![ text.into(), ]) diff --git a/src/components/explorer/current_epoch.rs b/src/components/explorer/current_epoch.rs index 27b5866..66d14e7 100644 --- a/src/components/explorer/current_epoch.rs +++ b/src/components/explorer/current_epoch.rs @@ -6,7 +6,7 @@ use ratatui::{ Frame, }; -use super::Component; +use super::{Component, PartialComponent, CurrentTab}; use crate::{ config::Config, action::Action, @@ -34,6 +34,10 @@ impl CurrentEpoch { } } +impl PartialComponent for CurrentEpoch { + fn set_active(&mut self, _current_tab: CurrentTab) {} +} + impl Component for CurrentEpoch { fn register_config_handler(&mut self, config: Config) -> Result<()> { if let Some(style) = config.styles.get(&crate::app::Mode::Explorer) { @@ -97,7 +101,7 @@ impl Component for CurrentEpoch { let big_text = BigText::builder() .centered() .pixel_size(PixelSize::Quadrant) - .style(self.palette.create_text_style(false)) + .style(self.palette.create_basic_style(false)) .lines(vec![ text.into(), ]) diff --git a/src/components/explorer/current_era.rs b/src/components/explorer/current_era.rs index d217f78..be71fd0 100644 --- a/src/components/explorer/current_era.rs +++ b/src/components/explorer/current_era.rs @@ -6,7 +6,7 @@ use ratatui::{ Frame, }; -use super::Component; +use super::{Component, PartialComponent, CurrentTab}; use crate::{ config::Config, action::Action, @@ -35,6 +35,10 @@ impl CurrentEra { } } +impl PartialComponent for CurrentEra { + fn set_active(&mut self, _current_tab: CurrentTab) {} +} + impl Component for CurrentEra { fn register_config_handler(&mut self, config: Config) -> Result<()> { if let Some(style) = config.styles.get(&crate::app::Mode::Explorer) { @@ -111,7 +115,7 @@ impl Component for CurrentEra { let big_text = BigText::builder() .centered() .pixel_size(PixelSize::Quadrant) - .style(self.palette.create_text_style(false)) + .style(self.palette.create_basic_style(false)) .lines(vec![ text.into(), ]) diff --git a/src/components/explorer/explorer_blocks.rs b/src/components/explorer/explorer_blocks.rs index 7b896c4..9ed8d52 100644 --- a/src/components/explorer/explorer_blocks.rs +++ b/src/components/explorer/explorer_blocks.rs @@ -1,7 +1,6 @@ use std::collections::{HashMap, VecDeque}; use color_eyre::Result; use crossterm::event::{KeyCode, KeyEvent}; -use primitive_types::H256; use ratatui::{ layout::{Alignment, Rect}, prelude::*, @@ -9,7 +8,8 @@ use ratatui::{ widgets::{Block, BorderType, Paragraph}, Frame }; -use sp_core::crypto::{AccountId32, Ss58Codec, Ss58AddressFormat}; +use subxt::ext::sp_core::crypto::{Ss58Codec, Ss58AddressFormat}; +use subxt::utils::H256; use codec::Decode; use super::Component; @@ -126,9 +126,10 @@ impl ExplorerBlocks { .authors .get(&hash) .map_or(String::from("..."), |author| { - let extended_author = AccountId32::decode(&mut author.as_ref()) + let extended_author = CasperAccountId::decode(&mut author.as_ref()) .expect("author should be valid AccountId32; qed"); - extended_author.to_ss58check_with_version(Ss58AddressFormat::custom(1996)) + let account_id = subxt::ext::sp_core::crypto::AccountId32::from(extended_author.0); + account_id.to_ss58check_with_version(Ss58AddressFormat::custom(1996)) }); if free_space < Self::LENGTH_OF_BLOCK_HASH + Self::LENGTH_OF_ADDRESS { @@ -177,7 +178,7 @@ impl ExplorerBlocks { let normal_style = self.palette.create_text_style(false); let active_style = self.palette.create_text_style(true); - let finalized_style = self.palette.create_tagged_style(); + let finalized_style = self.palette.create_highlight_style(); for (idx, current_block_info) in self.blocks.iter().skip(start_index).enumerate() { if idx == total_length { break; } @@ -459,7 +460,7 @@ impl Component for ExplorerBlocks { self.palette.with_hover_border_style(style.get("hover_border_style").copied()); self.palette.with_normal_title_style(style.get("normal_title_style").copied()); self.palette.with_hover_title_style(style.get("hover_title_style").copied()); - self.palette.with_tagged_style(style.get("tagged_style").copied()); + self.palette.with_highlight_style(style.get("highlight_style").copied()); } Ok(()) } @@ -499,7 +500,6 @@ impl Component for ExplorerBlocks { let (border_style_block, border_type_block) = self.palette.create_border_style(self.is_active && self.used_paragraph_index == 0); let (border_style_extrinsics, border_type_extrinsics) = self.palette.create_border_style(self.is_active && self.used_paragraph_index == 1); - // TODO: never used, revisit let (border_style_event, border_type_event) = self.palette.create_border_style(self.is_active && self.used_paragraph_index == 2); frame.render_widget(self.prepare_blocks_paragraph(blocks_place, border_style_block, border_type_block), blocks_place); diff --git a/src/components/explorer/extrinsic_explorer.rs b/src/components/explorer/extrinsic_explorer.rs new file mode 100644 index 0000000..5b6b225 --- /dev/null +++ b/src/components/explorer/extrinsic_explorer.rs @@ -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>, + extrinsics: HashMap>, + current_extrinsics: Option>, + block_numbers: VecDeque, + 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, + ) { + 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) -> 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> { + 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> { + 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::>(), + 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(()) + } +} diff --git a/src/components/explorer/extrinsics_chart.rs b/src/components/explorer/extrinsics_chart.rs index 018e68f..e187ae7 100644 --- a/src/components/explorer/extrinsics_chart.rs +++ b/src/components/explorer/extrinsics_chart.rs @@ -7,7 +7,7 @@ use ratatui::{ Frame, }; -use super::Component; +use super::{Component, PartialComponent, CurrentTab}; use crate::{palette::StylePalette, config::Config, action::Action}; #[derive(Debug, Default)] @@ -70,6 +70,10 @@ impl ExtrinsicsChart { } } +impl PartialComponent for ExtrinsicsChart { + fn set_active(&mut self, _current_tab: CurrentTab) {} +} + impl Component for ExtrinsicsChart { fn register_config_handler(&mut self, config: Config) -> Result<()> { if let Some(style) = config.styles.get(&crate::app::Mode::Explorer) { diff --git a/src/components/explorer/finalized_block.rs b/src/components/explorer/finalized_block.rs index 595e5b4..589e2af 100644 --- a/src/components/explorer/finalized_block.rs +++ b/src/components/explorer/finalized_block.rs @@ -5,7 +5,7 @@ use ratatui::{ Frame, }; -use super::Component; +use super::{Component, PartialComponent, CurrentTab}; use crate::{ config::Config, action::Action, @@ -26,6 +26,10 @@ impl FinalizedBlock { } } +impl PartialComponent for FinalizedBlock { + fn set_active(&mut self, _current_tab: CurrentTab) {} +} + impl Component for FinalizedBlock { fn register_config_handler(&mut self, config: Config) -> Result<()> { if let Some(style) = config.styles.get(&crate::app::Mode::Explorer) { @@ -73,7 +77,7 @@ impl Component for FinalizedBlock { let big_text = BigText::builder() .centered() .pixel_size(PixelSize::Quadrant) - .style(self.palette.create_text_style(false)) + .style(self.palette.create_basic_style(false)) .lines(vec![ text.into(), ]) diff --git a/src/components/explorer/latest_block.rs b/src/components/explorer/latest_block.rs index bf9bbdc..0a4058a 100644 --- a/src/components/explorer/latest_block.rs +++ b/src/components/explorer/latest_block.rs @@ -5,7 +5,7 @@ use ratatui::{ Frame, }; -use super::Component; +use super::{Component, PartialComponent, CurrentTab}; use crate::{ config::Config, action::Action, @@ -26,6 +26,10 @@ impl LatestBlock { } } +impl PartialComponent for LatestBlock { + fn set_active(&mut self, _current_tab: CurrentTab) {} +} + impl Component for LatestBlock { fn register_config_handler(&mut self, config: Config) -> Result<()> { if let Some(style) = config.styles.get(&crate::app::Mode::Explorer) { @@ -73,7 +77,7 @@ impl Component for LatestBlock { let big_text = BigText::builder() .centered() .pixel_size(PixelSize::Quadrant) - .style(self.palette.create_text_style(false)) + .style(self.palette.create_basic_style(false)) .lines(vec![ text.into(), ]) diff --git a/src/components/explorer/log_explorer.rs b/src/components/explorer/log_explorer.rs new file mode 100644 index 0000000..8b9bc5f --- /dev/null +++ b/src/components/explorer/log_explorer.rs @@ -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, + 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> { + 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(()) + } +} diff --git a/src/components/explorer/mod.rs b/src/components/explorer/mod.rs index 517a868..48367af 100644 --- a/src/components/explorer/mod.rs +++ b/src/components/explorer/mod.rs @@ -1,12 +1,13 @@ use color_eyre::Result; -use crossterm::event::KeyEvent; +use crossterm::event::{KeyCode, KeyEvent}; use ratatui::{ layout::{Constraint, Flex, Layout, Rect}, Frame, }; +use tokio::sync::mpsc::UnboundedSender; use super::Component; -use crate::{config::Config, action::Action}; +use crate::{action::Action, app::Mode, config::Config}; mod latest_block; mod finalized_block; @@ -14,7 +15,9 @@ mod block_ticker; mod current_era; mod current_epoch; mod extrinsics_chart; -mod explorer_blocks; +mod block_explorer; +mod extrinsic_explorer; +mod log_explorer; use latest_block::LatestBlock; use finalized_block::FinalizedBlock; @@ -22,15 +25,32 @@ use block_ticker::BlockTicker; use current_era::CurrentEra; use current_epoch::CurrentEpoch; use extrinsics_chart::ExtrinsicsChart; -use explorer_blocks::ExplorerBlocks; +use block_explorer::BlockExplorer; +use extrinsic_explorer::ExtrinsicExplorer; +use log_explorer::LogExplorer; + +#[derive(Debug, Clone, PartialEq)] +pub enum CurrentTab { + Nothing, + Blocks, + Extrinsics, +} + +pub trait PartialComponent: Component { + fn set_active(&mut self, current_tab: CurrentTab); +} pub struct Explorer { - components: Vec> + is_active: bool, + current_tab: CurrentTab, + components: Vec> } impl Default for Explorer { fn default() -> Self { Self { + is_active: false, + current_tab: CurrentTab::Nothing, components: vec![ Box::new(BlockTicker::default()), Box::new(LatestBlock::default()), @@ -38,13 +58,42 @@ impl Default for Explorer { Box::new(CurrentEra::default()), Box::new(CurrentEpoch::default()), Box::new(ExtrinsicsChart::default()), - Box::new(ExplorerBlocks::default()), + Box::new(BlockExplorer::default()), + Box::new(ExtrinsicExplorer::default()), + Box::new(LogExplorer::default()), ] } } } +impl Explorer { + fn move_left(&mut self) { + if let CurrentTab::Extrinsics = self.current_tab { + self.current_tab = CurrentTab::Blocks; + } + } + + fn move_right(&mut self) { + match self.current_tab { + CurrentTab::Nothing => self.current_tab = CurrentTab::Blocks, + CurrentTab::Blocks => self.current_tab = CurrentTab::Extrinsics, + _ => {} + } + } +} + +impl PartialComponent for Explorer { + fn set_active(&mut self, _current_tab: CurrentTab) {} +} + impl Component for Explorer { + fn register_action_handler(&mut self, tx: UnboundedSender) -> Result<()> { + for component in self.components.iter_mut() { + component.register_action_handler(tx.clone())?; + } + Ok(()) + } + fn register_config_handler(&mut self, config: Config) -> Result<()> { if let Some(_) = config.styles.get(&crate::app::Mode::Explorer) { for component in self.components.iter_mut() { @@ -55,13 +104,46 @@ impl Component for Explorer { } fn handle_key_event(&mut self, key: KeyEvent) -> Result> { - for component in self.components.iter_mut() { - component.handle_key_event(key)?; - } + 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> { + if let Action::SetActiveScreen(Mode::Explorer) = action { + self.is_active = true; + self.current_tab = CurrentTab::Blocks; + for component in self.components.iter_mut() { + component.set_active(self.current_tab.clone()); + } + } for component in self.components.iter_mut() { component.update(action.clone())?; } diff --git a/src/components/fps.rs b/src/components/fps.rs index d524d5d..5708c7f 100644 --- a/src/components/fps.rs +++ b/src/components/fps.rs @@ -41,7 +41,7 @@ impl FpsCounter { } } - fn app_tick(&mut self) -> Result<()> { + fn app_tick(&mut self) { self.tick_count += 1; let now = Instant::now(); let elapsed = (now - self.last_tick_update).as_secs_f64(); @@ -50,10 +50,9 @@ impl FpsCounter { self.last_tick_update = now; self.tick_count = 0; } - Ok(()) } - fn render_tick(&mut self) -> Result<()> { + fn render_tick(&mut self) { self.frame_count += 1; let now = Instant::now(); let elapsed = (now - self.last_frame_update).as_secs_f64(); @@ -62,15 +61,14 @@ impl FpsCounter { self.last_frame_update = now; self.frame_count = 0; } - Ok(()) } } impl Component for FpsCounter { fn update(&mut self, action: Action) -> Result> { match action { - Action::Tick => self.app_tick()?, - Action::Render => self.render_tick()?, + Action::Tick => self.app_tick(), + Action::Render => self.render_tick(), _ => {} }; Ok(None) diff --git a/src/components/health.rs b/src/components/health.rs index e003a3a..21ecdc2 100644 --- a/src/components/health.rs +++ b/src/components/health.rs @@ -39,28 +39,6 @@ impl Health { } } - fn set_sync_state( - &mut self, - peers: Option, - 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) -> Result<()> { - self.name = name; - Ok(()) - } - pub fn is_syncing_as_string(&self) -> String { if self.is_syncing { format!("syncing {}", VerticalBlocks::default().to_string()) @@ -90,10 +68,12 @@ impl Component for Health { fn update(&mut self, action: Action) -> Result> { match action { Action::SetSystemHealth(peers, is_syncing, should_have_peers) => { - self.set_sync_state(peers, is_syncing, should_have_peers)? + self.peers = peers; + self.is_syncing = is_syncing; + self.should_have_peers = should_have_peers; }, - Action::SetNodeName(name) => self.set_node_name(name)?, - Action::SetPendingExtrinsicsLength(length) => self.set_tx_pool_length(length)?, + Action::SetNodeName(name) => self.name = name, + Action::SetPendingExtrinsicsLength(length) => self.tx_pool_length = length, _ => {} }; Ok(None) diff --git a/src/components/menu.rs b/src/components/menu.rs index d972588..0e49348 100644 --- a/src/components/menu.rs +++ b/src/components/menu.rs @@ -9,8 +9,8 @@ use crate::{config::Config, action::Action, app::Mode}; pub struct Menu { command_tx: Option>, + list_state: ListState, items: Vec, - current_item_index: usize, is_active: bool, palette: StylePalette, } @@ -23,8 +23,9 @@ impl Default for Menu { impl Menu { pub fn new() -> Self { - Self { + let mut new_list = Self { command_tx: None, + list_state: ListState::default(), items: vec![ String::from("Explorer"), String::from("Wallet"), @@ -33,66 +34,62 @@ impl Menu { String::from("Governance"), String::from("Operations"), ], - current_item_index: Default::default(), - is_active: true, palette: StylePalette::default(), + is_active: true, + }; + new_list.list_state.select(Some(0)); + new_list + } + + fn next_row(&mut self) -> Result> { + 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<()> { - self.current_item_index = self - .current_item_index - .saturating_sub(1); - - if let Some(command_tx) = &self.command_tx { - match self.current_item_index { - 0 => command_tx.send(Action::SetMode(Mode::Explorer))?, - _ => command_tx.send(Action::SetMode(Mode::Empty))?, - } + fn previous_row(&mut self) -> Result> { + let i = match self.list_state.selected() { + Some(i) => { + if i == 0 { + 0 + } else { + i - 1 + } + }, + None => 0 }; - Ok(()) - } - - fn move_current_down(&mut self) -> Result<()> { - let new_current = self.current_item_index + 1; - if new_current < self.items.len() { - self.current_item_index = new_current; + 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))), } - - 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 { + fn update(&mut self, action: Action) -> Result> { + match action { + Action::SetActiveScreen(Mode::Menu) => self.is_active = true, + _ => {} + }; + Ok(None) + } + fn register_action_handler(&mut self, tx: UnboundedSender) -> Result<()> { self.command_tx = Some(tx); Ok(()) @@ -104,46 +101,48 @@ impl Component for Menu { self.palette.with_hover_style(style.get("hover_style").copied()); self.palette.with_normal_border_style(style.get("normal_border_style").copied()); self.palette.with_hover_border_style(style.get("hover_border_style").copied()); - self.palette.with_normal_title_style(style.get("normal_title_style").copied()); - self.palette.with_hover_title_style(style.get("hover_title_style").copied()); + self.palette.with_highlight_style(style.get("highlight_style").copied()); } Ok(()) } fn handle_key_event(&mut self, key: KeyEvent) -> Result> { match key.code { - KeyCode::Up | KeyCode::Char('k') if self.is_active => self.move_current_up()?, - KeyCode::Down | KeyCode::Char('j') if self.is_active => self.move_current_down()?, - KeyCode::Enter | KeyCode::Char('l') | KeyCode::Right if self.is_active => self.unset_active()?, - KeyCode::Esc if !self.is_active => self.set_active()?, - _ => {}, - }; - Ok(None) + KeyCode::Up | KeyCode::Char('k') if self.is_active => self.previous_row(), + KeyCode::Down | KeyCode::Char('j') if self.is_active => self.next_row(), + KeyCode::Enter | KeyCode::Char('l') | KeyCode::Right if self.is_active => { + self.is_active = false; + match self.list_state.selected() { + Some(0) => Ok(Some(Action::SetActiveScreen(Mode::Explorer))), + Some(1) => Ok(Some(Action::SetActiveScreen(Mode::Wallet))), + _ => Ok(Some(Action::SetActiveScreen(Mode::Empty))), + } + }, + _ => Ok(None), + } } fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { let [menu, _] = super::menu_layout(area); - let width = menu.as_size().width as usize; - - let mut text = vec![]; - for (position, item) in self.items.iter().enumerate() { - let active = position == self.current_item_index; - let line = Line::raw(format!("{:^width$}", item)) - .style(self.palette.create_text_style(active)) - .centered(); - text.push(line); - } let (color, border_type) = self.palette.create_border_style(self.is_active); let block = Block::bordered() .border_style(color) .border_type(border_type); - let paragraph = Paragraph::new(text) + let list = List::default() + .items(self.items + .iter() + .map(|item| ListItem::new( + Line::raw(item.as_str()).alignment(Alignment::Center) + )) + .collect::>() + ) .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(()) } } diff --git a/src/components/mod.rs b/src/components/mod.rs index f564860..1438632 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -13,6 +13,7 @@ pub mod health; pub mod menu; pub mod version; pub mod explorer; +pub mod wallet; pub mod empty; pub trait Component { diff --git a/src/components/version.rs b/src/components/version.rs index 8d6eb14..6ee3341 100644 --- a/src/components/version.rs +++ b/src/components/version.rs @@ -5,11 +5,14 @@ use ratatui::{ widgets::{Block, Paragraph, Wrap}, Frame, }; -use primitive_types::H256; +use subxt::utils::H256; use super::Component; use crate::{ - action::Action, palette::StylePalette, widgets::OghamCenter + config::Config, + action::Action, + palette::StylePalette, + widgets::OghamCenter, }; #[derive(Debug, Clone, Default)] @@ -21,21 +24,6 @@ pub struct Version { } impl Version { - fn set_chain_name(&mut self, chain_name: Option) -> Result<()> { - self.chain_name = chain_name; - Ok(()) - } - - fn set_node_version(&mut self, node_version: Option) -> Result<()> { - self.node_version = node_version; - Ok(()) - } - - fn set_genesis_hash(&mut self, genesis_hash: Option) -> Result<()> { - self.genesis_hash = genesis_hash; - Ok(()) - } - fn prepared_genesis_hash(&self) -> String { match self.genesis_hash { Some(genesis_hash) => genesis_hash.to_string(), @@ -45,20 +33,28 @@ impl Version { } impl Component for Version { + fn register_config_handler(&mut self, config: Config) -> Result<()> { + if let Some(style) = config.styles.get(&crate::app::Mode::Menu) { + self.palette.with_normal_style(style.get("normal_style").copied()); + self.palette.with_normal_border_style(style.get("normal_border_style").copied()); + } + Ok(()) + } + fn update(&mut self, action: Action) -> Result> { match action { - Action::SetChainName(maybe_name) => self.set_chain_name(maybe_name)?, - Action::SetChainVersion(version) => self.set_node_version(version)?, - Action::SetGenesisHash(maybe_genesis) => self.set_genesis_hash(maybe_genesis)?, + Action::SetChainName(maybe_name) => self.chain_name = maybe_name, + Action::SetChainVersion(version) => self.node_version = version, + Action::SetGenesisHash(maybe_genesis) => self.genesis_hash = maybe_genesis, _ => {} - }; + } Ok(None) } fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { let [_, version] = super::menu_layout(area); - let text_style = self.palette.create_text_style(false); + let text_style = self.palette.create_basic_style(false); let (border_style, border_type) = self.palette.create_border_style(false); let text = vec![ Line::styled(self.chain_name.clone().unwrap_or(OghamCenter::default().to_string()), text_style), diff --git a/src/components/wallet/accounts.rs b/src/components/wallet/accounts.rs new file mode 100644 index 0000000..341540c --- /dev/null +++ b/src/components/wallet/accounts.rs @@ -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>, + palette: StylePalette, + scroll_state: ScrollbarState, + table_state: TableState, + wallet_keys: Vec<(String, String, PairSigner)>, +} + +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::::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::::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) -> 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> { + match action { + _ => {} + }; + Ok(None) + } + + fn handle_key_event(&mut self, key: KeyEvent) -> Result> { + 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(()) + } +} diff --git a/src/components/wallet/add.rs b/src/components/wallet/add.rs new file mode 100644 index 0000000..c312f01 --- /dev/null +++ b/src/components/wallet/add.rs @@ -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> { + 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> { + 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(()) + } +} diff --git a/src/components/wallet/address_book.rs b/src/components/wallet/address_book.rs new file mode 100644 index 0000000..0e532c8 --- /dev/null +++ b/src/components/wallet/address_book.rs @@ -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> { + 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(()) + } +} diff --git a/src/components/wallet/balance.rs b/src/components/wallet/balance.rs new file mode 100644 index 0000000..0bd5476 --- /dev/null +++ b/src/components/wallet/balance.rs @@ -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, + transferable_balance: Option, + locked_balance: Option, + bonded_balance: Option, + 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> { + 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(()) + } +} diff --git a/src/components/wallet/event_logs.rs b/src/components/wallet/event_logs.rs new file mode 100644 index 0000000..6bd9598 --- /dev/null +++ b/src/components/wallet/event_logs.rs @@ -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> { + 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(()) + } +} diff --git a/src/components/wallet/mod.rs b/src/components/wallet/mod.rs new file mode 100644 index 0000000..ba3b521 --- /dev/null +++ b/src/components/wallet/mod.rs @@ -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>, +} + +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) -> 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> { + 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> { + 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) +} diff --git a/src/components/wallet/overview.rs b/src/components/wallet/overview.rs new file mode 100644 index 0000000..906a7e4 --- /dev/null +++ b/src/components/wallet/overview.rs @@ -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, + total_issuance: Option, + 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> { + 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(()) + } +} diff --git a/src/components/wallet/transfer.rs b/src/components/wallet/transfer.rs new file mode 100644 index 0000000..e014f0b --- /dev/null +++ b/src/components/wallet/transfer.rs @@ -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> { + 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> { + 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(()) + } +} diff --git a/src/config.rs b/src/config.rs index 293eaaa..8c8b77e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -17,13 +17,13 @@ const CONFIG: &str = include_str!("../config/config.json5"); pub struct AppConfig { #[serde(default)] pub data_dir: PathBuf, + #[allow(unused)] #[serde(default)] pub config_dir: PathBuf, } #[derive(Clone, Debug, Default, Deserialize)] pub struct Config { - #[allow(unused)] #[serde(default, flatten)] pub config: AppConfig, #[serde(default)] diff --git a/src/network/mod.rs b/src/network/mod.rs index 1c28d07..fa0dfd9 100644 --- a/src/network/mod.rs +++ b/src/network/mod.rs @@ -65,6 +65,10 @@ impl Network { Action::GetActiveEra => predefinded_calls::get_active_era(&self.action_tx, &self.online_client_api).await, Action::GetEpochProgress => predefinded_calls::get_epoch_progress(&self.action_tx, &self.online_client_api).await, Action::GetPendingExtrinsics => predefinded_calls::get_pending_extrinsics(&self.action_tx, &self.rpc_client).await, + + Action::GetExistentialDeposit => predefinded_calls::get_existential_deposit(&self.action_tx, &self.online_client_api).await, + Action::GetTotalIssuance => predefinded_calls::get_total_issuance(&self.action_tx, &self.online_client_api).await, + //Action::UsedAccount(account_id) => predefinded_calls::get_balance(&self.action_tx, &self.online_client_api, account_id).await, _ => Ok(()) } } diff --git a/src/network/predefinded_calls.rs b/src/network/predefinded_calls.rs index 6db040e..7171ec8 100644 --- a/src/network/predefinded_calls.rs +++ b/src/network/predefinded_calls.rs @@ -1,7 +1,8 @@ -use primitive_types::H256; use tokio::sync::mpsc::UnboundedSender; use color_eyre::Result; use subxt::{ + ext::sp_core::crypto::{AccountId32, Ss58Codec, Ss58AddressFormat}, + utils::H256, backend::rpc::RpcClient, client::OnlineClient, config::substrate::DigestItem, @@ -10,9 +11,12 @@ use subxt::{ use crate::{ action::Action, - types::EraInfo, - casper_network::{self, runtime_types::sp_consensus_slots}, - CasperConfig, + casper_network::{ + self, + runtime_types::sp_consensus_slots, + }, + types::EraInfo, + CasperAccountId, CasperConfig }; pub async fn get_block_author( @@ -39,7 +43,17 @@ pub async fn get_block_author( _ => None, }; - action_tx.send(Action::SetBlockAuthor(*at_hash, maybe_author.cloned()))?; + let validator = match maybe_author { + Some(author) => { + let extended_author = CasperAccountId::decode(&mut author.as_ref()) + .expect("author should be valid AccountId32; qed"); + let account_id = AccountId32::from(extended_author.0); + account_id.to_ss58check_with_version(Ss58AddressFormat::custom(1996)) + }, + None => "...".to_string(), + }; + + action_tx.send(Action::SetBlockAuthor(*at_hash, validator))?; Ok(()) } @@ -108,3 +122,44 @@ pub async fn get_pending_extrinsics( Ok(()) } +pub async fn get_total_issuance( + action_tx: &UnboundedSender, + api: &OnlineClient, +) -> 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, + api: &OnlineClient, +) -> 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, +// api: &OnlineClient, +// 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(()) +//} diff --git a/src/network/subscriptions.rs b/src/network/subscriptions.rs index 3fbba19..05eb09a 100644 --- a/src/network/subscriptions.rs +++ b/src/network/subscriptions.rs @@ -46,8 +46,8 @@ impl FinalizedSubscription { )); } - self.action_tx.send(Action::FinalizedBlockInformation( - block_hash, block_number, extrinsic_details))?; + self.action_tx.send(Action::FinalizedBlockInformation(block_hash, block_number))?; + self.action_tx.send(Action::ExtrinsicsForBlock(block_number, extrinsic_details))?; self.action_tx.send(Action::NewFinalizedHash(block_hash))?; self.action_tx.send(Action::NewFinalizedBlock(block_number))?; @@ -103,8 +103,8 @@ impl BestSubscription { )); } - self.action_tx.send(Action::BestBlockInformation( - block_hash, block_number, extrinsic_details))?; + self.action_tx.send(Action::BestBlockInformation(block_hash, block_number))?; + self.action_tx.send(Action::ExtrinsicsForBlock(block_number, extrinsic_details))?; self.action_tx.send(Action::NewBestHash(block_hash))?; self.action_tx.send(Action::BestBlockUpdated(block_number))?; self.action_tx.send(Action::NewBestBlock(block_number))?; @@ -113,6 +113,7 @@ impl BestSubscription { self.network_tx.send(Action::GetBlockAuthor(block_hash, block.header().digest.logs.clone()))?; self.network_tx.send(Action::GetActiveEra)?; self.network_tx.send(Action::GetEpochProgress)?; + self.network_tx.send(Action::GetTotalIssuance)?; } Ok(()) } diff --git a/src/palette.rs b/src/palette.rs index dc1d58b..d0ab449 100644 --- a/src/palette.rs +++ b/src/palette.rs @@ -12,7 +12,8 @@ pub struct StylePalette { normal_title_style: Option