use std::io::Write as _;
use clap::{ArgAction, Args, CommandFactory as _, Parser, Subcommand, ValueEnum};
use degenbot_cli_core::{
AaveCommand, CliContext, CliError, Command, DatabaseCommand, ExchangeCommand, FleetCommand,
PathCommand, PathDirection, PoolCommand, PoolFamily, PosturePatchEntry, DEFAULT_CHUNK_SIZE,
DEFAULT_TO_BLOCK, DEFAULT_VERIFY_ALL_INTERVAL,
};
use degenbot_config::{EnvVars, ProcessEnv, DEFAULT_CHAIN_ID_ENV};
use crate::VERSION_LINE;
#[derive(Debug, Parser)]
#[command(
name = "degenbot",
about = "Perform cli.",
version = VERSION_LINE,
arg_required_else_help = true,
disable_help_subcommand = true
)]
pub struct Cli {
#[command(subcommand)]
pub command: Option<Commands>,
#[arg(long, global = true, value_name = "PATH")]
pub database: Option<String>,
#[arg(long, global = true, value_name = "CHAIN_ID")]
pub chain_id: Option<String>,
#[arg(long, global = true, value_name = "URI")]
pub node_http: Option<String>,
#[arg(long, global = true, value_name = "URI")]
pub node_ws: Option<String>,
}
#[derive(Debug, Subcommand)]
pub enum Commands {
Database {
#[command(subcommand)]
command: DatabaseSub,
},
Exchange {
#[command(subcommand)]
command: ExchangeSub,
},
Pool {
#[command(subcommand)]
command: PoolSub,
},
Aave {
#[command(subcommand)]
command: AaveSub,
},
Fleet {
#[command(subcommand)]
command: FleetSub,
},
Path {
#[command(subcommand)]
command: PathSub,
},
}
#[derive(Debug, Subcommand)]
pub enum DatabaseSub {
Backup,
Reset {
#[arg(long)]
force: bool,
},
Upgrade {
#[arg(long)]
force: bool,
},
Compact,
Cutover {
#[arg(long)]
dry_run: bool,
#[arg(long)]
force: bool,
},
Heal {
#[arg(long)]
dry_run: bool,
#[arg(long)]
force: bool,
},
Inspect,
}
#[derive(Debug, Subcommand)]
pub enum ExchangeSub {
Activate {
#[arg(long, value_name = "CHAIN")]
chain: String,
#[arg(long, value_name = "NAME")]
name: String,
},
Deactivate {
#[arg(long, value_name = "CHAIN")]
chain: String,
#[arg(long, value_name = "NAME")]
name: String,
},
}
#[derive(Debug, Subcommand)]
pub enum PoolSub {
Update {
#[arg(long = "chunk", value_name = "BLOCKS", default_value_t = DEFAULT_CHUNK_SIZE)]
chunk_size: u64,
#[arg(long = "to-block", value_name = "BLOCK", default_value = DEFAULT_TO_BLOCK)]
to_block: String,
#[command(flatten)]
verify: VerifyFlags,
#[arg(
long = "verify-all-interval",
value_name = "BLOCKS",
default_value_t = DEFAULT_VERIFY_ALL_INTERVAL
)]
verify_all_interval: u64,
},
Verify {
#[arg(long = "rpc-url", value_name = "URL", required = true)]
rpc_url: String,
#[arg(long = "chain", value_name = "CHAIN_ID", required = true)]
pool_chain_id: i64,
#[arg(long = "block", value_name = "BLOCK", required = true)]
block_number: u64,
#[arg(
long = "pool",
value_name = "POOL",
required = true,
help = "The pool to verify. V3: the pool contract address. V4: the PoolId (pool_hash, bytes32 hex 0x…)."
)]
pool: String,
#[arg(
long = "family",
value_enum,
required = true,
help = "The pool family (selects ticks()/tickBitmap() vs PoolManager extsload)."
)]
family: FamilyArg,
#[arg(
long = "pool-manager",
value_name = "ADDRESS",
help = "(V4 only) The deployed V4 PoolManager singleton address (the V4 exchange's factory). Required for --family v4."
)]
pool_manager: Option<String>,
},
}
#[derive(Debug, Clone, Args)]
#[expect(
clippy::struct_excessive_bools,
reason = "the two on/off gate pairs are argv vocabulary, not state: each pair is two mutually-overriding flags"
)]
pub struct VerifyFlags {
#[arg(long = "verify-chunk", action = ArgAction::SetTrue, overrides_with = "no_verify_chunk")]
pub verify_chunk: bool,
#[arg(long = "no-verify-chunk", action = ArgAction::SetTrue, overrides_with = "verify_chunk")]
pub no_verify_chunk: bool,
#[arg(long = "verify-all", action = ArgAction::SetTrue, overrides_with = "no_verify_all")]
pub verify_all: bool,
#[arg(long = "no-verify-all", action = ArgAction::SetTrue, overrides_with = "verify_all")]
pub no_verify_all: bool,
}
impl VerifyFlags {
#[must_use]
pub const fn chunk_gate(&self) -> bool {
self.verify_chunk || !self.no_verify_chunk
}
#[must_use]
pub const fn all_gate(&self) -> bool {
self.verify_all && !self.no_verify_all
}
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum FamilyArg {
V3,
V4,
}
#[derive(Debug, Subcommand)]
pub enum AaveSub {
Activate,
Deactivate {
#[arg(
long = "name",
value_name = "MARKET",
default_value = "Aave Ethereum Market"
)]
market_name: String,
},
Update {
#[arg(long = "chunk", value_name = "BLOCKS", default_value_t = DEFAULT_CHUNK_SIZE)]
chunk_size: u64,
#[arg(long = "to-block", value_name = "BLOCK", default_value = DEFAULT_TO_BLOCK)]
to_block: String,
#[command(flatten)]
verify: VerifyFlags,
#[arg(
long = "verify-all-interval",
value_name = "BLOCKS",
default_value_t = DEFAULT_VERIFY_ALL_INTERVAL
)]
verify_all_interval: u64,
#[arg(long = "one-chunk", action = ArgAction::SetTrue)]
stop_after_one_chunk: bool,
#[arg(long = "dry-run", action = ArgAction::SetTrue)]
dry_run: bool,
#[arg(long = "backup", action = ArgAction::SetTrue, overrides_with = "no_backup")]
backup: bool,
#[arg(long = "no-backup", action = ArgAction::SetTrue, overrides_with = "backup")]
no_backup: bool,
},
Position {
#[command(subcommand)]
command: AavePositionSub,
},
}
#[derive(Debug, Subcommand)]
pub enum AavePositionSub {
Show {
#[arg(value_name = "ADDRESS")]
address: String,
#[arg(
long = "market",
value_name = "MARKET",
default_value = "Aave Ethereum Market"
)]
market: String,
},
}
#[derive(Debug, Subcommand)]
pub enum FleetSub {
Posture {
#[command(subcommand)]
command: FleetPostureSub,
},
}
#[derive(Debug, Subcommand)]
pub enum FleetPostureSub {
Show {
#[arg(
long,
value_name = "PATH",
help = "Unix domain socket path of the running bot's OperatorServer."
)]
socket: Option<String>,
},
Set {
#[arg(
long,
value_name = "PATH",
help = "Unix domain socket path of the running bot's OperatorServer."
)]
socket: Option<String>,
#[arg(long = "cordon-enter-events", value_name = "COUNT")]
cordon_enter_events: Option<u64>,
#[arg(long = "cordon-duty-percent", value_name = "PERCENT")]
cordon_duty_percent: Option<f64>,
#[arg(long = "cordon-enter-window-ms", value_name = "MS")]
cordon_enter_window_ms: Option<u64>,
#[arg(long = "cordon-duty-window-ms", value_name = "MS")]
cordon_duty_window_ms: Option<u64>,
#[arg(long = "cordon-exit-clean-ms", value_name = "MS")]
cordon_exit_clean_ms: Option<u64>,
#[arg(
long = "cordon-sim-intake-floor",
value_name = "COUNT|null",
help = "SimDriver new-lease cap while cordoned. The literal null restores half the slot cap."
)]
cordon_sim_intake_floor: Option<String>,
},
}
#[derive(Debug, Subcommand)]
pub enum PathSub {
Add {
#[arg(
long,
value_name = "PATH",
help = "Unix domain socket path of the running bot's OperatorServer."
)]
socket: Option<String>,
#[arg(long = "hop", value_name = "HOP", required = true)]
hops: Vec<String>,
#[arg(long = "direction", value_enum)]
direction: Option<DirectionArg>,
},
Discover {
#[arg(
long,
value_name = "PATH",
help = "Unix domain socket path of the running bot's OperatorServer."
)]
socket: Option<String>,
#[arg(long = "bound", value_name = "COUNT")]
bound: Option<u64>,
},
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum DirectionArg {
Zfo,
Ozf,
}
#[must_use]
pub fn context<'a>(cli: &Cli, env: &'a dyn EnvVars) -> CliContext<'a> {
let mut ctx = CliContext::new(env);
if let Some(database) = &cli.database {
ctx = ctx.with_database(database.clone());
}
if let Some(chain_id) = &cli.chain_id {
ctx = ctx.with_chain_id(chain_id.clone());
}
if let Some(node_http) = &cli.node_http {
ctx = ctx.with_node_http(node_http.clone());
}
if let Some(node_ws) = &cli.node_ws {
ctx = ctx.with_node_ws(node_ws.clone());
}
ctx
}
pub fn resolve(cli: &Cli) -> Result<Command, CliError> {
resolve_with_env(cli, &ProcessEnv)
}
pub fn resolve_with_env(cli: &Cli, env: &dyn EnvVars) -> Result<Command, CliError> {
let ctx = context(cli, env);
let Some(command) = &cli.command else {
return Err(CliError::InvalidArgument(
"a subcommand is required".to_string(),
));
};
match command {
Commands::Database { command } => Ok(Command::Database(database(command))),
Commands::Exchange { command } => Ok(Command::Exchange(exchange(command))),
Commands::Pool { command } => Ok(Command::Pool(pool(command))),
Commands::Aave { command } => Ok(Command::Aave(aave(command, cli, &ctx)?)),
Commands::Fleet { command } => Ok(Command::Fleet(fleet(command)?)),
Commands::Path { command } => Ok(Command::Path(path(command))),
}
}
pub fn write_missing_subcommand_error() {
let mut command = Cli::command();
let usage = command.render_usage().to_string();
let _ = writeln!(
std::io::stderr().lock(),
"error: a subcommand is required\n\n{usage}"
);
}
fn database(command: &DatabaseSub) -> DatabaseCommand {
match command {
DatabaseSub::Backup => DatabaseCommand::Backup,
DatabaseSub::Reset { force } => DatabaseCommand::Reset { force: *force },
DatabaseSub::Upgrade { force } => DatabaseCommand::Upgrade { force: *force },
DatabaseSub::Compact => DatabaseCommand::Compact,
DatabaseSub::Cutover { dry_run, force } => DatabaseCommand::Cutover {
dry_run: *dry_run,
force: *force,
},
DatabaseSub::Heal { dry_run, force } => DatabaseCommand::Heal {
dry_run: *dry_run,
force: *force,
},
DatabaseSub::Inspect => DatabaseCommand::Inspect,
}
}
fn exchange(command: &ExchangeSub) -> ExchangeCommand {
match command {
ExchangeSub::Activate { chain, name } => ExchangeCommand::Activate {
chain: chain.clone(),
name: name.clone(),
},
ExchangeSub::Deactivate { chain, name } => ExchangeCommand::Deactivate {
chain: chain.clone(),
name: name.clone(),
},
}
}
fn pool(command: &PoolSub) -> PoolCommand {
match command {
PoolSub::Update {
chunk_size,
to_block,
verify,
verify_all_interval,
} => PoolCommand::Update {
chunk_size: *chunk_size,
to_block: to_block.clone(),
verify_chunk: verify.chunk_gate(),
verify_all: verify.all_gate(),
verify_all_interval: *verify_all_interval,
},
PoolSub::Verify {
rpc_url,
pool_chain_id,
block_number,
pool,
family,
pool_manager,
} => PoolCommand::Verify {
rpc_url: rpc_url.clone(),
chain_id: *pool_chain_id,
block_number: *block_number,
pool: pool.clone(),
family: match family {
FamilyArg::V3 => PoolFamily::V3,
FamilyArg::V4 => PoolFamily::V4,
},
pool_manager: pool_manager.clone(),
},
}
}
fn aave(command: &AaveSub, cli: &Cli, ctx: &CliContext<'_>) -> Result<AaveCommand, CliError> {
match command {
AaveSub::Activate => Ok(AaveCommand::Activate {
chain_id: chain_or_default(cli, ctx, 1)?,
}),
AaveSub::Deactivate { market_name } => Ok(AaveCommand::Deactivate {
chain_id: chain_or_default(cli, ctx, 1)?,
market_name: market_name.clone(),
}),
AaveSub::Update {
chunk_size,
to_block,
verify,
verify_all_interval,
stop_after_one_chunk,
dry_run,
backup,
no_backup,
} => Ok(AaveCommand::Update {
chunk_size: *chunk_size,
to_block: to_block.clone(),
verify_chunk: verify.chunk_gate(),
verify_all: verify.all_gate(),
verify_all_interval: *verify_all_interval,
stop_after_one_chunk: *stop_after_one_chunk,
dry_run: *dry_run,
enable_backup: *backup && !*no_backup,
}),
AaveSub::Position { command } => match command {
AavePositionSub::Show { address, market } => Ok(AaveCommand::PositionShow {
address: address.clone(),
market: market.clone(),
chain_id: chain_or_default(cli, ctx, 1)?,
}),
},
}
}
fn fleet(command: &FleetSub) -> Result<FleetCommand, CliError> {
match command {
FleetSub::Posture { command } => match command {
FleetPostureSub::Show { socket } => Ok(FleetCommand::PostureShow {
socket: socket.clone(),
}),
FleetPostureSub::Set {
socket,
cordon_enter_events,
cordon_duty_percent,
cordon_enter_window_ms,
cordon_duty_window_ms,
cordon_exit_clean_ms,
cordon_sim_intake_floor,
} => {
let mut patch = Vec::new();
if let Some(value) = cordon_enter_events {
patch.push(PosturePatchEntry::int("cordon_enter_events", *value));
}
if let Some(value) = cordon_duty_percent {
patch.push(PosturePatchEntry::float("cordon_duty_percent", *value));
}
if let Some(value) = cordon_enter_window_ms {
patch.push(PosturePatchEntry::int("cordon_enter_window_ms", *value));
}
if let Some(value) = cordon_duty_window_ms {
patch.push(PosturePatchEntry::int("cordon_duty_window_ms", *value));
}
if let Some(value) = cordon_exit_clean_ms {
patch.push(PosturePatchEntry::int("cordon_exit_clean_ms", *value));
}
if let Some(value) = cordon_sim_intake_floor {
patch.push(PosturePatchEntry::new(
"cordon_sim_intake_floor",
degenbot_cli_core::parse_sim_intake_floor(value)?,
));
}
Ok(FleetCommand::PostureSet {
socket: socket.clone(),
patch,
})
}
},
}
}
fn path(command: &PathSub) -> PathCommand {
match command {
PathSub::Add {
socket,
hops,
direction,
} => PathCommand::Add {
socket: socket.clone(),
hops: hops.clone(),
direction: direction.map(|direction| match direction {
DirectionArg::Zfo => PathDirection::Zfo,
DirectionArg::Ozf => PathDirection::Ozf,
}),
},
PathSub::Discover { socket, bound } => PathCommand::Discover {
socket: socket.clone(),
bound: *bound,
},
}
}
fn chain_or_default(cli: &Cli, ctx: &CliContext<'_>, default: u64) -> Result<u64, CliError> {
let cli_layer = cli
.chain_id
.as_deref()
.is_some_and(|value| !value.is_empty());
let env_layer = ctx
.env()
.get(DEFAULT_CHAIN_ID_ENV)
.is_some_and(|value| !value.is_empty());
if cli_layer || env_layer {
return ctx
.chain_id()
.map(|resolved| resolved.value)
.map_err(CliError::from);
}
Ok(default)
}