diff --git a/Cargo.lock b/Cargo.lock index 6c7fdfc..517dbac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7567,6 +7567,7 @@ version = "0.1.1" dependencies = [ "ansi_term", "atty", + "chrono", "clap 4.5.27", "clap_complete 4.5.44", "clap_generate", diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index aac7ed2..6d72419 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -25,6 +25,7 @@ clap_generate = "3.0.3" clap_complete = "4.5.44" crossterm = "0.28.1" ratatui = "0.29.0" +chrono = "0.4" [features] default = ["cli"] diff --git a/crates/cli/src/cli/mod.rs b/crates/cli/src/cli/mod.rs index 3a8e2e5..e510423 100644 --- a/crates/cli/src/cli/mod.rs +++ b/crates/cli/src/cli/mod.rs @@ -1,4 +1,4 @@ -use clap::{CommandFactory, Parser, Subcommand}; +use clap::{ArgAction, CommandFactory, Parser, Subcommand}; use clap_complete::{Generator, Shell}; use hiro_system_kit::{self, Logger}; use std::{fs::File, process}; @@ -69,9 +69,12 @@ pub struct StartSimnet { /// Set the ip #[arg(long = "ip", short = 'i', default_value = DEFAULT_BINDING_ADDRESS )] pub network_binding_ip_address: String, - /// Display streams of logs instead of terminal UI dashboard + /// Display streams of logs instead of terminal UI dashboard (default: false) #[clap(long = "no-tui")] pub no_tui: bool, + /// Include debug logs (default: false) + #[clap(long = "debug", action=ArgAction::SetTrue)] + pub debug: bool, } #[derive(Parser, PartialEq, Clone, Debug)] diff --git a/crates/cli/src/cli/simnet/mod.rs b/crates/cli/src/cli/simnet/mod.rs index 199eb6b..5a8c4f0 100644 --- a/crates/cli/src/cli/simnet/mod.rs +++ b/crates/cli/src/cli/simnet/mod.rs @@ -22,39 +22,52 @@ pub fn handle_start_simnet_command(cmd: &StartSimnet, ctx: &Context) -> Result<( .map_err(|e| format!("{}", e))?; // Start frontend - kept on main thread if cmd.no_tui { - log_events(simnet_events_rx, ctx); + log_events(simnet_events_rx, cmd.debug, ctx); } else { - tui::simnet::start_app(simnet_events_rx).map_err(|e| format!("{}", e))?; + tui::simnet::start_app(simnet_events_rx, cmd.debug).map_err(|e| format!("{}", e))?; } handle.join().map_err(|_e| format!("unable to terminate"))? } -fn log_events(simnet_events_rx: Receiver, ctx: &Context) { +fn log_events(simnet_events_rx: Receiver, include_debug_logs: bool, ctx: &Context) { info!( ctx.expect_logger(), "Surfpool: The best place to train before surfing Solana" ); while let Ok(event) = simnet_events_rx.recv() { match event { - SimnetEvent::AccountUpdate(account) => { + SimnetEvent::AccountUpdate(_, account) => { info!( ctx.expect_logger(), "Account retrieved from Mainnet {}", account ); } + SimnetEvent::EpochInfoUpdate(epoch_info) => { + info!( + ctx.expect_logger(), + "Connection established. Epoch {}, Slot {}.", + epoch_info.epoch, + epoch_info.slot_index + ); + } SimnetEvent::ClockUpdate(clock) => { info!(ctx.expect_logger(), "Slot #{} ", clock.slot); } - SimnetEvent::ErroLog(log) => { + SimnetEvent::ErroLog(_, log) => { error!(ctx.expect_logger(), "{} ", log); } - SimnetEvent::InfoLog(log) => { + SimnetEvent::InfoLog(_, log) => { info!(ctx.expect_logger(), "{} ", log); } - SimnetEvent::WarnLog(log) => { + SimnetEvent::WarnLog(_, log) => { warn!(ctx.expect_logger(), "{} ", log); } - SimnetEvent::TransactionReceived(transaction) => { + SimnetEvent::DebugLog(_, log) => { + if include_debug_logs { + debug!(ctx.expect_logger(), "{} ", log); + } + } + SimnetEvent::TransactionReceived(_, _transaction) => { info!(ctx.expect_logger(), "Transaction received"); } SimnetEvent::BlockHashExpired => {} diff --git a/crates/cli/src/tui/simnet.rs b/crates/cli/src/tui/simnet.rs index de43600..9115f7e 100644 --- a/crates/cli/src/tui/simnet.rs +++ b/crates/cli/src/tui/simnet.rs @@ -1,5 +1,6 @@ use std::{collections::VecDeque, error::Error, io, sync::mpsc::Receiver, time::Duration}; +use chrono::{DateTime, Local}; use crossterm::{ event::{self, Event, KeyCode, KeyEventKind}, execute, @@ -10,36 +11,48 @@ use ratatui::{ style::palette::{self, tailwind}, widgets::*, }; -use surfpool_core::{simnet::SimnetEvent, solana_sdk::clock::Clock}; +use surfpool_core::{ + simnet::SimnetEvent, + solana_sdk::{clock::Clock, epoch_info::EpochInfo}, +}; const HELP_TEXT: &str = "(Esc) quit | (↑) move up | (↓) move down"; const TXTX_LINK: &str = "https://txtx.run"; -const ITEM_HEIGHT: usize = 4; - -struct TableColors { - buffer_bg: Color, - accent_color: Color, - primary_color: Color, - secondary_color: Color, - white_color: Color, - gray_color: Color, +const ITEM_HEIGHT: usize = 1; + +struct ColorTheme { + background: Color, + accent: Color, + primary: Color, + secondary: Color, + white: Color, + gray: Color, + error: Color, + warning: Color, + info: Color, + success: Color, } -impl TableColors { +impl ColorTheme { fn new(color: &tailwind::Palette) -> Self { Self { - buffer_bg: tailwind::SLATE.c950, - accent_color: color.c400, - primary_color: color.c500, - secondary_color: color.c900, - white_color: tailwind::SLATE.c200, - gray_color: tailwind::SLATE.c900, + background: tailwind::SLATE.c950, + accent: color.c400, + primary: color.c500, + secondary: color.c900, + white: tailwind::SLATE.c200, + gray: tailwind::SLATE.c900, + error: tailwind::RED.c500, + warning: tailwind::YELLOW.c500, + info: tailwind::BLUE.c500, + success: tailwind::GREEN.c500, } } } enum EventType { + Debug, Info, Success, Failure, @@ -49,35 +62,51 @@ enum EventType { struct App { state: TableState, scroll_state: ScrollbarState, - colors: TableColors, + colors: ColorTheme, simnet_events_rx: Receiver, clock: Clock, - events: VecDeque<(EventType, String)>, + epoch_info: EpochInfo, + events: VecDeque<(EventType, DateTime, String)>, + include_debug_logs: bool, } impl App { - fn new(simnet_events_rx: Receiver) -> App { + fn new(simnet_events_rx: Receiver, include_debug_logs: bool) -> App { App { state: TableState::default().with_selected(0), scroll_state: ScrollbarState::new(5 * ITEM_HEIGHT), - colors: TableColors::new(&palette::tailwind::EMERALD), + colors: ColorTheme::new(&palette::tailwind::EMERALD), simnet_events_rx, clock: Clock::default(), + epoch_info: EpochInfo { + epoch: 0, + slot_index: 0, + slots_in_epoch: 0, + absolute_slot: 0, + block_height: 0, + transaction_count: None, + }, events: VecDeque::new(), + include_debug_logs, } } - pub fn epoch(&self) -> usize { - self.clock.epoch.try_into().unwrap() - } - pub fn slot(&self) -> usize { self.clock.slot.try_into().unwrap() } + pub fn epoch_progress(&self) -> u16 { + let current = self.slot() as u64; + let expected = self.epoch_info.slots_in_epoch; + if expected == 0 { + return 100; + } + ((current.min(expected) as f64 / expected as f64) * 100.0) as u16 + } + pub fn next(&mut self) { let i = match self.state.selected() { - Some(i) => 0, + Some(i) => i, None => 0, }; self.state.select(Some(i)); @@ -86,7 +115,7 @@ impl App { pub fn previous(&mut self) { let i = match self.state.selected() { - Some(i) => 0, + Some(i) => i, None => 0, }; self.state.select(Some(i)); @@ -94,11 +123,14 @@ impl App { } pub fn set_colors(&mut self) { - self.colors = TableColors::new(&tailwind::EMERALD) + self.colors = ColorTheme::new(&tailwind::EMERALD) } } -pub fn start_app(simnet_events_rx: Receiver) -> Result<(), Box> { +pub fn start_app( + simnet_events_rx: Receiver, + include_debug_logs: bool, +) -> Result<(), Box> { // setup terminal enable_raw_mode()?; let mut stdout = io::stdout(); @@ -107,7 +139,7 @@ pub fn start_app(simnet_events_rx: Receiver) -> Result<(), Box(terminal: &mut Terminal, mut app: App) -> io::Result<( loop { while let Ok(event) = app.simnet_events_rx.try_recv() { match event { - SimnetEvent::AccountUpdate(account) => { + SimnetEvent::AccountUpdate(dt, account) => { app.events.push_front(( EventType::Success, - format!("Account retrieved from Mainnet"), + dt, + format!("Account {} retrieved from Mainnet", account), + )); + } + SimnetEvent::EpochInfoUpdate(epoch_info) => { + app.epoch_info = epoch_info; + app.events.push_front(( + EventType::Success, + Local::now(), + format!( + "Connection established. Epoch {}, Slot {}.", + app.epoch_info.epoch, app.epoch_info.slot_index + ), )); } SimnetEvent::ClockUpdate(clock) => { app.clock = clock; app.events - .push_front((EventType::Failure, "Clock updated".into())); + .push_front((EventType::Info, Local::now(), "Clock updated".into())); } - SimnetEvent::ErroLog(log) => { - app.events.push_front((EventType::Failure, log)); + SimnetEvent::ErroLog(dt, log) => { + app.events.push_front((EventType::Failure, dt, log)); } - SimnetEvent::InfoLog(log) => { - app.events.push_front((EventType::Info, log)); + SimnetEvent::InfoLog(dt, log) => { + app.events.push_front((EventType::Info, dt, log)); } - SimnetEvent::WarnLog(log) => { - app.events.push_front((EventType::Warning, log)); + SimnetEvent::DebugLog(dt, log) => { + if app.include_debug_logs { + app.events.push_front((EventType::Debug, dt, log)); + } } - SimnetEvent::TransactionReceived(transaction) => { - app.events - .push_front((EventType::Success, format!("Transaction received"))); + SimnetEvent::WarnLog(dt, log) => { + app.events.push_front((EventType::Warning, dt, log)); + } + SimnetEvent::TransactionReceived(dt, _transaction) => { + app.events.push_front(( + EventType::Success, + dt, + format!("Transaction received"), + )); } SimnetEvent::BlockHashExpired => {} } @@ -174,7 +226,7 @@ fn run_app(terminal: &mut Terminal, mut app: App) -> io::Result<( fn ui(f: &mut Frame, app: &mut App) { let rects = Layout::vertical([ - Constraint::Length(5), + Constraint::Length(7), Constraint::Min(5), Constraint::Length(3), ]) @@ -182,8 +234,8 @@ fn ui(f: &mut Frame, app: &mut App) { app.set_colors(); let default_style = Style::new() - .fg(app.colors.secondary_color) - .bg(app.colors.buffer_bg); + .fg(app.colors.secondary) + .bg(app.colors.background); let chrome = Block::default() .style(default_style.clone()) .borders(Borders::ALL) @@ -192,8 +244,8 @@ fn ui(f: &mut Frame, app: &mut App) { f.render_widget(chrome, f.area()); render_epoch(f, app, rects[0].inner(Margin::new(1, 1))); - render_events(f, app, rects[1].inner(Margin::new(2, 1))); - render_scrollbar(f, app, rects[1]); + render_events(f, app, rects[1].inner(Margin::new(2, 0))); + render_scrollbar(f, app, rects[1].inner(Margin::new(0, 0))); render_footer(f, app, rects[2].inner(Margin::new(2, 1))); } @@ -203,29 +255,50 @@ fn title_block(title: &str, alignment: Alignment) -> Block { } fn render_epoch(f: &mut Frame, app: &mut App, area: Rect) { - let rects = Layout::horizontal([ - Constraint::Length(6), // Slots Title + let columns = Layout::horizontal([ + Constraint::Length(7), // Slots Title Constraint::Min(30), // Slots - Constraint::Length(7), // Epoch Title - Constraint::Length(20), // Progress bar - Constraint::Length(40), // Leader Details + Constraint::Length(1), // Leader Details + Constraint::Length(50), // Leader Details ]) .split(area); + let titles = Layout::vertical([ + Constraint::Length(3), // Slots + Constraint::Length(1), // Divider + Constraint::Length(1), // Epoch + ]) + .split(columns[0]); + + let widgets = Layout::vertical([ + Constraint::Length(3), // Slots + Constraint::Length(1), // Divider + Constraint::Length(1), // Epoch + ]) + .split(columns[1]); + let title = title_block("Slots", Alignment::Center); - f.render_widget(title, rects[0].inner(Margin::new(1, 1))); + f.render_widget(title, titles[0].inner(Margin::new(1, 1))); - render_slots(f, app, rects[1].inner(Margin::new(1, 0))); + render_slots(f, app, widgets[0].inner(Margin::new(1, 0))); let title = title_block("Epoch", Alignment::Center); - f.render_widget(title, rects[2].inner(Margin::new(1, 1))); + f.render_widget(title, titles[2].inner(Margin::new(1, 0))); let epoch_progress = Gauge::default() - .gauge_style(app.colors.secondary_color) - .bg(app.colors.gray_color) - .label(Span::raw("")) - .percent(50); - f.render_widget(epoch_progress, rects[3].inner(Margin::new(1, 1))); + .gauge_style(app.colors.primary) + .bg(app.colors.gray) + .percent(app.epoch_progress()); + f.render_widget(epoch_progress, widgets[2].inner(Margin::new(1, 0))); + + let default_style = Style::new().fg(app.colors.gray); + + let chrome = Block::default() + .style(default_style.clone()) + .borders(Borders::LEFT) + .border_style(default_style) + .border_type(BorderType::QuadrantOutside); + f.render_widget(chrome, columns[3]); } fn render_slots(f: &mut Frame, app: &mut App, area: Rect) { @@ -243,58 +316,53 @@ fn render_slots(f: &mut Frame, app: &mut App, area: Rect) { .join("\n"); let title = Paragraph::new(text); - // title.style(app.colors.accent_color); - f.render_widget(title.style(app.colors.accent_color), area); + f.render_widget(title.style(app.colors.accent), area); } fn render_events(f: &mut Frame, app: &mut App, area: Rect) { + let rects = Layout::vertical([ + Constraint::Length(2), // Title + Constraint::Min(1), // Logs + ]) + .split(area); + let symbol = ['⠈', '⠘', '⠸', '⠼', '⠾', '⠿', '⠷', '⠧', '⠇', '⠃', '⠁', '⠀']; let cursor = symbol[app.slot() % 12]; let title = Block::new() .padding(Padding::symmetric(4, 4)) .borders(Borders::NONE) .title(Line::from(format!("Activity {}", cursor))); - - f.render_widget(title, area); - - // let selected_style = Style::default() - // .add_modifier(Modifier::REVERSED) - // .fg(app.colors.selected_style_fg); - - let rows = app.events.iter().enumerate().map(|(i, (event_type, log))| { - let row = vec![log.to_string()]; + f.render_widget(title, rects[0]); + + let rows = app.events.iter().map(|(event_type, dt, log)| { + let color = match event_type { + EventType::Failure => app.colors.error, + EventType::Info => app.colors.info, + EventType::Success => app.colors.success, + EventType::Warning => app.colors.warning, + EventType::Debug => app.colors.gray, + }; + let row = vec![ + Cell::new("⏐").style(color), + Cell::new(dt.format("%H:%M:%S.%3f").to_string()).style(app.colors.gray), + Cell::new(log.to_string()), + ]; Row::new(row) - .style(Style::new().fg(app.colors.white_color)) + .style(Style::new().fg(app.colors.white)) .height(1) }); - let bar = " █ "; - let t = Table::new( + let table = Table::new( rows, [ - // + 1 is for padding. - Constraint::Length(64), + Constraint::Length(1), + Constraint::Length(12), + Constraint::Min(1), ], ) - .style( - Style::new() - .fg(app.colors.white_color) - .bg(app.colors.buffer_bg), - ) - .highlight_symbol(Text::from(vec![ - "".into(), - bar.into(), - bar.into(), - "".into(), - ])) - .block( - Block::default() - .borders(Borders::LEFT) - .border_style(Style::new().fg(app.colors.accent_color)) - .border_type(BorderType::Plain), - ) + .style(Style::new().fg(app.colors.white).bg(app.colors.background)) .highlight_spacing(HighlightSpacing::Always); - f.render_stateful_widget(t, area, &mut app.state); + f.render_stateful_widget(table, rects[1], &mut app.state); } fn render_scrollbar(f: &mut Frame, app: &mut App, area: Rect) { @@ -318,11 +386,9 @@ fn render_footer(f: &mut Frame, app: &mut App, area: Rect) { ]) .split(area); - let help = - title_block(HELP_TEXT, Alignment::Left).style(Style::new().fg(app.colors.gray_color)); + let help = title_block(HELP_TEXT, Alignment::Left).style(Style::new().fg(app.colors.gray)); f.render_widget(help, rects[0]); - let link = - title_block(TXTX_LINK, Alignment::Right).style(Style::new().fg(app.colors.white_color)); + let link = title_block(TXTX_LINK, Alignment::Right).style(Style::new().fg(app.colors.white)); f.render_widget(link, rects[1]); } diff --git a/crates/core/src/rpc/minimal.rs b/crates/core/src/rpc/minimal.rs index b479ec4..f931cf7 100644 --- a/crates/core/src/rpc/minimal.rs +++ b/crates/core/src/rpc/minimal.rs @@ -1,7 +1,7 @@ use crate::rpc::utils::verify_pubkey; use super::{RpcContextConfig, RunloopContext}; -use jsonrpc_core::{Error, Result}; +use jsonrpc_core::Result; use jsonrpc_derive::rpc; use solana_client::{ rpc_config::{ diff --git a/crates/core/src/simnet/mod.rs b/crates/core/src/simnet/mod.rs index bab4de7..b203e55 100644 --- a/crates/core/src/simnet/mod.rs +++ b/crates/core/src/simnet/mod.rs @@ -1,4 +1,4 @@ -use chrono::Utc; +use chrono::{DateTime, Local, Utc}; use jsonrpc_core::MetaIoHandler; use jsonrpc_http_server::{DomainsValidation, ServerBuilder}; use litesvm::LiteSVM; @@ -25,12 +25,14 @@ pub struct GlobalState { pub enum SimnetEvent { ClockUpdate(Clock), + EpochInfoUpdate(EpochInfo), BlockHashExpired, - InfoLog(String), - ErroLog(String), - WarnLog(String), - TransactionReceived(VersionedTransaction), - AccountUpdate(Pubkey), + InfoLog(DateTime, String), + ErroLog(DateTime, String), + WarnLog(DateTime, String), + DebugLog(DateTime, String), + TransactionReceived(DateTime, VersionedTransaction), + AccountUpdate(DateTime, Pubkey), } pub async fn start( @@ -41,6 +43,7 @@ pub async fn start( // Todo: should check config first let rpc_client = RpcClient::new("https://api.mainnet-beta.solana.com"); let epoch_info = rpc_client.get_epoch_info().unwrap(); + let _ = simnet_events_tx.send(SimnetEvent::EpochInfoUpdate(epoch_info.clone())); // Question: can the value `slots_in_epoch` fluctuate over time? let slots_in_epoch = epoch_info.slots_in_epoch; @@ -81,7 +84,8 @@ pub async fn start( }; while let Ok(tx) = mempool_rx.try_recv() { - let _ = simnet_events_tx.send(SimnetEvent::TransactionReceived(tx.clone())); + let _ = + simnet_events_tx.send(SimnetEvent::TransactionReceived(Local::now(), tx.clone())); tx.verify_with_results(); let tx = tx.into_legacy_transaction().unwrap(); @@ -99,17 +103,17 @@ pub async fn start( let event = match res { Ok(account) => { let _ = ctx.svm.set_account(*program_id, account); - SimnetEvent::AccountUpdate(program_id.clone()) - } - Err(e) => { - SimnetEvent::ErroLog(format!("unable to retrieve account: {}", e)) + SimnetEvent::AccountUpdate(Local::now(), program_id.clone()) } + Err(e) => SimnetEvent::ErroLog( + Local::now(), + format!("unable to retrieve account: {}", e), + ), }; let _ = simnet_events_tx.send(event); } } let res = ctx.svm.send_transaction(tx); - // println!("{:?}", res); } ctx.epoch_info.slot_index += 1; ctx.epoch_info.absolute_slot += 1;