Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
671 changes: 638 additions & 33 deletions Cargo.lock

Large diffs are not rendered by default.

21 changes: 20 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ license = "MIT"

[workspace.lints.rust]
missing-debug-implementations = "warn"
rust-2018-idioms = "warn"
elided-lifetimes-in-paths = "allow"
trivial-casts = "warn"
unreachable-pub = "warn"
unused-must-use = "deny"
unnameable-types = "warn"
Expand Down Expand Up @@ -42,6 +45,12 @@ explicit-iter-loop = "warn"
iter-with-drain = "warn"
needless-pass-by-ref-mut = "warn"
string-lit-as-bytes = "warn"
needless-continue = "warn"
semicolon-if-nothing-returned = "warn"
manual-let-else = "warn"
map-unwrap-or = "warn"
inefficient-to-string = "warn"
checked-conversions = "warn"

[workspace.dependencies]
clap = { version = "4.0", features = ["derive", "env"] }
Expand Down Expand Up @@ -83,12 +92,22 @@ alloy-rpc-client = { version = "1.5.2" }
alloy-transport-http = { version = "1.5.2" }
alloy-sol-types = { version = "1.5.2" }
alloy-contract = { version = "1.5.2" }
alloy-signer = { version = "1.5.2" }
alloy-signer-local = { version = "1.5.2", features = ["mnemonic"] }
alloy-network = { version = "1.5.2" }
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "hickory-dns"] }
rand = "0.8"
humantime = "2.1"

# op-alloy
op-alloy-rpc-types = { version = "0.22.0", default-features = false }
op-alloy-rpc-types-engine = { version = "0.22.0", default-features = false }
op-alloy-consensus = { version = "0.22.0", default-features = false }
op-alloy-network = { version = "0.22.0" }

shellexpand = "3.1.1"

# base
base-flashtypes = { git = "https://github.com/base/base.git" }
base-flashtypes = { git = "https://github.com/base/base.git", rev = "720f6e1a" }
basectl-cli = { path = "crates/basectl" }
gobrr = { path = "crates/gobrr" }
2 changes: 2 additions & 0 deletions bin/basectl/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,5 @@ basectl-cli = { workspace = true }
clap = { workspace = true }
tokio = { workspace = true }
anyhow = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
12 changes: 10 additions & 2 deletions bin/basectl/main.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use basectl_cli::{
app::{ViewId, run_app, run_app_with_view},
app::{ViewId, run_app_with_view, run_loadtest_tui},
config::ChainConfig,
};
use clap::{Parser, Subcommand};
Expand Down Expand Up @@ -30,6 +30,13 @@ enum Commands {
/// Command center (combined view)
#[command(visible_alias = "cc")]
CommandCenter,
/// Run a load test with real-time TUI dashboard
#[command(visible_alias = "lt")]
Loadtest {
/// Path to gobrr YAML config file
#[arg(long = "file", short = 'f')]
file: String,
},
}

#[tokio::main]
Expand All @@ -39,12 +46,13 @@ async fn main() -> anyhow::Result<()> {
let chain_config = ChainConfig::load(&cli.config)?;

match cli.command {
Some(Commands::Loadtest { file }) => run_loadtest_tui(chain_config, file).await,
Some(Commands::Config) => run_app_with_view(chain_config, ViewId::Config).await,
Some(Commands::Flashblocks) => run_app_with_view(chain_config, ViewId::Flashblocks).await,
Some(Commands::Da) => run_app_with_view(chain_config, ViewId::DaMonitor).await,
Some(Commands::CommandCenter) => {
run_app_with_view(chain_config, ViewId::CommandCenter).await
}
None => run_app(chain_config).await,
None => run_app_with_view(chain_config, ViewId::Home).await,
}
}
3 changes: 3 additions & 0 deletions crates/basectl/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,6 @@ alloy-primitives = { workspace = true }
alloy-sol-types = { workspace = true }
alloy-contract = { workspace = true }
arboard = { workspace = true }
gobrr = { workspace = true }
tracing = { workspace = true }
shellexpand = { workspace = true }
3 changes: 3 additions & 0 deletions crates/basectl/src/app/action.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
use std::path::PathBuf;

use super::ViewId;

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Action {
None,
Quit,
SwitchView(ViewId),
StartLoadTest(PathBuf),
}
40 changes: 39 additions & 1 deletion crates/basectl/src/app/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ use std::io::Stdout;
use anyhow::Result;
use crossterm::event::{self, Event, KeyCode, KeyModifiers};
use ratatui::prelude::*;
use tokio::sync::oneshot;

