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,
|
||||||
|
casper_network::{
|
||||||
|
self,
|
||||||
|
runtime_types::sp_consensus_slots,
|
||||||
|
},
|
||||||
types::EraInfo,
|
types::EraInfo,
|
||||||
casper_network::{self, runtime_types::sp_consensus_slots},
|
CasperAccountId, CasperConfig
|
||||||
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