use num_bigint::{BigInt, BigUint};
use petgraph::stable_graph::NodeIndex;
use rustc_hash::FxHashMap;
use smallvec::SmallVec;
use tracing::trace;
use crate::{
algorithm::{most_liquid::DepthAndPrice, swap_cache::SwapResult},
graph::{EdgeData, TokenPath, TopologyGraph, INLINE_EDGES},
types::{ComponentId, RouteExclusions},
};
#[derive(Clone)]
pub(crate) struct HopResult {
pub(crate) pool_ix: usize,
pub(crate) amount_out: BigUint,
pub(crate) gas: BigUint,
}
pub(crate) struct PoolQuote {
pub(crate) paid: SwapResult,
pub(crate) net: BigInt,
}
pub(crate) struct FailedLegIx(pub(crate) usize);
fn best_paying_pool<'g, D>(
pools: &'g [EdgeData<D>],
mut usable: impl FnMut(&ComponentId) -> bool,
mut simulate: impl FnMut(&'g ComponentId) -> Option<PoolQuote>,
) -> Option<(HopResult, BigInt)> {
let mut best: Option<(HopResult, BigInt)> = None;
for (pool_ix, edge) in pools.iter().enumerate() {
if !usable(&edge.component_id) {
continue;
}
let Some(quote) = simulate(&edge.component_id) else {
trace!(component_id = edge.component_id, "simulation failed, skipping pool");
continue;
};
if best
.as_ref()
.is_none_or(|(_, best_net)| "e.net > best_net)
{
let hop = HopResult { pool_ix, amount_out: quote.paid.amount_out, gas: quote.paid.gas };
best = Some((hop, quote.net));
}
}
best
}
pub(crate) struct LegPools<'g, D, T> {
pub(crate) pair: (NodeIndex, NodeIndex),
pub(crate) pools: &'g [EdgeData<D>],
pub(crate) data: T,
}
pub(crate) struct WalkedPath {
pub(crate) hops: SmallVec<[HopResult; INLINE_EDGES]>,
pub(crate) amount_out: BigUint,
pub(crate) gas: BigUint,
}
pub(crate) struct PairWinners {
winner_by_pair: FxHashMap<(NodeIndex, NodeIndex), usize>,
reuse_winners: bool,
}
impl PairWinners {
pub(crate) fn new(reuse_winners: bool) -> Self {
Self { winner_by_pair: FxHashMap::default(), reuse_winners }
}
fn choose_pool_for_pair<'g, D>(
&mut self,
pair: (NodeIndex, NodeIndex),
pools: &'g [EdgeData<D>],
mut simulate: impl FnMut(&'g ComponentId) -> Option<PoolQuote>,
) -> Option<HopResult> {
if self.reuse_winners {
if let Some(&pool_ix) = self.winner_by_pair.get(&pair) {
if let Some(quote) = pools
.get(pool_ix)
.and_then(|edge| simulate(&edge.component_id))
{
return Some(HopResult {
pool_ix,
amount_out: quote.paid.amount_out,
gas: quote.paid.gas,
});
}
}
}
let (hop, _) = best_paying_pool(pools, |_| true, simulate)?;
if self.reuse_winners {
self.winner_by_pair
.insert(pair, hop.pool_ix);
}
Some(hop)
}
}
pub(crate) fn simulate_token_path<'g, D, T>(
legs: &[LegPools<'g, D, T>],
amount_in: &BigUint,
winners: &mut PairWinners,
mut simulate: impl FnMut(&LegPools<'g, D, T>, &BigUint, &'g ComponentId) -> Option<PoolQuote>,
) -> Result<WalkedPath, FailedLegIx> {
let mut amount = amount_in.clone();
let mut gas = BigUint::ZERO;
let mut hops: SmallVec<[HopResult; INLINE_EDGES]> = SmallVec::new();
let mut crossed: SmallVec<[&'g ComponentId; INLINE_EDGES]> = SmallVec::new();
for (leg_ix, leg) in legs.iter().enumerate() {
let at_amount = amount.clone();
let simulate_pool = |component_id: &'g ComponentId| simulate(leg, &at_amount, component_id);
let narrowed = leg
.pools
.iter()
.any(|edge| crossed.contains(&&edge.component_id));
let hop_result = if narrowed {
best_paying_pool(leg.pools, |id| !crossed.contains(&id), simulate_pool)
.map(|(hop, _)| hop)
} else {
winners.choose_pool_for_pair(leg.pair, leg.pools, simulate_pool)
};
let Some(hop_result) = hop_result else {
return Err(FailedLegIx(leg_ix));
};
let chosen = leg
.pools
.get(hop_result.pool_ix)
.expect("the chosen pool came from this leg's own list");
crossed.push(&chosen.component_id);
gas += &hop_result.gas;
amount = hop_result.amount_out.clone();
hops.push(hop_result);
}
Ok(WalkedPath { hops, amount_out: amount, gas })
}
fn heuristic_score(
graph: &TopologyGraph<DepthAndPrice>,
token_path: &[NodeIndex],
exclusions: &RouteExclusions,
) -> Option<f64> {
if token_path.len() < 2 {
return None;
}
let mut price = 1.0;
let mut min_depth = f64::MAX;
for pair in token_path.windows(2) {
let pools = graph.pools_between(pair[0], pair[1]);
let mut best_price = f64::MIN;
let mut best_depth = f64::MIN;
let mut has_pool = false;
for pool in pools {
if exclusions.excludes_pool(&pool.component_id) {
continue;
}
has_pool = true;
if let Some(data) = pool.data.as_ref() {
best_price = best_price.max(data.spot_price);
best_depth = best_depth.max(data.depth);
}
}
if !has_pool {
return None;
}
if best_price == f64::MIN {
min_depth = 0.0;
} else {
price *= best_price;
min_depth = min_depth.min(best_depth);
}
}
Some(price * min_depth)
}
pub(crate) fn rank_by_heuristic(
graph: &TopologyGraph<DepthAndPrice>,
token_paths: Vec<TokenPath>,
exclusions: &RouteExclusions,
) -> Vec<(TokenPath, f64)> {
let mut scored: Vec<(TokenPath, f64)> = token_paths
.into_iter()
.filter_map(|path| {
let score = heuristic_score(graph, &path, exclusions)?;
Some((path, score))
})
.collect();
scored.sort_by(|(_, left), (_, right)| right.total_cmp(left));
scored
}
#[cfg(test)]
mod tests {
use num_bigint::BigInt;
use tycho_simulation::tycho_core::models::Address;
use super::*;
use crate::{
algorithm::test_utils::fixtures::{addrs, linear_graph, parallel_graph},
graph::GraphManager,
};
fn pools(count: usize) -> Vec<EdgeData<()>> {
(0..count)
.map(|i| EdgeData::new(format!("pool{i}")))
.collect()
}
mod heuristic_score {
use super::*;
fn every_pool_excluded() -> RouteExclusions {
RouteExclusions::default().with_pools([
"ab1".to_string(),
"ab2".to_string(),
"ab3".to_string(),
])
}
#[test]
fn test_unmeasured_hop() {
let (a, b, c, _) = addrs();
let mut manager = linear_graph();
manager
.set_pool_weight(&"ab".to_string(), &a, &b, DepthAndPrice::new(2.0, 1000.0), false)
.unwrap();
let graph = manager.graph();
let node = |address: &Address| graph.get_token_ix(address).unwrap();
let unmeasured = heuristic_score(
graph,
&[node(&a), node(&b), node(&c)],
&RouteExclusions::default(),
);
assert_eq!(unmeasured, Some(0.0), "an unmeasured hop scores zero, not None");
assert!(
heuristic_score(graph, &[node(&a), node(&b)], &RouteExclusions::default())
.is_some_and(|measured| measured > 0.0),
"a fully measured sequence still outranks it"
);
}
#[test]
fn test_excluded_pool_on_a_leg() {
let (a, b, _, _) = addrs();
let mut manager = parallel_graph();
manager
.set_pool_weight(&"ab1".to_string(), &a, &b, DepthAndPrice::new(5.0, 5000.0), false)
.unwrap();
manager
.set_pool_weight(&"ab2".to_string(), &a, &b, DepthAndPrice::new(2.0, 1000.0), false)
.unwrap();
let graph = manager.graph();
let node = |address: &Address| graph.get_token_ix(address).unwrap();
let path = [node(&a), node(&b)];
let deep = heuristic_score(graph, &path, &RouteExclusions::default()).unwrap();
let without_deep_pool = heuristic_score(
graph,
&path,
&RouteExclusions::default().with_pools(["ab1".to_string()]),
)
.unwrap();
assert!(
without_deep_pool < deep,
"the excluded pool set the score: {without_deep_pool} vs {deep}"
);
assert_eq!(
heuristic_score(graph, &path, &every_pool_excluded()),
None,
"a leg with no pool left cannot carry a route"
);
}
}
mod pair_winners {
use super::*;
fn simulator(
multipliers: [u64; 2],
amount_in: u64,
) -> impl FnMut(&ComponentId) -> Option<PoolQuote> {
move |component_id: &ComponentId| {
let index: usize = component_id
.trim_start_matches("pool")
.parse()
.ok()?;
let amount = BigUint::from(amount_in * multipliers[index]);
let paid = SwapResult { amount_out: amount.clone(), gas: BigUint::from(10u64) };
Some(PoolQuote { paid, net: BigInt::from(amount) })
}
}
fn pair() -> (NodeIndex, NodeIndex) {
(NodeIndex::new(0), NodeIndex::new(1))
}
#[test]
fn test_full_scan() {
let mut winners = PairWinners::new(true);
let multipliers = [2u64, 5u64];
let pools = pools(multipliers.len());
let outcome = winners
.choose_pool_for_pair(pair(), &pools, simulator(multipliers, 100))
.unwrap();
assert_eq!(outcome.pool_ix, 1, "pool1 pays 500 against pool0's 200");
assert_eq!(outcome.amount_out, BigUint::from(500u64));
}
#[test]
fn test_remembered_winner() {
let mut winners = PairWinners::new(true);
let multipliers = [2u64, 5u64];
let pools = pools(multipliers.len());
winners
.choose_pool_for_pair(pair(), &pools, simulator(multipliers, 100))
.unwrap();
let mut asked = Vec::new();
let outcome = winners
.choose_pool_for_pair(pair(), &pools, |component_id: &ComponentId| {
asked.push(component_id.clone());
simulator(multipliers, 100)(component_id)
})
.unwrap();
assert_eq!(asked, vec!["pool1".to_string()], "only the winner should be asked");
assert_eq!(outcome.pool_ix, 1);
}
#[test]
fn test_winner_cannot_trade() {
let mut winners = PairWinners::new(true);
let pools = pools(2);
winners
.choose_pool_for_pair(pair(), &pools, simulator([2, 5], 100))
.unwrap();
let outcome = winners
.choose_pool_for_pair(pair(), &pools, |component_id: &ComponentId| {
(component_id == "pool0").then(|| {
let amount = BigUint::from(50u64);
PoolQuote {
paid: SwapResult {
amount_out: amount.clone(),
gas: BigUint::from(10u64),
},
net: BigInt::from(amount),
}
})
})
.unwrap();
assert_eq!(outcome.pool_ix, 0, "pool1 refused, so pool0 takes the pair");
assert_eq!(outcome.amount_out, BigUint::from(50u64));
}
#[test]
fn test_reuse_disabled() {
let mut winners = PairWinners::new(false);
let multipliers = [2u64, 5u64];
let pools = pools(multipliers.len());
let first = winners
.choose_pool_for_pair(pair(), &pools, simulator(multipliers, 100))
.unwrap();
let mut asked = 0usize;
let second = winners
.choose_pool_for_pair(pair(), &pools, |component_id: &ComponentId| {
asked += 1;
simulator(multipliers, 100)(component_id)
})
.unwrap();
assert_eq!(asked, 2, "both pools are asked again, so no winner was remembered");
assert_eq!(first.amount_out, second.amount_out);
}
#[test]
fn test_no_pool_trades() {
let mut winners = PairWinners::new(true);
let pools = pools(2);
let outcome = winners.choose_pool_for_pair(pair(), &pools, |_| None);
assert!(outcome.is_none());
}
}
mod simulate_token_path {
use super::*;
fn two_legs_sharing_pools(pools: &[EdgeData<()>]) -> Vec<LegPools<'_, (), ()>> {
vec![
LegPools { pair: (NodeIndex::new(0), NodeIndex::new(1)), pools, data: () },
LegPools { pair: (NodeIndex::new(1), NodeIndex::new(2)), pools, data: () },
]
}
#[test]
fn test_pool_already_crossed() {
let pools = pools(2);
let legs = two_legs_sharing_pools(&pools);
let mut winners = PairWinners::new(true);
let walked = simulate_token_path(
&legs,
&BigUint::from(100u64),
&mut winners,
|_, amount, id| {
let multiplier = if id == "pool1" { 5u64 } else { 2u64 };
let out = amount * BigUint::from(multiplier);
Some(PoolQuote {
paid: SwapResult { amount_out: out.clone(), gas: BigUint::from(10u64) },
net: BigInt::from(out),
})
},
)
.ok()
.expect("both legs have a pool left to trade");
let crossed: Vec<usize> = walked
.hops
.iter()
.map(|hop| hop.pool_ix)
.collect();
assert_eq!(crossed, vec![1, 0], "the second leg cannot reuse pool1");
assert_eq!(
walked.amount_out,
BigUint::from(1000u64),
"100 * 5 through pool1, then * 2"
);
}
#[test]
fn test_narrowed_scan() {
let pools = pools(2);
let legs = two_legs_sharing_pools(&pools);
let mut winners = PairWinners::new(true);
let quote = |amount: &BigUint, multiplier: u64| {
let out = amount * BigUint::from(multiplier);
Some(PoolQuote {
paid: SwapResult { amount_out: out.clone(), gas: BigUint::from(10u64) },
net: BigInt::from(out),
})
};
simulate_token_path(&legs, &BigUint::from(100u64), &mut winners, |_, amount, id| {
quote(amount, if id == "pool1" { 5 } else { 2 })
})
.ok()
.expect("both legs have a pool left to trade");
assert_eq!(
winners
.winner_by_pair
.get(&(NodeIndex::new(1), NodeIndex::new(2))),
None,
"the second leg scanned a narrowed field, so it recorded nothing"
);
assert_eq!(
winners
.winner_by_pair
.get(&(NodeIndex::new(0), NodeIndex::new(1))),
Some(&1),
"the first leg scanned every pool, so its winner stands"
);
}
#[test]
fn test_leg_cannot_trade() {
let pools = pools(2);
let legs = two_legs_sharing_pools(&pools);
let mut winners = PairWinners::new(true);
let failed = simulate_token_path(
&legs,
&BigUint::from(100u64),
&mut winners,
|leg, amount, _| {
(leg.pair.0 == NodeIndex::new(0)).then(|| {
let out = amount * BigUint::from(2u64);
PoolQuote {
paid: SwapResult { amount_out: out.clone(), gas: BigUint::from(10u64) },
net: BigInt::from(out),
}
})
},
)
.err()
.expect("the second leg has no pool that trades");
assert_eq!(failed.0, 1);
}
}
}