use std::sync::{Arc, Mutex};
use degenbot_arbitrage::{FeeOnTransferRegistry, PoolDivergence};
use degenbot_rpc::provider::AlloyProvider;
use degenbot_submission::{
Dispatcher, NonceLane, PathSuppression, ReceiptProbe, SubmissionTarget, TxSigner,
};
use degenbot_substrate::state_lock::StateLock;
use degenbot_substrate::BotState;
use parking_lot::RwLock as ParkingRwLock;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ExecutorPolicy {
pub sim_concurrency: usize,
pub min_profit_margin_bps: u64,
pub erc6909_profit: bool,
pub inject_code: bool,
pub inject_code_guard: bool,
}
impl From<°enbot_config::BotConfig> for ExecutorPolicy {
fn from(verdict: °enbot_config::BotConfig) -> Self {
let inject = verdict.simulation.inject_executor_code;
Self {
sim_concurrency: verdict.simulation.pipeline_concurrency.max(1),
min_profit_margin_bps: verdict.dispatch.min_profit_margin_bps,
erc6909_profit: verdict.dispatch.erc6909_profit,
inject_code: inject,
inject_code_guard: inject,
}
}
}
pub struct ExecutorRuntime {
pub max_candidates: usize,
pub use_v4_batch: bool,
pub dry_run: bool,
pub resolver: Arc<dyn crate::assembly::PathResolver>,
pub suppression: Arc<Mutex<PathSuppression>>,
pub divergence: Arc<Mutex<PoolDivergence>>,
pub fot: Arc<Mutex<FeeOnTransferRegistry>>,
pub provider: Arc<AlloyProvider>,
pub executor_owner: alloy::primitives::Address,
pub executor_address: alloy::primitives::Address,
pub weth_address: alloy::primitives::Address,
pub pool_manager_address: alloy::primitives::Address,
pub multicall3_address: alloy::primitives::Address,
pub injected_address: Option<alloy::primitives::Address>,
pub runtime_bytecode: alloy::primitives::Bytes,
pub warmup: degenbot_executor::WarmupSlots,
pub bot_state: Option<Arc<StateLock<BotState>>>,
pub warm_cache: Option<Arc<ParkingRwLock<degenbot_simulation::WarmCodeCacheInner>>>,
pub dispatcher: Arc<Mutex<Dispatcher>>,
pub signer: Arc<TxSigner>,
pub probe: Arc<dyn ReceiptProbe + Send + Sync>,
pub nonce_lane: Arc<NonceLane>,
pub extra_broadcast: Vec<Arc<AlloyProvider>>,
pub target: SubmissionTarget,
}
#[cfg(test)]
#[expect(
clippy::expect_used,
reason = "tests drive known-valid fixtures and assert on the outcomes"
)]
mod tests {
use super::*;
use degenbot_submission::SubmissionResult;
use std::future::Future;
use std::pin::Pin;
struct EmptyResolver;
impl crate::assembly::PathResolver for EmptyResolver {
fn resolve(&self, _path_id: u64) -> Option<degenbot_executor::composers::PathInfo> {
None
}
}
struct NoopProbe;
impl ReceiptProbe for NoopProbe {
fn receipt_found(
&self,
_tx_hash: alloy::primitives::B256,
) -> Pin<Box<dyn Future<Output = SubmissionResult<bool>> + Send + '_>> {
Box::pin(async { Ok(false) })
}
}
#[test]
fn the_declared_defaults_convert_verbatim() {
let verdict = degenbot_config::BotConfig::default();
let policy = ExecutorPolicy::from(&verdict);
assert_eq!(
policy.sim_concurrency, verdict.simulation.pipeline_concurrency,
"the cap is the verdict's declared default, not a second default"
);
assert_eq!(
policy.min_profit_margin_bps,
verdict.dispatch.min_profit_margin_bps
);
assert!(!policy.erc6909_profit);
assert!(!policy.inject_code);
assert!(!policy.inject_code_guard);
}
#[test]
fn a_zero_cap_floors_at_one_in_the_conversion() {
let mut verdict = degenbot_config::BotConfig::default();
verdict.simulation.pipeline_concurrency = 0;
assert_eq!(ExecutorPolicy::from(&verdict).sim_concurrency, 1);
}
#[test]
fn every_verdict_knob_lands_on_the_named_executor_value() {
let mut verdict = degenbot_config::BotConfig::default();
verdict.simulation.pipeline_concurrency = 5;
verdict.dispatch.min_profit_margin_bps = 30;
verdict.dispatch.erc6909_profit = true;
verdict.simulation.inject_executor_code = true;
let policy = ExecutorPolicy::from(&verdict);
assert_eq!(policy.sim_concurrency, 5);
assert_eq!(policy.min_profit_margin_bps, 30);
assert!(policy.erc6909_profit);
assert!(policy.inject_code, "the sim stance follows the verdict");
assert!(
policy.inject_code_guard,
"the submit guard follows the SAME verdict key"
);
}
#[tokio::test]
async fn from_verdict_composes_policy_and_runtime() {
let verdict = degenbot_config::BotConfig::default();
let provider = Arc::new(
AlloyProvider::new("http://127.0.0.1:1", 0)
.await
.expect("a lazy provider constructs offline"),
);
let executor_address = alloy::primitives::Address::ZERO;
let runtime = ExecutorRuntime {
max_candidates: 0,
use_v4_batch: false,
dry_run: false,
resolver: Arc::new(EmptyResolver),
suppression: Arc::new(Mutex::new(PathSuppression::new())),
divergence: Arc::new(Mutex::new(PoolDivergence::new())),
fot: Arc::new(Mutex::new(FeeOnTransferRegistry::new())),
provider,
executor_owner: alloy::primitives::Address::ZERO,
executor_address,
weth_address: alloy::primitives::Address::ZERO,
pool_manager_address: alloy::primitives::Address::ZERO,
multicall3_address: alloy::primitives::Address::ZERO,
injected_address: None,
runtime_bytecode: alloy::primitives::Bytes::new(),
warmup: degenbot_executor::compute_simulation_warmup_slots(
executor_address,
alloy::primitives::Address::ZERO,
),
bot_state: None,
warm_cache: None,
dispatcher: Arc::new(Mutex::new(Dispatcher::for_block(0))),
signer: Arc::new(
TxSigner::from_key_hex(
"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
1,
)
.expect("the anvil key is valid"),
),
probe: Arc::new(NoopProbe),
nonce_lane: Arc::new(NonceLane::new(
Arc::new(degenbot_substrate::nonce::NonceAuthority::new(0)),
Arc::new(degenbot_submission::SubmissionLedger::new()),
"settlement",
)),
extra_broadcast: Vec::new(),
target: SubmissionTarget::Public,
};
let config = crate::executor::ExecutorConfig::from_verdict(&verdict, runtime);
assert_eq!(config.sim_concurrency, 8);
assert_eq!(config.max_candidates, usize::MAX, "0 = uncapped");
assert_eq!(config.min_profit_margin_bps, 0);
assert!(!config.dry_run);
}
}