#![expect(clippy::doc_markdown)]
use alloy::primitives::{Address, B256, U256};
use degenbot_pools::slot_layout;
use degenbot_pools::{v3_storage_slots, v4_storage_slots, ClSlotLayout};
use hashbrown::HashMap;
use super::frame_replay::ReplayOutcome;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PoolFamily {
V2Pair,
V3 {
layout: ClSlotLayout,
tick_spacing: i32,
current_tick_hint: Option<i32>,
},
V4PoolManager { pools: V4PoolSet },
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct V4PoolDescriptor {
pub pool_id: B256,
pub tick_spacing: i32,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct V4PoolSet {
pools: &'static [V4PoolDescriptor],
}
impl V4PoolSet {
#[must_use]
pub const fn new(pools: &'static [V4PoolDescriptor]) -> Self {
Self { pools }
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.pools.is_empty()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct TouchedTickWord {
pub tick: i32,
pub liquidity_gross: u128,
pub liquidity_net: i128,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum TypedPoolPost {
V2 {
reserves: slot_layout::V2ReservesParts,
},
V3 {
sqrt_price_x96: Option<U256>,
tick: Option<i32>,
liquidity: Option<u128>,
touched_ticks: Vec<TouchedTickWord>,
},
V4 {
pool_id: B256,
sqrt_price_x96: Option<U256>,
tick: Option<i32>,
liquidity: Option<u128>,
touched_ticks: Vec<TouchedTickWord>,
},
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum PoolPostKind {
Typed(TypedPoolPost),
Unsupported,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PoolPostState {
pub address: Address,
pub family: PoolFamily,
pub kind: PoolPostKind,
}
#[must_use]
pub fn extract_pool_post_states(
outcome: &ReplayOutcome,
descriptors: &HashMap<Address, PoolFamily>,
) -> Vec<PoolPostState> {
let mut out = Vec::new();
for (address, slots) in &outcome.touched {
let Some(&family) = descriptors.get(address) else {
continue;
};
let journal_word = |slot: U256| {
outcome
.state
.get(address)
.and_then(|account| account.storage.get(&slot))
.map(|s| s.present_value)
};
if let PoolFamily::V4PoolManager { pools } = family {
let typed = extract_v4_pool_states(pools, slots, &journal_word);
if typed.is_empty() {
out.push(PoolPostState {
address: *address,
family,
kind: PoolPostKind::Unsupported,
});
} else {
out.extend(typed.into_iter().map(|post| PoolPostState {
address: *address,
family,
kind: PoolPostKind::Typed(post),
}));
}
continue;
}
let kind = match family {
PoolFamily::V4PoolManager { .. } => unreachable!("V4 handled above"),
PoolFamily::V2Pair => PoolPostKind::Typed(TypedPoolPost::V2 {
reserves: journal_word(U256::from(slot_layout::V2_RESERVES_SLOT)).map_or_else(
|| slot_layout::decode_v2_reserves_word(U256::ZERO),
slot_layout::decode_v2_reserves_word,
),
}),
PoolFamily::V3 {
layout,
tick_spacing,
current_tick_hint,
} => PoolPostKind::Typed(TypedPoolPost::V3 {
sqrt_price_x96: journal_word(U256::ZERO)
.map(v3_storage_slots::decode_v3_slot0)
.map(|parts| parts.sqrt_price_x96),
tick: journal_word(U256::ZERO)
.map(v3_storage_slots::decode_v3_slot0)
.map(|parts| parts.tick),
liquidity: journal_word(U256::from(layout.liquidity_slot()))
.map(|w| (w & U256::from(u128::MAX)).to::<u128>()),
touched_ticks: {
let post_tick =
journal_word(U256::ZERO).map(|w| v3_storage_slots::decode_v3_slot0(w).tick);
let mut anchors: Vec<i32> =
current_tick_hint.into_iter().chain(post_tick).collect();
anchors.sort_unstable();
anchors.dedup();
recover_touched_ticks(
slots,
U256::from(layout.ticks_mapping_slot()),
&[U256::ZERO, U256::from(layout.liquidity_slot())],
tick_spacing,
&anchors,
&journal_word,
)
},
}),
};
out.push(PoolPostState {
address: *address,
family,
kind,
});
}
out
}
fn extract_v4_pool_states(
pools: V4PoolSet,
slots: &[U256],
journal_word: &impl Fn(U256) -> Option<U256>,
) -> Vec<TypedPoolPost> {
let mut out = Vec::new();
for pool in pools.pools {
let base = v4_storage_slots::v4_pool_state_base_slot(pool.pool_id);
let slot0_slot = v4_storage_slots::v4_slot0_slot(base);
let liquidity_slot = v4_storage_slots::v4_liquidity_slot(base);
let ticks_base = v4_storage_slots::v4_ticks_mapping_base_slot(base);
let slot0_parts = journal_word(slot0_slot).map(v4_storage_slots::decode_v4_slot0);
let liquidity =
journal_word(liquidity_slot).map(|w| (w & U256::from(u128::MAX)).to::<u128>());
let anchors: Vec<i32> = slot0_parts
.as_ref()
.map(|parts| parts.tick)
.into_iter()
.collect();
let touched_ticks = recover_touched_ticks(
slots,
ticks_base,
&[slot0_slot, liquidity_slot],
pool.tick_spacing,
&anchors,
journal_word,
);
if slot0_parts.is_none() && liquidity.is_none() && touched_ticks.is_empty() {
continue;
}
out.push(TypedPoolPost::V4 {
pool_id: pool.pool_id,
sqrt_price_x96: slot0_parts.as_ref().map(|parts| parts.sqrt_price_x96),
tick: slot0_parts.as_ref().map(|parts| parts.tick),
liquidity,
touched_ticks,
});
}
out
}
#[must_use]
fn recover_touched_ticks(
slots: &[U256],
ticks_base: U256,
skip_slots: &[U256],
tick_spacing: i32,
anchors: &[i32],
journal_word: &impl Fn(U256) -> Option<U256>,
) -> Vec<TouchedTickWord> {
let spacing = tick_spacing.max(1);
let mut anchors: Vec<i32> = anchors.to_vec();
anchors.sort_unstable();
anchors.dedup();
let spread = match (anchors.first(), anchors.last()) {
(Some(&low), Some(&high)) => i64::from(high - low),
_ => 0,
};
let window: i64 = spread + 4 * i64::from(spacing) + 4_096;
let mut ticks = Vec::new();
for slot in slots {
if *slot == U256::ZERO || skip_slots.contains(slot) {
continue;
}
let Some(tick) = slot_layout::recover_tick_from_slot_at_base(
*slot,
ticks_base,
spacing,
&anchors,
i32::try_from(window).unwrap_or(i32::MAX),
) else {
continue;
};
let Some(word) = journal_word(*slot) else {
continue;
};
let (liquidity_gross, liquidity_net) = slot_layout::decode_tick_word(word);
ticks.push(TouchedTickWord {
tick,
liquidity_gross,
liquidity_net,
});
}
ticks.sort_unstable_by_key(|t| t.tick);
ticks
}