integration of subxt dependency

Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
This commit is contained in:
Uncle Stretch 2024-11-24 19:01:41 +03:00
parent 1508ed818f
commit 73db8ca32a
Signed by: str3tch
GPG Key ID: 84F3190747EE79AA
39 changed files with 734 additions and 895 deletions

View File

@ -1,6 +1,6 @@
[package] [package]
name = "ghost-eye" name = "ghost-eye"
version = "0.1.0" version = "0.2.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
@ -21,20 +21,13 @@ 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"
log = "0.4.22" primitive-types = "0.13.1"
once_cell = "1.20.2"
pretty_assertions = "1.4.1"
rand = "0.8.5"
ratatui = { version = "0.28.1", features = ["serde", "macros"] } ratatui = { version = "0.28.1", features = ["serde", "macros"] }
reqwest = { version = "0.12.8", features = ["json"] }
serde = { version = "1.0.210", features = ["derive"] } serde = { version = "1.0.210", features = ["derive"] }
serde_json = "1.0.128"
signal-hook = "0.3.17" signal-hook = "0.3.17"
sp-consensus-babe = "0.40.0"
sp-core = "34.0.0" sp-core = "34.0.0"
sp-runtime = "39.0.2"
strip-ansi-escapes = "0.2.0"
strum = { version = "0.26.3", features = ["derive"] } strum = { version = "0.26.3", features = ["derive"] }
subxt = { version = "0.38.0", features = ["jsonrpsee"] }
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"

BIN
artifacts/casper.scale Normal file

Binary file not shown.

View File

@ -1,9 +1,12 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use strum::Display; use strum::Display;
use primitive_types::H256;
use crate::types::{ use subxt::config::substrate::DigestItem;
block::BlockInfo,
era::EraInfo, use crate::{
CasperAccountId,
types::{EraInfo, CasperExtrinsicDetails},
}; };
#[derive(Debug, Clone, PartialEq, Eq, Display, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Display, Serialize, Deserialize)]
@ -20,29 +23,39 @@ pub enum Action {
SetMode(crate::app::Mode), SetMode(crate::app::Mode),
NewBestBlock(u32),
NewBestHash(H256),
NewFinalizedBlock(u32),
NewFinalizedHash(H256),
BestBlockUpdated(u32),
ExtrinsicsLength(u32, usize),
GetBlockAuthor(H256, Vec<DigestItem>),
SetBlockAuthor(H256, Option<CasperAccountId>),
GetNodeName, GetNodeName,
GetSyncState, GetSystemHealth,
GetGenesisHash, GetGenesisHash,
GetChainName, GetChainName,
GetNodeVersion, GetChainVersion,
GetPendingExtrinsics, GetPendingExtrinsics,
GetLatestBlock, GetLatestBlock,
GetFinalizedBlock, GetFinalizedBlock,
GetActiveEra, GetActiveEra,
GetEpoch, GetEpochProgress,
GetValidators, GetValidators,
SetNodeName(Option<String>), SetNodeName(Option<String>),
SetSyncState(Option<u32>, bool, bool), SetSystemHealth(Option<usize>, bool, bool),
SetGenesisHash(Option<String>), SetGenesisHash(Option<H256>),
SetChainName(Option<String>), SetChainName(Option<String>),
SetNodeVersion(Option<String>), SetChainVersion(Option<String>),
SetLatestBlock(String, BlockInfo), BestBlockInformation(H256, u32, Vec<CasperExtrinsicDetails>),
SetFinalizedBlock(String, BlockInfo), FinalizedBlockInformation(H256, u32, Vec<CasperExtrinsicDetails>),
SetActiveEra(EraInfo), SetActiveEra(EraInfo),
SetEpoch(u64, u64), SetEpochProgress(u64, u64),
SetValidators(Vec<String>), SetValidatorsForExplorer(Vec<CasperAccountId>), // TODO: change to BlockAuthor
SetPendingExtrinsicsLength(usize), SetPendingExtrinsicsLength(usize), // TODO: rename in oreder to match tx.pool
} }

View File

@ -138,9 +138,8 @@ impl App {
Event::Render => action_tx.send(Action::Render)?, Event::Render => action_tx.send(Action::Render)?,
Event::Resize(x, y) => action_tx.send(Action::Resize(x, y))?, Event::Resize(x, y) => action_tx.send(Action::Resize(x, y))?,
Event::Key(key) => self.handle_key_event(key)?, Event::Key(key) => self.handle_key_event(key)?,
Event::Node => self.trigger_node_events()?, Event::Oneshot => self.trigger_oneshot_node_events()?,
Event::FastNode => self.trigger_node_fast_events()?, Event::Node => self.trigger_node_fast_events()?,
Event::Runtime => self.trigger_runtime_events()?,
_ => {} _ => {}
} }
@ -157,21 +156,12 @@ impl App {
Ok(()) Ok(())
} }
fn trigger_node_events(&mut self) -> Result<()> { fn trigger_oneshot_node_events(&mut self) -> Result<()> {
self.network_tx.send(Action::GetNodeName)?; self.network_tx.send(Action::GetNodeName)?;
self.network_tx.send(Action::GetSyncState)?; self.network_tx.send(Action::GetSystemHealth)?;
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::GetNodeVersion)?; self.network_tx.send(Action::GetChainVersion)?;
Ok(())
}
fn trigger_runtime_events(&mut self) -> Result<()> {
self.network_tx.send(Action::GetLatestBlock)?;
self.network_tx.send(Action::GetFinalizedBlock)?;
self.network_tx.send(Action::GetActiveEra)?;
self.network_tx.send(Action::GetEpoch)?;
self.network_tx.send(Action::GetValidators)?;
Ok(()) Ok(())
} }

33
src/casper.rs Normal file
View File

@ -0,0 +1,33 @@
//! Casper specific configuration
use subxt::{
Config, blocks::Block, client::OnlineClient,
config::{DefaultExtrinsicParams, SubstrateConfig},
};
/// Default set of commonly used type by Casper nodes.
#[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
pub enum CasperConfig {}
impl Config for CasperConfig {
type Hash = <SubstrateConfig as Config>::Hash;
type AccountId = <SubstrateConfig as Config>::AccountId;
type Address = <SubstrateConfig as Config>::Address;
type Signature = <SubstrateConfig as Config>::Signature;
type Hasher = <SubstrateConfig as Config>::Hasher;
type Header = <SubstrateConfig as Config>::Header;
type ExtrinsicParams = CasperExtrinsicParams<Self>;
type AssetId = u32;
}
pub type CasperAccountId = subxt::utils::AccountId32;
pub type CasperBlock = Block<CasperConfig, OnlineClient<CasperConfig>>;
/// A struct representing the signed extra and additional parameters required to construct a
/// transaction for a polkadot node.
pub type CasperExtrinsicParams<T> = DefaultExtrinsicParams<T>;
///// A builder which leads to [`CasperExtrinsicParams`] being constructed. This is what we provide
///// to methods like `sign_and_submit()`.
//pub type CasperExtrinsicParamsBuilder<T> = DefaultExtrinsicParamsBuilder<T>;

View File

