use metrics::counter;
use rustc_hash::{FxHashMap, FxHashSet};
use tycho_simulation::tycho_common::models::{protocol::ProtocolComponent, Address};
use crate::{
feed::{events::MarketEvent, market_data::MarketDataView},
propamm_fallback::{is_pamm, must_withhold_pamm, FallbackPoolIndex, FeeTiers, SharedFeeTiers},
types::ComponentId,
};
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) enum PammState {
Admitted,
Withheld,
}
pub(crate) struct PammManager {
fee_tiers: SharedFeeTiers,
fallback_pools: FallbackPoolIndex,
states: FxHashMap<ComponentId, PammState>,
built_with_fee_tiers: Option<FeeTiers>,
worker_pool_name: String,
}
impl PammManager {
pub(crate) fn new(worker_pool_name: String) -> Self {
Self {
fee_tiers: SharedFeeTiers::default(),
fallback_pools: FallbackPoolIndex::default(),
states: FxHashMap::default(),
built_with_fee_tiers: None,
worker_pool_name,
}
}
pub(crate) fn with_fee_tiers(mut self, fee_tiers: SharedFeeTiers) -> Self {
self.fee_tiers = fee_tiers;
self
}
pub(crate) fn fee_tiers(&self) -> Option<FeeTiers> {
self.fee_tiers.snapshot()
}
pub(crate) fn fallback_pools(&self) -> &FallbackPoolIndex {
&self.fallback_pools
}
pub(crate) fn needs_rebuild(&self, fee_tiers: Option<&FeeTiers>) -> bool {
fee_tiers != self.built_with_fee_tiers.as_ref()
}
pub(crate) fn withhold_from_graph(
&mut self,
market: &MarketDataView<'_>,
topology: &FxHashMap<ComponentId, Vec<Address>>,
caller_drops: &dyn Fn(&ProtocolComponent) -> bool,
) -> FxHashSet<ComponentId> {
self.fallback_pools = FallbackPoolIndex::build(market);
let fee_tiers = self.fee_tiers.snapshot();
self.states.clear();
let mut withheld = FxHashSet::default();
for component_id in topology.keys() {
let Some(component) = market.get_component(component_id) else {
continue;
};
if !is_pamm(component) || caller_drops(component) {
continue;
}
let state = if must_withhold_pamm(component, fee_tiers.as_ref(), &self.fallback_pools) {
withheld.insert(component_id.clone());
PammState::Withheld
} else {
PammState::Admitted
};
self.states
.insert(component_id.clone(), state);
}
self.built_with_fee_tiers = fee_tiers;
withheld
}
pub(crate) fn apply_pamm_admission(
&mut self,
market: &MarketDataView<'_>,
fee_tiers: Option<&FeeTiers>,
caller_drops: &dyn Fn(&ProtocolComponent) -> bool,
event: &mut MarketEvent,
) {
self.fallback_pools
.apply_event(market, event);
let MarketEvent::MarketUpdated { added_components, removed_components, .. } = event;
if added_components.is_empty() && removed_components.is_empty() {
return;
}
let Self { fallback_pools, states, worker_pool_name, .. } = self;
let unbacked = |component_id: &ComponentId| {
market
.get_component(component_id)
.is_some_and(|component| must_withhold_pamm(component, fee_tiers, fallback_pools))
};
let count = |outcome: &'static str| {
counter!("propamm_admissions_total", "outcome" => outcome, "pool" => worker_pool_name.clone())
.increment(1);
};
for component_id in removed_components.iter() {
states.remove(component_id);
}
added_components.retain(|component_id, _| {
let Some(component) = market.get_component(component_id) else {
return true;
};
if !is_pamm(component) || caller_drops(component) {
return true;
}
if must_withhold_pamm(component, fee_tiers, fallback_pools) {
states.insert(component_id.clone(), PammState::Withheld);
count("withheld");
return false;
}
states.insert(component_id.clone(), PammState::Admitted);
count("admitted");
true
});
let readmitted: Vec<_> = states
.iter()
.filter(|(_, state)| **state == PammState::Withheld)
.filter_map(|(component_id, _)| {
let component = market.get_component(component_id)?;
(!must_withhold_pamm(component, fee_tiers, fallback_pools))
.then(|| (component_id.clone(), component.tokens.clone()))
})
.collect();
for (component_id, tokens) in readmitted {
states.insert(component_id.clone(), PammState::Admitted);
count("admitted");
added_components.insert(component_id, tokens);
}
let mut evicted = Vec::new();
for (component_id, state) in states.iter() {
if *state == PammState::Admitted && unbacked(component_id) {
evicted.push(component_id.clone());
}
}
for component_id in &evicted {
states.insert(component_id.clone(), PammState::Withheld);
count("evicted");
}
if !evicted.is_empty() {
tracing::debug!(
pool = %worker_pool_name,
components = ?evicted,
"dropping pAMM components whose Uniswap V3 fallback left the market"
);
removed_components.extend(evicted);
}
}
#[cfg(test)]
pub(crate) fn rebuild_pools(&mut self, market: &MarketDataView<'_>) {
self.fallback_pools = FallbackPoolIndex::build(market);
}
#[cfg(test)]
pub(crate) fn state_of(&self, component_id: &str) -> Option<PammState> {
self.states.get(component_id).copied()
}
#[cfg(test)]
pub(crate) fn count_in(&self, state: PammState) -> usize {
self.states
.values()
.filter(|recorded| **recorded == state)
.count()
}
#[cfg(test)]
pub(crate) fn built_with_fee_tiers(&self) -> Option<&FeeTiers> {
self.built_with_fee_tiers.as_ref()
}
}