#![expect(clippy::unwrap_used, clippy::expect_used)]
use hashbrown::HashMap;
use alloy::primitives::{aliases::U112, Address, B256, U256};
use crate::bot_core::BotState;
use crate::decoders::v4_swap_decoder::V4PoolId;
use crate::pools::v3_state::{PoolTickCoverage, RegisterV3PoolParams, V3PoolState};
use crate::pools::v4_state::{RegisterV4PoolParams, V4PoolKey, V4PoolState};
use crate::pools::TickInfo;
use crate::{DexVariant, RegisterV2PoolParams};
use super::fixture::{PoolData, TickJson};
pub fn tick_map(data: &HashMap<String, TickJson>) -> HashMap<i32, TickInfo> {
data.iter()
.map(|(t, v)| {
(
t.parse::<i32>().unwrap(),
TickInfo {
liquidity_gross: alloy::primitives::U128::from(
v.liquidity_gross.parse::<u128>().unwrap(),
),
liquidity_net: v.liquidity_net.parse::<i128>().unwrap(),
block: 0,
},
)
})
.collect()
}
pub fn v4_pool_id_bytes(hex: &str) -> V4PoolId {
hex.trim_start_matches("0x")
.as_bytes()
.chunks(2)
.map(|c| u8::from_str_radix(std::str::from_utf8(c).unwrap(), 16).unwrap())
.collect::<Vec<u8>>()
.try_into()
.unwrap()
}
pub const V2_DEFAULT_FEE: (u64, u64) = (997, 1000);
pub fn register_v2(core: &mut BotState, p: &PoolData) -> Result<u64, String> {
let fee = match (p.fee_gamma, p.fee_denom) {
(Some(g), Some(d)) => (g, d),
_ => V2_DEFAULT_FEE,
};
core.register_v2_pool(&RegisterV2PoolParams {
address: p.address.expect("v2 pool address"),
token0: p.token0.expect("v2 token0"),
token1: p.token1.expect("v2 token1"),
reserve0: U112::try_from(u128::try_from(p.reserve0.expect("v2 reserve0").0).unwrap())
.unwrap(),
reserve1: U112::try_from(u128::try_from(p.reserve1.expect("v2 reserve1").0).unwrap())
.unwrap(),
fee_token0: fee,
fee_token1: fee,
factory: Address::ZERO,
deployer: Address::ZERO,
init_hash: B256::ZERO,
update_block: p.liquidity_update_block.unwrap_or(0),
variant: DexVariant::UniswapV2,
stable_swap: false,
fee_denominator: None,
})
.map_err(|e| format!("register_v2: {e:?}"))
}
pub fn register_v3(core: &mut BotState, p: &PoolData) -> Result<u64, String> {
register_v3_with(core, p, None, None)
}
pub fn register_v3_with(
core: &mut BotState,
p: &PoolData,
sqrt_override: Option<U256>,
tick_override: Option<i32>,
) -> Result<u64, String> {
let sqrt_price_x96 = sqrt_override.unwrap_or(p.sqrt_price_x96.expect("v3 sqrt_price_x96").0);
let tick = tick_override.unwrap_or(p.tick.expect("v3 tick"));
core.register_v3_pool(&RegisterV3PoolParams {
address: p.address.expect("v3 pool address"),
token0: p.token0.expect("v3 token0"),
token1: p.token1.expect("v3 token1"),
fee: p.fee_token0.expect("v3 fee"),
tick_spacing: p.tick_spacing.expect("v3 tick_spacing"),
factory: Address::ZERO,
sqrt_price_x96,
liquidity: u128::try_from(p.liquidity.expect("v3 liquidity").0).unwrap(),
tick,
tick_data: tick_map(&p.tick_data),
update_block: p.liquidity_update_block.expect("v3 update_block"),
tick_data_block: None,
coverage: PoolTickCoverage::Tracked,
deployer: Address::ZERO,
init_hash: B256::ZERO,
..Default::default()
})
.map_err(|e| format!("register_v3: {e:?}"))
}
pub fn register_v4(core: &mut BotState, p: &PoolData) -> Result<u64, String> {
register_v4_with(core, p, None)
}
pub fn register_v4_with(
core: &mut BotState,
p: &PoolData,
protocol_fee_override: Option<u32>,
) -> Result<u64, String> {
core.register_v4_pool(&RegisterV4PoolParams {
pool_manager: p.pool_manager.expect("v4 pool_manager"),
pool_id: v4_pool_id_bytes(p.pool_id.as_ref().expect("v4 pool_id")),
pool_key: V4PoolKey {
currency0: p.currency0.expect("v4 currency0"),
currency1: p.currency1.expect("v4 currency1"),
fee: p.fee_currency0.expect("v4 fee"),
tick_spacing: p.tick_spacing.expect("v4 tick_spacing"),
hooks: p.hooks.unwrap_or_default(),
},
hook_flags: crate::bot_core::pool_builder::builder::derive_hook_flags(
p.hooks.unwrap_or_default(),
),
protocol_fee: protocol_fee_override.unwrap_or(p.protocol_fee.unwrap_or(0)),
sqrt_price_x96: p.sqrt_price_x96.expect("v4 sqrt_price_x96").0,
liquidity: u128::try_from(p.liquidity.expect("v4 liquidity").0).unwrap(),
tick: p.tick.expect("v4 tick"),
tick_data: tick_map(&p.tick_data),
update_block: p.liquidity_update_block.expect("v4 update_block"),
tick_data_block: None,
coverage: PoolTickCoverage::Tracked,
fetcher: None,
})
.map_err(|e| format!("register_v4: {e:?}"))
}
pub fn build_v3_state(p: &PoolData) -> V3PoolState {
let (_identity, state) = V3PoolState::from_params(
RegisterV3PoolParams {
address: p.address.expect("v3 pool address"),
token0: p.token0.expect("v3 token0"),
token1: p.token1.expect("v3 token1"),
fee: p.fee_token0.expect("v3 fee"),
tick_spacing: p.tick_spacing.expect("v3 tick_spacing"),
factory: Address::ZERO,
sqrt_price_x96: p.sqrt_price_x96.expect("v3 sqrt_price_x96").0,
liquidity: u128::try_from(p.liquidity.expect("v3 liquidity").0).unwrap(),
tick: p.tick.expect("v3 tick"),
tick_data: tick_map(&p.tick_data),
update_block: p.liquidity_update_block.expect("v3 update_block"),
tick_data_block: None,
coverage: PoolTickCoverage::Tracked,
deployer: Address::ZERO,
init_hash: B256::ZERO,
..Default::default()
},
8,
);
state
}
pub fn build_v4_state(p: &PoolData) -> V4PoolState {
let params = RegisterV4PoolParams {
pool_manager: p.pool_manager.expect("v4 pool_manager"),
pool_id: v4_pool_id_bytes(p.pool_id.as_ref().expect("v4 pool_id")),
pool_key: V4PoolKey {
currency0: p.currency0.expect("v4 currency0"),
currency1: p.currency1.expect("v4 currency1"),
fee: p.fee_currency0.expect("v4 fee"),
tick_spacing: p.tick_spacing.expect("v4 tick_spacing"),
hooks: p.hooks.unwrap_or_default(),
},
hook_flags: crate::bot_core::pool_builder::builder::derive_hook_flags(
p.hooks.unwrap_or_default(),
),
protocol_fee: p.protocol_fee.unwrap_or(0),
sqrt_price_x96: p.sqrt_price_x96.expect("v4 sqrt_price_x96").0,
liquidity: u128::try_from(p.liquidity.expect("v4 liquidity").0).unwrap(),
tick: p.tick.expect("v4 tick"),
tick_data: tick_map(&p.tick_data),
update_block: p.liquidity_update_block.expect("v4 update_block"),
tick_data_block: None,
coverage: PoolTickCoverage::Tracked,
fetcher: None,
};
let (_identity, state) = V4PoolState::from_params(params, 8);
state
}
#[cfg(test)]
mod hook_wiring_tests {
use super::*;
use crate::bot_core::BotState;
use crate::investigation::fixture::Amount;
#[test]
fn replay_v4_pool_carries_hook_from_fixture() {
let t0 = Address::new([0x22u8; 20]);
let t1 = Address::new([0xa0u8; 20]);
let mut hook_bytes = [0u8; 20];
hook_bytes[19] = 0x80; let hook = Address::new(hook_bytes);
let pm = Address::new([0x44u8; 20]);
let pool_id = crate::pools::v4_storage_slots::v4_pool_id(&V4PoolKey {
currency0: t0,
currency1: t1,
fee: 100,
tick_spacing: 4,
hooks: hook,
});
let p = PoolData {
pool_manager: Some(pm),
pool_id: Some(format!("0x{}", alloy::hex::encode(pool_id))),
currency0: Some(t0),
currency1: Some(t1),
fee_currency0: Some(100),
tick_spacing: Some(4),
tick: Some(0),
sqrt_price_x96: Some(Amount(U256::from(1u128 << 96))),
liquidity: Some(Amount(U256::from(1_000_000u64))),
liquidity_update_block: Some(0),
hooks: Some(hook),
..Default::default()
};
let mut core = BotState::new();
let registered = register_v4(&mut core, &p).expect("register");
let identity = core.get_v4_identity(registered).expect("identity");
assert_eq!(
identity.pool_key.hooks, hook,
"hook address must survive replay registration"
);
assert_eq!(
crate::pools::v4_storage_slots::v4_pool_id(&identity.pool_key),
identity.pool_id,
"replayed identity must round-trip the pool_id hash",
);
}
}