@ -14,7 +14,7 @@ pub struct Cli {
pub frame_rate: f32, pub frame_rate: f32,
/// RPC Endpoint to the nodes JSON RPC /// RPC Endpoint to the nodes JSON RPC
#[arg(short, long, default_value_t = String::from("http://localhost:9945"))] #[arg(short, long, default_value_t = String::from("ws://localhost:9945"))]
pub rpc_endpoint: String, pub rpc_endpoint: String,
/// Request timeout in seconds /// Request timeout in seconds

View File

@ -36,9 +36,7 @@ impl BlockTicker {
} }
} }
fn block_found(&mut self, block: &str) -> Result<()> { fn block_found(&mut self, block: u32) -> Result<()> {
let block = block.trim_start_matches("0x");
let block = u32::from_str_radix(&block, 16)?;
if self.last_block < block { if self.last_block < block {
self.last_block_time = Instant::now(); self.last_block_time = Instant::now();
self.last_block = block; self.last_block = block;
@ -50,8 +48,7 @@ impl BlockTicker {
impl Component for BlockTicker { impl Component for BlockTicker {
fn update(&mut self, action: Action) -> Result<Option<Action>> { fn update(&mut self, action: Action) -> Result<Option<Action>> {
match action { match action {
Action::SetLatestBlock(_, block_info) => Action::BestBlockUpdated(block) => self.block_found(block)?,
self.block_found(&block_info.header.number)?,
_ => {} _ => {}
}; };
Ok(None) Ok(None)
@ -83,7 +80,7 @@ impl Component for BlockTicker {
.border_type(border_type) .border_type(border_type)
.title_alignment(Alignment::Right) .title_alignment(Alignment::Right)
.title_style(self.palette.create_title_style()) .title_style(self.palette.create_title_style())
.padding(Padding::new(0, 0, (height - 2) / 2, 0)) .padding(Padding::new(0, 0, height.saturating_sub(2) / 2, 0))
.title("Passed")) .title("Passed"))
.alignment(Alignment::Center) .alignment(Alignment::Center)
.wrap(Wrap { trim: true }); .wrap(Wrap { trim: true });
@ -108,7 +105,7 @@ impl Component for BlockTicker {
.wrap(Wrap { trim: true }); .wrap(Wrap { trim: true });
frame.render_widget(paragraph, place); frame.render_widget(paragraph, place);
let height_offset = (height - 2) / 2; let height_offset = height.saturating_sub(2) / 2;
let place = place.offset(ratatui::layout::Offset { x: 1, y: height_offset as i32 }) let place = place.offset(ratatui::layout::Offset { x: 1, y: height_offset as i32 })
.intersection(place); .intersection(place);
frame.render_widget(big_text, place); frame.render_widget(big_text, place);

View File

@ -22,7 +22,7 @@ impl CurrentEpoch {
const SECONDS_IN_DAY: u64 = 86_400; const SECONDS_IN_DAY: u64 = 86_400;
const SECONDS_IN_HOUR: u64 = 3_600; const SECONDS_IN_HOUR: u64 = 3_600;
fn update_epoch(&mut self, number: u64, progress: u64) -> Result<()> { fn update_epoch_progress(&mut self, number: u64, progress: u64) -> Result<()> {
self.number = number; self.number = number;
self.progress = progress; self.progress = progress;
Ok(()) Ok(())
@ -32,7 +32,8 @@ impl CurrentEpoch {
impl Component for CurrentEpoch { impl Component for CurrentEpoch {
fn update(&mut self, action: Action) -> Result<Option<Action>> { fn update(&mut self, action: Action) -> Result<Option<Action>> {
match action { match action {
Action::SetEpoch(number, progress) => self.update_epoch(number, progress)?, Action::SetEpochProgress(number, progress) =>
self.update_epoch_progress(number, progress)?,
_ => {} _ => {}
}; };
Ok(None) Ok(None)
@ -70,7 +71,7 @@ impl Component for CurrentEpoch {
.border_type(border_type) .border_type(border_type)
.title_alignment(Alignment::Right) .title_alignment(Alignment::Right)
.title_style(self.palette.create_title_style()) .title_style(self.palette.create_title_style())
.padding(Padding::new(0, 0, (height - 3) / 2, 0)) .padding(Padding::new(0, 0, height.saturating_sub(3) / 2, 0))
.title("Epoch")) .title("Epoch"))
.alignment(Alignment::Center) .alignment(Alignment::Center)
.wrap(Wrap { trim: true }); .wrap(Wrap { trim: true });
@ -100,7 +101,7 @@ impl Component for CurrentEpoch {
.wrap(Wrap { trim: true }); .wrap(Wrap { trim: true });
frame.render_widget(paragraph, place); frame.render_widget(paragraph, place);
let height_offset = (height - 2) / 2; let height_offset = height.saturating_sub(2) / 2;
let place = place.offset(ratatui::layout::Offset { x: 1, y: height_offset as i32 }) let place = place.offset(ratatui::layout::Offset { x: 1, y: height_offset as i32 })
.intersection(place); .intersection(place);
frame.render_widget(big_text, place); frame.render_widget(big_text, place);

View File

@ -7,7 +7,12 @@ use ratatui::{
}; };
use super::Component; use super::Component;
use crate::{action::Action, palette::StylePalette, types::era::EraInfo, widgets::{PixelSize, BigText}}; use crate::{
action::Action,
palette::StylePalette,
types::EraInfo,
widgets::{PixelSize, BigText},
};
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct CurrentEra{ pub struct CurrentEra{
@ -84,7 +89,7 @@ impl Component for CurrentEra {
.border_type(border_type) .border_type(border_type)
.title_alignment(Alignment::Right) .title_alignment(Alignment::Right)
.title_style(self.palette.create_title_style()) .title_style(self.palette.create_title_style())
.padding(Padding::new(0, 0, (height - 3) / 2, 0)) .padding(Padding::new(0, 0, (height.saturating_sub(3)) / 2, 0))
.title("Era")) .title("Era"))
.alignment(Alignment::Center) .alignment(Alignment::Center)
.wrap(Wrap { trim: true }); .wrap(Wrap { trim: true });
@ -114,7 +119,7 @@ impl Component for CurrentEra {
.wrap(Wrap { trim: true }); .wrap(Wrap { trim: true });
frame.render_widget(paragraph, place); frame.render_widget(paragraph, place);
let height_offset = (height - 2) / 2; let height_offset = height.saturating_sub(2) / 2;
let place = place.offset(ratatui::layout::Offset { x: 1, y: height_offset as i32 }) let place = place.offset(ratatui::layout::Offset { x: 1, y: height_offset as i32 })
.intersection(place); .intersection(place);
frame.render_widget(big_text, place); frame.render_widget(big_text, place);

View File

@ -1,6 +1,7 @@
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,106 +10,83 @@ use ratatui::{
widgets::{Block, BorderType, Paragraph}, widgets::{Block, BorderType, Paragraph},
Frame Frame
}; };
use sp_consensus_babe::digests::PreDigest; use sp_core::crypto::{AccountId32, Ss58Codec, Ss58AddressFormat};
use sp_runtime::DigestItem;
use codec::Decode; use codec::Decode;
use super::Component; use super::Component;
use crate::{action::Action, app::Mode, palette::StylePalette}; use crate::{
types::CasperExtrinsicDetails, CasperAccountId,
action::Action, app::Mode, palette::StylePalette,
};
#[derive(Debug, Clone)]
struct BlockInfo { struct BlockInfo {
block_number: u32, block_number: u32,
finalized: bool, finalized: bool,
hash: String,
} }
#[derive(Debug, Default)] #[derive(Default)]
pub struct ExplorerBlocks { pub struct ExplorerBlocks {
blocks: VecDeque<BlockInfo>, blocks: VecDeque<BlockInfo>,
extrinsics: HashMap<String, Vec<String>>, block_headers: HashMap<u32, H256>,
logs: HashMap<String, Vec<String>>, authors: HashMap<H256, CasperAccountId>,
validators: Vec<String>, extrinsics: HashMap<H256, Vec<CasperExtrinsicDetails>>,
palette: StylePalette, palette: StylePalette,
max_block_len: u32, current_block_digit_length: u32,
is_active: bool, is_active: bool,
used_paragraph_index: usize, used_paragraph_index: usize,
used_block_index: Option<(usize, u32)>, used_block_number: Option<u32>,
used_ext_index: Option<(String, usize, usize)>, used_ext_index: Option<(H256, usize)>,
} }
impl ExplorerBlocks { impl ExplorerBlocks {
const MAX_BLOCKS: usize = 50; const MAX_BLOCKS: usize = 50;
const LENGTH_OF_BLOCK_HASH: u16 = 66; // hash + 0x prefix const LENGTH_OF_BLOCK_HASH: u16 = 13;
const LENGTH_OF_ADDRESS: u16 = 49; const LENGTH_OF_ADDRESS: u16 = 49;
const TOTAL_OFFSETS: u16 = 18; const TOTAL_OFFSETS: u16 = 18;
fn update_validator_list(&mut self, validators: Vec<String>) -> Result<()> { fn update_block_author(
self.validators = validators; &mut self,
hash: H256,
maybe_author: Option<CasperAccountId>,
) -> Result<()> {
if let Some(author) = maybe_author {
self.authors.insert(hash, author);
}
Ok(()) Ok(())
} }
fn get_author_from_digest(&self, logs: Vec<String>) -> Option<String> {
let logs = logs
.iter()
.map_while(|log| {
hex::decode(log.trim_start_matches("0x"))
.ok()
.map(|log_hex| DigestItem::decode(&mut &log_hex[..]).ok())
})
.filter_map(|digest| digest)
.collect::<Vec<_>>();
let maybe_author = match logs.iter().find(|item| matches!(item, DigestItem::PreRuntime(..))) {
Some(DigestItem::PreRuntime(engine, data)) if *engine == [b'B', b'A', b'B', b'E'] => {
match PreDigest::decode(&mut &data[..]) {
Ok(PreDigest::Primary(primary)) => self.validators.get(primary.authority_index as usize),
Ok(PreDigest::SecondaryPlain(secondary)) => self.validators.get(secondary.authority_index as usize),
Ok(PreDigest::SecondaryVRF(secondary)) => self.validators.get(secondary.authority_index as usize),
_ => None,
}
},
_ => None,
};
maybe_author.cloned()
}
fn update_latest_block_info( fn update_latest_block_info(
&mut self, &mut self,
hash: String, hash: H256,
number_hex_str: String, block_number: u32,
logs: Vec<String>, extrinsics: Vec<CasperExtrinsicDetails>,
extrinsics: Vec<String>,
) -> Result<()> { ) -> Result<()> {
let number_hex_str = number_hex_str.trim_start_matches("0x"); let front_block_number = match self.blocks.front() {
let block_number = u32::from_str_radix(&number_hex_str, 16)?; Some(block_info) => block_info.block_number,
let front_block_number = if self.blocks.is_empty() { None => 0,
0
} else {
self.blocks.front().unwrap().block_number
}; };
if front_block_number < block_number { if front_block_number < block_number {
self.blocks.push_front(BlockInfo { self.blocks.push_front(BlockInfo {
block_number, block_number,
finalized: false, finalized: false,
hash: hash.clone(),
}); });
self.extrinsics.insert(hash.clone(), extrinsics); self.extrinsics.insert(hash, extrinsics);
self.logs.insert(hash, logs); self.block_headers.insert(block_number, hash);
let block_length = block_number.checked_ilog10().unwrap_or(0) + 1; let block_length = block_number.checked_ilog10().unwrap_or(0) + 1;
if self.max_block_len < block_length { if self.current_block_digit_length < block_length {
self.max_block_len = block_length; self.current_block_digit_length = block_length;
} }
if self.blocks.len() > Self::MAX_BLOCKS { if self.blocks.len() > Self::MAX_BLOCKS {
if let Some(removed_block_info) = self.blocks.pop_back() { if let Some(block) = self.blocks.pop_back() {
self.extrinsics.remove(&removed_block_info.hash); if let Some(hash) = self.block_headers.remove(&block.block_number) {
self.logs.remove(&removed_block_info.hash); self.extrinsics.remove(&hash);
self.authors.remove(&hash);
}
} }
} }
} }
@ -117,23 +95,14 @@ impl ExplorerBlocks {
fn update_finalized_block_info( fn update_finalized_block_info(
&mut self, &mut self,
hash: String, _hash: H256,
number_hex_str: String, block_number: u32,
logs: Vec<String>, _extrinsics: Vec<CasperExtrinsicDetails>,
extrinsics: Vec<String>,
) -> Result<()> { ) -> Result<()> {
let number_hex_str = number_hex_str.trim_start_matches("0x");
let block_number = u32::from_str_radix(&number_hex_str, 16)?;
for idx in 0..self.blocks.len() { for idx in 0..self.blocks.len() {
if self.blocks[idx].finalized { break; } if self.blocks[idx].finalized { break; }
else if self.blocks[idx].block_number > block_number { continue; } else if self.blocks[idx].block_number > block_number { continue; }
else { else { self.blocks[idx].finalized = true; }
self.blocks[idx].finalized = true;
self.blocks[idx].hash = hash.clone();
self.extrinsics.insert(hash.clone(), extrinsics.clone());
self.logs.insert(hash.clone(), logs.clone());
}
} }
Ok(()) Ok(())
@ -141,49 +110,52 @@ impl ExplorerBlocks {
fn prepare_block_line_info(&self, current_block: &BlockInfo, width: u16) -> Line { fn prepare_block_line_info(&self, current_block: &BlockInfo, width: u16) -> Line {
let block_number_length = self let block_number_length = self
.max_block_len .current_block_digit_length
.max(current_block.block_number.checked_ilog10().unwrap_or(0) + 1) as usize; .max(current_block.block_number.checked_ilog10().unwrap_or(0) + 1) as usize;
let free_space = width let free_space = width
.saturating_sub(block_number_length as u16) .saturating_sub(block_number_length as u16)
.saturating_sub(Self::TOTAL_OFFSETS); .saturating_sub(Self::TOTAL_OFFSETS);
let default_hash = H256::repeat_byte(69u8);
let hash = self
.block_headers
.get(&current_block.block_number)
.unwrap_or(&default_hash);
let author = self let author = self
.logs .authors
.get(&current_block.hash) .get(&hash)
.map_or(String::from("..."), |maybe_logs| { .map_or(String::from("..."), |author| {
self.get_author_from_digest(maybe_logs.to_vec()) let extended_author = AccountId32::decode(&mut author.as_ref())
.map_or(String::from("..."), |maybe_author| maybe_author) .expect("author should be valid AccountId32; qed");
extended_author.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 {
let len_for_author = free_space * Self::LENGTH_OF_ADDRESS / (Self::LENGTH_OF_BLOCK_HASH + Self::LENGTH_OF_ADDRESS); let len_for_author = free_space * Self::LENGTH_OF_ADDRESS / (Self::LENGTH_OF_BLOCK_HASH + Self::LENGTH_OF_ADDRESS);
let len_for_hash = (free_space - len_for_author) / 2;
let hash_to_print = format!("{}...{}",
&current_block.hash[..len_for_hash as usize],
&current_block.hash[(Self::LENGTH_OF_BLOCK_HASH - len_for_hash) as usize..]);
if &author == "..." { if &author == "..." {
Line::raw(format!("{:^left$}| {} | {:^right$}", Line::raw(format!("{:^left$}| {} | {:^right$}",
current_block.block_number, current_block.block_number,
hash_to_print, hash.to_string(),
author, author,
left=block_number_length, left=block_number_length,
right=(len_for_author + 2) as usize)) right=(len_for_author + 2) as usize))
} else { } else {
Line::raw(format!("{} | {} | {}", Line::raw(format!("{} | {} | {}",
current_block.block_number, current_block.block_number,
hash_to_print, hash.to_string(),
format!("{}...", &author[..(len_for_author) as usize]))) format!("{}...", &author[..(len_for_author) as usize])))
} }
} else { } else {
let total_space_used = block_number_length as u16 + Self::LENGTH_OF_BLOCK_HASH + Self::LENGTH_OF_ADDRESS; let total_space_used = block_number_length as u16 + Self::LENGTH_OF_BLOCK_HASH + Self::LENGTH_OF_ADDRESS;
let margin = (width - total_space_used) as usize / 3; let str_length = width.saturating_sub(2).saturating_sub(total_space_used) as usize / 3;
Line::raw(format!("{:^margin$}|{:^margin$}|{:^margin$}", Line::raw(format!("{:^length_block_number$}|{:^length_hash$}|{:^length_author$}",
current_block.block_number, current_block.block_number, hash.to_string(), author,
current_block.hash, length_block_number=str_length+block_number_length,
author)) length_hash=str_length+(Self::LENGTH_OF_BLOCK_HASH as usize),
length_author=str_length+(Self::LENGTH_OF_ADDRESS as usize)))
} }
} }
@ -196,11 +168,11 @@ impl ExplorerBlocks {
let latest_style = self.palette.create_text_style(false); let latest_style = self.palette.create_text_style(false);
let finalized_style = Style::new().fg(self.palette.foreground_hover()); let finalized_style = Style::new().fg(self.palette.foreground_hover());
let start_index = match self.used_block_index { let start_index = match self.used_block_number {
Some((_, used_block)) if total_length < self.blocks.len() => { Some(used_block) if total_length < self.blocks.len() => {
self.blocks self.blocks
.iter() .iter()
.position(|b| b.block_number == used_block) .position(|info| info.block_number == used_block)
.unwrap_or_default() .unwrap_or_default()
.saturating_add(1) .saturating_add(1)
.saturating_sub(total_length) .saturating_sub(total_length)
@ -211,8 +183,8 @@ impl ExplorerBlocks {
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; }
let style = match self.used_block_index { let style = match self.used_block_number {
Some((_, used_block)) if current_block_info.block_number == used_block => active_style, Some(used_block) if current_block_info.block_number == used_block => active_style,
_ => { _ => {
if current_block_info.finalized { finalized_style } else { latest_style } if current_block_info.finalized { finalized_style } else { latest_style }
} }
@ -224,13 +196,36 @@ impl ExplorerBlocks {
items items
} }
fn prepare_ext_line_info(&self, index: usize, extrinsic: String, width: u16) -> Line { fn prepare_ext_line_info(
let index_len = index.checked_ilog10().unwrap_or(0) + 1; &self,
let len_for_ext = width.saturating_sub(index_len as u16 + 17) as usize; index: usize,
let len_extrinsic_hash = extrinsic.len(); width: u16,
Line::from(format!("{} MODULE METHOD {}", pallet_name: &str,
index, variant_name: &str,
format!("{}...{}", &extrinsic[..len_for_ext], &extrinsic[len_extrinsic_hash - len_for_ext..]))) hash: &str,
) -> Line {
let index_length = 4; // always 4, two digits and two spaces
let hash_length = Self::LENGTH_OF_BLOCK_HASH as usize + 2;
let pallet_name_length = pallet_name.len();
let variant_name_length = variant_name.len();
let offset_variant = (width as usize)
.saturating_sub(index_length)
.saturating_sub(pallet_name_length)
.saturating_sub(variant_name_length)
.saturating_sub(hash_length)
.saturating_sub(2) / 2;
let offset_pallet = if offset_variant % 2 == 0 {
offset_variant
} else {
offset_variant + 1
};
Line::from(format!("{:^index_length$}{:>pallet_name_offset$}::{:<variant_name_offset$}{:^hash_length$}",
index, pallet_name, variant_name, hash,
pallet_name_offset=offset_pallet+pallet_name_length,
variant_name_offset=offset_variant+variant_name_length))
} }
fn prepare_ext_lines(&mut self, rect: Rect) -> Vec<Line> { fn prepare_ext_lines(&mut self, rect: Rect) -> Vec<Line> {
@ -241,28 +236,31 @@ 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);
if let Some((_, used_block_number)) = self.used_block_index { if let Some(used_block_number) = self.used_block_number {
let hash = self.blocks let default_hash = H256::repeat_byte(69u8);
.iter() let hash = self.block_headers
.find(|b| b.block_number == used_block_number) .get(&used_block_number)
.map(|b| b.hash.clone()) .unwrap_or(&default_hash);
.unwrap_or_default();
if let Some(exts) = self.extrinsics.get(&hash) { if let Some(exts) = self.extrinsics.get(&hash) {
for (index, ext) in exts.iter().enumerate() { for (index, ext) in exts.iter().enumerate() {
if total_length == 0 { break; } if total_length == 0 { break; }
let style = if let Some((_, _, used_ext_index)) = self.used_ext_index { let style = if let Some((_, used_ext_index)) = self.used_ext_index {
if index == used_ext_index { active_style } else { normal_style } if index == used_ext_index { active_style } else { normal_style }
} else { } else {
normal_style normal_style
}; };
items.push(self.prepare_ext_line_info(0, ext.to_string(), width).style(style)); items.push(self.prepare_ext_line_info(
index,
width.saturating_sub(2),
&ext.pallet_name,
&ext.variant_name,
&ext.hash.to_string()).style(style));
total_length -= 1; total_length -= 1;
} }
} }
} }
items items
@ -271,17 +269,20 @@ impl ExplorerBlocks {
fn prepare_event_lines(&mut self, rect: Rect) -> Line { fn prepare_event_lines(&mut self, rect: Rect) -> Line {
let _width = rect.as_size().width; let _width = rect.as_size().width;
let style = self.palette.create_text_style(false); match self.used_ext_index {
Some((header, used_index)) if self.extrinsics.get(&header).is_some() => {
let exts = self.extrinsics
.get(&header)
.expect("extrinsics should exists, checked before");
if let Some((hash, _, ext_index)) = &self.used_ext_index { let details = exts
if let Some(exts) = &self.extrinsics.get(&hash.clone()) { .get(used_index)
Line::from(format!("{}", exts.get(*ext_index).unwrap_or(&String::from("nothing here")))).style(style) .map_or(Vec::new(), |ext| ext.field_bytes.clone());
} else {
Line::from("nothing here") Line::from(format!("{}", hex::encode(&details)))
} },
} else { _ => Line::from(""),
Line::from("nothing here") }.style(self.palette.create_text_style(false))
}
} }
fn move_right(&mut self) { fn move_right(&mut self) {
@ -316,31 +317,21 @@ impl ExplorerBlocks {
fn move_up_extrinsics(&mut self) { fn move_up_extrinsics(&mut self) {
match &self.used_ext_index { match &self.used_ext_index {
Some((header, block_index, used_index)) => { Some((header, used_index)) => {
if *used_index == 0 { return } let new_index = used_index.saturating_sub(1);
let new_index = used_index - 1; if let Some(exts) = self.extrinsics.get(header) {
if exts.get(new_index).is_some() {
let maybe_exts = self.extrinsics.get(&*header); self.used_ext_index = Some((*header, new_index));
if maybe_exts.is_none() { return } }
let found = maybe_exts
.unwrap()
.get(new_index)
.is_some();
if found {
self.used_ext_index =
Some(((&*header).clone(), *block_index, new_index));
} }
}, },
None => { None => {
self.used_ext_index = self.blocks self.used_ext_index = self.used_block_number
.front() .map(|block_number| {
.map(|block| { let header = self.block_headers
self.extrinsics .get(&block_number)
.get(&block.hash) .expect("header exists for each block number; qed");
.map(|_| (block.hash.clone(), 0, 0)) self.extrinsics.get(&header).map(|_| (*header, 0usize))
}) })
.flatten() .flatten()
} }
@ -348,44 +339,35 @@ impl ExplorerBlocks {
} }
fn move_up_blocks(&mut self) { fn move_up_blocks(&mut self) {
self.used_block_index = match &self.used_block_index { self.used_block_number = match &self.used_block_number {
Some((used_index, used_block)) => { Some(block_number) => {
Some(self.blocks Some(self.blocks
.iter() .iter()
.enumerate() .find(|info| info.block_number == block_number + 1)
.find(|(_, b)| b.block_number == used_block + 1) .map(|info| info.block_number)
.map(|(idx, b)| (idx, b.block_number)) .unwrap_or(*block_number))
.unwrap_or((*used_index, *used_block)))
}, },
None => self.blocks.front().map(|b| (0usize, b.block_number)), None => self.blocks.front().map(|info| info.block_number),
} }
} }
fn move_down_extrinsics(&mut self) { fn move_down_extrinsics(&mut self) {
match &self.used_ext_index { match &self.used_ext_index {
Some((header, block_index, used_index)) => { Some((header, used_index)) => {
let new_index = used_index + 1; let new_index = used_index + 1;
if let Some(exts) = self.extrinsics.get(&header) {
let maybe_exts = self.extrinsics.get(&*header); if new_index < exts.len() && exts.get(new_index).is_some() {
if maybe_exts.is_none() { return } self.used_ext_index = Some((*header, new_index));
let exts = maybe_exts.unwrap(); }
let found = exts
.get(new_index)
.is_some();
if found && new_index < exts.len() {
self.used_ext_index =
Some(((&*header).clone(), *block_index, new_index));
} }
}, },
None => { None => {
self.used_ext_index = self.blocks self.used_ext_index = self.used_block_number
.front() .map(|block_number| {
.map(|block| { let header = self.block_headers
self.extrinsics .get(&block_number)
.get(&block.hash) .expect("header exists for each block number; qed");
.map(|_| (block.hash.clone(), 0, 0)) self.extrinsics.get(&header).map(|_| (*header, 0usize))
}) })
.flatten() .flatten()
} }
@ -393,17 +375,16 @@ impl ExplorerBlocks {
} }
fn move_down_blocks(&mut self) { fn move_down_blocks(&mut self) {
self.used_block_index = match &self.used_block_index { self.used_block_number = match &self.used_block_number {
Some((used_index, used_block)) => { Some(block_number) => {
Some(self.blocks Some(self.blocks
.iter() .iter()
.enumerate() .find(|info| info.block_number == block_number.saturating_sub(1))
.find(|(_, b)| b.block_number == used_block.saturating_sub(1)) .map(|info| info.block_number)
.map(|(idx, b)| (idx, b.block_number)) .unwrap_or(*block_number))
.unwrap_or((*used_index, *used_block)))
}, },
None => { None => {
self.blocks.front().map(|b| (0usize, b.block_number)) self.blocks.front().map(|info| info.block_number)
} }
} }
} }
@ -478,7 +459,7 @@ impl Component for ExplorerBlocks {
KeyCode::Char('l') | KeyCode::Right if self.is_active => self.move_right(), KeyCode::Char('l') | KeyCode::Right if self.is_active => self.move_right(),
KeyCode::Char('h') | KeyCode::Left if self.is_active => self.move_left(), KeyCode::Char('h') | KeyCode::Left if self.is_active => self.move_left(),
KeyCode::Esc => { KeyCode::Esc => {
self.used_block_index = None; self.used_block_number = None;
self.used_ext_index = None; self.used_ext_index = None;
self.used_paragraph_index = 0; self.used_paragraph_index = 0;
}, },
@ -490,9 +471,9 @@ impl Component for ExplorerBlocks {
fn update(&mut self, action: Action) -> Result<Option<Action>> { fn update(&mut self, action: Action) -> Result<Option<Action>> {
match action { match action {
Action::SetLatestBlock(hash, block) => self.update_latest_block_info(hash, block.header.number, block.header.digest.logs, block.extrinsics)?, Action::BestBlockInformation(hash, block_number, extrinsics) => self.update_latest_block_info(hash, block_number, extrinsics)?,
Action::SetFinalizedBlock(hash, block) => self.update_finalized_block_info(hash, block.header.number, block.header.digest.logs, block.extrinsics)?, Action::FinalizedBlockInformation(hash, block_number, extrinsics) => self.update_finalized_block_info(hash, block_number, extrinsics)?,
Action::SetValidators(validators) => self.update_validator_list(validators)?, Action::SetBlockAuthor(hash, maybe_author) => self.update_block_author(hash, maybe_author)?,
Action::SetMode(Mode::ExplorerActive) if !self.is_active => self.set_active()?, Action::SetMode(Mode::ExplorerActive) if !self.is_active => self.set_active()?,
Action::SetMode(_) if self.is_active => self.unset_active()?, Action::SetMode(_) if self.is_active => self.unset_active()?,
_ => {} _ => {}

View File

@ -44,21 +44,18 @@ impl ExtrinsicsChart {
.text_value(ext_len.to_string()) .text_value(ext_len.to_string())
} }
fn update_extrinsics(&mut self, block_number: String, extrinsics_number: usize) -> Result<()> { fn update_extrinsics_length(&mut self, block_number: u32, extrinsics_length: usize) -> Result<()> {
let block_number = block_number.trim_start_matches("0x");
let block_number = u32::from_str_radix(&block_number, 16)?;
match self.extrinsics.back() { match self.extrinsics.back() {
Some(back) => { Some(back) => {
if back.0 < block_number { if back.0 < block_number {
self.extrinsics.push_back((block_number, extrinsics_number)); self.extrinsics.push_back((block_number, extrinsics_length));
if self.extrinsics.len() > Self::MAX_LEN { if self.extrinsics.len() > Self::MAX_LEN {
let _ = self.extrinsics.pop_front(); let _ = self.extrinsics.pop_front();
} }
} }
}, },
None => { None => {
self.extrinsics.push_back((block_number, extrinsics_number)); self.extrinsics.push_back((block_number, extrinsics_length));
} }
}; };
@ -69,7 +66,7 @@ impl ExtrinsicsChart {
impl Component for ExtrinsicsChart { impl Component for ExtrinsicsChart {
fn update(&mut self, action: Action) -> Result<Option<Action>> { fn update(&mut self, action: Action) -> Result<Option<Action>> {
match action { match action {
Action::SetLatestBlock(_, block) => self.update_extrinsics(block.header.number, block.extrinsics.len())?, Action::ExtrinsicsLength(block, length) => self.update_extrinsics_length(block, length)?,
_ => {} _ => {}
}; };
Ok(None) Ok(None)

View File

@ -15,9 +15,8 @@ pub struct FinalizedBlock {
} }
impl FinalizedBlock { impl FinalizedBlock {
fn update_block_number(&mut self, number_hex_str: String) -> Result<()> { fn update_block_number(&mut self, number: u32) -> Result<()> {
let number_hex_str = number_hex_str.trim_start_matches("0x"); self.number = number;
self.number = u32::from_str_radix(&number_hex_str, 16)?;
Ok(()) Ok(())
} }
} }
@ -25,7 +24,7 @@ impl FinalizedBlock {
impl Component for FinalizedBlock { impl Component for FinalizedBlock {
fn update(&mut self, action: Action) -> Result<Option<Action>> { fn update(&mut self, action: Action) -> Result<Option<Action>> {
match action { match action {
Action::SetFinalizedBlock(_, block) => self.update_block_number(block.header.number)?, Action::NewFinalizedBlock(number) => self.update_block_number(number)?,
_ => {} _ => {}
}; };
Ok(None) Ok(None)
@ -48,7 +47,7 @@ impl Component for FinalizedBlock {
.border_type(border_type) .border_type(border_type)
.title_alignment(Alignment::Right) .title_alignment(Alignment::Right)
.title_style(self.palette.create_title_style()) .title_style(self.palette.create_title_style())
.padding(Padding::new(0, 0, (height - 2) / 2, 0)) .padding(Padding::new(0, 0, height.saturating_sub(2) / 2, 0))
.title("Latest")) .title("Latest"))
.alignment(Alignment::Center) .alignment(Alignment::Center)
.wrap(Wrap { trim: true }); .wrap(Wrap { trim: true });
@ -73,7 +72,7 @@ impl Component for FinalizedBlock {
.wrap(Wrap { trim: true }); .wrap(Wrap { trim: true });
frame.render_widget(paragraph, place); frame.render_widget(paragraph, place);
let height_offset = (height - 2) / 2; let height_offset = height.saturating_sub(2) / 2;
let place = place.offset(ratatui::layout::Offset { x: 1, y: height_offset as i32 }) let place = place.offset(ratatui::layout::Offset { x: 1, y: height_offset as i32 })
.intersection(place); .intersection(place);
frame.render_widget(big_text, place); frame.render_widget(big_text, place);

View File

@ -15,9 +15,8 @@ pub struct LatestBlock {
} }
impl LatestBlock { impl LatestBlock {
fn update_block_number(&mut self, number_hex_str: String) -> Result<()> { fn update_block_number(&mut self, number: u32) -> Result<()> {
let number_hex_str = number_hex_str.trim_start_matches("0x"); self.number = number;
self.number = u32::from_str_radix(&number_hex_str, 16)?;
Ok(()) Ok(())
} }
} }
@ -25,7 +24,7 @@ impl LatestBlock {
impl Component for LatestBlock { impl Component for LatestBlock {
fn update(&mut self, action: Action) -> Result<Option<Action>> { fn update(&mut self, action: Action) -> Result<Option<Action>> {
match action { match action {
Action::SetLatestBlock(_, block) => self.update_block_number(block.header.number)?, Action::NewBestBlock(number) => self.update_block_number(number)?,
_ => {} _ => {}
}; };
Ok(None) Ok(None)
@ -48,7 +47,7 @@ impl Component for LatestBlock {
.border_type(border_type) .border_type(border_type)
.title_alignment(Alignment::Right) .title_alignment(Alignment::Right)
.title_style(self.palette.create_title_style()) .title_style(self.palette.create_title_style())
.padding(Padding::new(0, 0, (height - 2) / 2, 0)) .padding(Padding::new(0, 0, height.saturating_sub(2) / 2, 0))
.title("Latest")) .title("Latest"))
.alignment(Alignment::Center) .alignment(Alignment::Center)
.wrap(Wrap { trim: true }); .wrap(Wrap { trim: true });
@ -73,7 +72,7 @@ impl Component for LatestBlock {
.wrap(Wrap { trim: true }); .wrap(Wrap { trim: true });
frame.render_widget(paragraph, place); frame.render_widget(paragraph, place);
let height_offset = (height - 2) / 2; let height_offset = height.saturating_sub(2) / 2;
let place = place.offset(ratatui::layout::Offset { x: 1, y: height_offset as i32 }) let place = place.offset(ratatui::layout::Offset { x: 1, y: height_offset as i32 })
.intersection(place); .intersection(place);
frame.render_widget(big_text, place); frame.render_widget(big_text, place);

View File

@ -16,7 +16,7 @@ use crate::{
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub struct Health { pub struct Health {
name: Option<String>, name: Option<String>,
peers: Option<u32>, peers: Option<usize>,
is_syncing: bool, is_syncing: bool,
should_have_peers: bool, should_have_peers: bool,
tx_pool_length: usize, tx_pool_length: usize,
@ -39,7 +39,12 @@ impl Health {
} }
} }
fn set_sync_state(&mut self, peers: Option<u32>, is_syncing: bool, should_have_peers: bool) -> Result<()> { fn set_sync_state(
&mut self,
peers: Option<usize>,
is_syncing: bool,
should_have_peers: bool,
) -> Result<()> {
self.peers = peers; self.peers = peers;
self.is_syncing = is_syncing; self.is_syncing = is_syncing;
self.should_have_peers = should_have_peers; self.should_have_peers = should_have_peers;
@ -84,7 +89,7 @@ impl Health {
impl Component for Health { 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::SetSyncState(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.set_sync_state(peers, is_syncing, should_have_peers)?
}, },
Action::SetNodeName(name) => self.set_node_name(name)?, Action::SetNodeName(name) => self.set_node_name(name)?,

View File

@ -5,6 +5,7 @@ use ratatui::{
widgets::{Block, Paragraph, Wrap}, widgets::{Block, Paragraph, Wrap},
Frame, Frame,
}; };
use primitive_types::H256;
use super::Component; use super::Component;
use crate::{ use crate::{
@ -13,7 +14,7 @@ use crate::{
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct Version { pub struct Version {
genesis_hash: Option<String>, genesis_hash: Option<H256>,
node_version: Option<String>, node_version: Option<String>,
chain_name: Option<String>, chain_name: Option<String>,
palette: StylePalette, palette: StylePalette,
@ -30,18 +31,15 @@ impl Version {
Ok(()) Ok(())
} }
fn set_genesis_hash(&mut self, genesis_hash: Option<String>) -> Result<()> { fn set_genesis_hash(&mut self, genesis_hash: Option<H256>) -> Result<()> {
self.genesis_hash = genesis_hash; self.genesis_hash = genesis_hash;
Ok(()) Ok(())
} }
fn prepared_genesis_hash(&self) -> String { fn prepared_genesis_hash(&self) -> String {
if self.genesis_hash.is_some() { match self.genesis_hash {
let genesis_hash = self.genesis_hash.clone().unwrap(); Some(genesis_hash) => genesis_hash.to_string(),
let len = genesis_hash.len(); None => OghamCenter::default().to_string(),
format!("Genesis: {}...{}", &genesis_hash[0..4], &genesis_hash[len-6..])
} else {
OghamCenter::default().to_string()
} }
} }
} }
@ -49,9 +47,9 @@ impl Version {
impl Component for Version { impl Component for Version {
fn update(&mut self, action: Action) -> Result<Option<Action>> { fn update(&mut self, action: Action) -> Result<Option<Action>> {
match action { match action {
Action::SetChainName(name) => self.set_chain_name(name)?, Action::SetChainName(maybe_name) => self.set_chain_name(maybe_name)?,
Action::SetNodeVersion(version) => self.set_node_version(version)?, Action::SetChainVersion(version) => self.set_node_version(version)?,
Action::SetGenesisHash(genesis) => self.set_genesis_hash(genesis)?, Action::SetGenesisHash(maybe_genesis) => self.set_genesis_hash(maybe_genesis)?,
_ => {} _ => {}
}; };
Ok(None) Ok(None)

View File

@ -1,5 +1,9 @@
use clap::Parser; use clap::Parser;
use color_eyre::Result; use color_eyre::Result;
use subxt::{
OnlineClient,
backend::{legacy::LegacyRpcMethods, rpc::RpcClient},
};
mod action; mod action;
mod app; mod app;
@ -13,9 +17,15 @@ mod network;
mod widgets; mod widgets;
mod types; mod types;
mod palette; mod palette;
mod casper;
use casper::{CasperAccountId, CasperConfig};
#[subxt::subxt(runtime_metadata_path = "./artifacts/casper.scale")]
pub mod casper_network {}
#[tokio::main] #[tokio::main]
async fn start_tokio( async fn start_tokio_action_loop(
io_rx: std::sync::mpsc::Receiver<action::Action>, io_rx: std::sync::mpsc::Receiver<action::Action>,
network: &mut network::Network, network: &mut network::Network,
) { ) {
@ -24,6 +34,16 @@ async fn start_tokio(
} }
} }
#[tokio::main]
async fn start_tokio_finalized_subscription(sub: &mut network::FinalizedSubscription) {
let _ = sub.subscribe_finalized_blocks().await;
}
#[tokio::main]
async fn start_tokio_best_subscription(sub: &mut network::BestSubscription) {
let _ = sub.subscribe_best_blocks().await;
}
#[tokio::main] #[tokio::main]
async fn main() -> Result<()> { async fn main() -> Result<()> {
crate::errors::init()?; crate::errors::init()?;
@ -33,12 +53,45 @@ async fn main() -> Result<()> {
let (sync_io_tx, sync_io_rx) = std::sync::mpsc::channel(); let (sync_io_tx, sync_io_rx) = std::sync::mpsc::channel();
let (action_tx, action_rx) = tokio::sync::mpsc::unbounded_channel(); let (action_tx, action_rx) = tokio::sync::mpsc::unbounded_channel();
let rpc_client = RpcClient::from_url(args.rpc_endpoint).await?;
let legacy_client_api = LegacyRpcMethods::<CasperConfig>::new(rpc_client.clone());
let online_client =
OnlineClient::<CasperConfig>::from_rpc_client(rpc_client.clone()).await?;
let finalized_blocks_sub = online_client.blocks().subscribe_finalized().await?;
let best_blocks_sub = online_client.blocks().subscribe_best().await?;
let cloned_action_tx = action_tx.clone(); let cloned_action_tx = action_tx.clone();
std::thread::spawn(move || { std::thread::spawn(move || {
let mut network = network::Network::new(cloned_action_tx) let mut network = network::Network::new(
.with_url(&args.rpc_endpoint) cloned_action_tx,
.with_timeout(args.timeout); online_client,
start_tokio(sync_io_rx, &mut network); legacy_client_api,
rpc_client,
);
start_tokio_action_loop(sync_io_rx, &mut network);
});
let cloned_action_tx = action_tx.clone();
let cloned_sync_tx = sync_io_tx.clone();
std::thread::spawn(move || {
let mut subscription = network::FinalizedSubscription::new(
cloned_action_tx,
cloned_sync_tx,
finalized_blocks_sub,
);
start_tokio_finalized_subscription(&mut subscription);
});
let cloned_action_tx = action_tx.clone();
let cloned_sync_tx = sync_io_tx.clone();
std::thread::spawn(move || {
let mut subscription = network::BestSubscription::new(
cloned_action_tx,
cloned_sync_tx,
best_blocks_sub,
);
start_tokio_best_subscription(&mut subscription);
}); });
app::App::new(sync_io_tx, action_tx, action_rx)? app::App::new(sync_io_tx, action_tx, action_rx)?

View File

@ -1,33 +0,0 @@
use color_eyre::Result;
use codec::Decode;
use crate::network::GhostRequest;
use crate::action::Action;
use crate::rpc_params;
use crate::types::{era::EraInfo, storage::GhostStorage};
#[derive(Debug)]
pub struct ActiveEraRequest<'a>(pub GhostRequest<'a>);
impl<'a> ActiveEraRequest<'a> {
pub async fn send(self) -> Result<()> {
let storage_key = GhostStorage::new()
.with_module("Staking")
.with_method("ActiveEra")
.build_storage_key();
let result_hex = self
.0
.send::<String>("state_getStorage", rpc_params![storage_key])
.await
.map(|r| {
hex::decode(r.result.trim_start_matches("0x")).or::<Vec<u8>>(Ok(vec![]))
})
.unwrap()
.unwrap();
let active_era = EraInfo::decode(&mut &result_hex[..])?;
self.0.action_tx.send(Action::SetActiveEra(active_era))?;
Ok(())
}
}

View File

@ -1,20 +0,0 @@
use color_eyre::Result;
use crate::network::GhostRequest;
use crate::action::Action;
use crate::types::params::RpcParams;
#[derive(Debug)]
pub struct ChainNameRequest<'a>(pub GhostRequest<'a>);
impl<'a> ChainNameRequest<'a> {
pub async fn send(self) -> Result<()> {
let chain_name = self
.0
.send::<String>("system_chain", RpcParams::new())
.await
.ok()
.map(|response| response.result);
self.0.action_tx.send(Action::SetChainName(chain_name))?;
Ok(())
}
}

View File

@ -1,41 +0,0 @@
use color_eyre::Result;
use codec::Decode;
use crate::network::GhostRequest;
use crate::action::Action;
use crate::rpc_params;
use crate::types::storage::GhostStorage;
#[derive(Debug)]
pub struct CurrentEpochRequest<'a>(pub GhostRequest<'a>);
impl<'a> CurrentEpochRequest<'a> {
async fn do_request(&self, module: &str, method: &str) -> Result<u64> {
let storage_key = GhostStorage::new()
.with_module(module)
.with_method(method)
.build_storage_key();
let result_hex = self
.0
.send::<String>("state_getStorage", rpc_params![storage_key])
.await
.map(|r| {
hex::decode(r.result.trim_start_matches("0x")).or::<Vec<u8>>(Ok(vec![]))
})
.unwrap()
.unwrap();
let value = u64::decode(&mut &result_hex[..])?;
Ok(value)
}
pub async fn send(self) -> Result<()> {
let current_slot = self.do_request("Babe", "CurrentSlot").await?;
let epoch_index = self.do_request("Babe", "EpochIndex").await?;
let genesis_slot = self.do_request("Babe", "GenesisSlot").await?;
let epoch_start_slot = epoch_index * 2_400 + genesis_slot;
let progress = current_slot.saturating_sub(epoch_start_slot);
self.0.action_tx.send(Action::SetEpoch(epoch_index, progress))?;
Ok(())
}
}

View File

@ -1,35 +0,0 @@
use color_eyre::Result;
use serde::Deserialize;
use crate::network::GhostRequest;
use crate::action::Action;
use crate::rpc_params;
use crate::types::block::BlockInfo;
use crate::types::params::RpcParams;
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct LatestBlockResponse {
pub block: BlockInfo,
// justifications: Option<String>
}
#[derive(Debug)]
pub struct FinalizedBlockRequest<'a>(pub GhostRequest<'a>);
impl<'a> FinalizedBlockRequest<'a> {
pub async fn send(self) -> Result<()> {
let finalized_head = self
.0
.send::<String>("chain_getFinalizedHead", RpcParams::new())
.await
.map_or(String::new(), |response| response.result);
let finalized_block = self
.0
.send::<LatestBlockResponse>("chain_getBlock", rpc_params![finalized_head.clone()])
.await
.map_or(BlockInfo::default(), |response| response.result.block);
self.0.action_tx.send(Action::SetFinalizedBlock(finalized_head, finalized_block))?;
Ok(())
}
}

View File

@ -1,18 +0,0 @@
use color_eyre::Result;
use crate::network::GhostRequest;
use crate::action::Action;
use crate::rpc_params;
#[derive(Debug)]
pub struct GenesisHashRequest<'a>(pub GhostRequest<'a>);
impl<'a> GenesisHashRequest<'a> {
pub async fn send(self) -> Result<()> {
let genesis_hash = self.0.send::<String>("chain_getBlockHash", rpc_params!["0"])
.await
.ok()
.map(|response| response.result);
self.0.action_tx.send(Action::SetGenesisHash(genesis_hash))?;
Ok(())
}
}

View File

@ -1,37 +0,0 @@
use color_eyre::Result;
use serde::Deserialize;
use crate::network::GhostRequest;
use crate::action::Action;
use crate::types::params::RpcParams;
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct HealthCheckResponse {
pub peers: u32,
pub is_syncing: bool,
pub should_have_peers: bool
}
#[derive(Debug)]
pub struct HealthCheckRequest<'a>(pub GhostRequest<'a>);
impl<'a> HealthCheckRequest<'a> {
pub async fn send(self) -> Result<()> {
let (peers, is_syncing, should_have_peers) = self
.0
.send::<HealthCheckResponse>("system_health", RpcParams::new())
.await
.ok()
.map_or((None, false, false), |response| (
Some(response.result.peers),
response.result.is_syncing,
response.result.should_have_peers,
));
self.0.action_tx.send(Action::SetSyncState(
peers,
is_syncing,
should_have_peers))?;
Ok(())
}
}

View File

@ -1,35 +0,0 @@
use color_eyre::Result;
use serde::Deserialize;
use crate::network::GhostRequest;
use crate::action::Action;
use crate::rpc_params;
use crate::types::block::BlockInfo;
use crate::types::params::RpcParams;
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct LatestBlockResponse {
pub block: BlockInfo,
// justifications: Option<String>
}
#[derive(Debug)]
pub struct LatestBlockRequest<'a>(pub GhostRequest<'a>);
impl<'a> LatestBlockRequest<'a> {
pub async fn send(self) -> Result<()> {
let latest_head = self
.0
.send::<String>("chain_getBlockHash", RpcParams::new())
.await
.map_or(String::new(), |response| response.result);
let latest_block = self
.0
.send::<LatestBlockResponse>("chain_getBlock", rpc_params![latest_head.clone()])
.await
.map_or(BlockInfo::default(), |response| response.result.block);
self.0.action_tx.send(Action::SetLatestBlock(latest_head, latest_block))?;
Ok(())
}
}

View File

@ -0,0 +1,70 @@
use tokio::sync::mpsc::UnboundedSender;
use color_eyre::Result;
use subxt::backend::legacy::rpc_methods::LegacyRpcMethods;
use crate::{action::Action, casper::CasperConfig};
pub async fn get_node_name(
action_tx: &UnboundedSender<Action>,
api: &LegacyRpcMethods<CasperConfig>,
) -> Result<()> {
let maybe_node_name = api.system_name().await.ok();
action_tx.send(Action::SetNodeName(maybe_node_name))?;
Ok(())
}
pub async fn get_system_health(
action_tx: &UnboundedSender<Action>,
api: &LegacyRpcMethods<CasperConfig>,
) -> Result<()> {
let (maybe_peers, is_syncing, should_have_peers) = api
.system_health()
.await
.ok()
.map_or((None, false, false), |health| (
Some(health.peers),
health.is_syncing,
health.should_have_peers,
));
action_tx.send(Action::SetSystemHealth(
maybe_peers,
is_syncing,
should_have_peers))?;
Ok(())
}
pub async fn get_genesis_hash(
action_tx: &UnboundedSender<Action>,
api: &LegacyRpcMethods<CasperConfig>,
) -> Result<()> {
let maybe_genesis_hash = api
.genesis_hash()
.await
.ok();
action_tx.send(Action::SetGenesisHash(maybe_genesis_hash))?;
Ok(())
}
pub async fn get_chain_name(
action_tx: &UnboundedSender<Action>,
api: &LegacyRpcMethods<CasperConfig>,
) -> Result<()> {
let maybe_chain_name = api
.system_chain()
.await
.ok();
action_tx.send(Action::SetChainName(maybe_chain_name))?;
Ok(())
}
pub async fn get_system_version(
action_tx: &UnboundedSender<Action>,
api: &LegacyRpcMethods<CasperConfig>,
) -> Result<()> {
let maybe_system_version = api
.system_version()
.await
.ok();
action_tx.send(Action::SetChainVersion(maybe_system_version))?;
Ok(())
}

View File

@ -1,164 +1,70 @@
use std::sync::Arc;
use tokio::sync::mpsc::UnboundedSender; use tokio::sync::mpsc::UnboundedSender;
use once_cell::sync::Lazy;
use reqwest::Client;
use color_eyre::Result; use color_eyre::Result;
use rand::RngCore; use subxt::{
use serde::{Deserialize, de::DeserializeOwned}; backend::{
legacy::LegacyRpcMethods,
rpc::RpcClient,
},
utils::H256,
OnlineClient
};
use crate::{action::Action, types::params::RpcParams}; mod legacy_rpc_calls;
mod predefinded_calls;
mod subscriptions;
mod active_era; use crate::{
mod health_check; action::Action,
mod node_name; casper::CasperConfig,
mod genesis_hash; };
mod chain_name;
mod node_version;
mod latest_block;
mod finalized_block;
mod current_epoch;
mod validators;
mod tx_pool;
pub use active_era::ActiveEraRequest; pub use subscriptions::{FinalizedSubscription, BestSubscription};
pub use health_check::HealthCheckRequest;
pub use node_name::NodeNameRequest;
pub use genesis_hash::GenesisHashRequest;
pub use chain_name::ChainNameRequest;
pub use node_version::NodeVersionRequest;
pub use latest_block::LatestBlockRequest;
pub use finalized_block::FinalizedBlockRequest;
pub use current_epoch::CurrentEpochRequest;
pub use validators::ValidatorsRequest;
pub use tx_pool::TxPoolRequest;
static CLIENT: Lazy<Arc<Client>> = Lazy::new(|| Arc::new(Client::new())); pub struct Network {
const DEFAULT_URL: &str = "http://localhost:9945"; action_tx: UnboundedSender<Action>,
online_client_api: OnlineClient<CasperConfig>,
pub type AppActionSender = UnboundedSender<Action>; legacy_client_api: LegacyRpcMethods<CasperConfig>,
rpc_client: RpcClient,
#[derive(Debug, Deserialize)] best_hash: Option<H256>,
pub struct GhostResponse<ResponseType> { finalized_hash: Option<H256>,
result: ResponseType,
} }
#[derive(Default)] impl Network {
struct GhostRequestBuilder<'a> { pub fn new(
action_tx: Option<AppActionSender>, action_tx: UnboundedSender<Action>,
id: u32, online_client_api: OnlineClient<CasperConfig>,
url: &'a str, legacy_client_api: LegacyRpcMethods<CasperConfig>,
timeout: std::time::Duration, rpc_client: RpcClient,
} ) -> Self {
impl<'a> GhostRequestBuilder<'a> {
pub fn with_id(mut self, id: u32) -> Self {
self.id = id;
self
}
pub fn with_url(mut self, url: &'a str) -> Self {
self.url = url;
self
}
pub fn with_timeout(mut self, timeout: std::time::Duration) -> Self {
self.timeout = timeout;
self
}
pub fn with_action_tx(mut self, action_tx: AppActionSender) -> Self {
self.action_tx = Some(action_tx);
self
}
pub fn build(self) -> GhostRequest<'a> {
GhostRequest {
action_tx: self.action_tx.expect("channel sender should exist"),
id: self.id,
url: self.url,
timeout: self.timeout,
}
}
}
#[derive(Debug)]
pub struct GhostRequest<'a> {
action_tx: AppActionSender,
id: u32,
url: &'a str,
timeout: std::time::Duration,
}
impl<'a> GhostRequest<'a> {
pub async fn send<ResponseType>(
&self,
method: &str,
params: RpcParams,
) -> Result<GhostResponse<ResponseType>>
where
ResponseType: DeserializeOwned,
{
Ok(CLIENT
.post(self.url)
.header(reqwest::header::CONTENT_TYPE, "application/json")
.body(format!("{{\"id\":{},\"jsonrpc\":\"2.0\",\"method\":{:?},\"params\":{}}}",
self.id, method, params.build()))
.timeout(self.timeout)
.send()
.await?
.json::<GhostResponse<ResponseType>>()
.await?
)
}
}
pub struct Network<'a> {
action_tx: AppActionSender,
timeout: std::time::Duration,
internal_randomness: rand::rngs::ThreadRng,
url: &'a str,
}
impl<'a> Network<'a> {
pub fn new(action_tx: AppActionSender) -> Self {
Self { Self {
action_tx, action_tx,
timeout: Default::default(), online_client_api,
internal_randomness: rand::thread_rng(), legacy_client_api,
url: DEFAULT_URL, rpc_client,
best_hash: None,
finalized_hash: None,
} }
} }
pub fn with_url(mut self, url: &'a str) -> Self {
self.url = url;
self
}
pub fn with_timeout(mut self, timeout: u64) -> Self {
self.timeout = std::time::Duration::from_secs(timeout);
self
}
pub async fn handle_network_event(&mut self, io_event: Action) -> Result<()> { pub async fn handle_network_event(&mut self, io_event: Action) -> Result<()> {
let request = GhostRequestBuilder::default()
.with_action_tx(self.action_tx.clone())
.with_id(self.internal_randomness.next_u32())
.with_url(self.url)
.with_timeout(self.timeout)
.build();
match io_event { match io_event {
Action::GetSyncState => HealthCheckRequest(request).send().await, Action::NewBestHash(hash) => {
Action::GetNodeName => NodeNameRequest(request).send().await, self.best_hash = Some(hash);
Action::GetGenesisHash => GenesisHashRequest(request).send().await, Ok(())
Action::GetChainName => ChainNameRequest(request).send().await, },
Action::GetNodeVersion => NodeVersionRequest(request).send().await, Action::NewFinalizedHash(hash) => {
Action::GetLatestBlock => LatestBlockRequest(request).send().await, self.finalized_hash = Some(hash);
Action::GetFinalizedBlock => FinalizedBlockRequest(request).send().await, Ok(())
Action::GetActiveEra => ActiveEraRequest(request).send().await, },
Action::GetEpoch => CurrentEpochRequest(request).send().await, Action::GetSystemHealth => legacy_rpc_calls::get_system_health(&self.action_tx, &self.legacy_client_api).await,
Action::GetValidators => ValidatorsRequest(request).send().await, Action::GetNodeName => legacy_rpc_calls::get_node_name(&self.action_tx, &self.legacy_client_api).await,
Action::GetPendingExtrinsics => TxPoolRequest(request).send().await, Action::GetGenesisHash => legacy_rpc_calls::get_genesis_hash(&self.action_tx, &self.legacy_client_api).await,
Action::GetChainName => legacy_rpc_calls::get_chain_name(&self.action_tx, &self.legacy_client_api).await,
Action::GetChainVersion => legacy_rpc_calls::get_system_version(&self.action_tx, &self.legacy_client_api).await,
Action::GetBlockAuthor(hash, logs) => predefinded_calls::get_block_author(&self.action_tx, &self.online_client_api, &logs, &hash).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::GetPendingExtrinsics => predefinded_calls::get_pending_extrinsics(&self.action_tx, &self.rpc_client).await,
_ => Ok(()) _ => Ok(())
} }
} }

View File

@ -1,20 +0,0 @@
use color_eyre::Result;
use crate::network::GhostRequest;
use crate::action::Action;
use crate::types::params::RpcParams;
#[derive(Debug)]
pub struct NodeNameRequest<'a>(pub GhostRequest<'a>);
impl<'a> NodeNameRequest<'a> {
pub async fn send(self) -> Result<()> {
let name = self
.0
.send::<String>("system_name", RpcParams::new())
.await
.ok()
.map(|response| response.result);
self.0.action_tx.send(Action::SetNodeName(name))?;
Ok(())
}
}

View File

@ -1,20 +0,0 @@
use color_eyre::Result;
use crate::network::GhostRequest;
use crate::action::Action;
use crate::types::params::RpcParams;
#[derive(Debug)]
pub struct NodeVersionRequest<'a>(pub GhostRequest<'a>);
impl<'a> NodeVersionRequest<'a> {
pub async fn send(self) -> Result<()> {
let version = self
.0
.send::<String>("system_version", RpcParams::new())
.await
.ok()
.map(|response| response.result);
self.0.action_tx.send(Action::SetNodeVersion(version))?;
Ok(())
}
}

View File

@ -0,0 +1,110 @@
use primitive_types::H256;
use tokio::sync::mpsc::UnboundedSender;
use color_eyre::Result;
use subxt::{
backend::rpc::RpcClient,
client::OnlineClient,
config::substrate::DigestItem,
rpc_params,
};
use crate::{
action::Action,
types::EraInfo,
casper_network::{self, runtime_types::sp_consensus_slots},
CasperConfig,
};
pub async fn get_block_author(
action_tx: &UnboundedSender<Action>,
api: &OnlineClient<CasperConfig>,
logs: &Vec<DigestItem>,
at_hash: &H256,
) -> Result<()> {
use codec::Decode;
use crate::casper_network::runtime_types::sp_consensus_babe::digests::PreDigest;
let storage_key = casper_network::storage().session().validators();
let validators = api.storage().at(*at_hash).fetch(&storage_key).await?.unwrap_or_default();
let maybe_author = match logs.iter().find(|item| matches!(item, DigestItem::PreRuntime(..))) {
Some(DigestItem::PreRuntime(engine, data)) if *engine == [b'B', b'A', b'B', b'E'] => {
match PreDigest::decode(&mut &data[..]) {
Ok(PreDigest::Primary(primary)) => validators.get(primary.authority_index as usize),
Ok(PreDigest::SecondaryPlain(secondary)) => validators.get(secondary.authority_index as usize),
Ok(PreDigest::SecondaryVRF(secondary)) => validators.get(secondary.authority_index as usize),
_ => None,
}
},
_ => None,
};
action_tx.send(Action::SetBlockAuthor(*at_hash, maybe_author.cloned()))?;
Ok(())
}
pub async fn get_active_era(
action_tx: &UnboundedSender<Action>,
api: &OnlineClient<CasperConfig>,
) -> Result<()> {
let storage_key = casper_network::storage().staking().active_era();
if let Some(active_era) = api.storage().at_latest().await?.fetch(&storage_key).await? {
action_tx.send(Action::SetActiveEra(EraInfo {
index: active_era.index,
start: active_era.start,
}))?;
}
Ok(())
}
pub async fn get_epoch_progress(
action_tx: &UnboundedSender<Action>,
api: &OnlineClient<CasperConfig>,
) -> Result<()> {
let storage_key = casper_network::storage().babe().current_slot();
let current_slot = api.storage()
.at_latest()
.await?
.fetch(&storage_key)
.await?
.unwrap_or(sp_consensus_slots::Slot(0u64));
let storage_key = casper_network::storage().babe().epoch_index();
let epoch_index = api.storage()
.at_latest()
.await?
.fetch(&storage_key)
.await?
.unwrap_or_default();
let storage_key = casper_network::storage().babe().genesis_slot();
let genesis_slot = api.storage()
.at_latest()
.await?
.fetch(&storage_key)
.await?
.unwrap_or(sp_consensus_slots::Slot(0u64));
let constant_query = casper_network::constants().babe().epoch_duration();
let epoch_duration = api.constants().at(&constant_query)?;
let epoch_start_slot = epoch_index * epoch_duration + genesis_slot.0;
let progress = current_slot.0.saturating_sub(epoch_start_slot);
action_tx.send(Action::SetEpochProgress(epoch_index, progress))?;
Ok(())
}
pub async fn get_pending_extrinsics(
action_tx: &UnboundedSender<Action>,
rpc_client: &RpcClient,
) -> Result<()> {
let pending_extrinsics: Vec<String> = rpc_client
.request("author_pendingExtrinsics", rpc_params![])
.await?;
action_tx.send(Action::SetPendingExtrinsicsLength(pending_extrinsics.len()))?;
Ok(())
}

View File

@ -0,0 +1,119 @@
use crate::{types::CasperExtrinsicDetails, action::Action, casper::CasperBlock};
use color_eyre::Result;
pub struct FinalizedSubscription {
action_tx: tokio::sync::mpsc::UnboundedSender<Action>,
network_tx: std::sync::mpsc::Sender<Action>,
finalized_blocks_sub: subxt::backend::StreamOfResults<CasperBlock>,
}
impl FinalizedSubscription {
pub fn new(
action_tx: tokio::sync::mpsc::UnboundedSender<Action>,
network_tx: std::sync::mpsc::Sender<Action>,
finalized_blocks_sub: subxt::backend::StreamOfResults<CasperBlock>,
) -> Self {
Self { action_tx, network_tx, finalized_blocks_sub }
}
pub async fn subscribe_finalized_blocks(&mut self) -> Result<()> {
while let Some(block) = self.finalized_blocks_sub.next().await {
let block = block?;
let block_number = block.header().number;
let block_hash = block.hash();
let extrinsics = block.extrinsics().await?;
let mut extrinsic_details = Vec::new();
for ext in extrinsics.iter() {
let pallet_name = match ext.pallet_name() {
Ok(pallet_name) => pallet_name,
Err(_) => continue,
};
let variant_name = match ext.variant_name() {
Ok(variant_name) => variant_name,
Err(_) => continue,
};
extrinsic_details.push(CasperExtrinsicDetails::new(
ext.index(),
ext.hash(),
ext.is_signed(),
ext.field_bytes().to_vec(),
ext.address_bytes().map(|addr| addr.to_vec()),
pallet_name.to_string(),
variant_name.to_string(),
));
}
self.action_tx.send(Action::FinalizedBlockInformation(
block_hash, block_number, extrinsic_details))?;
self.action_tx.send(Action::NewFinalizedHash(block_hash))?;
self.action_tx.send(Action::NewFinalizedBlock(block_number))?;
self.network_tx.send(Action::GetBlockAuthor(block_hash, block.header().digest.logs.clone()))?;
}
Ok(())
}
}
pub struct BestSubscription {
action_tx: tokio::sync::mpsc::UnboundedSender<Action>,
network_tx: std::sync::mpsc::Sender<Action>,
best_blocks_sub: subxt::backend::StreamOfResults<CasperBlock>,
}
impl BestSubscription {
pub fn new(
action_tx: tokio::sync::mpsc::UnboundedSender<Action>,
network_tx: std::sync::mpsc::Sender<Action>,
best_blocks_sub: subxt::backend::StreamOfResults<CasperBlock>,
) -> Self {
Self { action_tx, network_tx, best_blocks_sub }
}
pub async fn subscribe_best_blocks(&mut self) -> Result<()> {
while let Some(block) = self.best_blocks_sub.next().await {
let block = block?;
let block_number = block.header().number;
let block_hash = block.hash();
let extrinsics = block.extrinsics().await?;
let extrinsics_length = extrinsics.len();
let mut extrinsic_details = Vec::new();
for ext in extrinsics.iter() {
let pallet_name = match ext.pallet_name() {
Ok(pallet_name) => pallet_name,
Err(_) => continue,
};
let variant_name = match ext.variant_name() {
Ok(variant_name) => variant_name,
Err(_) => continue,
};
extrinsic_details.push(CasperExtrinsicDetails::new(
ext.index(),
ext.hash(),
ext.is_signed(),
ext.field_bytes().to_vec(),
ext.address_bytes().map(|addr| addr.to_vec()),
pallet_name.to_string(),
variant_name.to_string(),
));
}
self.action_tx.send(Action::BestBlockInformation(
block_hash, block_number, extrinsic_details))?;
self.action_tx.send(Action::NewBestHash(block_hash))?;
self.action_tx.send(Action::BestBlockUpdated(block_number))?;
self.action_tx.send(Action::NewBestBlock(block_number))?;
self.action_tx.send(Action::ExtrinsicsLength(block_number, extrinsics_length))?;
self.network_tx.send(Action::GetBlockAuthor(block_hash, block.header().digest.logs.clone()))?;
self.network_tx.send(Action::GetActiveEra)?;
self.network_tx.send(Action::GetEpochProgress)?;
}
Ok(())
}
}

View File

@ -1,21 +0,0 @@
use color_eyre::Result;
use crate::network::GhostRequest;
use crate::action::Action;
use crate::types::params::RpcParams;
#[derive(Debug)]
pub struct TxPoolRequest<'a>(pub GhostRequest<'a>);
impl<'a> TxPoolRequest<'a> {
pub async fn send(self) -> Result<()> {
let tx_pool = self
.0
.send::<Vec<String>>("author_pendingExtrinsics", RpcParams::new())
.await
.ok()
.map(|response| response.result)
.unwrap_or_default();
self.0.action_tx.send(Action::SetPendingExtrinsicsLength(tx_pool.len()))?;
Ok(())
}
}

View File

@ -1,37 +0,0 @@
use color_eyre::Result;
use codec::Decode;
use sp_core::crypto::{AccountId32, Ss58AddressFormat, Ss58Codec};
use crate::network::GhostRequest;
use crate::action::Action;
use crate::rpc_params;
use crate::types::storage::GhostStorage;
#[derive(Debug)]
pub struct ValidatorsRequest<'a>(pub GhostRequest<'a>);
impl<'a> ValidatorsRequest<'a> {
pub async fn send(self) -> Result<()> {
let storage_key = GhostStorage::new()
.with_module("Session")
.with_method("Validators")
.build_storage_key();
let result_hex = self.0.send::<String>("state_getStorage", rpc_params![storage_key])
.await
.map(|response| {
hex::decode(response.result.trim_start_matches("0x"))
.ok()
.unwrap_or_default()
})
.unwrap();
let validators = <Vec<AccountId32>>::decode(&mut &result_hex[..])
.ok()
.unwrap_or_default()
.iter()
.map(|v| v.to_ss58check_with_version(Ss58AddressFormat::custom(1996)))
.collect::<Vec<_>>();
self.0.action_tx.send(Action::SetValidators(validators))?;
Ok(())
}
}

View File

@ -39,9 +39,8 @@ pub enum Event {
Key(KeyEvent), Key(KeyEvent),
Mouse(MouseEvent), Mouse(MouseEvent),
Resize(u16, u16), Resize(u16, u16),
Oneshot,
Node, Node,
FastNode,
Runtime,
} }
pub struct Tui { pub struct Tui {
@ -97,9 +96,8 @@ impl Tui {
let mut tick_interval = interval(Duration::from_secs_f64(1.0 / tick_rate)); let mut tick_interval = interval(Duration::from_secs_f64(1.0 / tick_rate));
let mut render_interval = interval(Duration::from_secs_f64(1.0 / frame_rate)); let mut render_interval = interval(Duration::from_secs_f64(1.0 / frame_rate));
let mut normal_node_interval = interval(Duration::from_secs_f64(9.0)); let mut oneshot_node_interval = interval(Duration::from_secs_f64(86_400.0));
let mut fast_node_interval = interval(Duration::from_secs_f64(9.0)); let mut fast_node_interval = interval(Duration::from_secs_f64(2.0));
let mut runtime_interval = interval(Duration::from_secs_f64(1.69));
event_tx event_tx
.send(Event::Init) .send(Event::Init)
@ -112,9 +110,8 @@ impl Tui {
}, },
_ = tick_interval.tick() => Event::Tick, _ = tick_interval.tick() => Event::Tick,
_ = render_interval.tick() => Event::Render, _ = render_interval.tick() => Event::Render,
_ = normal_node_interval.tick() => Event::Node, _ = oneshot_node_interval.tick() => Event::Oneshot,
_ = fast_node_interval.tick() => Event::FastNode, _ = fast_node_interval.tick() => Event::Node,
_ = runtime_interval.tick() => Event::Runtime,
crossterm_event = event_stream.next().fuse() => match crossterm_event { crossterm_event = event_stream.next().fuse() => match crossterm_event {
Some(Ok(event)) => match event { Some(Ok(event)) => match event {
CrosstermEvent::Key(key) if key.kind == KeyEventKind::Press => Event::Key(key), CrosstermEvent::Key(key) if key.kind == KeyEventKind::Press => Event::Key(key),

View File

@ -1,24 +0,0 @@
use serde::{Serialize, Deserialize};
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LogInfo {
pub logs: Vec<String>,
}
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HeaderInfo {
pub parent_hash: String,
pub number: String,
pub state_root: String,
pub extrinsics_root: String,
pub digest: LogInfo
}
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BlockInfo {
pub header: HeaderInfo,
pub extrinsics: Vec<String>,
}

35
src/types/extrinsics.rs Normal file
View File

@ -0,0 +1,35 @@
use serde::{Serialize, Deserialize};
use subxt::utils::H256;
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CasperExtrinsicDetails {
pub index: u32,
pub hash: H256,
pub is_signed: bool,
pub field_bytes: Vec<u8>,
pub address_bytes: Option<Vec<u8>>,
pub pallet_name: String,
pub variant_name: String,
}
impl CasperExtrinsicDetails {
pub fn new(
index: u32,
hash: H256,
is_signed: bool,
field_bytes: Vec<u8>,
address_bytes: Option<Vec<u8>>,
pallet_name: String,
variant_name: String,
) -> Self {
Self {
index,
hash,
is_signed,
field_bytes,
address_bytes,
pallet_name,
variant_name,
}
}
}

View File

@ -1,14 +0,0 @@
#[macro_export]
macro_rules! rpc_params {
($($param:expr),*) => {{
use crate::types::params::RpcParams;
let mut params = RpcParams::new();
$(
if let Err(err) = params.insert($param) {
panic!("parameter `{}` cannot be serialized: {:?}", stringify!($param), err);
}
)*
params
}}
}

View File

@ -1,5 +1,5 @@
pub mod block; mod era;
pub mod params; mod extrinsics;
pub mod storage;
pub mod macros; pub use extrinsics::CasperExtrinsicDetails;
pub mod era; pub use era::EraInfo;

View File

@ -1,75 +0,0 @@
use color_eyre::Result;
use serde::Serialize;
#[derive(Debug)]
pub struct RpcParams(ParamsBuilder);
impl RpcParams {
pub fn new() -> Self {
Self::default()
}
pub fn insert<P: Serialize>(&mut self, value: P) -> Result<()> {
self.0.insert(value)
}
pub fn build(self) -> String {
self.0.build()
}
}
impl Default for RpcParams {
fn default() -> Self {
Self(ParamsBuilder::positional())
}
}
const PARAM_BYTES_CAPACITY: usize = 128;
#[derive(Debug)]
pub struct ParamsBuilder {
bytes: Vec<u8>,
start: char,
end: char,
}
impl ParamsBuilder {
fn new(start: char, end: char) -> Self {
Self { bytes: Vec::new(), start, end }
}
fn positional() -> Self {
Self::new('[', ']')
}
fn maybe_initialize(&mut self) {
if self.bytes.is_empty() {
self.bytes.reserve(PARAM_BYTES_CAPACITY);
self.bytes.push(self.start as u8);
}
}
pub fn build(mut self) -> String {
if self.bytes.is_empty() {
return format!("{}{}", self.start, self.end);
}
let index = self.bytes.len() - 1;
if self.bytes[index] == b',' {
self.bytes[index] = self.end as u8;
} else {
self.bytes.push(self.end as u8);
}
unsafe { String::from_utf8_unchecked(self.bytes) }
}
pub fn insert<P: Serialize>(&mut self, value: P) -> Result<()> {
self.maybe_initialize();
serde_json::to_writer(&mut self.bytes, &value)?;
self.bytes.push(b',');
Ok(())
}
}

View File

@ -1,32 +0,0 @@
#[derive(Debug, Default)]
pub struct GhostStorage<'a> {
module: &'a str,
method: &'a str,
}
impl<'a> GhostStorage<'a> {
pub fn new() -> Self {
Self {
module: "",
method: "",
}
}
pub fn with_module(mut self, module: &'a str) -> Self {
self.module = module;
self
}
pub fn with_method(mut self, method: &'a str) -> Self {
self.method = method;
self
}
pub fn build_storage_key(&self) -> String {
let module_hex = hex::encode(sp_core::twox_128(self.module.as_bytes()));
let method_hex = hex::encode(sp_core::twox_128(self.method.as_bytes()));
let storage_key = format!("0x{}{}", module_hex, method_hex);
storage_key
}
}