use std::{
collections::{HashMap, HashSet, VecDeque},
time::{Duration, Instant},
};
use metrics::{counter, histogram};
use num_bigint::{BigInt, BigUint};
use num_traits::ToPrimitive;
use petgraph::prelude::EdgeRef;
use tracing::{debug, instrument, trace};
use tycho_simulation::{
tycho_common::simulation::protocol_sim::ProtocolSim,
tycho_core::models::{token::Token, Address},
};
use super::{Algorithm, AlgorithmConfig, NoPathReason};
use crate::{
derived::{computation::ComputationRequirements, types::TokenGasPrices, SharedDerivedDataRef},
feed::market_data::{SharedMarketData, SharedMarketDataRef},
graph::{petgraph::StableDiGraph, Path, PetgraphStableDiGraphManager},
types::{ComponentId, Order, Route, RouteResult, Swap},
AlgorithmError,
};
pub struct MostLiquidAlgorithm {
min_hops: usize,
max_hops: usize,
timeout: Duration,
max_routes: Option<usize>,
}
#[derive(Debug, Clone, Default)]
pub struct DepthAndPrice {
pub spot_price: f64,
pub depth: f64,
}
impl DepthAndPrice {
#[cfg(test)]
pub fn new(spot_price: f64, depth: f64) -> Self {
Self { spot_price, depth }
}
#[cfg(test)]
pub fn from_protocol_sim(
sim: &impl ProtocolSim,
token_in: &Token,
token_out: &Token,
) -> Result<Self, AlgorithmError> {
Ok(Self {
spot_price: sim
.spot_price(token_in, token_out)
.map_err(|e| {
AlgorithmError::Other(format!("missing spot price for DepthAndPrice: {:?}", e))
})?,
depth: sim
.get_limits(token_in.address.clone(), token_out.address.clone())
.map_err(|e| {
AlgorithmError::Other(format!("missing depth for DepthAndPrice: {:?}", e))
})?
.0
.to_f64()
.ok_or_else(|| {
AlgorithmError::Other("depth conversion to f64 failed".to_string())
})?,
})
}
}
impl crate::graph::EdgeWeightFromSimAndDerived for DepthAndPrice {
fn from_sim_and_derived(
_sim: &dyn ProtocolSim,
component_id: &ComponentId,
token_in: &Token,
token_out: &Token,
derived: &crate::derived::DerivedData,
) -> Option<Self> {
let key = (component_id.clone(), token_in.address.clone(), token_out.address.clone());
let spot_price = match derived
.spot_prices()
.and_then(|p| p.get(&key).copied())
{
Some(p) => p,
None => {
trace!(component_id = %component_id, "spot price not found, skipping edge");
return None;
}
};
let raw_depth = match derived
.pool_depths()
.and_then(|d| d.get(&key))
{
Some(d) => d.to_f64().unwrap_or(0.0),
None => {
trace!(component_id = %component_id, "pool depth not found, skipping edge");
return None;
}
};
let depth = match derived
.token_prices()
.and_then(|p| p.get(&token_in.address))
{
Some(price) => {
let num = match price.numerator.to_f64() {
Some(v) if v > 0.0 => v,
Some(_) => {
trace!(
component_id = %component_id,
token_in = %token_in.address,
"token price numerator is zero, skipping edge"
);
return None;
}
None => {
trace!(
component_id = %component_id,
token_in = %token_in.address,
"token price numerator overflows f64, skipping edge"
);
return None;
}
};
let den = match price.denominator.to_f64() {
Some(v) if v > 0.0 => v,
Some(_) => {
trace!(
component_id = %component_id,
token_in = %token_in.address,
"token price denominator is zero, skipping edge"
);
return None;
}
None => {
trace!(
component_id = %component_id,
token_in = %token_in.address,
"token price denominator overflows f64, skipping edge"
);
return None;
}
};
raw_depth * den / num
}
None => {
trace!(
component_id = %component_id,
token_in = %token_in.address,
"token price not found, skipping edge"
);
return None;
}
};
Some(Self { spot_price, depth })
}
}
impl MostLiquidAlgorithm {
pub fn new() -> Self {
Self { min_hops: 1, max_hops: 3, timeout: Duration::from_millis(500), max_routes: None }
}
pub fn with_config(config: AlgorithmConfig) -> Result<Self, AlgorithmError> {
Ok(Self {
min_hops: config.min_hops(),
max_hops: config.max_hops(),
timeout: config.timeout(),
max_routes: config.max_routes(),
})
}
#[instrument(level = "debug", skip(graph))]
fn find_paths<'a>(
graph: &'a StableDiGraph<DepthAndPrice>,
from: &Address,
to: &Address,
min_hops: usize,
max_hops: usize,
) -> Result<Vec<Path<'a, DepthAndPrice>>, AlgorithmError> {
if min_hops == 0 || min_hops > max_hops {
return Err(AlgorithmError::InvalidConfiguration {
reason: format!(
"invalid hop configuration: min_hops={min_hops} max_hops={max_hops}",
),
});
}
let from_idx = graph
.node_indices()
.find(|&n| &graph[n] == from)
.ok_or(AlgorithmError::NoPath {
from: from.clone(),
to: to.clone(),
reason: NoPathReason::SourceTokenNotInGraph,
})?;
let to_idx = graph
.node_indices()
.find(|&n| &graph[n] == to)
.ok_or(AlgorithmError::NoPath {
from: from.clone(),
to: to.clone(),
reason: NoPathReason::DestinationTokenNotInGraph,
})?;
let mut paths = Vec::new();
let mut queue = VecDeque::new();
queue.push_back((from_idx, Path::new()));
while let Some((current_node, current_path)) = queue.pop_front() {
if current_path.len() >= max_hops {
continue;
}
for edge in graph.edges(current_node) {
let next_node = edge.target();
let next_addr = &graph[next_node];
let already_visited = current_path.tokens.contains(&next_addr);
let is_closing_circular_route = from_idx == to_idx && next_node == to_idx;
if already_visited && !is_closing_circular_route {
continue;
}
let mut new_path = current_path.clone();
new_path.add_hop(&graph[current_node], edge.weight(), next_addr);
if next_node == to_idx && new_path.len() >= min_hops {
paths.push(new_path.clone());
}
queue.push_back((next_node, new_path));
}
}
Ok(paths)
}
fn try_score_path(path: &Path<DepthAndPrice>) -> Option<f64> {
if path.is_empty() {
trace!("cannot score empty path");
return None;
}
let mut price = 1.0;
let mut min_depth = f64::MAX;
for edge in path.edge_iter() {
let Some(data) = edge.data.as_ref() else {
debug!(component_id = %edge.component_id, "edge missing weight data, path cannot be scored");
return None;
};
price *= data.spot_price;
min_depth = min_depth.min(data.depth);
}
Some(price * min_depth)
}
#[instrument(level = "trace", skip(path, market, token_prices), fields(hop_count = path.len()))]
pub(crate) fn simulate_path<D>(
path: &Path<D>,
market: &SharedMarketData,
token_prices: Option<&TokenGasPrices>,
amount_in: BigUint,
) -> Result<RouteResult, AlgorithmError> {
let mut current_amount = amount_in.clone();
let mut swaps = Vec::with_capacity(path.len());
let mut state_overrides: HashMap<&ComponentId, Box<dyn ProtocolSim>> = HashMap::new();
for (address_in, edge_data, address_out) in path.iter() {
let token_in = market
.get_token(address_in)
.ok_or_else(|| AlgorithmError::DataNotFound {
kind: "token",
id: Some(format!("{:?}", address_in)),
})?;
let token_out = market
.get_token(address_out)
.ok_or_else(|| AlgorithmError::DataNotFound {
kind: "token",
id: Some(format!("{:?}", address_out)),
})?;
let component_id = &edge_data.component_id;
let component = market
.get_component(component_id)
.ok_or_else(|| AlgorithmError::DataNotFound {
kind: "component",
id: Some(component_id.clone()),
})?;
let component_state = market
.get_simulation_state(component_id)
.ok_or_else(|| AlgorithmError::DataNotFound {
kind: "simulation state",
id: Some(component_id.clone()),
})?;
let state = state_overrides
.get(component_id)
.map(Box::as_ref)
.unwrap_or(component_state);
let result = state
.get_amount_out(current_amount.clone(), token_in, token_out)
.map_err(|e| AlgorithmError::Other(format!("simulation error: {:?}", e)))?;
swaps.push(Swap::new(
component_id.clone(),
component.protocol_system.clone(),
token_in.address.clone(),
token_out.address.clone(),
current_amount.clone(),
result.amount.clone(),
result.gas,
component.clone(),
state.clone_box(),
));
state_overrides.insert(component_id, result.new_state);
current_amount = result.amount;
}
let route = Route::new(swaps);
let output_amount = route
.swaps()
.last()
.map(|s| s.amount_out().clone())
.unwrap_or_else(|| BigUint::ZERO);
let gas_price = market
.gas_price()
.ok_or(AlgorithmError::DataNotFound { kind: "gas price", id: None })?
.effective_gas_price()
.clone();
let net_amount_out = if let Some(last_swap) = route.swaps().last() {
let total_gas = route.total_gas();
let gas_cost_wei = &total_gas * &gas_price;
let gas_cost_in_output_token: Option<BigUint> = token_prices
.and_then(|prices| prices.get(last_swap.token_out()))
.map(|price| {
&gas_cost_wei * &price.numerator / &price.denominator
});
match gas_cost_in_output_token {
Some(gas_cost) => BigInt::from(output_amount) - BigInt::from(gas_cost),
None => {
BigInt::from(output_amount)
}
}
} else {
BigInt::from(output_amount)
};
Ok(RouteResult::new(route, net_amount_out, gas_price))
}
}
impl Default for MostLiquidAlgorithm {
fn default() -> Self {
Self::new()
}
}
impl Algorithm for MostLiquidAlgorithm {
type GraphType = StableDiGraph<DepthAndPrice>;
type GraphManager = PetgraphStableDiGraphManager<DepthAndPrice>;
fn name(&self) -> &str {
"most_liquid"
}
#[instrument(level = "debug", skip_all, fields(order_id = %order.id()))]
async fn find_best_route(
&self,
graph: &Self::GraphType,
market: SharedMarketDataRef,
derived: Option<SharedDerivedDataRef>,
order: &Order,
) -> Result<RouteResult, AlgorithmError> {
let start = Instant::now();
if !order.is_sell() {
return Err(AlgorithmError::ExactOutNotSupported);
}
let token_prices = if let Some(ref derived) = derived {
derived
.read()
.await
.token_prices()
.cloned()
} else {
None
};
let amount_in = order.amount().clone();
let all_paths = Self::find_paths(
graph,
order.token_in(),
order.token_out(),
self.min_hops,
self.max_hops,
)?;
let paths_candidates = all_paths.len();
if paths_candidates == 0 {
return Err(AlgorithmError::NoPath {
from: order.token_in().clone(),
to: order.token_out().clone(),
reason: NoPathReason::NoGraphPath,
});
}
let mut scored_paths: Vec<(Path<DepthAndPrice>, f64)> = all_paths
.into_iter()
.filter_map(|path| {
let score = Self::try_score_path(&path)?;
Some((path, score))
})
.collect();
scored_paths.sort_by(|(_, a_score), (_, b_score)| {
b_score
.partial_cmp(a_score)
.unwrap_or(std::cmp::Ordering::Equal)
});
if let Some(max_routes) = self.max_routes {
scored_paths.truncate(max_routes);
}
let paths_to_simulate = scored_paths.len();
let scoring_failures = paths_candidates - paths_to_simulate;
if paths_to_simulate == 0 {
return Err(AlgorithmError::NoPath {
from: order.token_in().clone(),
to: order.token_out().clone(),
reason: NoPathReason::NoScorablePaths,
});
}
let component_ids: HashSet<ComponentId> = scored_paths
.iter()
.flat_map(|(path, _)| {
path.edge_iter()
.iter()
.map(|e| e.component_id.clone())
})
.collect();
let market = {
let market = market.read().await;
if market.gas_price().is_none() {
return Err(AlgorithmError::DataNotFound { kind: "gas price", id: None });
}
let market_subset = market.extract_subset(&component_ids);
drop(market);
market_subset
};
let mut paths_simulated = 0usize;
let mut simulation_failures = 0usize;
let mut best: Option<RouteResult> = None;
let timeout_ms = self.timeout.as_millis() as u64;
for (edge_path, _) in scored_paths {
let elapsed_ms = start.elapsed().as_millis() as u64;
if elapsed_ms > timeout_ms {
break;
}
let result = match Self::simulate_path(
&edge_path,
&market,
token_prices.as_ref(),
amount_in.clone(),
) {
Ok(r) => r,
Err(e) => {
trace!(error = %e, "simulation failed for path");
simulation_failures += 1;
continue;
}
};
if best
.as_ref()
.map(|best| result.net_amount_out() > best.net_amount_out())
.unwrap_or(true)
{
best = Some(result);
}
paths_simulated += 1;
}
let solve_time_ms = start.elapsed().as_millis() as u64;
let block_number = market
.last_updated()
.map(|b| b.number());
let coverage_pct = if paths_to_simulate == 0 {
100.0
} else {
(paths_simulated as f64 / paths_to_simulate as f64) * 100.0
};
counter!("algorithm.scoring_failures").increment(scoring_failures as u64);
counter!("algorithm.simulation_failures").increment(simulation_failures as u64);
histogram!("algorithm.simulation_coverage_pct").record(coverage_pct);
match &best {
Some(result) => {
let tokens = market.token_registry_ref();
let path_desc = result.route().path_description(tokens);
let protocols = result
.route()
.swaps()
.iter()
.map(|s| s.protocol())
.collect::<Vec<_>>();
let price = amount_in
.to_f64()
.filter(|&v| v > 0.0)
.and_then(|amt_in| {
result
.net_amount_out()
.to_f64()
.map(|amt_out| amt_out / amt_in)
})
.unwrap_or(f64::NAN);
debug!(
solve_time_ms,
block_number,
paths_candidates,
paths_to_simulate,
paths_simulated,
simulation_failures,
simulation_coverage_pct = coverage_pct,
components_considered = component_ids.len(),
tokens_considered = market.token_registry_ref().len(),
path = %path_desc,
amount_in = %amount_in,
net_amount_out = %result.net_amount_out(),
price_out_per_in = price,
hop_count = result.route().swaps().len(),
protocols = ?protocols,
"route found"
);
}
None => {
debug!(
solve_time_ms,
block_number,
paths_candidates,
paths_to_simulate,
paths_simulated,
simulation_failures,
simulation_coverage_pct = coverage_pct,
components_considered = component_ids.len(),
tokens_considered = market.token_registry_ref().len(),
"no viable route"
);
}
}
best.ok_or({
if solve_time_ms > timeout_ms {
AlgorithmError::Timeout { elapsed_ms: solve_time_ms }
} else {
AlgorithmError::InsufficientLiquidity
}
})
}
fn computation_requirements(&self) -> ComputationRequirements {
ComputationRequirements::none()
.allow_stale("token_prices")
.expect("Conflicting Computation Requirements")
}
fn timeout(&self) -> Duration {
self.timeout
}
}
#[cfg(test)]
mod tests {
use std::{collections::HashSet, sync::Arc};
use rstest::rstest;
use tokio::sync::RwLock;
use tycho_simulation::{
tycho_core::simulation::protocol_sim::Price,
tycho_ethereum::gas::{BlockGasPrice, GasPrice},
};
use super::*;
use crate::{
algorithm::test_utils::{
addr, component,
fixtures::{addrs, diamond_graph, linear_graph, parallel_graph},
market_read, order, setup_market, token, MockProtocolSim, ONE_ETH,
},
derived::{
computation::{FailedItem, FailedItemError},
types::TokenGasPrices,
DerivedData,
},
graph::GraphManager,
types::OrderSide,
};
fn wrap_market(market: SharedMarketData) -> SharedMarketDataRef {
Arc::new(RwLock::new(market))
}
fn setup_derived_with_token_prices(token_addresses: &[Address]) -> SharedDerivedDataRef {
let mut token_prices: TokenGasPrices = HashMap::new();
for addr in token_addresses {
token_prices.insert(
addr.clone(),
Price { numerator: BigUint::from(1u64), denominator: BigUint::from(1u64) },
);
}
let mut derived_data = DerivedData::new();
derived_data.set_token_prices(token_prices, vec![], 1, true);
Arc::new(RwLock::new(derived_data))
}
#[test]
fn test_try_score_path_calculates_correctly() {
let (a, b, c, _) = addrs();
let mut m = linear_graph();
m.set_edge_weight(&"ab".to_string(), &a, &b, DepthAndPrice::new(2.0, 1000.0), false)
.unwrap();
m.set_edge_weight(&"bc".to_string(), &b, &c, DepthAndPrice::new(0.5, 500.0), false)
.unwrap();
let graph = m.graph();
let paths = MostLiquidAlgorithm::find_paths(graph, &a, &c, 2, 2).unwrap();
assert_eq!(paths.len(), 1);
let path = &paths[0];
let expected = 2.0 * 0.5 * 500.0;
let score = MostLiquidAlgorithm::try_score_path(path).unwrap();
assert_eq!(score, expected, "expected {expected}, got {score}");
}
#[test]
fn test_try_score_path_empty_returns_none() {
let path: Path<DepthAndPrice> = Path::new();
assert_eq!(MostLiquidAlgorithm::try_score_path(&path), None);
}
#[test]
fn test_try_score_path_missing_weight_returns_none() {
let (a, b, _, _) = addrs();
let m = linear_graph();
let graph = m.graph();
let paths = MostLiquidAlgorithm::find_paths(graph, &a, &b, 1, 1).unwrap();
assert_eq!(paths.len(), 1);
assert!(MostLiquidAlgorithm::try_score_path(&paths[0]).is_none());
}
#[test]
fn test_try_score_path_circular_route() {
let (a, b, _, _) = addrs();
let mut m = linear_graph();
m.set_edge_weight(&"ab".to_string(), &a, &b, DepthAndPrice::new(2.0, 1000.0), false)
.unwrap();
m.set_edge_weight(&"ab".to_string(), &b, &a, DepthAndPrice::new(0.6, 800.0), false)
.unwrap();
let graph = m.graph();
let paths = MostLiquidAlgorithm::find_paths(graph, &a, &a, 2, 2).unwrap();
assert_eq!(paths.len(), 1);
let score = MostLiquidAlgorithm::try_score_path(&paths[0]).unwrap();
let expected = 2.0 * 0.6 * 800.0;
assert_eq!(score, expected, "expected {expected}, got {score}");
}
fn make_mock_sim() -> MockProtocolSim {
MockProtocolSim::new(2.0)
}
fn pair_key(comp: &str, b_in: u8, b_out: u8) -> (String, Address, Address) {
(comp.to_string(), addr(b_in), addr(b_out))
}
fn pair_key_str(comp: &str, b_in: u8, b_out: u8) -> String {
format!("{comp}/{}/{}", addr(b_in), addr(b_out))
}
fn make_token_prices(addresses: &[Address]) -> TokenGasPrices {
let mut prices = TokenGasPrices::new();
for addr in addresses {
prices.insert(
addr.clone(),
Price { numerator: BigUint::from(1u64), denominator: BigUint::from(1u64) },
);
}
prices
}
#[test]
fn test_from_sim_and_derived_failed_spot_price_returns_none() {
let key = pair_key("pool1", 0x01, 0x02);
let key_str = pair_key_str("pool1", 0x01, 0x02);
let tok_in = token(0x01, "A");
let tok_out = token(0x02, "B");
let mut derived = DerivedData::new();
derived.set_spot_prices(
Default::default(),
vec![FailedItem {
key: key_str,
error: FailedItemError::SimulationFailed("sim error".into()),
}],
10,
true,
);
derived.set_pool_depths(Default::default(), vec![], 10, true);
derived.set_token_prices(
make_token_prices(&[tok_in.address.clone(), tok_out.address.clone()]),
vec![],
10,
true,
);
let sim = make_mock_sim();
let result =
<DepthAndPrice as crate::graph::EdgeWeightFromSimAndDerived>::from_sim_and_derived(
&sim, &key.0, &tok_in, &tok_out, &derived,
);
assert!(result.is_none());
}
#[test]
fn test_from_sim_and_derived_failed_pool_depth_returns_none() {
let key = pair_key("pool1", 0x01, 0x02);
let key_str = pair_key_str("pool1", 0x01, 0x02);
let tok_in = token(0x01, "A");
let tok_out = token(0x02, "B");
let mut derived = DerivedData::new();
let mut prices = crate::derived::types::SpotPrices::default();
prices.insert(key.clone(), 1.5);
derived.set_spot_prices(prices, vec![], 10, true);
derived.set_pool_depths(
Default::default(),
vec![FailedItem {
key: key_str,
error: FailedItemError::SimulationFailed("depth error".into()),
}],
10,
true,
);
derived.set_token_prices(
make_token_prices(&[tok_in.address.clone(), tok_out.address.clone()]),
vec![],
10,
true,
);
let sim = make_mock_sim();
let result =
<DepthAndPrice as crate::graph::EdgeWeightFromSimAndDerived>::from_sim_and_derived(
&sim, &key.0, &tok_in, &tok_out, &derived,
);
assert!(result.is_none());
}
#[test]
fn test_from_sim_and_derived_both_failed_returns_none() {
let key = pair_key("pool1", 0x01, 0x02);
let key_str = pair_key_str("pool1", 0x01, 0x02);
let tok_in = token(0x01, "A");
let tok_out = token(0x02, "B");
let mut derived = DerivedData::new();
derived.set_spot_prices(
Default::default(),
vec![FailedItem {
key: key_str.clone(),
error: FailedItemError::SimulationFailed("spot error".into()),
}],
10,
true,
);
derived.set_pool_depths(
Default::default(),
vec![FailedItem {
key: key_str,
error: FailedItemError::SimulationFailed("depth error".into()),
}],
10,
true,
);
derived.set_token_prices(
make_token_prices(&[tok_in.address.clone(), tok_out.address.clone()]),
vec![],
10,
true,
);
let sim = make_mock_sim();
let result =
<DepthAndPrice as crate::graph::EdgeWeightFromSimAndDerived>::from_sim_and_derived(
&sim, &key.0, &tok_in, &tok_out, &derived,
);
assert!(result.is_none());
}
#[test]
fn test_from_sim_and_derived_missing_token_price_returns_none() {
let key = pair_key("pool1", 0x01, 0x02);
let tok_in = token(0x01, "A");
let tok_out = token(0x02, "B");
let mut derived = DerivedData::new();
let mut prices = crate::derived::types::SpotPrices::default();
prices.insert(key.clone(), 1.5);
derived.set_spot_prices(prices, vec![], 10, true);
let mut depths = crate::derived::types::PoolDepths::default();
depths.insert(key.clone(), BigUint::from(1000u64));
derived.set_pool_depths(depths, vec![], 10, true);
let sim = make_mock_sim();
let result =
<DepthAndPrice as crate::graph::EdgeWeightFromSimAndDerived>::from_sim_and_derived(
&sim, &key.0, &tok_in, &tok_out, &derived,
);
assert!(
result.is_none(),
"should return None when token price is missing for depth normalization"
);
}
#[test]
fn test_from_sim_and_derived_normalizes_depth_to_eth() {
let key = pair_key("pool1", 0x01, 0x02);
let tok_in = token(0x01, "A");
let tok_out = token(0x02, "B");
let mut derived = DerivedData::new();
let mut spot = crate::derived::types::SpotPrices::default();
spot.insert(key.clone(), 2.0);
derived.set_spot_prices(spot, vec![], 10, true);
let mut depths = crate::derived::types::PoolDepths::default();
depths.insert(key.clone(), BigUint::from(2_000_000u64));
derived.set_pool_depths(depths, vec![], 10, true);
let mut token_prices = TokenGasPrices::new();
token_prices.insert(
tok_in.address.clone(),
Price { numerator: BigUint::from(2000u64), denominator: BigUint::from(1u64) },
);
derived.set_token_prices(token_prices, vec![], 10, true);
let sim = make_mock_sim();
let result =
<DepthAndPrice as crate::graph::EdgeWeightFromSimAndDerived>::from_sim_and_derived(
&sim, &key.0, &tok_in, &tok_out, &derived,
);
let data = result.expect("should return Some when all data present");
assert!((data.spot_price - 2.0).abs() < f64::EPSILON, "spot price should be 2.0");
assert!(
(data.depth - 1000.0).abs() < f64::EPSILON,
"depth should be 1000.0 ETH, got {}",
data.depth
);
}
#[test]
fn test_from_sim_and_derived_normalizes_depth_fractional_price() {
let key = pair_key("pool1", 0x01, 0x02);
let tok_in = token(0x01, "A");
let tok_out = token(0x02, "B");
let mut derived = DerivedData::new();
let mut spot = crate::derived::types::SpotPrices::default();
spot.insert(key.clone(), 0.5);
derived.set_spot_prices(spot, vec![], 10, true);
let mut depths = crate::derived::types::PoolDepths::default();
depths.insert(key.clone(), BigUint::from(500u64));
derived.set_pool_depths(depths, vec![], 10, true);
let mut token_prices = TokenGasPrices::new();
token_prices.insert(
tok_in.address.clone(),
Price { numerator: BigUint::from(3u64), denominator: BigUint::from(2u64) },
);
derived.set_token_prices(token_prices, vec![], 10, true);
let sim = make_mock_sim();
let result =
<DepthAndPrice as crate::graph::EdgeWeightFromSimAndDerived>::from_sim_and_derived(
&sim, &key.0, &tok_in, &tok_out, &derived,
);
let data = result.expect("should return Some when all data present");
let expected_depth = 500.0 * 2.0 / 3.0;
assert!(
(data.depth - expected_depth).abs() < 1e-10,
"depth should be {expected_depth}, got {}",
data.depth
);
}
fn all_ids(paths: Vec<Path<'_, DepthAndPrice>>) -> HashSet<Vec<&str>> {
paths
.iter()
.map(|p| {
p.iter()
.map(|(_, e, _)| e.component_id.as_str())
.collect()
})
.collect()
}
#[test]
fn test_find_paths_linear_forward_and_reverse() {
let (a, b, c, d) = addrs();
let m = linear_graph();
let g = m.graph();
let p = MostLiquidAlgorithm::find_paths(g, &a, &b, 1, 1).unwrap();
assert_eq!(all_ids(p), HashSet::from([vec!["ab"]]));
let p = MostLiquidAlgorithm::find_paths(g, &a, &c, 1, 2).unwrap();
assert_eq!(all_ids(p), HashSet::from([vec!["ab", "bc"]]));
let p = MostLiquidAlgorithm::find_paths(g, &a, &d, 1, 3).unwrap();
assert_eq!(all_ids(p), HashSet::from([vec!["ab", "bc", "cd"]]));
let p = MostLiquidAlgorithm::find_paths(g, &d, &a, 1, 3).unwrap();
assert_eq!(all_ids(p), HashSet::from([vec!["cd", "bc", "ab"]]));
}
#[test]
fn test_find_paths_respects_hop_bounds() {
let (a, _, c, d) = addrs();
let m = linear_graph();
let g = m.graph();
assert!(MostLiquidAlgorithm::find_paths(g, &a, &d, 1, 2)
.unwrap()
.is_empty());
assert!(MostLiquidAlgorithm::find_paths(g, &a, &c, 3, 3)
.unwrap()
.is_empty());
}
#[test]
fn test_find_paths_parallel_pools() {
let (a, b, c, _) = addrs();
let m = parallel_graph();
let g = m.graph();
let p = MostLiquidAlgorithm::find_paths(g, &a, &b, 1, 1).unwrap();
assert_eq!(all_ids(p), HashSet::from([vec!["ab1"], vec!["ab2"], vec!["ab3"]]));
let p = MostLiquidAlgorithm::find_paths(g, &a, &c, 1, 2).unwrap();
assert_eq!(
all_ids(p),
HashSet::from([
vec!["ab1", "bc1"],
vec!["ab1", "bc2"],
vec!["ab2", "bc1"],
vec!["ab2", "bc2"],
vec!["ab3", "bc1"],
vec!["ab3", "bc2"],
])
);
}
#[test]
fn test_find_paths_diamond_multiple_routes() {
let (a, _, _, d) = addrs();
let m = diamond_graph();
let g = m.graph();
let p = MostLiquidAlgorithm::find_paths(g, &a, &d, 1, 2).unwrap();
assert_eq!(all_ids(p), HashSet::from([vec!["ab", "bd"], vec!["ac", "cd"]]));
}
#[test]
fn test_find_paths_no_intermediate_cycles() {
let (a, b, _, _) = addrs();
let m = linear_graph();
let g = m.graph();
let p = MostLiquidAlgorithm::find_paths(g, &a, &b, 1, 3).unwrap();
assert_eq!(all_ids(p), HashSet::from([vec!["ab"]]));
}
#[test]
fn test_find_paths_cyclic_same_source_dest() {
let (a, _, _, _) = addrs();
let m = parallel_graph();
let g = m.graph();
let p = MostLiquidAlgorithm::find_paths(g, &a, &a, 2, 2).unwrap();
assert_eq!(
all_ids(p),
HashSet::from([
vec!["ab1", "ab1"],
vec!["ab1", "ab2"],
vec!["ab1", "ab3"],
vec!["ab2", "ab1"],
vec!["ab2", "ab2"],
vec!["ab2", "ab3"],
vec!["ab3", "ab1"],
vec!["ab3", "ab2"],
vec!["ab3", "ab3"],
])
);
}
#[rstest]
#[case::source_not_in_graph(false, true)]
#[case::dest_not_in_graph(true, false)]
fn test_find_paths_token_not_in_graph(#[case] from_exists: bool, #[case] to_exists: bool) {
let (a, b, _, _) = addrs();
let non_existent = addr(0x99);
let m = linear_graph();
let g = m.graph();
let from = if from_exists { a } else { non_existent.clone() };
let to = if to_exists { b } else { non_existent };
let result = MostLiquidAlgorithm::find_paths(g, &from, &to, 1, 3);
assert!(matches!(result, Err(AlgorithmError::NoPath { .. })));
}
#[rstest]
#[case::min_greater_than_max(3, 1)]
#[case::min_hops_zero(0, 1)]
fn test_find_paths_invalid_configuration(#[case] min_hops: usize, #[case] max_hops: usize) {
let (a, b, _, _) = addrs();
let m = linear_graph();
let g = m.graph();
assert!(matches!(
MostLiquidAlgorithm::find_paths(g, &a, &b, min_hops, max_hops)
.err()
.unwrap(),
AlgorithmError::InvalidConfiguration { reason: _ }
));
}
#[test]
fn test_find_paths_bfs_ordering() {
let (a, b, c, d) = addrs();
let e = addr(0x0E);
let mut m = PetgraphStableDiGraphManager::<DepthAndPrice>::new();
let mut t = HashMap::new();
t.insert("ae".into(), vec![a.clone(), e.clone()]);
t.insert("ab".into(), vec![a.clone(), b.clone()]);
t.insert("be".into(), vec![b, e.clone()]);
t.insert("ac".into(), vec![a.clone(), c.clone()]);
t.insert("cd".into(), vec![c, d.clone()]);
t.insert("de".into(), vec![d, e.clone()]);
m.initialize_graph(&t);
let g = m.graph();
let p = MostLiquidAlgorithm::find_paths(g, &a, &e, 1, 3).unwrap();
assert_eq!(p.len(), 3, "Expected 3 paths total");
assert_eq!(p[0].len(), 1, "First path should be 1-hop");
assert_eq!(p[1].len(), 2, "Second path should be 2-hop");
assert_eq!(p[2].len(), 3, "Third path should be 3-hop");
}
#[test]
fn test_simulate_path_single_hop() {
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let (market, manager) =
setup_market(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
let paths = MostLiquidAlgorithm::find_paths(
manager.graph(),
&token_a.address,
&token_b.address,
1,
1,
)
.unwrap();
let path = paths.into_iter().next().unwrap();
let result = MostLiquidAlgorithm::simulate_path(
&path,
&market_read(&market),
None,
BigUint::from(100u64),
)
.unwrap();
assert_eq!(result.route().swaps().len(), 1);
assert_eq!(*result.route().swaps()[0].amount_in(), BigUint::from(100u64));
assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(200u64)); assert_eq!(result.route().swaps()[0].component_id(), "pool1");
}
#[test]
fn test_simulate_path_multi_hop_chains_amounts() {
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let token_c = token(0x03, "C");
let (market, manager) = setup_market(vec![
("pool1", &token_a, &token_b, MockProtocolSim::new(2.0)),
("pool2", &token_b, &token_c, MockProtocolSim::new(3.0)),
]);
let paths = MostLiquidAlgorithm::find_paths(
manager.graph(),
&token_a.address,
&token_c.address,
2,
2,
)
.unwrap();
let path = paths.into_iter().next().unwrap();
let result = MostLiquidAlgorithm::simulate_path(
&path,
&market_read(&market),
None,
BigUint::from(10u64),
)
.unwrap();
assert_eq!(result.route().swaps().len(), 2);
assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(20u64));
assert_eq!(*result.route().swaps()[1].amount_in(), BigUint::from(20u64));
assert_eq!(*result.route().swaps()[1].amount_out(), BigUint::from(60u64));
}
#[test]
fn test_simulate_path_same_pool_twice_uses_updated_state() {
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let (market, manager) =
setup_market(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
let paths = MostLiquidAlgorithm::find_paths(
manager.graph(),
&token_a.address,
&token_a.address,
2,
2,
)
.unwrap();
assert_eq!(paths.len(), 1);
let path = paths[0].clone();
let result = MostLiquidAlgorithm::simulate_path(
&path,
&market_read(&market),
None,
BigUint::from(10u64),
)
.unwrap();
assert_eq!(result.route().swaps().len(), 2);
assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(20u64));
assert_eq!(*result.route().swaps()[1].amount_out(), BigUint::from(6u64));
}
#[test]
fn test_simulate_path_missing_token_returns_data_not_found() {
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let token_c = token(0x03, "C");
let (market, _) =
setup_market(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
let market = market_read(&market);
let mut topology = market.component_topology();
topology
.insert("pool2".to_string(), vec![token_b.address.clone(), token_c.address.clone()]);
let mut manager = PetgraphStableDiGraphManager::default();
manager.initialize_graph(&topology);
let graph = manager.graph();
let paths =
MostLiquidAlgorithm::find_paths(graph, &token_a.address, &token_c.address, 2, 2)
.unwrap();
let path = paths.into_iter().next().unwrap();
let result =
MostLiquidAlgorithm::simulate_path(&path, &market, None, BigUint::from(100u64));
assert!(matches!(result, Err(AlgorithmError::DataNotFound { kind: "token", .. })));
}
#[test]
fn test_simulate_path_missing_component_returns_data_not_found() {
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let (market, manager) =
setup_market(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
let mut market_write = market.try_write().unwrap();
market_write.remove_components([&"pool1".to_string()]);
drop(market_write);
let graph = manager.graph();
let paths =
MostLiquidAlgorithm::find_paths(graph, &token_a.address, &token_b.address, 1, 1)
.unwrap();
let path = paths.into_iter().next().unwrap();
let result = MostLiquidAlgorithm::simulate_path(
&path,
&market_read(&market),
None,
BigUint::from(100u64),
);
assert!(matches!(result, Err(AlgorithmError::DataNotFound { kind: "component", .. })));
}
#[tokio::test]
async fn test_find_best_route_single_path() {
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let (market, manager) =
setup_market(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
let algorithm = MostLiquidAlgorithm::with_config(
AlgorithmConfig::new(1, 1, Duration::from_millis(100), None).unwrap(),
)
.unwrap();
let order = order(&token_a, &token_b, ONE_ETH, OrderSide::Sell);
let result = algorithm
.find_best_route(manager.graph(), market, None, &order)
.await
.unwrap();
assert_eq!(result.route().swaps().len(), 1);
assert_eq!(*result.route().swaps()[0].amount_in(), BigUint::from(ONE_ETH));
assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(ONE_ETH * 2));
}
#[tokio::test]
async fn test_find_best_route_ranks_by_net_amount_out() {
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let (market, manager) = setup_market(vec![
("best", &token_a, &token_b, MockProtocolSim::new(3.0).with_gas(10)),
("low_out", &token_a, &token_b, MockProtocolSim::new(2.0).with_gas(5)),
("high_gas", &token_a, &token_b, MockProtocolSim::new(4.0).with_gas(30)),
]);
let algorithm = MostLiquidAlgorithm::with_config(
AlgorithmConfig::new(1, 1, Duration::from_millis(100), None).unwrap(),
)
.unwrap();
let order = order(&token_a, &token_b, 1000, OrderSide::Sell);
let derived = setup_derived_with_token_prices(std::slice::from_ref(&token_b.address));
let result = algorithm
.find_best_route(manager.graph(), market, Some(derived), &order)
.await
.unwrap();
assert_eq!(result.route().swaps().len(), 1);
assert_eq!(result.route().swaps()[0].component_id(), "best");
assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(3000u64));
assert_eq!(result.net_amount_out(), &BigInt::from(2000)); }
#[tokio::test]
async fn test_find_best_route_no_path_returns_error() {
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let token_c = token(0x03, "C");
let (market, manager) =
setup_market(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
let algorithm = MostLiquidAlgorithm::new();
let order = order(&token_a, &token_c, ONE_ETH, OrderSide::Sell);
let result = algorithm
.find_best_route(manager.graph(), market, None, &order)
.await;
assert!(matches!(result, Err(AlgorithmError::NoPath { .. })));
}
#[tokio::test]
async fn test_find_best_route_multi_hop() {
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let token_c = token(0x03, "C");
let (market, manager) = setup_market(vec![
("pool1", &token_a, &token_b, MockProtocolSim::new(2.0)),
("pool2", &token_b, &token_c, MockProtocolSim::new(3.0)),
]);
let algorithm = MostLiquidAlgorithm::with_config(
AlgorithmConfig::new(1, 2, Duration::from_millis(100), None).unwrap(),
)
.unwrap();
let order = order(&token_a, &token_c, ONE_ETH, OrderSide::Sell);
let result = algorithm
.find_best_route(manager.graph(), market, None, &order)
.await
.unwrap();
assert_eq!(result.route().swaps().len(), 2);
assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(ONE_ETH * 2));
assert_eq!(result.route().swaps()[0].component_id(), "pool1".to_string());
assert_eq!(*result.route().swaps()[1].amount_out(), BigUint::from(ONE_ETH * 2 * 3));
assert_eq!(result.route().swaps()[1].component_id(), "pool2".to_string());
}
#[tokio::test]
async fn test_find_best_route_skips_paths_without_edge_weights() {
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let mut market = SharedMarketData::new();
let pool1_state = MockProtocolSim::new(2.0);
let pool2_state = MockProtocolSim::new(3.0);
let pool1_comp = component("pool1", &[token_a.clone(), token_b.clone()]);
let pool2_comp = component("pool2", &[token_a.clone(), token_b.clone()]);
market.update_gas_price(BlockGasPrice {
block_number: 1,
block_hash: Default::default(),
block_timestamp: 0,
pricing: GasPrice::Legacy { gas_price: BigUint::from(1u64) },
});
market.upsert_components(vec![pool1_comp, pool2_comp]);
market.update_states(vec![
("pool1".to_string(), Box::new(pool1_state.clone()) as Box<dyn ProtocolSim>),
("pool2".to_string(), Box::new(pool2_state) as Box<dyn ProtocolSim>),
]);
market.upsert_tokens(vec![token_a.clone(), token_b.clone()]);
let mut manager = PetgraphStableDiGraphManager::default();
manager.initialize_graph(&market.component_topology());
let weight = DepthAndPrice::from_protocol_sim(&pool1_state, &token_a, &token_b).unwrap();
manager
.set_edge_weight(
&"pool1".to_string(),
&token_a.address,
&token_b.address,
weight,
false,
)
.unwrap();
let algorithm = MostLiquidAlgorithm::with_config(
AlgorithmConfig::new(1, 1, Duration::from_millis(100), None).unwrap(),
)
.unwrap();
let order = order(&token_a, &token_b, ONE_ETH, OrderSide::Sell);
let market = wrap_market(market);
let result = algorithm
.find_best_route(manager.graph(), market, None, &order)
.await
.unwrap();
assert_eq!(result.route().swaps().len(), 1);
assert_eq!(result.route().swaps()[0].component_id(), "pool1");
assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(ONE_ETH * 2));
}
#[tokio::test]
async fn test_find_best_route_no_scorable_paths() {
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let mut market = SharedMarketData::new();
let pool_state = MockProtocolSim::new(2.0);
let pool_comp = component("pool1", &[token_a.clone(), token_b.clone()]);
market.update_gas_price(BlockGasPrice {
block_number: 1,
block_hash: Default::default(),
block_timestamp: 0,
pricing: GasPrice::Eip1559 {
base_fee_per_gas: BigUint::from(1u64),
max_priority_fee_per_gas: BigUint::from(0u64),
},
});
market.upsert_components(vec![pool_comp]);
market.update_states(vec![(
"pool1".to_string(),
Box::new(pool_state) as Box<dyn ProtocolSim>,
)]);
market.upsert_tokens(vec![token_a.clone(), token_b.clone()]);
let mut manager = PetgraphStableDiGraphManager::default();
manager.initialize_graph(&market.component_topology());
let algorithm = MostLiquidAlgorithm::new();
let order = order(&token_a, &token_b, ONE_ETH, OrderSide::Sell);
let market = wrap_market(market);
let result = algorithm
.find_best_route(manager.graph(), market, None, &order)
.await;
assert!(matches!(
result,
Err(AlgorithmError::NoPath { reason: NoPathReason::NoScorablePaths, .. })
));
}
#[tokio::test]
async fn test_find_best_route_gas_exceeds_output_returns_negative_net() {
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let (market, manager) =
setup_market(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
let mut market_write = market.try_write().unwrap();
market_write.update_gas_price(BlockGasPrice {
block_number: 1,
block_hash: Default::default(),
block_timestamp: 0,
pricing: GasPrice::Eip1559 {
base_fee_per_gas: BigUint::from(1_000_000u64),
max_priority_fee_per_gas: BigUint::from(1_000_000u64),
},
});
drop(market_write);
let algorithm = MostLiquidAlgorithm::new();
let order = order(&token_a, &token_b, 1, OrderSide::Sell);
let derived = setup_derived_with_token_prices(std::slice::from_ref(&token_b.address));
let result = algorithm
.find_best_route(manager.graph(), market, Some(derived), &order)
.await
.expect("should return route even with negative net_amount_out");
assert_eq!(result.route().swaps().len(), 1);
assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(2u64));
let expected_net = BigInt::from(2) - BigInt::from(100_000_000_000u64);
assert_eq!(result.net_amount_out(), &expected_net);
}
#[tokio::test]
async fn test_find_best_route_insufficient_liquidity() {
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let (market, manager) = setup_market(vec![(
"pool1",
&token_a,
&token_b,
MockProtocolSim::new(2.0).with_liquidity(1000),
)]);
let algorithm = MostLiquidAlgorithm::new();
let order = order(&token_a, &token_b, ONE_ETH, OrderSide::Sell);
let result = algorithm
.find_best_route(manager.graph(), market, None, &order)
.await;
assert!(matches!(result, Err(AlgorithmError::InsufficientLiquidity)));
}
#[tokio::test]
async fn test_find_best_route_missing_gas_price_returns_error() {
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let mut market = SharedMarketData::new();
let pool_state = MockProtocolSim::new(2.0);
let pool_comp = component("pool1", &[token_a.clone(), token_b.clone()]);
market.upsert_components(vec![pool_comp]);
market.update_states(vec![(
"pool1".to_string(),
Box::new(pool_state.clone()) as Box<dyn ProtocolSim>,
)]);
market.upsert_tokens(vec![token_a.clone(), token_b.clone()]);
let mut manager = PetgraphStableDiGraphManager::default();
manager.initialize_graph(&market.component_topology());
let weight = DepthAndPrice::from_protocol_sim(&pool_state, &token_a, &token_b).unwrap();
manager
.set_edge_weight(
&"pool1".to_string(),
&token_a.address,
&token_b.address,
weight,
false,
)
.unwrap();
let algorithm = MostLiquidAlgorithm::new();
let order = order(&token_a, &token_b, ONE_ETH, OrderSide::Sell);
let market = wrap_market(market);
let result = algorithm
.find_best_route(manager.graph(), market, None, &order)
.await;
assert!(matches!(result, Err(AlgorithmError::DataNotFound { kind: "gas price", .. })));
}
#[tokio::test]
async fn test_find_best_route_circular_arbitrage() {
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let (market, manager) =
setup_market(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
let algorithm = MostLiquidAlgorithm::with_config(
AlgorithmConfig::new(2, 2, Duration::from_millis(100), None).unwrap(),
)
.unwrap();
let order = order(&token_a, &token_a, 100, OrderSide::Sell);
let result = algorithm
.find_best_route(manager.graph(), market, None, &order)
.await
.unwrap();
assert_eq!(result.route().swaps().len(), 2, "Should have 2 swaps for circular route");
assert_eq!(*result.route().swaps()[0].token_in(), token_a.address);
assert_eq!(*result.route().swaps()[0].token_out(), token_b.address);
assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(200u64));
assert_eq!(*result.route().swaps()[1].token_in(), token_b.address);
assert_eq!(*result.route().swaps()[1].token_out(), token_a.address);
assert_eq!(*result.route().swaps()[1].amount_out(), BigUint::from(66u64));
assert_eq!(result.route().swaps()[0].token_in(), result.route().swaps()[1].token_out());
}
#[tokio::test]
async fn test_find_best_route_respects_min_hops() {
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let token_c = token(0x03, "C");
let (market, manager) = setup_market(vec![
("pool_ab", &token_a, &token_b, MockProtocolSim::new(10.0)),
("pool_ac", &token_a, &token_c, MockProtocolSim::new(2.0)), ("pool_cb", &token_c, &token_b, MockProtocolSim::new(3.0)), ]);
let algorithm = MostLiquidAlgorithm::with_config(
AlgorithmConfig::new(2, 3, Duration::from_millis(100), None).unwrap(),
)
.unwrap();
let order = order(&token_a, &token_b, 100, OrderSide::Sell);
let derived = setup_derived_with_token_prices(std::slice::from_ref(&token_b.address));
let result = algorithm
.find_best_route(manager.graph(), market, Some(derived), &order)
.await
.unwrap();
assert_eq!(result.route().swaps().len(), 2, "Should use 2-hop path due to min_hops=2");
assert_eq!(result.route().swaps()[0].component_id(), "pool_ac");
assert_eq!(result.route().swaps()[1].component_id(), "pool_cb");
}
#[tokio::test]
async fn test_find_best_route_respects_max_hops() {
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let token_c = token(0x03, "C");
let (market, manager) = setup_market(vec![
("pool_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
("pool_bc", &token_b, &token_c, MockProtocolSim::new(3.0)),
]);
let algorithm = MostLiquidAlgorithm::with_config(
AlgorithmConfig::new(1, 1, Duration::from_millis(100), None).unwrap(),
)
.unwrap();
let order = order(&token_a, &token_c, 100, OrderSide::Sell);
let result = algorithm
.find_best_route(manager.graph(), market, None, &order)
.await;
assert!(
matches!(result, Err(AlgorithmError::NoPath { .. })),
"Should return NoPath when max_hops is insufficient"
);
}
#[tokio::test]
async fn test_find_best_route_timeout_returns_best_so_far() {
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let (market, manager) = setup_market(vec![
("pool1", &token_a, &token_b, MockProtocolSim::new(1.0)),
("pool2", &token_a, &token_b, MockProtocolSim::new(2.0)),
("pool3", &token_a, &token_b, MockProtocolSim::new(3.0)),
("pool4", &token_a, &token_b, MockProtocolSim::new(4.0)),
("pool5", &token_a, &token_b, MockProtocolSim::new(5.0)),
]);
let algorithm = MostLiquidAlgorithm::with_config(
AlgorithmConfig::new(1, 1, Duration::from_millis(0), None).unwrap(),
)
.unwrap();
let order = order(&token_a, &token_b, 100, OrderSide::Sell);
let result = algorithm
.find_best_route(manager.graph(), market, None, &order)
.await;
match result {
Ok(r) => {
assert_eq!(r.route().swaps().len(), 1);
}
Err(AlgorithmError::Timeout { .. }) => {
}
Err(e) => panic!("Unexpected error: {:?}", e),
}
}
#[rstest::rstest]
#[case::default_config(1, 3, 50)]
#[case::single_hop_only(1, 1, 100)]
#[case::multi_hop_min(2, 5, 200)]
#[case::zero_timeout(1, 3, 0)]
#[case::large_values(10, 100, 10000)]
fn test_algorithm_config_getters(
#[case] min_hops: usize,
#[case] max_hops: usize,
#[case] timeout_ms: u64,
) {
use crate::algorithm::Algorithm;
let algorithm = MostLiquidAlgorithm::with_config(
AlgorithmConfig::new(min_hops, max_hops, Duration::from_millis(timeout_ms), None)
.unwrap(),
)
.unwrap();
assert_eq!(algorithm.max_hops, max_hops);
assert_eq!(algorithm.timeout, Duration::from_millis(timeout_ms));
assert_eq!(algorithm.name(), "most_liquid");
}
#[test]
fn test_algorithm_default_config() {
use crate::algorithm::Algorithm;
let algorithm = MostLiquidAlgorithm::new();
assert_eq!(algorithm.max_hops, 3);
assert_eq!(algorithm.timeout, Duration::from_millis(500));
assert_eq!(algorithm.name(), "most_liquid");
}
#[tokio::test]
async fn test_find_best_route_respects_max_routes_cap() {
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let (market, manager) = setup_market(vec![
("pool1", &token_a, &token_b, MockProtocolSim::new(4.0).with_liquidity(1_000_000)),
("pool2", &token_a, &token_b, MockProtocolSim::new(3.0).with_liquidity(2_000_000)),
("pool3", &token_a, &token_b, MockProtocolSim::new(2.0).with_liquidity(3_000_000)),
("pool4", &token_a, &token_b, MockProtocolSim::new(1.0).with_liquidity(4_000_000)),
]);
let algorithm = MostLiquidAlgorithm::with_config(
AlgorithmConfig::new(1, 1, Duration::from_millis(100), Some(2)).unwrap(),
)
.unwrap();
let order = order(&token_a, &token_b, 1000, OrderSide::Sell);
let result = algorithm
.find_best_route(manager.graph(), market, None, &order)
.await
.unwrap();
assert_eq!(result.route().swaps().len(), 1);
assert_eq!(result.route().swaps()[0].component_id(), "pool3");
assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(2000u64));
}
#[tokio::test]
async fn test_find_best_route_no_cap_when_max_routes_is_none() {
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let (market, manager) = setup_market(vec![
("pool1", &token_a, &token_b, MockProtocolSim::new(4.0).with_liquidity(1_000_000)),
("pool2", &token_a, &token_b, MockProtocolSim::new(3.0).with_liquidity(2_000_000)),
("pool3", &token_a, &token_b, MockProtocolSim::new(2.0).with_liquidity(3_000_000)),
("pool4", &token_a, &token_b, MockProtocolSim::new(1.0).with_liquidity(4_000_000)),
]);
let algorithm = MostLiquidAlgorithm::with_config(
AlgorithmConfig::new(1, 1, Duration::from_millis(100), None).unwrap(),
)
.unwrap();
let order = order(&token_a, &token_b, 1000, OrderSide::Sell);
let result = algorithm
.find_best_route(manager.graph(), market, None, &order)
.await
.unwrap();
assert_eq!(result.route().swaps().len(), 1);
assert_eq!(result.route().swaps()[0].component_id(), "pool1");
assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(4000u64));
}
#[test]
fn test_algorithm_config_rejects_zero_max_routes() {
let result = AlgorithmConfig::new(1, 3, Duration::from_millis(100), Some(0));
assert!(matches!(
result,
Err(AlgorithmError::InvalidConfiguration { reason }) if reason.contains("max_routes must be at least 1")
));
}
#[test]
fn test_algorithm_config_rejects_zero_min_hops() {
let result = AlgorithmConfig::new(0, 3, Duration::from_millis(100), None);
assert!(matches!(
result,
Err(AlgorithmError::InvalidConfiguration { reason }) if reason.contains("min_hops must be at least 1")
));
}
#[test]
fn test_algorithm_config_rejects_min_greater_than_max() {
let result = AlgorithmConfig::new(5, 3, Duration::from_millis(100), None);
assert!(matches!(
result,
Err(AlgorithmError::InvalidConfiguration { reason }) if reason.contains("cannot exceed")
));
}
}