Skip to content
Closed
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
38 changes: 30 additions & 8 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ codegen-units = 1
# local
base-bundles = { path = "crates/bundles" }
base-fbal = { path = "crates/fbal" }
base-reth-cli = { path = "crates/cli" }
base-cli = { path = "crates/cli" }
base-reth-rpc = { path = "crates/rpc" }
base-reth-rpc-types = { path = "crates/reth-rpc-types" }
base-tracex = { path = "crates/tracex" }
Expand Down
4 changes: 4 additions & 0 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ build-maxperf:
build-node:
cargo build --bin base-reth-node

# Runs the base cli binary
base *args:
cargo run -p base-bin -- {{args}}

# Build the contracts used for tests
build-contracts:
cd crates/test-utils/contracts && forge build
Expand Down
49 changes: 49 additions & 0 deletions bin/base/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
[package]
name = "base-bin"
description = "Base CLI"

version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true

[[bin]]
name = "base"
path = "src/main.rs"

[lints]
workspace = true

[dependencies]
# Local
base-cli.workspace = true

# CLI
clap = { workspace = true, features = ["derive"] }

# Async
tokio = { workspace = true, features = ["full", "signal"] }

# Error handling
anyhow = "1"

# Chains
alloy-chains = "0.2"

# Alloy - WebSocket provider for mempool subscriptions
alloy-provider = { workspace = true, features = ["pubsub", "ws"] }
alloy-rpc-types-eth.workspace = true

# Async utilities
futures-util.workspace = true

# Derive
derive_more = { workspace = true, features = ["display"] }

# Logging
tracing.workspace = true

[features]
default = []
Empty file added bin/base/README.md
Empty file.
71 changes: 71 additions & 0 deletions bin/base/src/cli.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
//! Contains the CLI entry point for the Base binary.

use anyhow::Result;
use clap::{Parser, builder::styling::Styles};

use crate::{commands::Commands, flags::GlobalArgs, version};

/// Returns the CLI styles.
const fn cli_styles() -> Styles {
Styles::plain()
}

/// The CLI.
#[derive(Parser, Clone, Debug)]
#[command(
author,
version = version::SHORT_VERSION,
long_version = version::LONG_VERSION,
about,
styles = cli_styles(),
long_about = None
)]
pub struct Cli {
/// The subcommand to run.
#[command(subcommand)]
pub subcommand: Commands,
/// Global arguments for the CLI.
#[command(flatten)]
pub global: GlobalArgs,
}

impl Cli {
/// Runs the CLI.
pub fn run(self) -> Result<()> {
// TODO: Initialize telemetry - allow subcommands to customize the filter.

// TODO: Initialize unified metrics

// TODO: Allow subcommands to initialize cli metrics.

// Run the subcommand.
match self.subcommand {
Commands::Consensus(c) => Self::run_until_ctrl_c(c.run(&self.global)),
Commands::Execution(e) => Self::run_until_ctrl_c(e.run(&self.global)),
Commands::Mempool(m) => Self::run_until_ctrl_c(m.run(&self.global)),
}
}

/// Run until ctrl-c is pressed.
pub fn run_until_ctrl_c<F>(fut: F) -> Result<()>
where
F: std::future::Future<Output = Result<()>>,
{
let rt = Self::tokio_runtime().map_err(|e| anyhow::anyhow!(e))?;
rt.block_on(async move {
tokio::select! {
res = fut => res,
_ = tokio::signal::ctrl_c() => {
tracing::info!(target: "cli", "Received Ctrl-C, shutting down...");
Ok(())
}
}
})
}

/// Creates a new default tokio multi-thread [Runtime](tokio::runtime::Runtime) with all
/// features enabled
pub fn tokio_runtime() -> Result<tokio::runtime::Runtime, std::io::Error> {
tokio::runtime::Builder::new_multi_thread().enable_all().build()
}
}
21 changes: 21 additions & 0 deletions bin/base/src/commands/consensus.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//! Consensus command for the Base Stack.
use anyhow::Result;
use clap::Args;

use crate::flags::GlobalArgs;

/// The consensus command.
#[derive(Debug, Clone, Args)]
pub struct ConsensusCommand {
// TODO: Add consensus-specific arguments here.
}

impl ConsensusCommand {
/// Runs the consensus command.
pub async fn run(&self, _global: &GlobalArgs) -> Result<()> {
tracing::info!(target: "cli", "Running consensus command...");
// TODO: Implement consensus logic.
Ok(())
}
}
21 changes: 21 additions & 0 deletions bin/base/src/commands/execution.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//! Execution command for the Base Stack.

use anyhow::Result;
use clap::Args;

use crate::flags::GlobalArgs;

/// The execution command.
#[derive(Debug, Clone, Args)]
pub struct ExecutionCommand {
// TODO: Add execution-specific arguments here.
}

