use std::time::{Duration, Instant};
use metrics::{counter, histogram};
use num_bigint::{BigInt, BigUint};
use num_traits::ToPrimitive;
use petgraph::stable_graph::NodeIndex;
use rustc_hash::{FxHashMap, FxHashSet};
use smallvec::SmallVec;
use tracing::{debug, instrument, trace};
use tycho_simulation::{
tycho_common::simulation::protocol_sim::{Price, ProtocolSim},
tycho_core::models::{token::Token, Address},
};
use super::{Algorithm, AlgorithmConfig, NoPathReason};
use crate::{
algorithm::{
path_scoring::{
rank_by_heuristic, simulate_token_path, FailedLegIx, HopResult, LegPools, PairWinners,
PoolQuote,
},
paths,
request::{SolveParts, SolveRequest},
sim_guard::GuardedProtocolSim,
swap_cache::{PoolDirection, Refusal, SwapCache, SwapResult},
},
derived::{computation::ComputationRequirements, types::TokenGasPrices},
feed::market_data::{MarketData, MarketState, StateLabel},
graph::{
GraphQueryFilter, RouteSearch, TokenPath, TopologyGraph, TopologyGraphManager, INLINE_EDGES,
},
types::{ComponentId, Route, RouteExclusions, RouteResult, Swap},
AlgorithmError,
};
const SELECTION: &str = "selection";
#[derive(Debug, thiserror::Error)]
enum MostLiquidError {
#[error("no pool between {from:?} and {to:?} could trade the amount")]
HopNotTradable { from: NodeIndex, to: NodeIndex },
#[error("token {0:?} not in the market subset")]
TokenMissing(NodeIndex),
}
type LegTokens<'a> = (&'a Token, &'a Token, Option<&'a Price>);
#[derive(Clone, Copy)]
struct SolveContext<'a> {
graph: &'a TopologyGraph<DepthAndPrice>,
market: &'a MarketState,
exclusions: &'a RouteExclusions,
token_prices: Option<&'a TokenGasPrices>,
amount_in: &'a BigUint,
gas_price: &'a BigUint,
start: Instant,
}
struct SolvedRoute {
hops: SmallVec<[HopResult; INLINE_EDGES]>,
net_amount_out: BigInt,
}
fn price_part(
value: &BigUint,
part: &'static str,
component_id: &ComponentId,
token_in: &Token,
) -> Option<f64> {
match value.to_f64() {
Some(v) if v > 0.0 => Some(v),
Some(_) => {
trace!(
component_id = %component_id,
token_in = %token_in.address,
part,
"token price part is zero, skipping edge"
);
None
}
None => {
trace!(
component_id = %component_id,
token_in = %token_in.address,
part,
"token price part overflows f64, skipping edge"
);
None
}
}
}
#[derive(Default)]
struct SolveReport {
paths_candidates: usize,
paths_to_simulate: usize,
paths_simulated: usize,
scoring_failures: usize,
simulation_failures: usize,
validation_failures: usize,
}
impl SolveReport {
fn coverage_pct(&self) -> f64 {
(self.paths_simulated as f64 / self.paths_to_simulate as f64) * 100.0
}
fn record(
&self,
best: Option<&RouteResult>,
market: &MarketState,
amount_in: &BigUint,
solve_time_ms: u64,
components_considered: usize,
) {
counter!("algorithm.scoring_failures").increment(self.scoring_failures as u64);
counter!("algorithm.simulation_failures").increment(self.simulation_failures as u64);
counter!("algorithm.validation_failures").increment(self.validation_failures as u64);
histogram!("algorithm.simulation_coverage_pct").record(self.coverage_pct());
let block_number = market
.last_updated()
.map(|b| b.number());
let tokens_considered = market.token_registry_ref().len();
let Some(result) = best else {
debug!(
solve_time_ms,
block_number,
paths_candidates = self.paths_candidates,
paths_to_simulate = self.paths_to_simulate,
paths_simulated = self.paths_simulated,
simulation_failures = self.simulation_failures,
validation_failures = self.validation_failures,
simulation_coverage_pct = self.coverage_pct(),
components_considered,
tokens_considered,
"no viable route"
);
return;
};
let path_desc = result
.route()
.path_description(market.token_registry_ref());
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 = self.paths_candidates,
paths_to_simulate = self.paths_to_simulate,
paths_simulated = self.paths_simulated,
simulation_failures = self.simulation_failures,
validation_failures = self.validation_failures,
simulation_coverage_pct = self.coverage_pct(),
components_considered,
tokens_considered,
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"
);
}
}
fn swap_on_route(
route: &Route,
token_prices: Option<&TokenGasPrices>,
gas_price: &BigUint,
) -> BigInt {
let Some(last) = route.swaps().last() else {
return BigInt::ZERO;
};
let amount_out = BigInt::from(last.amount_out().clone());
let Some(price) = token_prices.and_then(|prices| prices.get(last.token_out())) else {
return amount_out;
};
let mut gas = BigUint::ZERO;
for swap in route.swaps() {
gas += swap.gas_estimate();
}
amount_out - BigInt::from(gas * gas_price * &price.numerator / &price.denominator)
}
pub struct MostLiquidAlgorithm {
query: GraphQueryFilter,
timeout: Duration,
max_routes: Option<usize>,
cache_pair_swaps: bool,
}
#[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(any(test, feature = "test-utils"))]
pub fn from_protocol_sim<S: ProtocolSim + ?Sized>(
sim: &S,
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
.component_depths()
.and_then(|d| d.get(&key))
{
Some(d) => d.to_f64().unwrap_or(0.0),
None => {
trace!(component_id = %component_id, "component 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 = price_part(&price.numerator, "numerator", component_id, token_in)?;
let den = price_part(&price.denominator, "denominator", component_id, token_in)?;
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 {
query: GraphQueryFilter { min_hops: 1, max_hops: 3, connector_tokens: None },
timeout: Duration::from_millis(500),
max_routes: None,
cache_pair_swaps: true,
}
}
pub fn with_config(config: AlgorithmConfig) -> Result<Self, AlgorithmError> {
if config.min_hops() == 0 || config.min_hops() > config.max_hops() {
return Err(AlgorithmError::InvalidConfiguration {
reason: format!(
"invalid hop configuration: min_hops={} max_hops={}",
config.min_hops(),
config.max_hops()
),
});
}
Ok(Self {
query: GraphQueryFilter {
min_hops: config.min_hops(),
max_hops: config.max_hops(),
connector_tokens: config.connector_tokens().cloned(),
},
timeout: config.timeout(),
max_routes: config.max_routes(),
cache_pair_swaps: true,
})
}
fn build_route(
ctx: &SolveContext<'_>,
token_path: &[NodeIndex],
solved: &SolvedRoute,
) -> Result<Route, AlgorithmError> {
let SolveContext { graph, market, amount_in, .. } = *ctx;
let mut current_amount = amount_in.clone();
let mut swaps = Vec::with_capacity(solved.hops.len());
let mut tokens: FxHashMap<Address, Token> = FxHashMap::default();
let mut state_overrides: FxHashMap<ComponentId, Box<dyn ProtocolSim>> =
FxHashMap::default();
for (pair, hop) in token_path.windows(2).zip(&solved.hops) {
let address_in = &graph[pair[0]];
let address_out = &graph[pair[1]];
let token_in = paths::get_token(market, address_in)?;
let token_out = paths::get_token(market, address_out)?;
let component_id = graph
.pools_between(pair[0], pair[1])
.get(hop.pool_ix)
.map(|edge| &edge.component_id)
.ok_or_else(|| AlgorithmError::DataNotFound {
kind: "pool",
id: Some(format!("{address_in:?} -> {address_out:?}")),
})?;
let component = market
.get_component(component_id)
.ok_or_else(|| AlgorithmError::DataNotFound {
kind: "component",
id: Some(component_id.clone()),
})?;
let state = state_overrides
.get(component_id)
.map(Box::as_ref)
.or_else(|| market.get_simulation_state(component_id))
.ok_or_else(|| AlgorithmError::DataNotFound {
kind: "simulation state",
id: Some(component_id.clone()),
})?;
let result = state
.get_amount_out_guarded(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(),
));
tokens
.entry(token_in.address.clone())
.or_insert_with(|| token_in.clone());
tokens
.entry(token_out.address.clone())
.or_insert_with(|| token_out.clone());
state_overrides.insert(component_id.clone(), result.new_state);
current_amount = result.amount;
}
Ok(Route::new(swaps, tokens)?)
}
async fn snapshot_market_state(
graph: &TopologyGraph<DepthAndPrice>,
market: MarketData,
label: Option<StateLabel>,
scored_paths: &[(TokenPath, f64)],
exclusions: &RouteExclusions,
) -> Result<MarketState, AlgorithmError> {
let mut pairs: FxHashSet<(NodeIndex, NodeIndex)> = FxHashSet::default();
for (token_path, _) in scored_paths {
for pair in token_path.windows(2) {
pairs.insert((pair[0], pair[1]));
}
}
let mut component_ids: FxHashSet<&ComponentId> = FxHashSet::default();
for &(from, to) in &pairs {
for pool in graph.pools_between(from, to) {
if exclusions.excludes_pool(&pool.component_id) {
continue;
}
component_ids.insert(&pool.component_id);
}
}
let market = paths::read_market(&market, label).await?;
let market_subset = market.extract_subset_with_overlay(&component_ids);
drop(market);
Ok(market_subset)
}
fn solve_for_best_path(
&self,
scored_paths: &[(TokenPath, f64)],
report: &mut SolveReport,
ctx: &SolveContext,
) -> Result<RouteResult, AlgorithmError> {
let mut best_route: Option<(&TokenPath, SolvedRoute)> = None;
let mut winners = PairWinners::new(self.cache_pair_swaps);
let mut swaps = SwapCache::new();
let timeout_ms = self.timeout.as_millis() as u64;
for (token_path, _) in scored_paths {
let elapsed_ms = ctx.start.elapsed().as_millis() as u64;
if elapsed_ms > timeout_ms {
break;
}
let solved = match Self::solve_token_path(ctx, token_path, &mut winners, &mut swaps) {
Ok(solved) => solved,
Err(e) => {
trace!(error = %e, "could not solve path");
report.simulation_failures += 1;
continue;
}
};
if best_route
.as_ref()
.is_none_or(|(_, previous): &(&TokenPath, SolvedRoute)| {
solved.net_amount_out > previous.net_amount_out
})
{
best_route = Some((token_path, solved));
}
report.paths_simulated += 1;
}
let best = match best_route {
Some((token_path, solved)) => {
let route = Self::build_route(ctx, token_path, &solved)?;
if let Err(e) = route.validate() {
trace!(error = %e, "best route failed validation");
report.validation_failures += 1;
None
} else {
let amount_out = swap_on_route(&route, ctx.token_prices, ctx.gas_price);
Some(RouteResult::new(route, amount_out, ctx.gas_price.clone()))
}
}
None => None,
};
let solve_time_ms = ctx.start.elapsed().as_millis() as u64;
report.record(
best.as_ref(),
ctx.market,
ctx.amount_in,
solve_time_ms,
ctx.market.component_count(),
);
match best {
Some(best_route) => Ok(best_route),
None => {
if solve_time_ms > timeout_ms {
Err(AlgorithmError::Timeout { elapsed_ms: solve_time_ms })
} else {
Err(AlgorithmError::InsufficientLiquidity)
}
}
}
}
fn solve_token_path<'g>(
ctx: &SolveContext<'g>,
token_path: &[NodeIndex],
winners: &mut PairWinners,
swaps: &mut SwapCache<'g>,
) -> Result<SolvedRoute, MostLiquidError> {
let (graph, market, gas_price) = (ctx.graph, ctx.market, ctx.gas_price);
let mut legs: SmallVec<[LegPools<'g, DepthAndPrice, LegTokens<'g>>; INLINE_EDGES]> =
SmallVec::new();
for pair in token_path.windows(2) {
legs.push(LegPools {
pair: (pair[0], pair[1]),
pools: graph.pools_between(pair[0], pair[1]),
data: Self::get_pair_data(ctx, pair)?,
});
}
let scored =
simulate_token_path(&legs, ctx.amount_in, winners, |leg, amount, component_id| {
if ctx
.exclusions
.excludes_pool(component_id)
{
return None;
}
let (token_in, token_out, token_out_gas_price) = leg.data;
let paid = swaps.swap(
PoolDirection {
component_id,
address_in: &token_in.address,
address_out: &token_out.address,
},
amount,
SELECTION,
|| {
let Some(state) = market.get_simulation_state(component_id) else {
return Err(Refusal::Failed);
};
state
.get_amount_out_guarded(amount.clone(), token_in, token_out)
.map(|result| SwapResult { amount_out: result.amount, gas: result.gas })
.map_err(|error| Refusal::of(&error))
},
true,
)?;
let net = match token_out_gas_price {
Some(price) => {
let cost = &paid.gas * gas_price * &price.numerator / &price.denominator;
BigInt::from(paid.amount_out.clone()) - BigInt::from(cost)
}
None => BigInt::from(paid.amount_out.clone()),
};
Some(PoolQuote { paid, net })
})
.map_err(|FailedLegIx(leg_ix)| MostLiquidError::HopNotTradable {
from: legs[leg_ix].pair.0,
to: legs[leg_ix].pair.1,
})?;
let net_amount_out = match legs.last().and_then(|leg| leg.data.2) {
Some(price) => {
let cost = scored.gas * ctx.gas_price * &price.numerator / &price.denominator;
BigInt::from(scored.amount_out) - BigInt::from(cost)
}
None => BigInt::from(scored.amount_out),
};
Ok(SolvedRoute { hops: scored.hops, net_amount_out })
}
fn get_pair_data<'a>(
ctx: &SolveContext<'a>,
pair: &[NodeIndex],
) -> Result<LegTokens<'a>, MostLiquidError> {
let token_in = ctx
.market
.get_token(&ctx.graph[pair[0]])
.ok_or(MostLiquidError::TokenMissing(pair[0]))?;
let token_out = ctx
.market
.get_token(&ctx.graph[pair[1]])
.ok_or(MostLiquidError::TokenMissing(pair[1]))?;
let token_out_gas_price = ctx
.token_prices
.and_then(|prices| prices.get(&token_out.address));
Ok((token_in, token_out, token_out_gas_price))
}
}
impl Default for MostLiquidAlgorithm {
fn default() -> Self {
Self::new()
}
}
impl Algorithm for MostLiquidAlgorithm {
type GraphType = TopologyGraph<DepthAndPrice>;
type GraphManager = TopologyGraphManager<DepthAndPrice>;
fn name(&self) -> &str {
"most_liquid"
}
#[instrument(level = "debug", skip_all, fields(order_id = %request.order().id()))]
async fn find_best_route(
&self,
request: SolveRequest<'_, Self::GraphType>,
) -> Result<RouteResult, AlgorithmError> {
let SolveParts { graph, order, market, label, derived, exclusions } = request.into_parts();
let start = Instant::now();
if !order.is_sell() {
return Err(AlgorithmError::ExactOutNotSupported);
}
let token_prices = match derived.as_ref() {
Some(derived) => derived
.read()
.await
.token_prices_shared(),
None => None,
};
let amount_in = order.amount().clone();
let search = RouteSearch { bounds: &self.query, exclusions: &exclusions };
let all_paths =
paths::find_token_paths(graph, order.token_in(), order.token_out(), search)?;
let n_paths = all_paths.len();
let no_path = |reason| AlgorithmError::NoPath {
from: order.token_in().clone(),
to: order.token_out().clone(),
reason,
};
if all_paths.is_empty() {
return Err(no_path(NoPathReason::NoGraphPath));
}
let mut scored_paths = rank_by_heuristic(graph, all_paths, &exclusions);
if scored_paths.is_empty() {
return Err(no_path(NoPathReason::NoScorablePaths));
}
let mut report = SolveReport {
paths_candidates: n_paths,
scoring_failures: n_paths - scored_paths.len(),
..SolveReport::default()
};
if let Some(max_routes) = self.max_routes {
scored_paths.truncate(max_routes);
}
report.paths_to_simulate = scored_paths.len();
let market =
Self::snapshot_market_state(graph, market, label, &scored_paths, &exclusions).await?;
let gas_price = paths::fetch_gas_price(&market)?;
let ctx = SolveContext {
graph,
market: &market,
token_prices: token_prices.as_deref(),
amount_in: &amount_in,
gas_price: &gas_price,
start,
exclusions: &exclusions,
};
self.solve_for_best_path(&scored_paths, &mut report, &ctx)
}
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 tycho_simulation::{
tycho_core::simulation::protocol_sim::Price,
tycho_ethereum::gas::{BlockGasPrice, GasPrice},
};
use super::*;
use crate::{
algorithm::test_utils::{
addr, component, order, setup_market_weighted, token, MockProtocolSim, ONE_ETH,
},
derived::{
computation::{FailedItem, FailedItemError},
types::TokenGasPrices,
DerivedData, SharedDerivedDataRef,
},
graph::GraphManager,
types::OrderSide,
};
fn wrap_market(market: MarketState) -> MarketData {
MarketData::new(std::sync::Arc::new(tokio::sync::RwLock::new(market)))
}
fn setup_derived_with_token_prices(token_addresses: &[Address]) -> SharedDerivedDataRef {
let mut token_prices: TokenGasPrices = FxHashMap::default();
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);
std::sync::Arc::new(tokio::sync::RwLock::new(derived_data))
}
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::default();
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("component1", 0x01, 0x02);
let key_str = pair_key_str("component1", 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_component_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_component_depth_returns_none() {
let key = pair_key("component1", 0x01, 0x02);
let key_str = pair_key_str("component1", 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_component_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("component1", 0x01, 0x02);
let key_str = pair_key_str("component1", 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_component_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("component1", 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::ComponentDepths::default();
depths.insert(key.clone(), BigUint::from(1000u64));
derived.set_component_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("component1", 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::ComponentDepths::default();
depths.insert(key.clone(), BigUint::from(2_000_000u64));
derived.set_component_depths(depths, vec![], 10, true);
let mut token_prices = TokenGasPrices::default();
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("component1", 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::ComponentDepths::default();
depths.insert(key.clone(), BigUint::from(500u64));
derived.set_component_depths(depths, vec![], 10, true);
let mut token_prices = TokenGasPrices::default();
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
);
}
#[tokio::test]
async fn test_find_best_route_with_excluded_endpoints() {
let a = token(0x01, "A");
let b = token(0x02, "B");
let c = token(0x03, "C");
let (market, manager) = setup_market_weighted(vec![
("ab", &a, &b, MockProtocolSim::new(2.0)),
("bc", &b, &c, MockProtocolSim::new(2.0)),
]);
let order = order(&a, &c, 1000, OrderSide::Sell);
let result = MostLiquidAlgorithm::with_config(
AlgorithmConfig::new(1, 2, Duration::from_secs(1), None).unwrap(),
)
.unwrap()
.find_best_route(SolveRequest::new(manager.graph(), market, &order).with_exclusions(
RouteExclusions::default().with_tokens([a.address.clone(), c.address.clone()]),
))
.await
.unwrap();
assert_eq!(result.route().swaps().len(), 2);
assert_eq!(result.route().swaps()[0].component_id(), "ab");
assert_eq!(result.route().swaps()[1].component_id(), "bc");
}
#[tokio::test]
async fn test_find_best_route_with_excluded_pool() {
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let (market, manager) = setup_market_weighted(vec![
("best", &token_a, &token_b, MockProtocolSim::new(3.0)),
("second", &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, 1000, OrderSide::Sell);
let result = algorithm
.find_best_route(
SolveRequest::new(manager.graph(), market, &order)
.with_exclusions(RouteExclusions::default().with_pools(["best".to_string()])),
)
.await
.unwrap();
assert_eq!(result.route().swaps()[0].component_id(), "second");
}
#[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_weighted(vec![(
"component1",
&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(SolveRequest::new(manager.graph(), market, &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_weighted(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(
SolveRequest::new(manager.graph(), market, &order).with_derived(derived),
)
.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_weighted(vec![(
"component1",
&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(SolveRequest::new(manager.graph(), market, &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_weighted(vec![
("component1", &token_a, &token_b, MockProtocolSim::new(2.0)),
("component2", &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(SolveRequest::new(manager.graph(), market, &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(), "component1".to_string());
assert_eq!(*result.route().swaps()[1].amount_out(), BigUint::from(ONE_ETH * 2 * 3));
assert_eq!(result.route().swaps()[1].component_id(), "component2".to_string());
}
fn setup_market_multi_token(
components: Vec<(&str, Vec<Token>, MockProtocolSim)>,
tokens: &[Token],
) -> (MarketData, TopologyGraphManager<DepthAndPrice>) {
let mut market = MarketState::new();
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_tokens(tokens.to_vec());
let protocol_components: Vec<_> = components
.iter()
.map(|(id, tokens, _)| component(id, tokens))
.collect();
market.upsert_components(protocol_components);
let states: Vec<_> = components
.into_iter()
.map(|(id, _, sim)| (id.to_string(), Box::new(sim) as Box<dyn ProtocolSim>))
.collect();
market.update_states(states);
let mut manager = TopologyGraphManager::default();
manager.initialize_graph(&market.component_topology());
(wrap_market(market), manager)
}
#[tokio::test]
async fn test_find_best_route_never_takes_one_pool_twice() {
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let token_c = token(0x03, "C");
let (market, manager) = setup_market_multi_token(
vec![
(
"multi",
vec![token_a.clone(), token_b.clone(), token_c.clone()],
MockProtocolSim::new(3.0),
),
("bc", vec![token_b.clone(), token_c.clone()], MockProtocolSim::new(2.0)),
],
&[token_a.clone(), token_b.clone(), token_c.clone()],
);
let algorithm = MostLiquidAlgorithm::with_config(
AlgorithmConfig::new(2, 2, Duration::from_millis(100), None).unwrap(),
)
.unwrap();
let order = order(&token_a, &token_c, 1000, OrderSide::Sell);
let result = algorithm
.find_best_route(SolveRequest::new(manager.graph(), market, &order))
.await
.unwrap();
assert_eq!(result.route().swaps().len(), 2);
assert_eq!(result.route().swaps()[0].component_id(), "multi");
assert_eq!(result.route().swaps()[1].component_id(), "bc");
assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(3000u64));
assert_eq!(*result.route().swaps()[1].amount_out(), BigUint::from(6000u64));
}
#[tokio::test]
async fn test_find_best_route_drops_sequence_with_no_pool_left() {
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let token_c = token(0x03, "C");
let (market, manager) = setup_market_multi_token(
vec![(
"multi",
vec![token_a.clone(), token_b.clone(), token_c.clone()],
MockProtocolSim::new(3.0),
)],
&[token_a.clone(), token_b.clone(), token_c.clone()],
);
let algorithm = MostLiquidAlgorithm::with_config(
AlgorithmConfig::new(2, 2, Duration::from_millis(100), None).unwrap(),
)
.unwrap();
let order = order(&token_a, &token_c, 1000, OrderSide::Sell);
let result = algorithm
.find_best_route(SolveRequest::new(manager.graph(), market, &order))
.await;
assert!(matches!(result, Err(AlgorithmError::InsufficientLiquidity)));
}
#[tokio::test]
async fn test_find_best_route_uses_pools_without_edge_weights() {
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let mut market = MarketState::new();
let component1_state = MockProtocolSim::new(2.0);
let component2_state = MockProtocolSim::new(3.0);
let component1_comp = component("component1", &[token_a.clone(), token_b.clone()]);
let component2_comp = component("component2", &[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![component1_comp, component2_comp]);
market.update_states(vec![
("component1".to_string(), Box::new(component1_state.clone()) as Box<dyn ProtocolSim>),
("component2".to_string(), Box::new(component2_state) as Box<dyn ProtocolSim>),
]);
market.upsert_tokens(vec![token_a.clone(), token_b.clone()]);
let mut manager = TopologyGraphManager::default();
manager.initialize_graph(&market.component_topology());
let weight =
DepthAndPrice::from_protocol_sim(&component1_state, &token_a, &token_b).unwrap();
manager
.set_pool_weight(
&"component1".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(SolveRequest::new(manager.graph(), market, &order))
.await
.unwrap();
assert_eq!(result.route().swaps().len(), 1);
assert_eq!(result.route().swaps()[0].component_id(), "component2");
assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(ONE_ETH * 3));
}
#[tokio::test]
async fn test_find_best_route_without_any_derived_data() {
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let mut market = MarketState::new();
let component_state = MockProtocolSim::new(2.0);
let comp = component("component1", &[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![comp]);
market.update_states(vec![(
"component1".to_string(),
Box::new(component_state) as Box<dyn ProtocolSim>,
)]);
market.upsert_tokens(vec![token_a.clone(), token_b.clone()]);
let mut manager = TopologyGraphManager::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(SolveRequest::new(manager.graph(), market, &order))
.await
.expect("an unranked route is still a route");
assert_eq!(result.route().swaps().len(), 1);
assert_eq!(result.route().swaps()[0].component_id(), "component1");
assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(ONE_ETH * 2));
}
#[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_weighted(vec![(
"component1",
&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(
SolveRequest::new(manager.graph(), market, &order).with_derived(derived),
)
.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_weighted(vec![(
"component1",
&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(SolveRequest::new(manager.graph(), market, &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 = MarketState::new();
let component_state = MockProtocolSim::new(2.0);
let comp = component("component1", &[token_a.clone(), token_b.clone()]);
market.upsert_components(vec![comp]);
market.update_states(vec![(
"component1".to_string(),
Box::new(component_state.clone()) as Box<dyn ProtocolSim>,
)]);
market.upsert_tokens(vec![token_a.clone(), token_b.clone()]);
let mut manager = TopologyGraphManager::default();
manager.initialize_graph(&market.component_topology());
let weight =
DepthAndPrice::from_protocol_sim(&component_state, &token_a, &token_b).unwrap();
manager
.set_pool_weight(
&"component1".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(SolveRequest::new(manager.graph(), market, &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_weighted(vec![
("component1", &token_a, &token_b, MockProtocolSim::new(2.0)),
("component2", &token_a, &token_b, MockProtocolSim::new(3.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(SolveRequest::new(manager.graph(), market, &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].component_id(), "component2");
assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(300u64));
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].component_id(), "component1");
assert_eq!(*result.route().swaps()[1].amount_out(), BigUint::from(150u64));
assert_eq!(result.route().swaps()[0].token_in(), result.route().swaps()[1].token_out());
}
#[tokio::test]
async fn test_find_best_route_circular_needs_two_pools() {
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let (market, manager) = setup_market_weighted(vec![(
"component1",
&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(SolveRequest::new(manager.graph(), market, &order))
.await;
assert!(matches!(result, Err(AlgorithmError::InsufficientLiquidity)));
}
#[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_weighted(vec![
("component_ab", &token_a, &token_b, MockProtocolSim::new(10.0)),
("component_ac", &token_a, &token_c, MockProtocolSim::new(2.0)), ("component_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(
SolveRequest::new(manager.graph(), market, &order).with_derived(derived),
)
.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(), "component_ac");
assert_eq!(result.route().swaps()[1].component_id(), "component_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_weighted(vec![
("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
("component_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(SolveRequest::new(manager.graph(), market, &order))
.await;
assert!(
matches!(result, Err(AlgorithmError::NoPath { reason: NoPathReason::NoGraphPath, .. })),
"Should fail 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_weighted(vec![
("component1", &token_a, &token_b, MockProtocolSim::new(1.0)),
("component2", &token_a, &token_b, MockProtocolSim::new(2.0)),
("component3", &token_a, &token_b, MockProtocolSim::new(3.0)),
("component4", &token_a, &token_b, MockProtocolSim::new(4.0)),
("component5", &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(SolveRequest::new(manager.graph(), market, &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.query.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.query.max_hops, 3);
assert_eq!(algorithm.timeout, Duration::from_millis(500));
assert_eq!(algorithm.name(), "most_liquid");
}
#[tokio::test]
async fn test_each_leg_takes_its_best_paying_pool() {
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let token_c = token(0x03, "C");
let (market, manager) = setup_market_weighted(vec![
("ab1", &token_a, &token_b, MockProtocolSim::new(1.0).with_liquidity(9_000_000)),
("ab2", &token_a, &token_b, MockProtocolSim::new(3.0).with_liquidity(1_000_000)),
("bc1", &token_b, &token_c, MockProtocolSim::new(1.0).with_liquidity(9_000_000)),
("bc2", &token_b, &token_c, MockProtocolSim::new(5.0).with_liquidity(1_000_000)),
]);
let algorithm = MostLiquidAlgorithm::with_config(
AlgorithmConfig::new(2, 2, Duration::from_millis(100), None).unwrap(),
)
.unwrap();
let order = order(&token_a, &token_c, 1000, OrderSide::Sell);
let result = algorithm
.find_best_route(SolveRequest::new(manager.graph(), market, &order))
.await
.unwrap();
let chosen: Vec<&str> = result
.route()
.swaps()
.iter()
.map(|swap| swap.component_id())
.collect();
assert_eq!(chosen, vec!["ab2", "bc2"], "each hop must take its best-paying pool");
assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(3000u64));
assert_eq!(*result.route().swaps()[1].amount_in(), BigUint::from(3000u64));
assert_eq!(*result.route().swaps()[1].amount_out(), BigUint::from(15000u64));
}
#[tokio::test]
async fn test_max_routes_caps_token_sequences_not_pools_on_a_pair() {
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let (market, manager) = setup_market_weighted(vec![
("component1", &token_a, &token_b, MockProtocolSim::new(4.0).with_liquidity(1_000_000)),
("component2", &token_a, &token_b, MockProtocolSim::new(3.0).with_liquidity(2_000_000)),
("component3", &token_a, &token_b, MockProtocolSim::new(2.0).with_liquidity(3_000_000)),
("component4", &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(SolveRequest::new(manager.graph(), market, &order))
.await
.unwrap();
assert_eq!(result.route().swaps().len(), 1);
assert_eq!(result.route().swaps()[0].component_id(), "component1");
assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(4000u64));
}
#[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_weighted(vec![
("component1", &token_a, &token_b, MockProtocolSim::new(4.0).with_liquidity(1_000_000)),
("component2", &token_a, &token_b, MockProtocolSim::new(3.0).with_liquidity(2_000_000)),
("component3", &token_a, &token_b, MockProtocolSim::new(2.0).with_liquidity(3_000_000)),
("component4", &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(SolveRequest::new(manager.graph(), market, &order))
.await
.unwrap();
assert_eq!(result.route().swaps().len(), 1);
assert_eq!(result.route().swaps()[0].component_id(), "component1");
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")
));
}
}