#![expect(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::print_stdout
)]
use std::path::PathBuf;
use std::sync::Arc;
use alloy::network::Ethereum;
use alloy::primitives::{address, aliases::U112, Address, Bytes, I256, U128, U256};
use alloy::providers::{Provider, ProviderBuilder};
use alloy::rpc::client::ClientBuilder;
use alloy::transports::mock::{Asserter, MockTransport};
use degenbot::arbitrage::{
simulate_in_process_with_db, FailBuckets, SimulateContext, SimulatePath, SolveStep,
};
use degenbot::cmd_executor::composers::{EncodeOptions, HopInfo, PathInfo, V2HopInfo};
use degenbot::cmd_executor::compute_simulation_warmup_slots;
use degenbot::db::snapshot_db::SnapshotDb;
use degenbot::dex_identity::UNISWAP_V2;
use degenbot::math::balancer::{mul_down, ONE};
use degenbot::math::curve::{
calculate_dy, stableswap_get_d, DVariant, DyCalculationInputs, YVariant,
};
use degenbot::math::solidly::{calc_d as solidly_calc_d, calc_f as solidly_calc_f};
use degenbot::simulation::apply_simulation_overrides;
use degenbot::substrate::swap_simulation::{SwapRead, SwapRequest};
use degenbot::{bot_core::Bot, BotState, RegisterV2PoolParams};
use revm::bytecode::Bytecode;
use revm::database::CacheDB;
use revm::database_interface::EmptyDB;
use revm::state::AccountInfo;
use degenbot::bot_core::construction_io::{ConstructionIo, NoDb, RpcConstruction};
use degenbot::bot_core::pool_builder::builder::{
build_aerodrome_v2, build_balancer_stable, build_balancer_weighted, build_curve_pool,
build_erc20_metadata, build_v2, build_v3, build_v4, probe_pool_type, resolve_v4_identity,
PoolBuilderError, V4PoolBuildIdentity, V4PoolBuildOverrides,
};
use degenbot::errors::ProviderError;
use degenbot::rpc::provider::EthBlock;
struct FailingConstruction;
#[async_trait::async_trait]
impl RpcConstruction for FailingConstruction {
async fn get_block_number(&self) -> Result<u64, ProviderError> {
Err(ProviderError::RpcError {
code: -32000,
message: "no rpc".into(),
})
}
async fn get_block(&self, _block_number: u64) -> Result<Option<EthBlock>, ProviderError> {
Err(ProviderError::RpcError {
code: -32000,
message: "no rpc".into(),
})
}
async fn get_block_timestamp(&self, _block_number: u64) -> Result<Option<u64>, ProviderError> {
Err(ProviderError::RpcError {
code: -32000,
message: "no rpc".into(),
})
}
async fn get_code(
&self,
_address: Address,
_block: Option<u64>,
) -> Result<Bytes, ProviderError> {
Err(ProviderError::RpcError {
code: -32000,
message: "no rpc".into(),
})
}
async fn get_balance(
&self,
_address: Address,
_block: Option<u64>,
) -> Result<U256, ProviderError> {
Err(ProviderError::RpcError {
code: -32000,
message: "no rpc".into(),
})
}
async fn call(
&self,
_to: Address,
_data: Bytes,
_block: Option<u64>,
) -> Result<Bytes, ProviderError> {
Err(ProviderError::RpcError {
code: -32000,
message: "no rpc".into(),
})
}
}
fn fixture_db_path() -> PathBuf {
if let Ok(p) = std::env::var("DEGENBOT_FIXTURE_DB") {
return PathBuf::from(p);
}
std::env::set_var(degenbot::db::AUTO_HEAL_ENV, "0");
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../foundation")
.join("degenbot-db")
.join("tests")
.join("fixtures")
.join("parity.db")
}
fn fixture_snapshot_seed_block() -> Option<u64> {
let db_path = fixture_db_path();
let snap = SnapshotDb::open(&db_path)
.unwrap_or_else(|e| panic!("open fixture DB at {}: {e}", db_path.display()));
let snapshot_bot = Bot::new(8453);
snapshot_bot
.load_snapshot_from_db(&snap.0, 8453)
.expect("load_snapshot_from_db on the fixture DB returns Ok");
let seed_block = snapshot_bot
.state_arc()
.read_at(degenbot_substrate::state_lock::LockSite::Core)
.snapshot_seed_block();
assert_eq!(
seed_block,
Some(12_340_000),
"fixture DB at chain 8453 has V3+V4 exchanges → S = MIN(12345000, 12340000) = 12_340_000"
);
drop(snapshot_bot);
drop(snap);
seed_block
}
#[expect(clippy::too_many_lines)]
fn main() {
use degenbot::bot::arb_engine::{EngineStages, PumpPhase};
let mut bot = BotState::new();
let token0 = address!("000000000000000000000000000000000000000A");
let token1 = address!("000000000000000000000000000000000000000B");
let pool = address!("000000000000000000000000000000000000000C");
let reserve0 = U256::from(1_000_000_000_000_u64); let reserve1 = U256::from(500_000_000_000_000_000_u64);
let params = RegisterV2PoolParams {
address: pool,
token0,
token1,
reserve0: reserve0.to::<U112>(),
reserve1: reserve1.to::<U112>(),
fee_token0: UNISWAP_V2.fee_token0,
fee_token1: UNISWAP_V2.fee_token1,
factory: UNISWAP_V2.factory,
update_block: 19_000_000,
variant: UNISWAP_V2.variant,
stable_swap: false,
fee_denominator: None,
..Default::default()
};
let pool_id = bot
.register_v2_pool(¶ms)
.expect("standalone: register V2");
assert_eq!(pool_id, 1, "first registered pool gets id 1");
let lifecycle_core = Arc::new(degenbot::substrate::state_lock::StateLock::new(
BotState::new(),
));
let stages = EngineStages::with_core(
lifecycle_core,
Arc::new(degenbot::substrate::EpochDelta::new(0u64)),
);
assert_eq!(stages.current_phase(), PumpPhase::Created);
assert!(stages.current_phase().allow_subscribe("subscribe").is_ok());
stages.set_phase(PumpPhase::Subscribed);
assert_eq!(stages.current_phase(), PumpPhase::Subscribed);
let amount_in = U256::from(1_000_000_000_u64); let amount_out = match bot.swap_simulation(
0,
pool_id,
SwapRequest {
zero_for_one: true,
amount_specified: -I256::try_from(amount_in).expect("amount fits I256"),
sqrt_price_limit: None,
},
) {
SwapRead::Computed(outcome) => outcome.delivered_unsigned(),
f => panic!(
"small non-overflowing V2 amount; standalone calc must not miss or overflow: {f:?}"
),
};
assert!(
amount_out > U256::ZERO,
"expected a non-zero swap output, got {amount_out}"
);
let bigger_in = amount_in * U256::from(2_u64);
let bigger_out = match bot.swap_simulation(
0,
pool_id,
SwapRequest {
zero_for_one: true,
amount_specified: -I256::try_from(bigger_in).expect("amount fits I256"),
sqrt_price_limit: None,
},
) {
SwapRead::Computed(outcome) => outcome.delivered_unsigned(),
f => panic!(
"small non-overflowing V2 amount; standalone calc must not miss or overflow: {f:?}"
),
};
assert!(
bigger_out > amount_out,
"constant-product calc must be monotonic: {bigger_out} !> {amount_out}"
);
let amount_in_with_fee = amount_in * U256::from(997_u64);
let numer = amount_in_with_fee * reserve1;
let denom = reserve0 * U256::from(1000_u64) + amount_in_with_fee;
let expected = numer / denom;
assert_eq!(
amount_out, expected,
"Rust core calc must match the closed-form Uniswap V2 getAmountOut"
);
println!("standalone degenbot consumer OK: pool_id={pool_id} amount_out={amount_out}");
let xp = [
U256::from(1_000_000_000_000_000_000_u64),
U256::from(1_000_000_000_000_000_000_u64),
];
let n_coins = U256::from(2u64);
let a_precision = U256::from(100u64);
let amp = U256::from(2000u64); let d = stableswap_get_d(&xp, amp, n_coins, a_precision, DVariant::Standard)
.expect("stableswap_get_d converged");
let sum = xp[0] + xp[1];
assert!(
d.abs_diff(sum) <= U256::from(1u64),
"balanced StableSwap D must ≈ sum(xp): {d} vs {sum}"
);
let val = U256::from(42_000_000_000_000_000_000_u128); assert_eq!(mul_down(val, ONE).unwrap(), val, "mul_down(x, ONE) == x");
let x0 = U256::from(2_000_000_000_000_000_000_u64); let y = U256::from(3_000_000_000_000_000_000_u64); let got_d = solidly_calc_d(x0, y);
let yy = y * y / ONE;
let term1 = U256::from(3u64) * x0 * yy / ONE;
let x0x0 = x0 * x0 / ONE;
let term2 = x0x0 * x0 / ONE;
let expected_d = term1 + term2;
assert_eq!(got_d, expected_d, "solidly calc_d direct port");
let got_f = solidly_calc_f(x0, y);
let a = x0 * y / ONE;
let b = x0 * x0 / ONE + y * y / ONE;
let expected_f = a * b / ONE;
assert_eq!(got_f, expected_f, "solidly calc_f direct port");
let dx_inputs = DyCalculationInputs {
precision: U256::from(1_000_000_000_000_000_000_u64),
fee_denominator: U256::from(10_000_000_000_u64),
fee: U256::from(500_000_u64),
n_coins: 2,
balances: vec![
U256::from(3_000_000_000_000_000_000_000_u128),
U256::from(6_000_000_000_000_000_000_000_u128),
],
rate_multipliers: vec![
U256::from(1_000_000_000_000_000_000_u64),
U256::from(1_000_000_000_000_000_000_u64),
],
precision_multipliers: vec![U256::from(1_u8), U256::from(1_u8)],
offpeg_fee_multiplier: U256::ZERO,
fee_gamma: U256::ZERO,
mid_fee: U256::ZERO,
out_fee: U256::ZERO,
address: Address::ZERO,
resolved_rates: vec![
U256::from(1_000_000_000_000_000_000_u64),
U256::from(1_000_000_000_000_000_000_u64),
],
xp: vec![
U256::from(3_000_000_000_000_000_000_000_u128),
U256::from(6_000_000_000_000_000_000_000_u128),
],
block_number: 0,
block_timestamp: 0,
amp: U256::from(10_000_u64),
d_variant: DVariant::Standard,
y_variant: YVariant::Standard,
a_precision: U256::from(100_u64),
swap_style: 1,
metapool: false,
metapool_rate_style: 1,
metapool_underlying_style: 1,
d: None,
gamma: None,
price_scale: None,
live_balances: None,
admin_balances: None,
effective_balances: None,
virtual_price: None,
scaled_redemption_price: None,
};
let dy = calculate_dy(0, 1, U256::from(1_000_000_000_000_000_000_u64), &dx_inputs)
.expect("calculate_dy converged");
assert_eq!(
dy,
U256::from(1_008_296_947_143_911_861_u64),
"curve get_dy direct port"
);
println!("standalone degenbot consumer OK: curve D={d} dy={dy} balancer fp.mul_down(identity) solidly calc_d={got_d}");
let seed_block = fixture_snapshot_seed_block();
let rpc_url = std::env::var("SMOKE_RPC_URL").ok();
if let Some(rpc) = rpc_url.as_deref() {
println!("standalone degenbot consumer OK: fixture DB at chain 8453 loaded with S={seed_block:?}; SMOKE_RPC_URL={rpc} set — wire `BlockPump::subscribe({rpc}, bot_arc, sink, reorg, shutdown)` then `pump.resume_from_subscribe(state)` and the pump auto-backfills S+1..W-1 internally (state-only `eth_getLogs` via `BotState::process_backfill_logs`) before entering the live loop with current_block = W.");
} else {
println!("standalone degenbot consumer OK: fixture DB at chain 8453 loaded with S={seed_block:?} (set SMOKE_RPC_URL='ws://...' to drive `BlockPump::subscribe()`+`resume_from_subscribe()` — gated so the example stays CI-runnable without a node; the auto-backfill path is covered by `block_pump::tests::resume_anchors_to_subscribe_block`).");
}
in_process_sim_standalone_slice();
registration_lifecycle_standalone_slice();
session_registry_get_or_create_standalone_slice();
operator_config_standalone_slice();
}
fn operator_config_standalone_slice() {
use degenbot::config::{
load_process_config, node_http_env_name, node_ipc_env_name, node_ws_env_name,
resolve_database_path, resolve_node_request_uri, NodeOverrides, Source, DB_PATH_ENV,
};
let chain_id = 8453_u64;
let db_file = "/tmp/degenbot-standalone/operator.db";
let http_file = "https://file.example/rpc";
let operator = std::env::temp_dir().join(format!(
"degenbot-standalone-operator-{}.toml",
std::process::id()
));
std::fs::write(
&operator,
format!(
"[database]\npath = \"{db_file}\"\n\n[nodes]\nhttp = {{ {chain_id} = \"{http_file}\" }}\n"
),
)
.expect("standalone: temp operator file writes");
std::env::remove_var(DB_PATH_ENV);
for env_name in [
node_ipc_env_name(chain_id),
node_ws_env_name(chain_id),
node_http_env_name(chain_id),
] {
std::env::remove_var(env_name);
}
std::env::set_var("DEGENBOT_CONFIG", &operator);
let file_cfg = load_process_config().expect("standalone: operator file must load");
let node = resolve_node_request_uri(&file_cfg, chain_id, &NodeOverrides::new())
.expect("standalone: operator file declares the chain's http endpoint");
assert_eq!(
node.value, http_file,
"standalone: node join resolves from the operator file"
);
assert_eq!(
node.source,
Source::File,
"standalone: the FILE layer supplied the node endpoint"
);
let db = resolve_database_path(&file_cfg, None);
assert_eq!(
db.value,
PathBuf::from(db_file),
"standalone: database path resolves from the operator file"
);
assert_eq!(
db.source,
Source::File,
"standalone: the FILE layer supplied the database path"
);
let db_env = "/tmp/degenbot-standalone/env.db";
let http_env = "https://env.example/rpc";
std::env::set_var(DB_PATH_ENV, db_env);
std::env::set_var(node_http_env_name(chain_id), http_env);
let env_cfg =
load_process_config().expect("standalone: operator file plus env layers must load");
let node = resolve_node_request_uri(&env_cfg, chain_id, &NodeOverrides::new())
.expect("standalone: env layer supplies the chain's http endpoint");
assert_eq!(
node.value, http_env,
"standalone: env overrides the file node endpoint"
);
assert_eq!(
node.source,
Source::Env,
"standalone: the ENV layer won the node endpoint"
);
let db = resolve_database_path(&env_cfg, None);
assert_eq!(
db.value,
PathBuf::from(db_env),
"standalone: env overrides the file database path"
);
assert_eq!(
db.source,
Source::Env,
"standalone: the ENV layer won the database path"
);
std::env::remove_var(DB_PATH_ENV);
std::env::remove_var(node_http_env_name(chain_id));
std::env::remove_var("DEGENBOT_CONFIG");
let _ = std::fs::remove_file(&operator);
println!(
"standalone degenbot consumer OK: operator file — node {http_file} + database {db_file} resolved from the FILE layer, env overrides win from the ENV layer"
);
}
fn session_registry_get_or_create_standalone_slice() {
use degenbot::substrate::session_registry::{PoolIdentity, SessionObjectRegistry};
let registry = SessionObjectRegistry::new(1);
assert_eq!(registry.pool_count(), 0, "a fresh session holds no pools");
let shared_address = address!("000000000000000000000000000000000000000C");
let first = registry.get_or_create_pool(PoolIdentity::v2(shared_address));
assert_eq!(
first.chain_id(),
1,
"the object names its own session scope"
);
assert_eq!(first.family_tag(), "v2", "the object names its own family");
assert_eq!(
registry.pool_count(),
1,
"get-or-create registered one pool"
);
let second = registry.get_or_create_pool(PoolIdentity::v2(shared_address));
assert!(
Arc::ptr_eq(&first, &second),
"get-or-create must return the one canonical object for one identity"
);
assert_eq!(
registry.pool_count(),
1,
"a repeat get-or-create registers nothing"
);
let other_family = registry.get_or_create_pool(PoolIdentity::v3(shared_address));
assert_eq!(
registry.pool_count(),
2,
"a distinct family is a distinct object"
);
assert_ne!(
first.family_tag(),
other_family.family_tag(),
"family is part of the canonical identity"
);
let by_address = registry
.resolve_pool_by_address(&shared_address)
.expect("the address view resolves a claimed address");
assert!(
Arc::ptr_eq(&first, &by_address),
"the address view names the object that first claimed the address"
);
let unclaimed = address!("00000000000000000000000000000000000000EE");
assert!(
registry.resolve_pool_by_address(&unclaimed).is_err(),
"an unclaimed address refuses instead of registering"
);
assert_eq!(
registry.pool_count(),
2,
"the refused read registered nothing"
);
println!("standalone degenbot consumer OK: session registry get-or-create returns one canonical object");
}
fn registration_lifecycle_standalone_slice() {
use hashbrown::HashMap;
use degenbot::bot_core::{PoolTickCoverage, RegistrationLifecycle, TickInfo};
use degenbot::substrate::state_lock::StateLock;
use degenbot::{run_cl_v3_lifecycle, RegisterV3PoolParams};
let core = StateLock::new(BotState::new());
let addr = address!("000000000000000000000000000000000000000D");
let mut tick_data = HashMap::new();
tick_data.insert(
60,
TickInfo {
liquidity_gross: U128::from(100),
liquidity_net: 100i128,
block: 0,
},
);
let pid = core
.write_at(degenbot_substrate::state_lock::LockSite::Core)
.register_v3_pool(&RegisterV3PoolParams {
address: addr,
token0: address!("000000000000000000000000000000000000000A"),
token1: address!("000000000000000000000000000000000000000B"),
fee: 3000,
tick_spacing: 60,
sqrt_price_x96: U256::from(1u128) << 96,
liquidity: 1_000_000,
tick: 0,
tick_data,
coverage: PoolTickCoverage::Tracked,
..Default::default()
})
.expect("standalone: Tracked V3 registration");
assert_eq!(
core.read_at(degenbot_substrate::state_lock::LockSite::Core)
.get_v3_pool(pid)
.unwrap()
.registration_lifecycle,
RegistrationLifecycle::Quarantined,
"Tracked registers Quarantined"
);
degenbot::runtime::get_runtime()
.block_on(run_cl_v3_lifecycle::<_, _, _, _, ()>(
&core,
addr,
Some(42),
|_seed, _block| async move { Ok(()) },
|_td, _block| async move { Ok(()) },
))
.expect("standalone: lifecycle must pass");
assert_eq!(
core.read_at(degenbot_substrate::state_lock::LockSite::Core)
.get_v3_pool(pid)
.unwrap()
.registration_lifecycle,
RegistrationLifecycle::Live,
"Tracked lands Live only after verification"
);
println!(
"standalone degenbot consumer OK: registration lifecycle — tracked V3 Quarantined → verified → Live"
);
}
#[expect(clippy::too_many_lines)]
fn in_process_sim_standalone_slice() {
const OWNER: Address = address!("9c56a29c7231974c269e24f9fb3c29203039089e");
const EXECUTOR: Address = address!("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
const WETH: Address = address!("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2");
const PM: Address = address!("000000000004444c5dc75cb358380d2e3de08a90");
const MULTICALL3: Address = address!("c411372f0b8ae58585e33b78aea9e0596da9a6f1");
const TOKEN1: Address = address!("1111111111111111111111111111111111111111");
const POOL_B: Address = address!("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
const VAULT: Address = address!("ba12222222228d8ba445958a75a0704d566bf2c8");
const POOL_C: Address = address!("cccccccccccccccccccccccccccccccccccccccc");
const GIFT: Address = address!("dddddddddddddddddddddddddddddddddddddddd");
const ONE_ETH: U256 = U256::from_limbs([1_000_000_000_000_000_000u64, 0, 0, 0]);
const MULTICALL3_BYTECODE: [u8; 12] = [
0x60, 0x04, 0x35, 0x31, 0x60, 0x00, 0x52, 0x60, 0x20, 0x60, 0x00, 0xF3,
];
const GIFT_BYTECODE: [u8; 2] = [0x33, 0xFF];
let mut executor_bc = vec![
0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x73,
];
executor_bc.extend_from_slice(GIFT.as_slice());
executor_bc.extend_from_slice(&[0x5A, 0xF1, 0x50, 0x00]);
let asserter = Asserter::new();
let client = ClientBuilder::default().transport(MockTransport::new(asserter), true);
let dyn_provider = ProviderBuilder::new().connect_client(client).erased();
let provider = degenbot::rpc::provider::AlloyProvider::from_provider(
Arc::new(dyn_provider) as Arc<dyn Provider<Ethereum>>
);
let warmup = compute_simulation_warmup_slots(EXECUTOR, WETH);
let ctx = SimulateContext {
provider: &provider,
executor_owner: OWNER,
executor_address: EXECUTOR,
weth_address: WETH,
pool_manager_address: PM,
multicall3_address: MULTICALL3,
inject_code: true,
injected_address: Some(EXECUTOR),
runtime_bytecode: Bytes::from(executor_bc),
warmup,
base_fee_next: 1_000_000_000u128,
current_block: 100,
block_timestamp: 0,
block_priority_fees: None,
};
let mut cache_db: CacheDB<EmptyDB> = CacheDB::new(EmptyDB::default());
apply_simulation_overrides(&mut cache_db, &ctx.override_params())
.expect("standalone: overrides apply over EmptyDB");
cache_db.insert_account_info(
MULTICALL3,
AccountInfo {
balance: U256::ZERO,
nonce: 1,
code: Some(Bytecode::new_raw(Bytes::from(MULTICALL3_BYTECODE.to_vec()))),
..Default::default()
},
);
cache_db.insert_account_info(
GIFT,
AccountInfo {
balance: ONE_ETH,
nonce: 1,
code: Some(Bytecode::new_raw(Bytes::from(GIFT_BYTECODE.to_vec()))),
..Default::default()
},
);
let path = SimulatePath {
path_id: 42,
optimal_input: 1_000_000_000_000_000_000u128,
steps: Box::new([
SolveStep {
output: 1_100_000_000_000_000_000u128,
consumed_input: 1_100_000_000_000_000_000u128,
state_nonce: 0,
},
SolveStep {
output: 1_210_000_000_000_000_000u128,
consumed_input: 1_210_000_000_000_000_000u128,
state_nonce: 0,
},
]),
path_info: PathInfo::new(vec![
HopInfo::V2(V2HopInfo {
pool_address: POOL_B,
token0_address: WETH,
token1_address: TOKEN1,
fee: 30,
zfo: true,
}),
HopInfo::V2(V2HopInfo {
pool_address: POOL_C,
token0_address: TOKEN1,
token1_address: WETH,
fee: 30,
zfo: true,
}),
]),
solve_block: 100,
opts: EncodeOptions {
erc6909_profit: false,
use_v4_batch: false,
..Default::default()
},
};
let mut buckets = FailBuckets::new();
let result = simulate_in_process_with_db(&ctx, cache_db, &path, &mut buckets)
.expect("standalone: in-process sim over CacheDB<EmptyDB> cannot RPC-fail");
let sim = result.expect(
"standalone: the SELFDESTRUCT-gift fixture must produce a non-None SimResult (gross_profit = 1 ETH)",
);
assert_eq!(
sim.gross_profit, ONE_ETH,
"standalone: gross_profit must be 1 ETH (closed form)"
);
assert!(sim.gas_used > 0, "standalone: gas_used must be non-zero");
assert!(
sim.net_profit > U256::ZERO,
"standalone: net_profit must be positive"
);
println!(
"standalone degenbot consumer OK: in-process revm sim — gross_profit=1ETH gas_used={} priority_fee={} net_profit={} wei",
sim.gas_used, sim.priority_fee, sim.net_profit
);
let io = Arc::new(ConstructionIo::new(
Arc::new(NoDb),
Arc::new(FailingConstruction),
));
let probe = degenbot::runtime::get_runtime().block_on(probe_pool_type(&io, POOL_B, None));
assert!(
matches!(
probe,
Err(PoolBuilderError::Rpc(ProviderError::RpcError { .. }))
),
"a dead RPC must surface, never classify the pool as Curve: {probe:?}"
);
let err = degenbot::runtime::get_runtime().block_on(build_v2(1, POOL_B, &io, None));
assert!(
matches!(err, Err(PoolBuilderError::Rpc(_))),
"build_v2 over a failing RPC must yield a typed Rpc error, got {err:?}"
);
let err = degenbot::runtime::get_runtime().block_on(build_erc20_metadata(&io, 1, POOL_B, None));
assert!(
matches!(err, Err(PoolBuilderError::Rpc(_))),
"build_erc20_metadata over a failing RPC must yield a typed Rpc error, got {err:?}"
);
let err = degenbot::runtime::get_runtime().block_on(build_v3(1, POOL_B, None, &io, None));
assert!(
matches!(err, Err(PoolBuilderError::Rpc(_))),
"build_v3 over a failing RPC must yield a typed Rpc error, got {err:?}"
);
let v4_id = V4PoolBuildIdentity {
pool_manager: POOL_B,
state_view: POOL_B,
pool_id: [0xAA; 32],
currency0: TOKEN1,
currency1: POOL_C,
fee: 0x10_0000,
tick_spacing: 1,
hook_address: Address::ZERO,
};
let err = degenbot::runtime::get_runtime().block_on(build_v4(v4_id, None, &io, None));
assert!(
matches!(err, Err(PoolBuilderError::Rpc(_))),
"build_v4 over a failing RPC must yield a typed Rpc error, got {err:?}"
);
let overrides = V4PoolBuildOverrides::default();
let err = degenbot::runtime::get_runtime()
.block_on(resolve_v4_identity(1, POOL_B, [0xBB; 32], &overrides, &io));
assert!(
matches!(err, Err(PoolBuilderError::MissingIdentity { .. })),
"resolve_v4_identity with empty overrides must yield MissingIdentity, got {err:?}"
);
let err = degenbot::runtime::get_runtime().block_on(build_aerodrome_v2(1, POOL_B, &io, None));
assert!(
matches!(err, Err(PoolBuilderError::Rpc(_))),
"build_aerodrome_v2 over a failing RPC must yield a typed Rpc error, got {err:?}"
);
let err = degenbot::runtime::get_runtime()
.block_on(build_balancer_weighted(VAULT, POOL_B, &io, None));
assert!(
matches!(err, Err(PoolBuilderError::Rpc(_))),
"build_balancer_weighted over a failing RPC must yield a typed Rpc error, got {err:?}"
);
let err = degenbot::runtime::get_runtime()
.block_on(build_balancer_stable(VAULT, POOL_B, &io, None, None));
assert!(
matches!(err, Err(PoolBuilderError::Rpc(_))),
"build_balancer_stable over a failing RPC must yield a typed Rpc error, got {err:?}"
);
let err = degenbot::runtime::get_runtime().block_on(build_curve_pool(POOL_B, &[], &io, None));
assert!(
matches!(err, Err(PoolBuilderError::Rpc(_))),
"build_curve_pool over a failing RPC must yield a typed Rpc error, got {err:?}"
);
println!(
"standalone degenbot consumer OK: PoolBuilder probe+dispatch reachable (probe={probe:?})"
);
assert_eq!(
degenbot::pools::PANCAKE_V3_LIQUIDITY_SLOT,
5,
"fork liquidity@5 (Uniswap@4)"
);
assert_eq!(
degenbot::pools::PANCAKE_V3_TICKS_MAPPING_SLOT,
6,
"fork ticks base@6 (Uniswap@5)"
);
assert_eq!(
degenbot::pools::PANCAKE_V3_TICK_BITMAP_MAPPING_SLOT,
7,
"fork tickBitmap base@7 (Uniswap@6)"
);
let fork_tick_slot = degenbot::pools::pancake_v3_tick_mapping_slot(0);
let uniswap_tick_slot = degenbot::pools::v3_tick_mapping_slot(0);
assert_ne!(
fork_tick_slot, uniswap_tick_slot,
"fork + Uniswap tick mapping slots must differ for the same tick"
);
assert_eq!(
degenbot::pools::encode_pancake_v3_slot0_word1(0, true),
U256::from(1u64) << 32
);
println!("standalone degenbot consumer OK: PancakeSwap V3 fork slot encoders reachable");
}