use super::{Action, Resources, Router, View, ViewId};
use super::{Action, Resources, Router, View, ViewId, resources::LoadTestSetup, runner};
use crate::{
commands::common::EVENT_POLL_TIMEOUT,
tui::{AppFrame, restore_terminal, setup_terminal},
Expand Down Expand Up @@ -46,6 +47,11 @@ impl App {
self.resources.da.poll();
self.resources.flash.poll();
self.resources.poll_sys_config();
if let Some(ref mut lt) = self.resources.loadtest {
lt.poll();
}

self.poll_loadtest_setup();

let action = current_view.tick(&mut self.resources);
if self.handle_action(action, &mut current_view, view_factory) {
Expand Down Expand Up @@ -95,6 +101,34 @@ impl App {
Ok(())
}

fn poll_loadtest_setup(&mut self) {
match self.resources.loadtest_setup.take() {
Some(LoadTestSetup::Starting { config_path, mut result_rx }) => {
match result_rx.try_recv() {
Ok(Ok(handle)) => {
let config_file = config_path.display().to_string();
runner::activate_loadtest(&mut self.resources, handle, config_file);
}
Ok(Err(e)) => {
self.resources.loadtest_setup =
Some(LoadTestSetup::Failed { config_path, error: format!("{e:#}") });
}
Err(oneshot::error::TryRecvError::Empty) => {
self.resources.loadtest_setup =
Some(LoadTestSetup::Starting { config_path, result_rx });
}
Err(oneshot::error::TryRecvError::Closed) => {
self.resources.loadtest_setup = Some(LoadTestSetup::Failed {
config_path,
error: "Setup task panicked".to_string(),
});
}
}
}
other => self.resources.loadtest_setup = other,
}
}

fn handle_action<F>(
&mut self,
action: Action,
Expand All @@ -113,6 +147,10 @@ impl App {
self.show_help = false;
false
}
Action::StartLoadTest(path) => {
runner::spawn_loadtest_setup(&mut self.resources, path);
false
}
}
}
}
4 changes: 2 additions & 2 deletions crates/basectl/src/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ pub mod views;
pub use core::App;

pub use action::Action;
pub use resources::{DaState, FlashState, Resources};
pub use resources::{DaState, FlashState, LoadTestSetup, Resources};
pub use router::{Router, ViewId};
pub use runner::{run_app, run_app_with_view};
pub use runner::{run_app_with_view, run_loadtest_tui};
pub use view::View;
20 changes: 18 additions & 2 deletions crates/basectl/src/app/resources.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::collections::VecDeque;
use std::{collections::VecDeque, path::PathBuf};

use base_flashtypes::Flashblock;
use tokio::sync::mpsc;
use tokio::sync::{mpsc, oneshot};

use crate::{
commands::common::{DaTracker, FlashblockEntry, LoadingState},
Expand All @@ -19,6 +19,20 @@ pub struct Resources {
pub flash: FlashState,
pub system_config: Option<FullSystemConfig>,
sys_config_rx: Option<mpsc::Receiver<FullSystemConfig>>,
pub loadtest: Option<gobrr::LoadTestState>,
pub loadtest_setup: Option<LoadTestSetup>,
}

#[derive(Debug)]
pub enum LoadTestSetup {
Starting {
config_path: PathBuf,
result_rx: oneshot::Receiver<anyhow::Result<gobrr::LoadTestHandle>>,
},
Failed {
config_path: PathBuf,
error: String,
},
}

#[derive(Debug)]
Expand Down Expand Up @@ -55,6 +69,8 @@ impl Resources {
flash: FlashState::new(),
system_config: None,
sys_config_rx: None,
loadtest: None,
loadtest_setup: None,
}
}

Expand Down
25 changes: 4 additions & 21 deletions crates/basectl/src/app/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,41 +5,24 @@ pub enum ViewId {
DaMonitor,
Flashblocks,
Config,
LoadTest,
}

#[derive(Debug)]
pub struct Router {
current: ViewId,
history: Vec<ViewId>,
}

impl Router {
pub const fn new(initial: ViewId) -> Self {
Self { current: initial, history: Vec::new() }
Self { current: initial }
}

pub const fn current(&self) -> ViewId {
self.current
}

pub fn switch_to(&mut self, view: ViewId) {
if view != self.current {
self.history.push(self.current);
self.current = view;
}
}

pub fn back(&mut self) -> bool {
if let Some(prev) = self.history.pop() {
self.current = prev;
true
} else {
false
}
}

pub fn go_home(&mut self) {
self.history.clear();
self.current = ViewId::Home;
pub const fn switch_to(&mut self, view: ViewId) {
self.current = view;
}
}
45 changes: 37 additions & 8 deletions crates/basectl/src/app/runner.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use std::time::Duration;
use std::{path::PathBuf, time::Duration};

use anyhow::Result;
use base_flashtypes::Flashblock;
use tokio::sync::mpsc;
use tokio::sync::{mpsc, oneshot};

use super::{App, Resources, ViewId, views::create_view};
use super::{App, Resources, ViewId, resources::LoadTestSetup, views::create_view};
use crate::{
config::ChainConfig,
l1_client::{FullSystemConfig, fetch_full_system_config},
Expand All @@ -15,24 +15,53 @@ use crate::{
},
};

pub async fn run_app(config: ChainConfig) -> Result<()> {
pub async fn run_app_with_view(config: ChainConfig, initial_view: ViewId) -> Result<()> {
let mut resources = Resources::new(config.clone());

start_background_services(&config, &mut resources);

let app = App::new(resources, ViewId::Home);
let app = App::new(resources, initial_view);
app.run(create_view).await
}

pub async fn run_app_with_view(config: ChainConfig, initial_view: ViewId) -> Result<()> {
/// Run load test with TUI dashboard.
/// Pre-populates the setup state so the `LoadTestView` transitions through
/// Starting → Dashboard automatically.
pub async fn run_loadtest_tui(config: ChainConfig, file: String) -> Result<()> {
let mut resources = Resources::new(config.clone());

start_background_services(&config, &mut resources);

let app = App::new(resources, initial_view);
// Use the same async setup path as the TUI-initiated flow
spawn_loadtest_setup(&mut resources, PathBuf::from(&file));

let app = App::new(resources, ViewId::LoadTest);
app.run(create_view).await
}

/// Spawn an async task that calls `gobrr::start_load_test` and stores the
/// result receiver in `resources.loadtest_setup`.
pub(super) fn spawn_loadtest_setup(resources: &mut Resources, config_path: PathBuf) {
let (tx, rx) = oneshot::channel();
let config_str = config_path.display().to_string();

tokio::spawn(async move {
let result = gobrr::start_load_test(&config_str).await;
let _ = tx.send(result);
});

resources.loadtest_setup = Some(LoadTestSetup::Starting { config_path, result_rx: rx });
}

/// Given a successful `LoadTestHandle`, activate the load test via gobrr's
/// orchestrator and store the resulting state.
pub(super) fn activate_loadtest(
resources: &mut Resources,
handle: gobrr::LoadTestHandle,
config_file: String,
) {
resources.loadtest = Some(gobrr::activate(handle, config_file));
}

fn start_background_services(config: &ChainConfig, resources: &mut Resources) {
let (fb_tx, fb_rx) = mpsc::channel::<TimestampedFlashblock>(100);
let (da_fb_tx, da_fb_rx) = mpsc::channel::<Flashblock>(100);
Expand Down
8 changes: 4 additions & 4 deletions crates/basectl/src/app/views/command_center.rs
Original file line number Diff line number Diff line change
Expand Up @@ -296,9 +296,9 @@ fn render_config_panel(f: &mut Frame, area: Rect, resources: &Resources) {
let denominator = sys.eip1559_denominator.unwrap_or(0);

let basefee_scalar =
sys.basefee_scalar.map(|s| s.to_string()).unwrap_or_else(|| "-".to_string());
sys.basefee_scalar.map_or_else(|| "-".to_string(), |s| s.to_string());
let blobbasefee_scalar =
sys.blobbasefee_scalar.map(|s| s.to_string()).unwrap_or_else(|| "-".to_string());
sys.blobbasefee_scalar.map_or_else(|| "-".to_string(), |s| s.to_string());

vec![
Line::from(vec![
Expand Down Expand Up @@ -370,7 +370,7 @@ fn render_stats_panel(f: &mut Frame, area: Rect, resources: &Resources) {
Line::from(vec![
Span::styled("Last batch: ", Style::default().fg(Color::DarkGray)),
Span::styled(
time_since.map(format_duration).unwrap_or_else(|| "-".to_string()),
time_since.map_or_else(|| "-".to_string(), format_duration),
Style::default().fg(Color::White),
),
Span::raw(" "),
Expand Down Expand Up @@ -519,7 +519,7 @@ fn render_flash_panel(
};

let (base_fee_str, base_fee_style) = if entry.index == 0 {
let fee_str = entry.base_fee.map(format_gwei).unwrap_or_else(|| "-".to_string());
let fee_str = entry.base_fee.map_or_else(|| "-".to_string(), format_gwei);
let style = match (entry.base_fee, entry.prev_base_fee) {
(Some(curr), Some(prev)) if curr > prev => Style::default().fg(Color::Red),
(Some(curr), Some(prev)) if curr < prev => Style::default().fg(Color::Green),
Expand Down
Loading