impl ExecutionCommand {
/// Runs the execution command.
pub async fn run(&self, _global: &GlobalArgs) -> Result<()> {
tracing::info!(target: "cli", "Running execution command...");
// TODO: Implement execution logic.
Ok(())
}
}
111 changes: 111 additions & 0 deletions bin/base/src/commands/mempool.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
//! Mempool command - streams pending transactions via WebSocket.

use alloy_chains::Chain;
// Re-export traits for accessing tx fields
use alloy_provider::network::TransactionResponse;
use alloy_provider::{Provider, ProviderBuilder, WsConnect};
use alloy_rpc_types_eth::{Transaction, TransactionTrait};
use anyhow::Result;
use clap::Args;
use futures_util::StreamExt;

use crate::flags::GlobalArgs;

/// Returns the default public WebSocket RPC URL for the given chain.
///
/// These are free, public endpoints from PublicNode that support eth_subscribe.
/// See: <https://chainlist.org/chain/8453>
fn default_ws_url(chain: &Chain) -> Option<&'static str> {
match chain.id() {
8453 => Some("wss://base-rpc.publicnode.com"), // Base Mainnet
84532 => Some("wss://base-sepolia-rpc.publicnode.com"), // Base Sepolia
1 => Some("wss://ethereum-rpc.publicnode.com"), // Ethereum Mainnet
11155111 => Some("wss://ethereum-sepolia-rpc.publicnode.com"), // Ethereum Sepolia
_ => None,
}
}

/// Prints a transaction to stdout in human-readable format.
fn print_transaction(tx: &Transaction) {
println!("tx: {}", tx.tx_hash());
println!(" from: {}", tx.from());
if let Some(to) = tx.to() {
println!(" to: {to}");
}
println!(" value: {}", tx.value());
println!(" gas: {}", tx.gas_limit());
println!();
}

/// The mempool command - streams pending transactions via WebSocket.
///
/// Uses free public WebSocket endpoints by default (from PublicNode).
/// You can override with `--rpc-url` for a custom endpoint.
#[derive(Debug, Clone, Args)]
pub struct MempoolCommand {
/// WebSocket RPC URL. Defaults to a free public endpoint for supported networks.
///
/// Examples:
/// - PublicNode: wss://base-rpc.publicnode.com (default for Base)
/// - Alchemy: wss://base-mainnet.g.alchemy.com/v2/YOUR_API_KEY
/// - QuickNode: wss://YOUR_ENDPOINT.base-mainnet.quiknode.pro/YOUR_API_KEY
#[arg(long = "rpc-url", env = "BASE_RPC_URL")]
pub rpc_url: Option<String>,

/// Show full transaction details instead of just hashes.
#[arg(long = "full", short = 'f', default_value = "false")]
pub full: bool,
}

impl MempoolCommand {
/// Runs the mempool command.
pub async fn run(&self, global: &GlobalArgs) -> Result<()> {
// Determine WebSocket URL
let ws_url = match &self.rpc_url {
Some(url) => url.clone(),
None => default_ws_url(&global.network)
.ok_or_else(|| {
anyhow::anyhow!(
"No default WebSocket URL for network {}. Provide --rpc-url.",
global.network
)
})?
.to_string(),
};

println!("Connecting to {}...", ws_url);

// Connect to WebSocket provider
let ws = WsConnect::new(&ws_url);
let provider = ProviderBuilder::new().connect_ws(ws).await?;

println!("Connected! Subscribing to pending transactions on {}...", global.network);

// Subscribe to pending transactions
let sub = provider.subscribe_pending_transactions().await?;
let mut stream = sub.into_stream();

println!("Subscribed! Streaming transactions (Ctrl-C to stop)...");
println!();

// Stream transactions
while let Some(tx_hash) = stream.next().await {
if self.full {
// Fetch full transaction details
match provider.get_transaction_by_hash(tx_hash).await {
Ok(Some(tx)) => {
print_transaction(&tx);
}
Ok(None) => println!("tx: {tx_hash} (not found)"),
Err(e) => {
eprintln!("Failed to fetch tx {}: {}", tx_hash, e);
}
}
} else {
println!("tx: {tx_hash}");
}
}

Ok(())
}
}
27 changes: 27 additions & 0 deletions bin/base/src/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//! Contains cli commands.

use clap::Subcommand;

pub mod consensus;
pub use consensus::ConsensusCommand;

pub mod execution;
pub use execution::ExecutionCommand;

pub mod mempool;
pub use mempool::MempoolCommand;

/// Subcommands for the CLI.
#[derive(Debug, Clone, Subcommand)]
#[allow(clippy::large_enum_variant)]
pub enum Commands {
/// Runs the consensus layer for the Base Stack.
#[command(alias = "c")]
Consensus(ConsensusCommand),
/// Runs the execution layer for the Base Stack.
#[command(alias = "e")]
Execution(ExecutionCommand),
/// Streams pending transactions from the mempool.
#[command(alias = "m")]
Mempool(MempoolCommand),
}
Loading