1use std::time::{Duration, Instant};
13
14use metrics::{counter, histogram};
15use num_bigint::{BigInt, BigUint};
16use num_traits::ToPrimitive;
17use petgraph::stable_graph::NodeIndex;
18use rustc_hash::{FxHashMap, FxHashSet};
19use smallvec::SmallVec;
20use tracing::{debug, instrument, trace};
21use tycho_simulation::{
22 tycho_common::simulation::protocol_sim::{Price, ProtocolSim},
23 tycho_core::models::{token::Token, Address},
24};
25
26use super::{Algorithm, AlgorithmConfig, NoPathReason};
27use crate::{
28 algorithm::{
29 paths,
30 sim_guard::GuardedProtocolSim,
31 swap_cache::{PoolDirection, Refusal, SwapCache, SwapResult},
32 },
33 derived::{computation::ComputationRequirements, types::TokenGasPrices, SharedDerivedDataRef},
34 feed::market_data::{MarketData, MarketState, StateLabel},
35 graph::{
36 EdgeData, GraphQueryFilter, TokenPath, TopologyGraph, TopologyGraphManager, INLINE_EDGES,
37 },
38 types::{ComponentId, Order, Route, RouteResult, Swap},
39 AlgorithmError,
40};
41
42const SELECTION: &str = "selection";
45
46#[derive(Debug, thiserror::Error)]
51enum MostLiquidError {
52 #[error("no pool between {from:?} and {to:?} could trade the amount")]
54 HopNotTradable { from: NodeIndex, to: NodeIndex },
55 #[error("token {0:?} not in the market subset")]
57 TokenMissing(NodeIndex),
58}
59
60#[derive(Clone, Copy)]
65struct SolveContext<'a> {
66 graph: &'a TopologyGraph<DepthAndPrice>,
67 market: &'a MarketState,
68 token_prices: Option<&'a TokenGasPrices>,
69 amount_in: &'a BigUint,
70 gas_price: &'a BigUint,
72 start: Instant,
74}
75
76struct SolvedRoute {
78 hops: SmallVec<[HopResult; INLINE_EDGES]>,
79 net_amount_out: BigInt,
83}
84
85fn price_part(
90 value: &BigUint,
91 part: &'static str,
92 component_id: &ComponentId,
93 token_in: &Token,
94) -> Option<f64> {
95 match value.to_f64() {
96 Some(v) if v > 0.0 => Some(v),
97 Some(_) => {
98 trace!(
99 component_id = %component_id,
100 token_in = %token_in.address,
101 part,
102 "token price part is zero, skipping edge"
103 );
104 None
105 }
106 None => {
107 trace!(
108 component_id = %component_id,
109 token_in = %token_in.address,
110 part,
111 "token price part overflows f64, skipping edge"
112 );
113 None
114 }
115 }
116}
117
118#[derive(Default)]
123struct SolveReport {
124 paths_candidates: usize,
126 paths_to_simulate: usize,
128 paths_simulated: usize,
130 scoring_failures: usize,
132 simulation_failures: usize,
134 validation_failures: usize,
136}
137
138impl SolveReport {
139 fn coverage_pct(&self) -> f64 {
141 (self.paths_simulated as f64 / self.paths_to_simulate as f64) * 100.0
142 }
143
144 fn record(
146 &self,
147 best: Option<&RouteResult>,
148 market: &MarketState,
149 amount_in: &BigUint,
150 solve_time_ms: u64,
151 components_considered: usize,
152 ) {
153 counter!("algorithm.scoring_failures").increment(self.scoring_failures as u64);
154 counter!("algorithm.simulation_failures").increment(self.simulation_failures as u64);
155 counter!("algorithm.validation_failures").increment(self.validation_failures as u64);
156 histogram!("algorithm.simulation_coverage_pct").record(self.coverage_pct());
157
158 let block_number = market
159 .last_updated()
160 .map(|b| b.number());
161 let tokens_considered = market.token_registry_ref().len();
162 let Some(result) = best else {
163 debug!(
164 solve_time_ms,
165 block_number,
166 paths_candidates = self.paths_candidates,
167 paths_to_simulate = self.paths_to_simulate,
168 paths_simulated = self.paths_simulated,
169 simulation_failures = self.simulation_failures,
170 validation_failures = self.validation_failures,
171 simulation_coverage_pct = self.coverage_pct(),
172 components_considered,
173 tokens_considered,
174 "no viable route"
175 );
176 return;
177 };
178
179 let path_desc = result
180 .route()
181 .path_description(market.token_registry_ref());
182 let protocols = result
183 .route()
184 .swaps()
185 .iter()
186 .map(|s| s.protocol())
187 .collect::<Vec<_>>();
188 let price = amount_in
189 .to_f64()
190 .filter(|&v| v > 0.0)
191 .and_then(|amt_in| {
192 result
193 .net_amount_out()
194 .to_f64()
195 .map(|amt_out| amt_out / amt_in)
196 })
197 .unwrap_or(f64::NAN);
198
199 debug!(
200 solve_time_ms,
201 block_number,
202 paths_candidates = self.paths_candidates,
203 paths_to_simulate = self.paths_to_simulate,
204 paths_simulated = self.paths_simulated,
205 simulation_failures = self.simulation_failures,
206 validation_failures = self.validation_failures,
207 simulation_coverage_pct = self.coverage_pct(),
208 components_considered,
209 tokens_considered,
210 path = %path_desc,
211 amount_in = %amount_in,
212 net_amount_out = %result.net_amount_out(),
213 price_out_per_in = price,
214 hop_count = result.route().swaps().len(),
215 protocols = ?protocols,
216 "route found"
217 );
218 }
219}
220
221fn swap_on_route(
228 route: &Route,
229 token_prices: Option<&TokenGasPrices>,
230 gas_price: &BigUint,
231) -> BigInt {
232 let Some(last) = route.swaps().last() else {
233 return BigInt::ZERO;
234 };
235 let amount_out = BigInt::from(last.amount_out().clone());
236
237 let Some(price) = token_prices.and_then(|prices| prices.get(last.token_out())) else {
238 return amount_out;
239 };
240 let mut gas = BigUint::ZERO;
241 for swap in route.swaps() {
242 gas += swap.gas_estimate();
243 }
244
245 amount_out - BigInt::from(gas * gas_price * &price.numerator / &price.denominator)
246}
247
248#[derive(Clone)]
253struct HopResult {
254 pool_ix: usize,
256 amount_out: BigUint,
258 gas: BigUint,
260}
261
262pub struct MostLiquidAlgorithm {
264 query: GraphQueryFilter,
267 timeout: Duration,
268 max_routes: Option<usize>,
269 cache_pair_swaps: bool,
270}
271
272#[derive(Debug, Clone, Default)]
278pub struct DepthAndPrice {
279 pub spot_price: f64,
281 pub depth: f64,
283}
284
285impl DepthAndPrice {
286 #[cfg(test)]
288 pub fn new(spot_price: f64, depth: f64) -> Self {
289 Self { spot_price, depth }
290 }
291
292 #[cfg(test)]
294 pub fn from_protocol_sim<S: ProtocolSim + ?Sized>(
295 sim: &S,
296 token_in: &Token,
297 token_out: &Token,
298 ) -> Result<Self, AlgorithmError> {
299 Ok(Self {
300 spot_price: sim
301 .spot_price(token_in, token_out)
302 .map_err(|e| {
303 AlgorithmError::Other(format!("missing spot price for DepthAndPrice: {:?}", e))
304 })?,
305 depth: sim
306 .get_limits(token_in.address.clone(), token_out.address.clone())
307 .map_err(|e| {
308 AlgorithmError::Other(format!("missing depth for DepthAndPrice: {:?}", e))
309 })?
310 .0
311 .to_f64()
312 .ok_or_else(|| {
313 AlgorithmError::Other("depth conversion to f64 failed".to_string())
314 })?,
315 })
316 }
317}
318
319impl crate::graph::EdgeWeightFromSimAndDerived for DepthAndPrice {
320 fn from_sim_and_derived(
321 _sim: &dyn ProtocolSim,
322 component_id: &ComponentId,
323 token_in: &Token,
324 token_out: &Token,
325 derived: &crate::derived::DerivedData,
326 ) -> Option<Self> {
327 let key = (component_id.clone(), token_in.address.clone(), token_out.address.clone());
328
329 let spot_price = match derived
331 .spot_prices()
332 .and_then(|p| p.get(&key).copied())
333 {
334 Some(p) => p,
335 None => {
336 trace!(component_id = %component_id, "spot price not found, skipping edge");
337 return None;
338 }
339 };
340
341 let raw_depth = match derived
343 .component_depths()
344 .and_then(|d| d.get(&key))
345 {
346 Some(d) => d.to_f64().unwrap_or(0.0),
347 None => {
348 trace!(component_id = %component_id, "component depth not found, skipping edge");
349 return None;
350 }
351 };
352
353 let depth = match derived
358 .token_prices()
359 .and_then(|p| p.get(&token_in.address))
360 {
361 Some(price) => {
362 let num = price_part(&price.numerator, "numerator", component_id, token_in)?;
363 let den = price_part(&price.denominator, "denominator", component_id, token_in)?;
364 raw_depth * den / num
365 }
366 None => {
367 trace!(
368 component_id = %component_id,
369 token_in = %token_in.address,
370 "token price not found, skipping edge"
371 );
372 return None;
373 }
374 };
375
376 Some(Self { spot_price, depth })
377 }
378}
379
380impl MostLiquidAlgorithm {
381 pub fn new() -> Self {
383 Self {
384 query: GraphQueryFilter { min_hops: 1, max_hops: 3, connector_tokens: None },
385 timeout: Duration::from_millis(500),
386 max_routes: None,
387 cache_pair_swaps: true,
388 }
389 }
390
391 pub fn with_config(config: AlgorithmConfig) -> Result<Self, AlgorithmError> {
393 if config.min_hops() == 0 || config.min_hops() > config.max_hops() {
394 return Err(AlgorithmError::InvalidConfiguration {
395 reason: format!(
396 "invalid hop configuration: min_hops={} max_hops={}",
397 config.min_hops(),
398 config.max_hops()
399 ),
400 });
401 }
402
403 Ok(Self {
404 query: GraphQueryFilter {
405 min_hops: config.min_hops(),
406 max_hops: config.max_hops(),
407 connector_tokens: config.connector_tokens().cloned(),
408 },
409 timeout: config.timeout(),
410 max_routes: config.max_routes(),
411 cache_pair_swaps: true,
412 })
413 }
414
415 fn score_token_path(
434 graph: &TopologyGraph<DepthAndPrice>,
435 token_path: &[NodeIndex],
436 ) -> Option<f64> {
437 if token_path.len() < 2 {
438 return None;
439 }
440
441 let mut price = 1.0;
442 let mut min_depth = f64::MAX;
443
444 for pair in token_path.windows(2) {
445 let pools = graph.pools_between(pair[0], pair[1]);
446 if pools.is_empty() {
447 return None;
448 }
449
450 let mut best_price = f64::MIN;
451 let mut best_depth = f64::MIN;
452 for pool in pools {
453 if let Some(data) = pool.data.as_ref() {
454 best_price = best_price.max(data.spot_price);
455 best_depth = best_depth.max(data.depth);
456 }
457 }
458
459 if best_price == f64::MIN {
460 min_depth = 0.0;
463 } else {
464 price *= best_price;
465 min_depth = min_depth.min(best_depth);
466 }
467 }
468
469 Some(price * min_depth)
470 }
471
472 fn build_route(
480 ctx: &SolveContext<'_>,
481 token_path: &[NodeIndex],
482 solved: &SolvedRoute,
483 ) -> Result<Route, AlgorithmError> {
484 let SolveContext { graph, market, amount_in, .. } = *ctx;
485 let mut current_amount = amount_in.clone();
486 let mut swaps = Vec::with_capacity(solved.hops.len());
487 let mut tokens: FxHashMap<Address, Token> = FxHashMap::default();
488 let mut state_overrides: FxHashMap<ComponentId, Box<dyn ProtocolSim>> =
489 FxHashMap::default();
490
491 for (pair, hop) in token_path.windows(2).zip(&solved.hops) {
492 let address_in = &graph[pair[0]];
493 let address_out = &graph[pair[1]];
494 let token_in = paths::get_token(market, address_in)?;
495 let token_out = paths::get_token(market, address_out)?;
496
497 let component_id = graph
498 .pools_between(pair[0], pair[1])
499 .get(hop.pool_ix)
500 .map(|edge| &edge.component_id)
501 .ok_or_else(|| AlgorithmError::DataNotFound {
502 kind: "pool",
503 id: Some(format!("{address_in:?} -> {address_out:?}")),
504 })?;
505 let component = market
506 .get_component(component_id)
507 .ok_or_else(|| AlgorithmError::DataNotFound {
508 kind: "component",
509 id: Some(component_id.clone()),
510 })?;
511 let state = state_overrides
512 .get(component_id)
513 .map(Box::as_ref)
514 .or_else(|| market.get_simulation_state(component_id))
515 .ok_or_else(|| AlgorithmError::DataNotFound {
516 kind: "simulation state",
517 id: Some(component_id.clone()),
518 })?;
519 let result = state
520 .get_amount_out_guarded(current_amount.clone(), token_in, token_out)
521 .map_err(|e| AlgorithmError::Other(format!("simulation error: {e:?}")))?;
522
523 swaps.push(Swap::new(
524 component_id.clone(),
525 component.protocol_system.clone(),
526 token_in.address.clone(),
527 token_out.address.clone(),
528 current_amount.clone(),
529 result.amount.clone(),
530 result.gas,
531 component.clone(),
532 state.clone_box(),
533 ));
534 tokens
535 .entry(token_in.address.clone())
536 .or_insert_with(|| token_in.clone());
537 tokens
538 .entry(token_out.address.clone())
539 .or_insert_with(|| token_out.clone());
540
541 state_overrides.insert(component_id.clone(), result.new_state);
542 current_amount = result.amount;
543 }
544
545 Ok(Route::new(swaps, tokens)?)
546 }
547
548 fn score_paths(
553 graph: &TopologyGraph<DepthAndPrice>,
554 all_paths: Vec<TokenPath>,
555 ) -> Vec<(TokenPath, f64)> {
556 let mut scored_paths: Vec<(TokenPath, f64)> = all_paths
557 .into_iter()
558 .filter_map(|path| {
559 let score = Self::score_token_path(graph, &path)?;
560 Some((path, score))
561 })
562 .collect();
563
564 scored_paths.sort_by(|(_, a_score), (_, b_score)| {
565 b_score
567 .partial_cmp(a_score)
568 .unwrap_or(std::cmp::Ordering::Equal)
569 });
570
571 scored_paths
572 }
573
574 async fn snapshot_market_state(
575 graph: &TopologyGraph<DepthAndPrice>,
576 market: MarketData,
577 label: Option<StateLabel>,
578 scored_paths: &[(TokenPath, f64)],
579 ) -> Result<MarketState, AlgorithmError> {
580 let mut pairs: FxHashSet<(NodeIndex, NodeIndex)> = FxHashSet::default();
581 for (token_path, _) in scored_paths {
582 for pair in token_path.windows(2) {
583 pairs.insert((pair[0], pair[1]));
584 }
585 }
586 let mut component_ids: FxHashSet<&ComponentId> = FxHashSet::default();
587 for &(from, to) in &pairs {
588 for pool in graph.pools_between(from, to) {
589 component_ids.insert(&pool.component_id);
590 }
591 }
592
593 let market = paths::read_market(&market, label).await?;
594 let market_subset = market.extract_subset_with_overlay(&component_ids);
595 drop(market);
596 Ok(market_subset)
597 }
598
599 fn solve_for_best_path(
600 &self,
601 scored_paths: &[(TokenPath, f64)],
602 report: &mut SolveReport,
603 ctx: &SolveContext,
604 ) -> Result<RouteResult, AlgorithmError> {
605 let mut best_route: Option<(&TokenPath, SolvedRoute)> = None;
606 let mut winners = PairWinners::new(self.cache_pair_swaps);
607 let mut swaps = SwapCache::new();
608 let timeout_ms = self.timeout.as_millis() as u64;
609
610 for (token_path, _) in scored_paths {
611 let elapsed_ms = ctx.start.elapsed().as_millis() as u64;
613 if elapsed_ms > timeout_ms {
614 break;
615 }
616
617 let solved = match Self::solve_token_path(ctx, token_path, &mut winners, &mut swaps) {
618 Ok(solved) => solved,
619 Err(e) => {
620 trace!(error = %e, "could not solve path");
621 report.simulation_failures += 1;
622 continue;
623 }
624 };
625
626 if best_route
628 .as_ref()
629 .is_none_or(|(_, previous): &(&TokenPath, SolvedRoute)| {
630 solved.net_amount_out > previous.net_amount_out
631 })
632 {
633 best_route = Some((token_path, solved));
634 }
635
636 report.paths_simulated += 1;
637 }
638
639 let best = match best_route {
642 Some((token_path, solved)) => {
643 let route = Self::build_route(ctx, token_path, &solved)?;
644 if let Err(e) = route.validate() {
645 trace!(error = %e, "best route failed validation");
646 report.validation_failures += 1;
647 None
648 } else {
649 let amount_out = swap_on_route(&route, ctx.token_prices, ctx.gas_price);
650 Some(RouteResult::new(route, amount_out, ctx.gas_price.clone()))
651 }
652 }
653 None => None,
654 };
655
656 let solve_time_ms = ctx.start.elapsed().as_millis() as u64;
657 report.record(
658 best.as_ref(),
659 ctx.market,
660 ctx.amount_in,
661 solve_time_ms,
662 ctx.market.component_count(),
663 );
664
665 match best {
666 Some(best_route) => Ok(best_route),
667 None => {
668 if solve_time_ms > timeout_ms {
669 Err(AlgorithmError::Timeout { elapsed_ms: solve_time_ms })
670 } else {
671 Err(AlgorithmError::InsufficientLiquidity)
672 }
673 }
674 }
675 }
676
677 fn solve_token_path<'g>(
701 ctx: &SolveContext<'g>,
702 token_path: &[NodeIndex],
703 winners: &mut PairWinners,
704 swaps: &mut SwapCache<'g>,
705 ) -> Result<SolvedRoute, MostLiquidError> {
706 let (graph, market, gas_price) = (ctx.graph, ctx.market, ctx.gas_price);
707 let mut current_amount = ctx.amount_in.clone();
708 let mut hops: SmallVec<[HopResult; INLINE_EDGES]> = SmallVec::new();
709 let mut used_components: SmallVec<[&ComponentId; INLINE_EDGES]> = SmallVec::new();
710 let mut gas = BigUint::ZERO;
711 let mut route_gas_price = None;
714
715 for pair in token_path.windows(2) {
716 let (token_in, token_out, token_out_gas_price) = Self::get_pair_data(ctx, pair)?;
717
718 let simulate = |component_id: &'g ComponentId| {
719 let paid = swaps.swap(
720 PoolDirection {
721 component_id,
722 address_in: &token_in.address,
723 address_out: &token_out.address,
724 },
725 ¤t_amount,
726 SELECTION,
727 || {
728 let Some(state) = market.get_simulation_state(component_id) else {
729 return Err(Refusal::Failed);
730 };
731 state
732 .get_amount_out_guarded(current_amount.clone(), token_in, token_out)
733 .map(|result| SwapResult { amount_out: result.amount, gas: result.gas })
734 .map_err(|error| Refusal::of(&error))
735 },
736 true,
737 )?;
738 let net = match token_out_gas_price {
739 Some(price) => {
740 let cost = &paid.gas * gas_price * &price.numerator / &price.denominator;
741 BigInt::from(paid.amount_out.clone()) - BigInt::from(cost)
742 }
743 None => BigInt::from(paid.amount_out.clone()),
744 };
745 Some((paid, net))
746 };
747
748 let pools = graph.pools_between(pair[0], pair[1]);
749
750 let restricted = pools
755 .iter()
756 .any(|edge| used_components.contains(&&edge.component_id));
757 let hop_result = if restricted {
758 best_paying_pool(pools, |id| !used_components.contains(&id), simulate)
759 } else {
760 winners.hop((pair[0], pair[1]), pools, simulate)
761 };
762 let Some(hop_result) = hop_result else {
763 return Err(MostLiquidError::HopNotTradable { from: pair[0], to: pair[1] })
764 };
765
766 let chosen = pools
767 .get(hop_result.pool_ix)
768 .expect("Pool used not present in the pools available");
769
770 used_components.push(&chosen.component_id);
771
772 gas += &hop_result.gas;
773 current_amount = hop_result.amount_out.clone();
774 route_gas_price = token_out_gas_price;
775 hops.push(hop_result);
776 }
777
778 let net_amount_out = match route_gas_price {
779 Some(price) => {
780 let cost = gas * ctx.gas_price * &price.numerator / &price.denominator;
781 BigInt::from(current_amount) - BigInt::from(cost)
782 }
783 None => BigInt::from(current_amount),
784 };
785
786 Ok(SolvedRoute { hops, net_amount_out })
787 }
788
789 fn get_pair_data<'a>(
790 ctx: &SolveContext<'a>,
791 pair: &[NodeIndex],
792 ) -> Result<(&'a Token, &'a Token, Option<&'a Price>), MostLiquidError> {
793 let token_in = ctx
794 .market
795 .get_token(&ctx.graph[pair[0]])
796 .ok_or(MostLiquidError::TokenMissing(pair[0]))?;
797 let token_out = ctx
798 .market
799 .get_token(&ctx.graph[pair[1]])
800 .ok_or(MostLiquidError::TokenMissing(pair[1]))?;
801 let token_out_gas_price = ctx
802 .token_prices
803 .and_then(|prices| prices.get(&token_out.address));
804 Ok((token_in, token_out, token_out_gas_price))
805 }
806}
807
808impl Default for MostLiquidAlgorithm {
809 fn default() -> Self {
810 Self::new()
811 }
812}
813
814impl Algorithm for MostLiquidAlgorithm {
815 type GraphType = TopologyGraph<DepthAndPrice>;
816 type GraphManager = TopologyGraphManager<DepthAndPrice>;
817
818 fn name(&self) -> &str {
819 "most_liquid"
820 }
821
822 #[instrument(level = "debug", skip_all, fields(order_id = %order.id()))]
824 async fn find_best_route(
825 &self,
826 graph: &Self::GraphType,
827 market: MarketData,
828 label: Option<StateLabel>,
829 derived: Option<SharedDerivedDataRef>,
830 order: &Order,
831 ) -> Result<RouteResult, AlgorithmError> {
832 let start = Instant::now();
833
834 if !order.is_sell() {
836 return Err(AlgorithmError::ExactOutNotSupported);
837 }
838
839 let token_prices = match derived.as_ref() {
842 Some(derived) => derived
843 .read()
844 .await
845 .token_prices_shared(),
846 None => None,
847 };
848
849 let amount_in = order.amount().clone();
850
851 let all_paths =
854 paths::find_token_paths(graph, order.token_in(), order.token_out(), &self.query)?;
855 let n_paths = all_paths.len();
856 let no_path = |reason| AlgorithmError::NoPath {
857 from: order.token_in().clone(),
858 to: order.token_out().clone(),
859 reason,
860 };
861 if all_paths.is_empty() {
862 return Err(no_path(NoPathReason::NoGraphPath));
863 }
864
865 let mut scored_paths = Self::score_paths(graph, all_paths);
868 if scored_paths.is_empty() {
869 return Err(no_path(NoPathReason::NoScorablePaths));
870 }
871
872 let mut report = SolveReport {
873 paths_candidates: n_paths,
874 scoring_failures: n_paths - scored_paths.len(),
875 ..SolveReport::default()
876 };
877
878 if let Some(max_routes) = self.max_routes {
879 scored_paths.truncate(max_routes);
880 }
881 report.paths_to_simulate = scored_paths.len();
882
883 let market = Self::snapshot_market_state(graph, market, label, &scored_paths).await?;
885 let gas_price = paths::fetch_gas_price(&market)?;
886
887 let ctx = SolveContext {
889 graph,
890 market: &market,
891 token_prices: token_prices.as_deref(),
892 amount_in: &amount_in,
893 gas_price: &gas_price,
894 start,
895 };
896
897 self.solve_for_best_path(&scored_paths, &mut report, &ctx)
898 }
899
900 fn computation_requirements(&self) -> ComputationRequirements {
901 ComputationRequirements::none()
909 .allow_stale("token_prices")
910 .expect("Conflicting Computation Requirements")
911 }
912
913 fn timeout(&self) -> Duration {
914 self.timeout
915 }
916}
917
918fn best_paying_pool<'g, D>(
923 pools: &'g [EdgeData<D>],
924 mut usable: impl FnMut(&ComponentId) -> bool,
925 mut simulate: impl FnMut(&'g ComponentId) -> Option<(SwapResult, BigInt)>,
926) -> Option<HopResult> {
927 let mut best: Option<(usize, SwapResult, BigInt)> = None;
928
929 for (pool_ix, edge) in pools.iter().enumerate() {
930 if !usable(&edge.component_id) {
931 continue;
932 }
933 let Some((result, net)) = simulate(&edge.component_id) else {
934 trace!(component_id = edge.component_id, "simulation failed, skipping pool");
935 continue;
936 };
937 if best
938 .as_ref()
939 .is_none_or(|(_, _, best_net)| net > *best_net)
940 {
941 best = Some((pool_ix, result, net));
942 }
943 }
944
945 let (pool_ix, paid, _) = best?;
946 Some(HopResult { pool_ix, amount_out: paid.amount_out, gas: paid.gas })
947}
948
949struct PairWinners {
963 winner_by_pair: FxHashMap<(NodeIndex, NodeIndex), usize>,
964 enabled: bool,
965}
966
967impl PairWinners {
968 fn new(enabled: bool) -> Self {
969 Self { winner_by_pair: FxHashMap::default(), enabled }
970 }
971
972 fn hop<'g, D>(
980 &mut self,
981 pair: (NodeIndex, NodeIndex),
982 pools: &'g [EdgeData<D>],
983 mut simulate: impl FnMut(&'g ComponentId) -> Option<(SwapResult, BigInt)>,
984 ) -> Option<HopResult> {
985 if let Some(&pool_ix) = self.winner_by_pair.get(&pair) {
988 if let Some((paid, _)) = pools
989 .get(pool_ix)
990 .and_then(|edge| simulate(&edge.component_id))
991 {
992 return Some(HopResult { pool_ix, amount_out: paid.amount_out, gas: paid.gas });
993 }
994 }
995
996 let hop_result = best_paying_pool(pools, |_| true, simulate)?;
997 if self.enabled {
998 self.winner_by_pair
999 .insert(pair, hop_result.pool_ix);
1000 }
1001 Some(hop_result)
1002 }
1003}
1004
1005#[cfg(test)]
1006mod tests {
1007 use tycho_simulation::{
1008 tycho_core::simulation::protocol_sim::Price,
1009 tycho_ethereum::gas::{BlockGasPrice, GasPrice},
1010 };
1011
1012 use super::*;
1013 use crate::{
1014 algorithm::test_utils::{
1015 addr, component,
1016 fixtures::{addrs, linear_graph},
1017 order, setup_market_weighted, token, MockProtocolSim, ONE_ETH,
1018 },
1019 derived::{
1020 computation::{FailedItem, FailedItemError},
1021 types::TokenGasPrices,
1022 DerivedData,
1023 },
1024 graph::GraphManager,
1025 types::OrderSide,
1026 };
1027
1028 fn wrap_market(market: MarketState) -> MarketData {
1029 MarketData::new(std::sync::Arc::new(tokio::sync::RwLock::new(market)))
1030 }
1031
1032 fn setup_derived_with_token_prices(token_addresses: &[Address]) -> SharedDerivedDataRef {
1037 let mut token_prices: TokenGasPrices = FxHashMap::default();
1038 for addr in token_addresses {
1039 token_prices.insert(
1041 addr.clone(),
1042 Price { numerator: BigUint::from(1u64), denominator: BigUint::from(1u64) },
1043 );
1044 }
1045
1046 let mut derived_data = DerivedData::new();
1047 derived_data.set_token_prices(token_prices, vec![], 1, true);
1048 std::sync::Arc::new(tokio::sync::RwLock::new(derived_data))
1049 }
1050
1051 fn make_mock_sim() -> MockProtocolSim {
1052 MockProtocolSim::new(2.0)
1053 }
1054
1055 fn pair_key(comp: &str, b_in: u8, b_out: u8) -> (String, Address, Address) {
1056 (comp.to_string(), addr(b_in), addr(b_out))
1057 }
1058
1059 fn pair_key_str(comp: &str, b_in: u8, b_out: u8) -> String {
1060 format!("{comp}/{}/{}", addr(b_in), addr(b_out))
1061 }
1062
1063 fn make_token_prices(addresses: &[Address]) -> TokenGasPrices {
1064 let mut prices = TokenGasPrices::default();
1065 for addr in addresses {
1066 prices.insert(
1068 addr.clone(),
1069 Price { numerator: BigUint::from(1u64), denominator: BigUint::from(1u64) },
1070 );
1071 }
1072 prices
1073 }
1074
1075 #[test]
1076 fn test_from_sim_and_derived_failed_spot_price_returns_none() {
1077 let key = pair_key("component1", 0x01, 0x02);
1078 let key_str = pair_key_str("component1", 0x01, 0x02);
1079 let tok_in = token(0x01, "A");
1080 let tok_out = token(0x02, "B");
1081
1082 let mut derived = DerivedData::new();
1083 derived.set_spot_prices(
1085 Default::default(),
1086 vec![FailedItem {
1087 key: key_str,
1088 error: FailedItemError::SimulationFailed("sim error".into()),
1089 }],
1090 10,
1091 true,
1092 );
1093 derived.set_component_depths(Default::default(), vec![], 10, true);
1094 derived.set_token_prices(
1095 make_token_prices(&[tok_in.address.clone(), tok_out.address.clone()]),
1096 vec![],
1097 10,
1098 true,
1099 );
1100
1101 let sim = make_mock_sim();
1102 let result =
1103 <DepthAndPrice as crate::graph::EdgeWeightFromSimAndDerived>::from_sim_and_derived(
1104 &sim, &key.0, &tok_in, &tok_out, &derived,
1105 );
1106
1107 assert!(result.is_none());
1108 }
1109
1110 #[test]
1111 fn test_from_sim_and_derived_failed_component_depth_returns_none() {
1112 let key = pair_key("component1", 0x01, 0x02);
1113 let key_str = pair_key_str("component1", 0x01, 0x02);
1114 let tok_in = token(0x01, "A");
1115 let tok_out = token(0x02, "B");
1116
1117 let mut derived = DerivedData::new();
1118 let mut prices = crate::derived::types::SpotPrices::default();
1120 prices.insert(key.clone(), 1.5);
1121 derived.set_spot_prices(prices, vec![], 10, true);
1122 derived.set_component_depths(
1124 Default::default(),
1125 vec![FailedItem {
1126 key: key_str,
1127 error: FailedItemError::SimulationFailed("depth error".into()),
1128 }],
1129 10,
1130 true,
1131 );
1132 derived.set_token_prices(
1133 make_token_prices(&[tok_in.address.clone(), tok_out.address.clone()]),
1134 vec![],
1135 10,
1136 true,
1137 );
1138
1139 let sim = make_mock_sim();
1140 let result =
1141 <DepthAndPrice as crate::graph::EdgeWeightFromSimAndDerived>::from_sim_and_derived(
1142 &sim, &key.0, &tok_in, &tok_out, &derived,
1143 );
1144
1145 assert!(result.is_none());
1146 }
1147
1148 #[test]
1149 fn test_from_sim_and_derived_both_failed_returns_none() {
1150 let key = pair_key("component1", 0x01, 0x02);
1151 let key_str = pair_key_str("component1", 0x01, 0x02);
1152 let tok_in = token(0x01, "A");
1153 let tok_out = token(0x02, "B");
1154
1155 let mut derived = DerivedData::new();
1156 derived.set_spot_prices(
1157 Default::default(),
1158 vec![FailedItem {
1159 key: key_str.clone(),
1160 error: FailedItemError::SimulationFailed("spot error".into()),
1161 }],
1162 10,
1163 true,
1164 );
1165 derived.set_component_depths(
1166 Default::default(),
1167 vec![FailedItem {
1168 key: key_str,
1169 error: FailedItemError::SimulationFailed("depth error".into()),
1170 }],
1171 10,
1172 true,
1173 );
1174 derived.set_token_prices(
1175 make_token_prices(&[tok_in.address.clone(), tok_out.address.clone()]),
1176 vec![],
1177 10,
1178 true,
1179 );
1180
1181 let sim = make_mock_sim();
1182 let result =
1183 <DepthAndPrice as crate::graph::EdgeWeightFromSimAndDerived>::from_sim_and_derived(
1184 &sim, &key.0, &tok_in, &tok_out, &derived,
1185 );
1186
1187 assert!(result.is_none());
1188 }
1189
1190 #[test]
1191 fn test_from_sim_and_derived_missing_token_price_returns_none() {
1192 let key = pair_key("component1", 0x01, 0x02);
1193 let tok_in = token(0x01, "A");
1194 let tok_out = token(0x02, "B");
1195
1196 let mut derived = DerivedData::new();
1197 let mut prices = crate::derived::types::SpotPrices::default();
1199 prices.insert(key.clone(), 1.5);
1200 derived.set_spot_prices(prices, vec![], 10, true);
1201
1202 let mut depths = crate::derived::types::ComponentDepths::default();
1203 depths.insert(key.clone(), BigUint::from(1000u64));
1204 derived.set_component_depths(depths, vec![], 10, true);
1205
1206 let sim = make_mock_sim();
1209 let result =
1210 <DepthAndPrice as crate::graph::EdgeWeightFromSimAndDerived>::from_sim_and_derived(
1211 &sim, &key.0, &tok_in, &tok_out, &derived,
1212 );
1213
1214 assert!(
1215 result.is_none(),
1216 "should return None when token price is missing for depth normalization"
1217 );
1218 }
1219
1220 #[test]
1221 fn test_from_sim_and_derived_normalizes_depth_to_eth() {
1222 let key = pair_key("component1", 0x01, 0x02);
1223 let tok_in = token(0x01, "A");
1224 let tok_out = token(0x02, "B");
1225
1226 let mut derived = DerivedData::new();
1227
1228 let mut spot = crate::derived::types::SpotPrices::default();
1230 spot.insert(key.clone(), 2.0);
1231 derived.set_spot_prices(spot, vec![], 10, true);
1232
1233 let mut depths = crate::derived::types::ComponentDepths::default();
1235 depths.insert(key.clone(), BigUint::from(2_000_000u64));
1236 derived.set_component_depths(depths, vec![], 10, true);
1237
1238 let mut token_prices = TokenGasPrices::default();
1241 token_prices.insert(
1242 tok_in.address.clone(),
1243 Price { numerator: BigUint::from(2000u64), denominator: BigUint::from(1u64) },
1244 );
1245 derived.set_token_prices(token_prices, vec![], 10, true);
1246
1247 let sim = make_mock_sim();
1248 let result =
1249 <DepthAndPrice as crate::graph::EdgeWeightFromSimAndDerived>::from_sim_and_derived(
1250 &sim, &key.0, &tok_in, &tok_out, &derived,
1251 );
1252
1253 let data = result.expect("should return Some when all data present");
1254 assert!((data.spot_price - 2.0).abs() < f64::EPSILON, "spot price should be 2.0");
1255 assert!(
1257 (data.depth - 1000.0).abs() < f64::EPSILON,
1258 "depth should be 1000.0 ETH, got {}",
1259 data.depth
1260 );
1261 }
1262
1263 #[test]
1264 fn test_from_sim_and_derived_normalizes_depth_fractional_price() {
1265 let key = pair_key("component1", 0x01, 0x02);
1266 let tok_in = token(0x01, "A");
1267 let tok_out = token(0x02, "B");
1268
1269 let mut derived = DerivedData::new();
1270
1271 let mut spot = crate::derived::types::SpotPrices::default();
1272 spot.insert(key.clone(), 0.5);
1273 derived.set_spot_prices(spot, vec![], 10, true);
1274
1275 let mut depths = crate::derived::types::ComponentDepths::default();
1277 depths.insert(key.clone(), BigUint::from(500u64));
1278 derived.set_component_depths(depths, vec![], 10, true);
1279
1280 let mut token_prices = TokenGasPrices::default();
1283 token_prices.insert(
1284 tok_in.address.clone(),
1285 Price { numerator: BigUint::from(3u64), denominator: BigUint::from(2u64) },
1286 );
1287 derived.set_token_prices(token_prices, vec![], 10, true);
1288
1289 let sim = make_mock_sim();
1290 let result =
1291 <DepthAndPrice as crate::graph::EdgeWeightFromSimAndDerived>::from_sim_and_derived(
1292 &sim, &key.0, &tok_in, &tok_out, &derived,
1293 );
1294
1295 let data = result.expect("should return Some when all data present");
1296 let expected_depth = 500.0 * 2.0 / 3.0;
1297 assert!(
1298 (data.depth - expected_depth).abs() < 1e-10,
1299 "depth should be {expected_depth}, got {}",
1300 data.depth
1301 );
1302 }
1303
1304 #[tokio::test]
1307 async fn test_find_best_route_single_path() {
1308 let token_a = token(0x01, "A");
1309 let token_b = token(0x02, "B");
1310
1311 let (market, manager) = setup_market_weighted(vec![(
1312 "component1",
1313 &token_a,
1314 &token_b,
1315 MockProtocolSim::new(2.0),
1316 )]);
1317
1318 let algorithm = MostLiquidAlgorithm::with_config(
1319 AlgorithmConfig::new(1, 1, Duration::from_millis(100), None).unwrap(),
1320 )
1321 .unwrap();
1322 let order = order(&token_a, &token_b, ONE_ETH, OrderSide::Sell);
1323 let result = algorithm
1324 .find_best_route(manager.graph(), market, None, None, &order)
1325 .await
1326 .unwrap();
1327
1328 assert_eq!(result.route().swaps().len(), 1);
1329 assert_eq!(*result.route().swaps()[0].amount_in(), BigUint::from(ONE_ETH));
1330 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(ONE_ETH * 2));
1331 }
1332
1333 #[tokio::test]
1334 async fn test_find_best_route_ranks_by_net_amount_out() {
1335 let token_a = token(0x01, "A");
1346 let token_b = token(0x02, "B");
1347
1348 let (market, manager) = setup_market_weighted(vec![
1349 ("best", &token_a, &token_b, MockProtocolSim::new(3.0).with_gas(10)),
1350 ("low_out", &token_a, &token_b, MockProtocolSim::new(2.0).with_gas(5)),
1351 ("high_gas", &token_a, &token_b, MockProtocolSim::new(4.0).with_gas(30)),
1352 ]);
1353
1354 let algorithm = MostLiquidAlgorithm::with_config(
1355 AlgorithmConfig::new(1, 1, Duration::from_millis(100), None).unwrap(),
1356 )
1357 .unwrap();
1358 let order = order(&token_a, &token_b, 1000, OrderSide::Sell);
1359
1360 let derived = setup_derived_with_token_prices(std::slice::from_ref(&token_b.address));
1362
1363 let result = algorithm
1364 .find_best_route(manager.graph(), market, None, Some(derived), &order)
1365 .await
1366 .unwrap();
1367
1368 assert_eq!(result.route().swaps().len(), 1);
1370 assert_eq!(result.route().swaps()[0].component_id(), "best");
1371 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(3000u64));
1372 assert_eq!(result.net_amount_out(), &BigInt::from(2000)); }
1374
1375 #[tokio::test]
1376 async fn test_find_best_route_no_path_returns_error() {
1377 let token_a = token(0x01, "A");
1378 let token_b = token(0x02, "B");
1379 let token_c = token(0x03, "C"); let (market, manager) = setup_market_weighted(vec![(
1382 "component1",
1383 &token_a,
1384 &token_b,
1385 MockProtocolSim::new(2.0),
1386 )]);
1387
1388 let algorithm = MostLiquidAlgorithm::new();
1389 let order = order(&token_a, &token_c, ONE_ETH, OrderSide::Sell);
1390
1391 let result = algorithm
1392 .find_best_route(manager.graph(), market, None, None, &order)
1393 .await;
1394 assert!(matches!(result, Err(AlgorithmError::NoPath { .. })));
1395 }
1396
1397 #[tokio::test]
1398 async fn test_find_best_route_multi_hop() {
1399 let token_a = token(0x01, "A");
1400 let token_b = token(0x02, "B");
1401 let token_c = token(0x03, "C");
1402
1403 let (market, manager) = setup_market_weighted(vec![
1404 ("component1", &token_a, &token_b, MockProtocolSim::new(2.0)),
1405 ("component2", &token_b, &token_c, MockProtocolSim::new(3.0)),
1406 ]);
1407
1408 let algorithm = MostLiquidAlgorithm::with_config(
1409 AlgorithmConfig::new(1, 2, Duration::from_millis(100), None).unwrap(),
1410 )
1411 .unwrap();
1412 let order = order(&token_a, &token_c, ONE_ETH, OrderSide::Sell);
1413
1414 let result = algorithm
1415 .find_best_route(manager.graph(), market, None, None, &order)
1416 .await
1417 .unwrap();
1418
1419 assert_eq!(result.route().swaps().len(), 2);
1421 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(ONE_ETH * 2));
1422 assert_eq!(result.route().swaps()[0].component_id(), "component1".to_string());
1423 assert_eq!(*result.route().swaps()[1].amount_out(), BigUint::from(ONE_ETH * 2 * 3));
1424 assert_eq!(result.route().swaps()[1].component_id(), "component2".to_string());
1425 }
1426
1427 fn setup_market_multi_token(
1433 components: Vec<(&str, Vec<Token>, MockProtocolSim)>,
1434 tokens: &[Token],
1435 ) -> (MarketData, TopologyGraphManager<DepthAndPrice>) {
1436 let mut market = MarketState::new();
1437 market.update_gas_price(BlockGasPrice {
1438 block_number: 1,
1439 block_hash: Default::default(),
1440 block_timestamp: 0,
1441 pricing: GasPrice::Legacy { gas_price: BigUint::from(1u64) },
1442 });
1443 market.upsert_tokens(tokens.to_vec());
1444 let protocol_components: Vec<_> = components
1445 .iter()
1446 .map(|(id, tokens, _)| component(id, tokens))
1447 .collect();
1448 market.upsert_components(protocol_components);
1449 let states: Vec<_> = components
1450 .into_iter()
1451 .map(|(id, _, sim)| (id.to_string(), Box::new(sim) as Box<dyn ProtocolSim>))
1452 .collect();
1453 market.update_states(states);
1454
1455 let mut manager = TopologyGraphManager::default();
1456 manager.initialize_graph(&market.component_topology());
1457 (wrap_market(market), manager)
1458 }
1459
1460 #[tokio::test]
1466 async fn test_find_best_route_never_takes_one_pool_twice() {
1467 let token_a = token(0x01, "A");
1468 let token_b = token(0x02, "B");
1469 let token_c = token(0x03, "C");
1470
1471 let (market, manager) = setup_market_multi_token(
1472 vec![
1473 (
1474 "multi",
1475 vec![token_a.clone(), token_b.clone(), token_c.clone()],
1476 MockProtocolSim::new(3.0),
1477 ),
1478 ("bc", vec![token_b.clone(), token_c.clone()], MockProtocolSim::new(2.0)),
1479 ],
1480 &[token_a.clone(), token_b.clone(), token_c.clone()],
1481 );
1482
1483 let algorithm = MostLiquidAlgorithm::with_config(
1485 AlgorithmConfig::new(2, 2, Duration::from_millis(100), None).unwrap(),
1486 )
1487 .unwrap();
1488 let order = order(&token_a, &token_c, 1000, OrderSide::Sell);
1489
1490 let result = algorithm
1491 .find_best_route(manager.graph(), market, None, None, &order)
1492 .await
1493 .unwrap();
1494
1495 assert_eq!(result.route().swaps().len(), 2);
1496 assert_eq!(result.route().swaps()[0].component_id(), "multi");
1497 assert_eq!(result.route().swaps()[1].component_id(), "bc");
1498 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(3000u64));
1500 assert_eq!(*result.route().swaps()[1].amount_out(), BigUint::from(6000u64));
1501 }
1502
1503 #[tokio::test]
1507 async fn test_find_best_route_drops_sequence_with_no_pool_left() {
1508 let token_a = token(0x01, "A");
1509 let token_b = token(0x02, "B");
1510 let token_c = token(0x03, "C");
1511
1512 let (market, manager) = setup_market_multi_token(
1513 vec![(
1514 "multi",
1515 vec![token_a.clone(), token_b.clone(), token_c.clone()],
1516 MockProtocolSim::new(3.0),
1517 )],
1518 &[token_a.clone(), token_b.clone(), token_c.clone()],
1519 );
1520
1521 let algorithm = MostLiquidAlgorithm::with_config(
1522 AlgorithmConfig::new(2, 2, Duration::from_millis(100), None).unwrap(),
1523 )
1524 .unwrap();
1525 let order = order(&token_a, &token_c, 1000, OrderSide::Sell);
1526
1527 let result = algorithm
1528 .find_best_route(manager.graph(), market, None, None, &order)
1529 .await;
1530
1531 assert!(matches!(result, Err(AlgorithmError::InsufficientLiquidity)));
1532 }
1533
1534 #[tokio::test]
1541 async fn test_find_best_route_uses_pools_without_edge_weights() {
1542 let token_a = token(0x01, "A");
1544 let token_b = token(0x02, "B");
1545
1546 let mut market = MarketState::new();
1548 let component1_state = MockProtocolSim::new(2.0);
1549 let component2_state = MockProtocolSim::new(3.0); let component1_comp = component("component1", &[token_a.clone(), token_b.clone()]);
1552 let component2_comp = component("component2", &[token_a.clone(), token_b.clone()]);
1553
1554 market.update_gas_price(BlockGasPrice {
1556 block_number: 1,
1557 block_hash: Default::default(),
1558 block_timestamp: 0,
1559 pricing: GasPrice::Legacy { gas_price: BigUint::from(1u64) },
1560 });
1561
1562 market.upsert_components(vec![component1_comp, component2_comp]);
1564
1565 market.update_states(vec![
1567 ("component1".to_string(), Box::new(component1_state.clone()) as Box<dyn ProtocolSim>),
1568 ("component2".to_string(), Box::new(component2_state) as Box<dyn ProtocolSim>),
1569 ]);
1570
1571 market.upsert_tokens(vec![token_a.clone(), token_b.clone()]);
1573
1574 let mut manager = TopologyGraphManager::default();
1576 manager.initialize_graph(&market.component_topology());
1577
1578 let weight =
1580 DepthAndPrice::from_protocol_sim(&component1_state, &token_a, &token_b).unwrap();
1581 manager
1582 .set_pool_weight(
1583 &"component1".to_string(),
1584 &token_a.address,
1585 &token_b.address,
1586 weight,
1587 false,
1588 )
1589 .unwrap();
1590
1591 let algorithm = MostLiquidAlgorithm::with_config(
1593 AlgorithmConfig::new(1, 1, Duration::from_millis(100), None).unwrap(),
1594 )
1595 .unwrap();
1596 let order = order(&token_a, &token_b, ONE_ETH, OrderSide::Sell);
1597 let market = wrap_market(market);
1598 let result = algorithm
1599 .find_best_route(manager.graph(), market, None, None, &order)
1600 .await
1601 .unwrap();
1602
1603 assert_eq!(result.route().swaps().len(), 1);
1606 assert_eq!(result.route().swaps()[0].component_id(), "component2");
1607 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(ONE_ETH * 3));
1608 }
1609
1610 #[tokio::test]
1615 async fn test_find_best_route_without_any_derived_data() {
1616 let token_a = token(0x01, "A");
1618 let token_b = token(0x02, "B");
1619
1620 let mut market = MarketState::new();
1621 let component_state = MockProtocolSim::new(2.0);
1622 let comp = component("component1", &[token_a.clone(), token_b.clone()]);
1623
1624 market.update_gas_price(BlockGasPrice {
1626 block_number: 1,
1627 block_hash: Default::default(),
1628 block_timestamp: 0,
1629 pricing: GasPrice::Eip1559 {
1630 base_fee_per_gas: BigUint::from(1u64),
1631 max_priority_fee_per_gas: BigUint::from(0u64),
1632 },
1633 });
1634
1635 market.upsert_components(vec![comp]);
1636 market.update_states(vec![(
1637 "component1".to_string(),
1638 Box::new(component_state) as Box<dyn ProtocolSim>,
1639 )]);
1640 market.upsert_tokens(vec![token_a.clone(), token_b.clone()]);
1641
1642 let mut manager = TopologyGraphManager::default();
1644 manager.initialize_graph(&market.component_topology());
1645
1646 let algorithm = MostLiquidAlgorithm::new();
1647 let order = order(&token_a, &token_b, ONE_ETH, OrderSide::Sell);
1648 let market = wrap_market(market);
1649
1650 let result = algorithm
1651 .find_best_route(manager.graph(), market, None, None, &order)
1652 .await
1653 .expect("an unranked route is still a route");
1654
1655 assert_eq!(result.route().swaps().len(), 1);
1656 assert_eq!(result.route().swaps()[0].component_id(), "component1");
1657 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(ONE_ETH * 2));
1658 }
1659
1660 #[tokio::test]
1661 async fn test_find_best_route_gas_exceeds_output_returns_negative_net() {
1662 let token_a = token(0x01, "A");
1663 let token_b = token(0x02, "B");
1664
1665 let (market, manager) = setup_market_weighted(vec![(
1666 "component1",
1667 &token_a,
1668 &token_b,
1669 MockProtocolSim::new(2.0),
1670 )]);
1671 let mut market_write = market.try_write().unwrap();
1672
1673 market_write.update_gas_price(BlockGasPrice {
1676 block_number: 1,
1677 block_hash: Default::default(),
1678 block_timestamp: 0,
1679 pricing: GasPrice::Eip1559 {
1680 base_fee_per_gas: BigUint::from(1_000_000u64),
1681 max_priority_fee_per_gas: BigUint::from(1_000_000u64),
1682 },
1683 });
1684 drop(market_write); let algorithm = MostLiquidAlgorithm::new();
1687 let order = order(&token_a, &token_b, 1, OrderSide::Sell); let derived = setup_derived_with_token_prices(std::slice::from_ref(&token_b.address));
1691
1692 let result = algorithm
1694 .find_best_route(manager.graph(), market, None, Some(derived), &order)
1695 .await
1696 .expect("should return route even with negative net_amount_out");
1697
1698 assert_eq!(result.route().swaps().len(), 1);
1700 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(2u64)); let expected_net = BigInt::from(2) - BigInt::from(100_000_000_000u64);
1704 assert_eq!(result.net_amount_out(), &expected_net);
1705 }
1706
1707 #[tokio::test]
1708 async fn test_find_best_route_insufficient_liquidity() {
1709 let token_a = token(0x01, "A");
1711 let token_b = token(0x02, "B");
1712
1713 let (market, manager) = setup_market_weighted(vec![(
1714 "component1",
1715 &token_a,
1716 &token_b,
1717 MockProtocolSim::new(2.0).with_liquidity(1000),
1718 )]);
1719
1720 let algorithm = MostLiquidAlgorithm::new();
1721 let order = order(&token_a, &token_b, ONE_ETH, OrderSide::Sell); let result = algorithm
1724 .find_best_route(manager.graph(), market, None, None, &order)
1725 .await;
1726 assert!(matches!(result, Err(AlgorithmError::InsufficientLiquidity)));
1727 }
1728
1729 #[tokio::test]
1730 async fn test_find_best_route_missing_gas_price_returns_error() {
1731 let token_a = token(0x01, "A");
1733 let token_b = token(0x02, "B");
1734
1735 let mut market = MarketState::new();
1736 let component_state = MockProtocolSim::new(2.0);
1737 let comp = component("component1", &[token_a.clone(), token_b.clone()]);
1738
1739 market.upsert_components(vec![comp]);
1741 market.update_states(vec![(
1742 "component1".to_string(),
1743 Box::new(component_state.clone()) as Box<dyn ProtocolSim>,
1744 )]);
1745 market.upsert_tokens(vec![token_a.clone(), token_b.clone()]);
1746
1747 let mut manager = TopologyGraphManager::default();
1749 manager.initialize_graph(&market.component_topology());
1750 let weight =
1751 DepthAndPrice::from_protocol_sim(&component_state, &token_a, &token_b).unwrap();
1752 manager
1753 .set_pool_weight(
1754 &"component1".to_string(),
1755 &token_a.address,
1756 &token_b.address,
1757 weight,
1758 false,
1759 )
1760 .unwrap();
1761
1762 let algorithm = MostLiquidAlgorithm::new();
1763 let order = order(&token_a, &token_b, ONE_ETH, OrderSide::Sell);
1764 let market = wrap_market(market);
1765
1766 let result = algorithm
1767 .find_best_route(manager.graph(), market, None, None, &order)
1768 .await;
1769
1770 assert!(matches!(result, Err(AlgorithmError::DataNotFound { kind: "gas price", .. })));
1772 }
1773
1774 #[tokio::test]
1776 async fn test_find_best_route_circular_arbitrage() {
1777 let token_a = token(0x01, "A");
1778 let token_b = token(0x02, "B");
1779
1780 let (market, manager) = setup_market_weighted(vec![
1783 ("component1", &token_a, &token_b, MockProtocolSim::new(2.0)),
1784 ("component2", &token_a, &token_b, MockProtocolSim::new(3.0)),
1785 ]);
1786
1787 let algorithm = MostLiquidAlgorithm::with_config(
1789 AlgorithmConfig::new(2, 2, Duration::from_millis(100), None).unwrap(),
1790 )
1791 .unwrap();
1792
1793 let order = order(&token_a, &token_a, 100, OrderSide::Sell);
1795
1796 let result = algorithm
1797 .find_best_route(manager.graph(), market, None, None, &order)
1798 .await
1799 .unwrap();
1800
1801 assert_eq!(result.route().swaps().len(), 2, "Should have 2 swaps for circular route");
1803
1804 assert_eq!(*result.route().swaps()[0].token_in(), token_a.address);
1806 assert_eq!(*result.route().swaps()[0].token_out(), token_b.address);
1807 assert_eq!(result.route().swaps()[0].component_id(), "component2");
1808 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(300u64));
1809
1810 assert_eq!(*result.route().swaps()[1].token_in(), token_b.address);
1812 assert_eq!(*result.route().swaps()[1].token_out(), token_a.address);
1813 assert_eq!(result.route().swaps()[1].component_id(), "component1");
1814 assert_eq!(*result.route().swaps()[1].amount_out(), BigUint::from(150u64));
1815
1816 assert_eq!(result.route().swaps()[0].token_in(), result.route().swaps()[1].token_out());
1818 }
1819
1820 #[tokio::test]
1823 async fn test_find_best_route_circular_needs_two_pools() {
1824 let token_a = token(0x01, "A");
1825 let token_b = token(0x02, "B");
1826
1827 let (market, manager) = setup_market_weighted(vec![(
1828 "component1",
1829 &token_a,
1830 &token_b,
1831 MockProtocolSim::new(2.0),
1832 )]);
1833
1834 let algorithm = MostLiquidAlgorithm::with_config(
1835 AlgorithmConfig::new(2, 2, Duration::from_millis(100), None).unwrap(),
1836 )
1837 .unwrap();
1838 let order = order(&token_a, &token_a, 100, OrderSide::Sell);
1839
1840 let result = algorithm
1841 .find_best_route(manager.graph(), market, None, None, &order)
1842 .await;
1843
1844 assert!(matches!(result, Err(AlgorithmError::InsufficientLiquidity)));
1845 }
1846
1847 #[tokio::test]
1848 async fn test_find_best_route_respects_min_hops() {
1849 let token_a = token(0x01, "A");
1852 let token_b = token(0x02, "B");
1853 let token_c = token(0x03, "C");
1854
1855 let (market, manager) = setup_market_weighted(vec![
1856 ("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)), ]);
1862
1863 let algorithm = MostLiquidAlgorithm::with_config(
1865 AlgorithmConfig::new(2, 3, Duration::from_millis(100), None).unwrap(),
1866 )
1867 .unwrap();
1868 let order = order(&token_a, &token_b, 100, OrderSide::Sell);
1869
1870 let derived = setup_derived_with_token_prices(std::slice::from_ref(&token_b.address));
1873
1874 let result = algorithm
1875 .find_best_route(manager.graph(), market, None, Some(derived), &order)
1876 .await
1877 .unwrap();
1878
1879 assert_eq!(result.route().swaps().len(), 2, "Should use 2-hop path due to min_hops=2");
1881 assert_eq!(result.route().swaps()[0].component_id(), "component_ac");
1882 assert_eq!(result.route().swaps()[1].component_id(), "component_cb");
1883 }
1884
1885 #[tokio::test]
1886 async fn test_find_best_route_respects_max_hops() {
1887 let token_a = token(0x01, "A");
1890 let token_b = token(0x02, "B");
1891 let token_c = token(0x03, "C");
1892
1893 let (market, manager) = setup_market_weighted(vec![
1894 ("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
1895 ("component_bc", &token_b, &token_c, MockProtocolSim::new(3.0)),
1896 ]);
1897
1898 let algorithm = MostLiquidAlgorithm::with_config(
1900 AlgorithmConfig::new(1, 1, Duration::from_millis(100), None).unwrap(),
1901 )
1902 .unwrap();
1903 let order = order(&token_a, &token_c, 100, OrderSide::Sell);
1904
1905 let result = algorithm
1906 .find_best_route(manager.graph(), market, None, None, &order)
1907 .await;
1908 assert!(
1909 matches!(result, Err(AlgorithmError::NoPath { reason: NoPathReason::NoGraphPath, .. })),
1910 "Should fail when max_hops is insufficient"
1911 );
1912 }
1913
1914 #[tokio::test]
1915 async fn test_find_best_route_timeout_returns_best_so_far() {
1916 let token_a = token(0x01, "A");
1920 let token_b = token(0x02, "B");
1921
1922 let (market, manager) = setup_market_weighted(vec![
1924 ("component1", &token_a, &token_b, MockProtocolSim::new(1.0)),
1925 ("component2", &token_a, &token_b, MockProtocolSim::new(2.0)),
1926 ("component3", &token_a, &token_b, MockProtocolSim::new(3.0)),
1927 ("component4", &token_a, &token_b, MockProtocolSim::new(4.0)),
1928 ("component5", &token_a, &token_b, MockProtocolSim::new(5.0)),
1929 ]);
1930
1931 let algorithm = MostLiquidAlgorithm::with_config(
1933 AlgorithmConfig::new(1, 1, Duration::from_millis(0), None).unwrap(),
1934 )
1935 .unwrap();
1936 let order = order(&token_a, &token_b, 100, OrderSide::Sell);
1937
1938 let result = algorithm
1939 .find_best_route(manager.graph(), market, None, None, &order)
1940 .await;
1941
1942 match result {
1947 Ok(r) => {
1948 assert_eq!(r.route().swaps().len(), 1);
1950 }
1951 Err(AlgorithmError::Timeout { .. }) => {
1952 }
1954 Err(e) => panic!("Unexpected error: {:?}", e),
1955 }
1956 }
1957
1958 #[rstest::rstest]
1961 #[case::default_config(1, 3, 50)]
1962 #[case::single_hop_only(1, 1, 100)]
1963 #[case::multi_hop_min(2, 5, 200)]
1964 #[case::zero_timeout(1, 3, 0)]
1965 #[case::large_values(10, 100, 10000)]
1966 fn test_algorithm_config_getters(
1967 #[case] min_hops: usize,
1968 #[case] max_hops: usize,
1969 #[case] timeout_ms: u64,
1970 ) {
1971 use crate::algorithm::Algorithm;
1972
1973 let algorithm = MostLiquidAlgorithm::with_config(
1974 AlgorithmConfig::new(min_hops, max_hops, Duration::from_millis(timeout_ms), None)
1975 .unwrap(),
1976 )
1977 .unwrap();
1978
1979 assert_eq!(algorithm.query.max_hops, max_hops);
1980 assert_eq!(algorithm.timeout, Duration::from_millis(timeout_ms));
1981 assert_eq!(algorithm.name(), "most_liquid");
1982 }
1983
1984 #[test]
1985 fn test_algorithm_default_config() {
1986 use crate::algorithm::Algorithm;
1987
1988 let algorithm = MostLiquidAlgorithm::new();
1989
1990 assert_eq!(algorithm.query.max_hops, 3);
1991 assert_eq!(algorithm.timeout, Duration::from_millis(500));
1992 assert_eq!(algorithm.name(), "most_liquid");
1993 }
1994
1995 #[tokio::test]
2004 async fn test_each_leg_takes_its_best_paying_pool() {
2005 let token_a = token(0x01, "A");
2006 let token_b = token(0x02, "B");
2007 let token_c = token(0x03, "C");
2008
2009 let (market, manager) = setup_market_weighted(vec![
2011 ("ab1", &token_a, &token_b, MockProtocolSim::new(1.0).with_liquidity(9_000_000)),
2012 ("ab2", &token_a, &token_b, MockProtocolSim::new(3.0).with_liquidity(1_000_000)),
2013 ("bc1", &token_b, &token_c, MockProtocolSim::new(1.0).with_liquidity(9_000_000)),
2014 ("bc2", &token_b, &token_c, MockProtocolSim::new(5.0).with_liquidity(1_000_000)),
2015 ]);
2016
2017 let algorithm = MostLiquidAlgorithm::with_config(
2018 AlgorithmConfig::new(2, 2, Duration::from_millis(100), None).unwrap(),
2019 )
2020 .unwrap();
2021 let order = order(&token_a, &token_c, 1000, OrderSide::Sell);
2022 let result = algorithm
2023 .find_best_route(manager.graph(), market, None, None, &order)
2024 .await
2025 .unwrap();
2026
2027 let chosen: Vec<&str> = result
2028 .route()
2029 .swaps()
2030 .iter()
2031 .map(|swap| swap.component_id())
2032 .collect();
2033 assert_eq!(chosen, vec!["ab2", "bc2"], "each hop must take its best-paying pool");
2034
2035 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(3000u64));
2038 assert_eq!(*result.route().swaps()[1].amount_in(), BigUint::from(3000u64));
2039 assert_eq!(*result.route().swaps()[1].amount_out(), BigUint::from(15000u64));
2040 }
2041
2042 #[tokio::test]
2048 async fn test_max_routes_caps_token_sequences_not_pools_on_a_pair() {
2049 let token_a = token(0x01, "A");
2058 let token_b = token(0x02, "B");
2059
2060 let (market, manager) = setup_market_weighted(vec![
2061 ("component1", &token_a, &token_b, MockProtocolSim::new(4.0).with_liquidity(1_000_000)),
2062 ("component2", &token_a, &token_b, MockProtocolSim::new(3.0).with_liquidity(2_000_000)),
2063 ("component3", &token_a, &token_b, MockProtocolSim::new(2.0).with_liquidity(3_000_000)),
2064 ("component4", &token_a, &token_b, MockProtocolSim::new(1.0).with_liquidity(4_000_000)),
2065 ]);
2066
2067 let algorithm = MostLiquidAlgorithm::with_config(
2068 AlgorithmConfig::new(1, 1, Duration::from_millis(100), Some(2)).unwrap(),
2069 )
2070 .unwrap();
2071 let order = order(&token_a, &token_b, 1000, OrderSide::Sell);
2072 let result = algorithm
2073 .find_best_route(manager.graph(), market, None, None, &order)
2074 .await
2075 .unwrap();
2076
2077 assert_eq!(result.route().swaps().len(), 1);
2080 assert_eq!(result.route().swaps()[0].component_id(), "component1");
2081 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(4000u64));
2082 }
2083
2084 #[tokio::test]
2085 async fn test_find_best_route_no_cap_when_max_routes_is_none() {
2086 let token_a = token(0x01, "A");
2088 let token_b = token(0x02, "B");
2089
2090 let (market, manager) = setup_market_weighted(vec![
2091 ("component1", &token_a, &token_b, MockProtocolSim::new(4.0).with_liquidity(1_000_000)),
2092 ("component2", &token_a, &token_b, MockProtocolSim::new(3.0).with_liquidity(2_000_000)),
2093 ("component3", &token_a, &token_b, MockProtocolSim::new(2.0).with_liquidity(3_000_000)),
2094 ("component4", &token_a, &token_b, MockProtocolSim::new(1.0).with_liquidity(4_000_000)),
2095 ]);
2096
2097 let algorithm = MostLiquidAlgorithm::with_config(
2098 AlgorithmConfig::new(1, 1, Duration::from_millis(100), None).unwrap(),
2099 )
2100 .unwrap();
2101 let order = order(&token_a, &token_b, 1000, OrderSide::Sell);
2102 let result = algorithm
2103 .find_best_route(manager.graph(), market, None, None, &order)
2104 .await
2105 .unwrap();
2106
2107 assert_eq!(result.route().swaps().len(), 1);
2109 assert_eq!(result.route().swaps()[0].component_id(), "component1");
2110 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(4000u64));
2111 }
2112
2113 #[test]
2116 fn test_algorithm_config_rejects_zero_max_routes() {
2117 let result = AlgorithmConfig::new(1, 3, Duration::from_millis(100), Some(0));
2118 assert!(matches!(
2119 result,
2120 Err(AlgorithmError::InvalidConfiguration { reason }) if reason.contains("max_routes must be at least 1")
2121 ));
2122 }
2123
2124 #[test]
2125 fn test_algorithm_config_rejects_zero_min_hops() {
2126 let result = AlgorithmConfig::new(0, 3, Duration::from_millis(100), None);
2127 assert!(matches!(
2128 result,
2129 Err(AlgorithmError::InvalidConfiguration { reason }) if reason.contains("min_hops must be at least 1")
2130 ));
2131 }
2132
2133 #[test]
2134 fn test_algorithm_config_rejects_min_greater_than_max() {
2135 let result = AlgorithmConfig::new(5, 3, Duration::from_millis(100), None);
2136 assert!(matches!(
2137 result,
2138 Err(AlgorithmError::InvalidConfiguration { reason }) if reason.contains("cannot exceed")
2139 ));
2140 }
2141
2142 #[test]
2147 fn test_score_token_path_sinks_a_sequence_with_an_unmeasured_hop() {
2148 let (a, b, c, _) = addrs();
2149 let mut manager = linear_graph();
2150 manager
2152 .set_pool_weight(&"ab".to_string(), &a, &b, DepthAndPrice::new(2.0, 1000.0), false)
2153 .unwrap();
2154 let graph = manager.graph();
2155 let node = |address: &Address| graph.get_token_ix(address).unwrap();
2156
2157 let unmeasured =
2158 MostLiquidAlgorithm::score_token_path(graph, &[node(&a), node(&b), node(&c)]);
2159
2160 assert_eq!(unmeasured, Some(0.0), "an unmeasured hop scores zero, not None");
2161 assert!(
2162 MostLiquidAlgorithm::score_token_path(graph, &[node(&a), node(&b)])
2163 .is_some_and(|measured| measured > 0.0),
2164 "a fully measured sequence still outranks it"
2165 );
2166 }
2167
2168 #[test]
2171 fn test_score_token_path_drops_a_sequence_with_an_unconnected_pair() {
2172 let (a, _, c, _) = addrs();
2173 let manager = linear_graph();
2174 let graph = manager.graph();
2175 let node = |address: &Address| graph.get_token_ix(address).unwrap();
2176
2177 assert_eq!(MostLiquidAlgorithm::score_token_path(graph, &[node(&a), node(&c)]), None);
2178 assert_eq!(MostLiquidAlgorithm::score_token_path(graph, &[node(&a)]), None);
2179 }
2180
2181 fn pools(count: usize) -> Vec<EdgeData<()>> {
2185 (0..count)
2186 .map(|i| EdgeData::new(format!("pool{i}")))
2187 .collect()
2188 }
2189
2190 fn simulator(
2192 multipliers: [u64; 2],
2193 amount_in: u64,
2194 ) -> impl FnMut(&ComponentId) -> Option<(SwapResult, BigInt)> {
2195 move |component_id: &ComponentId| {
2196 let index: usize = component_id
2197 .trim_start_matches("pool")
2198 .parse()
2199 .ok()?;
2200 let amount = BigUint::from(amount_in * multipliers[index]);
2201 let paid = SwapResult { amount_out: amount.clone(), gas: BigUint::from(10u64) };
2202 Some((paid, BigInt::from(amount)))
2203 }
2204 }
2205
2206 fn pair() -> (NodeIndex, NodeIndex) {
2208 (NodeIndex::new(0), NodeIndex::new(1))
2209 }
2210
2211 #[test]
2212 fn test_winners_takes_the_pool_that_pays_most() {
2213 let mut winners = PairWinners::new(true);
2214 let multipliers = [2u64, 5u64];
2215 let pools = pools(multipliers.len());
2216
2217 let outcome = winners
2218 .hop(pair(), &pools, simulator(multipliers, 100))
2219 .unwrap();
2220
2221 assert_eq!(outcome.pool_ix, 1, "pool1 pays 500 against pool0's 200");
2222 assert_eq!(outcome.amount_out, BigUint::from(500u64));
2223 }
2224
2225 #[test]
2228 fn test_winners_asks_only_the_remembered_winner() {
2229 let mut winners = PairWinners::new(true);
2230 let multipliers = [2u64, 5u64];
2231 let pools = pools(multipliers.len());
2232 winners
2233 .hop(pair(), &pools, simulator(multipliers, 100))
2234 .unwrap();
2235
2236 let mut asked = Vec::new();
2237 let outcome = winners
2238 .hop(pair(), &pools, |component_id: &ComponentId| {
2239 asked.push(component_id.clone());
2240 simulator(multipliers, 100)(component_id)
2241 })
2242 .unwrap();
2243
2244 assert_eq!(asked, vec!["pool1".to_string()], "only the winner should be asked");
2245 assert_eq!(outcome.pool_ix, 1);
2246 }
2247
2248 #[test]
2251 fn test_winners_falls_back_to_every_pool_when_the_winner_cannot_trade() {
2252 let mut winners = PairWinners::new(true);
2253 let pools = pools(2);
2254 winners
2255 .hop(pair(), &pools, simulator([2, 5], 100))
2256 .unwrap();
2257
2258 let outcome = winners
2259 .hop(pair(), &pools, |component_id: &ComponentId| {
2260 (component_id == "pool0").then(|| {
2261 let amount = BigUint::from(50u64);
2262 (
2263 SwapResult { amount_out: amount.clone(), gas: BigUint::from(10u64) },
2264 BigInt::from(amount),
2265 )
2266 })
2267 })
2268 .unwrap();
2269
2270 assert_eq!(outcome.pool_ix, 0, "pool1 refused, so pool0 takes the pair");
2271 assert_eq!(outcome.amount_out, BigUint::from(50u64));
2272 }
2273
2274 #[test]
2276 fn test_winners_disabled_scans_every_pool_each_time() {
2277 let mut winners = PairWinners::new(false);
2278 let multipliers = [2u64, 5u64];
2279 let pools = pools(multipliers.len());
2280
2281 let first = winners
2282 .hop(pair(), &pools, simulator(multipliers, 100))
2283 .unwrap();
2284 let mut asked = 0usize;
2285 let second = winners
2286 .hop(pair(), &pools, |component_id: &ComponentId| {
2287 asked += 1;
2288 simulator(multipliers, 100)(component_id)
2289 })
2290 .unwrap();
2291
2292 assert_eq!(asked, 2, "both pools are asked again, so no winner was remembered");
2293 assert_eq!(first.amount_out, second.amount_out);
2294 }
2295
2296 #[test]
2298 fn test_winners_returns_none_when_no_pool_trades() {
2299 let mut winners = PairWinners::new(true);
2300 let pools = pools(2);
2301
2302 let outcome = winners.hop(pair(), &pools, |_| None);
2303
2304 assert!(outcome.is_none());
2305 }
2306}