1use std::{
11 collections::{HashMap, HashSet, VecDeque},
12 time::{Duration, Instant},
13};
14
15use metrics::{counter, histogram};
16use num_bigint::{BigInt, BigUint};
17use num_traits::ToPrimitive;
18use petgraph::prelude::EdgeRef;
19use tracing::{debug, instrument, trace};
20use tycho_simulation::{
21 tycho_common::simulation::protocol_sim::ProtocolSim,
22 tycho_core::models::{token::Token, Address},
23};
24
25use super::{Algorithm, AlgorithmConfig, NoPathReason};
26use crate::{
27 derived::{computation::ComputationRequirements, types::TokenGasPrices, SharedDerivedDataRef},
28 feed::market_data::{SharedMarketData, SharedMarketDataRef},
29 graph::{petgraph::StableDiGraph, Path, PetgraphStableDiGraphManager},
30 types::{ComponentId, Order, Route, RouteResult, Swap},
31 AlgorithmError,
32};
33pub struct MostLiquidAlgorithm {
35 min_hops: usize,
36 max_hops: usize,
37 timeout: Duration,
38 max_routes: Option<usize>,
39}
40
41#[derive(Debug, Clone, Default)]
47pub struct DepthAndPrice {
48 pub spot_price: f64,
50 pub depth: f64,
52}
53
54impl DepthAndPrice {
55 #[cfg(test)]
57 pub fn new(spot_price: f64, depth: f64) -> Self {
58 Self { spot_price, depth }
59 }
60
61 #[cfg(test)]
62 pub fn from_protocol_sim(
63 sim: &impl ProtocolSim,
64 token_in: &Token,
65 token_out: &Token,
66 ) -> Result<Self, AlgorithmError> {
67 Ok(Self {
68 spot_price: sim
69 .spot_price(token_in, token_out)
70 .map_err(|e| {
71 AlgorithmError::Other(format!("missing spot price for DepthAndPrice: {:?}", e))
72 })?,
73 depth: sim
74 .get_limits(token_in.address.clone(), token_out.address.clone())
75 .map_err(|e| {
76 AlgorithmError::Other(format!("missing depth for DepthAndPrice: {:?}", e))
77 })?
78 .0
79 .to_f64()
80 .ok_or_else(|| {
81 AlgorithmError::Other("depth conversion to f64 failed".to_string())
82 })?,
83 })
84 }
85}
86
87impl crate::graph::EdgeWeightFromSimAndDerived for DepthAndPrice {
88 fn from_sim_and_derived(
89 _sim: &dyn ProtocolSim,
90 component_id: &ComponentId,
91 token_in: &Token,
92 token_out: &Token,
93 derived: &crate::derived::DerivedData,
94 ) -> Option<Self> {
95 let key = (component_id.clone(), token_in.address.clone(), token_out.address.clone());
96
97 let spot_price = derived
99 .spot_prices()
100 .and_then(|prices| prices.get(&key).copied())?;
101
102 let depth = derived
104 .pool_depths()
105 .and_then(|depths| depths.get(&key))?
106 .to_f64()?;
107
108 Some(Self { spot_price, depth })
109 }
110}
111
112impl MostLiquidAlgorithm {
113 pub fn new() -> Self {
115 Self { min_hops: 1, max_hops: 3, timeout: Duration::from_millis(500), max_routes: None }
116 }
117
118 pub fn with_config(config: AlgorithmConfig) -> Result<Self, AlgorithmError> {
120 Ok(Self {
121 min_hops: config.min_hops(),
122 max_hops: config.max_hops(),
123 timeout: config.timeout(),
124 max_routes: config.max_routes(),
125 })
126 }
127
128 #[instrument(level = "debug", skip(graph))]
139 fn find_paths<'a>(
140 graph: &'a StableDiGraph<DepthAndPrice>,
141 from: &Address,
142 to: &Address,
143 min_hops: usize,
144 max_hops: usize,
145 ) -> Result<Vec<Path<'a, DepthAndPrice>>, AlgorithmError> {
146 if min_hops == 0 || min_hops > max_hops {
147 return Err(AlgorithmError::InvalidConfiguration {
148 reason: format!(
149 "invalid hop configuration: min_hops={min_hops} max_hops={max_hops}",
150 ),
151 });
152 }
153
154 let from_idx = graph
157 .node_indices()
158 .find(|&n| &graph[n] == from)
159 .ok_or(AlgorithmError::NoPath {
160 from: from.clone(),
161 to: to.clone(),
162 reason: NoPathReason::SourceTokenNotInGraph,
163 })?;
164 let to_idx = graph
165 .node_indices()
166 .find(|&n| &graph[n] == to)
167 .ok_or(AlgorithmError::NoPath {
168 from: from.clone(),
169 to: to.clone(),
170 reason: NoPathReason::DestinationTokenNotInGraph,
171 })?;
172
173 let mut paths = Vec::new();
174 let mut queue = VecDeque::new();
175 queue.push_back((from_idx, Path::new()));
176
177 while let Some((current_node, current_path)) = queue.pop_front() {
178 if current_path.len() >= max_hops {
179 continue;
180 }
181
182 for edge in graph.edges(current_node) {
183 let next_node = edge.target();
184 let next_addr = &graph[next_node];
185
186 let already_visited = current_path.tokens.contains(&next_addr);
191 let is_closing_circular_route = from_idx == to_idx && next_node == to_idx;
192 if already_visited && !is_closing_circular_route {
193 continue;
194 }
195
196 let mut new_path = current_path.clone();
197 new_path.add_hop(&graph[current_node], edge.weight(), next_addr);
198
199 if next_node == to_idx && new_path.len() >= min_hops {
200 paths.push(new_path.clone());
201 }
202
203 queue.push_back((next_node, new_path));
204 }
205 }
206
207 Ok(paths)
208 }
209
210 fn try_score_path(path: &Path<DepthAndPrice>) -> Option<f64> {
224 if path.is_empty() {
225 trace!("cannot score empty path");
226 return None;
227 }
228
229 let mut price = 1.0;
230 let mut min_depth = f64::MAX;
231
232 for edge in path.edge_iter() {
233 let Some(data) = edge.data.as_ref() else {
234 debug!(component_id = %edge.component_id, "edge missing weight data, path cannot be scored");
235 return None;
236 };
237
238 price *= data.spot_price;
239 min_depth = min_depth.min(data.depth);
240 }
241
242 Some(price * min_depth)
243 }
244
245 #[instrument(level = "trace", skip(path, market, token_prices), fields(hop_count = path.len()))]
258 pub(crate) fn simulate_path<D>(
259 path: &Path<D>,
260 market: &SharedMarketData,
261 token_prices: Option<&TokenGasPrices>,
262 amount_in: BigUint,
263 ) -> Result<RouteResult, AlgorithmError> {
264 let mut current_amount = amount_in.clone();
265 let mut swaps = Vec::with_capacity(path.len());
266
267 let mut state_overrides: HashMap<&ComponentId, Box<dyn ProtocolSim>> = HashMap::new();
269
270 for (address_in, edge_data, address_out) in path.iter() {
271 let token_in = market
273 .get_token(address_in)
274 .ok_or_else(|| AlgorithmError::DataNotFound {
275 kind: "token",
276 id: Some(format!("{:?}", address_in)),
277 })?;
278 let token_out = market
279 .get_token(address_out)
280 .ok_or_else(|| AlgorithmError::DataNotFound {
281 kind: "token",
282 id: Some(format!("{:?}", address_out)),
283 })?;
284
285 let component_id = &edge_data.component_id;
286 let component = market
287 .get_component(component_id)
288 .ok_or_else(|| AlgorithmError::DataNotFound {
289 kind: "component",
290 id: Some(component_id.clone()),
291 })?;
292 let component_state = market
293 .get_simulation_state(component_id)
294 .ok_or_else(|| AlgorithmError::DataNotFound {
295 kind: "simulation state",
296 id: Some(component_id.clone()),
297 })?;
298
299 let state = state_overrides
300 .get(component_id)
301 .map(Box::as_ref)
302 .unwrap_or(component_state);
303
304 let result = state
306 .get_amount_out(current_amount.clone(), token_in, token_out)
307 .map_err(|e| AlgorithmError::Other(format!("simulation error: {:?}", e)))?;
308
309 swaps.push(Swap::new(
311 component_id.clone(),
312 component.protocol_system.clone(),
313 token_in.address.clone(),
314 token_out.address.clone(),
315 current_amount.clone(),
316 result.amount.clone(),
317 result.gas,
318 component.clone(),
319 state.clone_box(),
320 ));
321
322 state_overrides.insert(component_id, result.new_state);
323 current_amount = result.amount;
324 }
325
326 let route = Route::new(swaps);
328 let output_amount = route
329 .swaps()
330 .last()
331 .map(|s| s.amount_out().clone())
332 .unwrap_or_else(|| BigUint::ZERO);
333
334 let gas_price = market
335 .gas_price()
336 .ok_or(AlgorithmError::DataNotFound { kind: "gas price", id: None })?
337 .effective_gas_price()
338 .clone();
339
340 let net_amount_out = if let Some(last_swap) = route.swaps().last() {
341 let total_gas = route.total_gas();
342 let gas_cost_wei = &total_gas * &gas_price;
343
344 let gas_cost_in_output_token: Option<BigUint> = token_prices
346 .and_then(|prices| prices.get(last_swap.token_out()))
347 .map(|price| {
348 &gas_cost_wei * &price.numerator / &price.denominator
351 });
352
353 match gas_cost_in_output_token {
354 Some(gas_cost) => BigInt::from(output_amount) - BigInt::from(gas_cost),
355 None => {
356 BigInt::from(output_amount)
359 }
360 }
361 } else {
362 BigInt::from(output_amount)
363 };
364
365 Ok(RouteResult::new(route, net_amount_out, gas_price))
366 }
367}
368
369impl Default for MostLiquidAlgorithm {
370 fn default() -> Self {
371 Self::new()
372 }
373}
374
375impl Algorithm for MostLiquidAlgorithm {
376 type GraphType = StableDiGraph<DepthAndPrice>;
377 type GraphManager = PetgraphStableDiGraphManager<DepthAndPrice>;
378
379 fn name(&self) -> &str {
380 "most_liquid"
381 }
382
383 #[instrument(level = "debug", skip_all, fields(order_id = %order.id()))]
385 async fn find_best_route(
386 &self,
387 graph: &Self::GraphType,
388 market: SharedMarketDataRef,
389 derived: Option<SharedDerivedDataRef>,
390 order: &Order,
391 ) -> Result<RouteResult, AlgorithmError> {
392 let start = Instant::now();
393
394 if !order.is_sell() {
396 return Err(AlgorithmError::ExactOutNotSupported);
397 }
398
399 let token_prices = if let Some(ref derived) = derived {
401 derived
402 .read()
403 .await
404 .token_prices()
405 .cloned()
406 } else {
407 None
408 };
409
410 let amount_in = order.amount().clone();
411
412 let all_paths = Self::find_paths(
414 graph,
415 order.token_in(),
416 order.token_out(),
417 self.min_hops,
418 self.max_hops,
419 )?;
420
421 let paths_candidates = all_paths.len();
422 if paths_candidates == 0 {
423 return Err(AlgorithmError::NoPath {
424 from: order.token_in().clone(),
425 to: order.token_out().clone(),
426 reason: NoPathReason::NoGraphPath,
427 });
428 }
429
430 let mut scored_paths: Vec<(Path<DepthAndPrice>, f64)> = all_paths
433 .into_iter()
434 .filter_map(|path| {
435 let score = Self::try_score_path(&path)?;
436 Some((path, score))
437 })
438 .collect();
439
440 scored_paths.sort_by(|(_, a_score), (_, b_score)| {
441 b_score
443 .partial_cmp(a_score)
444 .unwrap_or(std::cmp::Ordering::Equal)
445 });
446
447 if let Some(max_routes) = self.max_routes {
448 scored_paths.truncate(max_routes);
449 }
450
451 let paths_to_simulate = scored_paths.len();
452 let scoring_failures = paths_candidates - paths_to_simulate;
453 if paths_to_simulate == 0 {
454 return Err(AlgorithmError::NoPath {
455 from: order.token_in().clone(),
456 to: order.token_out().clone(),
457 reason: NoPathReason::NoScorablePaths,
458 });
459 }
460
461 let component_ids: HashSet<ComponentId> = scored_paths
463 .iter()
464 .flat_map(|(path, _)| {
465 path.edge_iter()
466 .iter()
467 .map(|e| e.component_id.clone())
468 })
469 .collect();
470
471 let market = {
473 let market = market.read().await;
474 if market.gas_price().is_none() {
475 return Err(AlgorithmError::DataNotFound { kind: "gas price", id: None });
476 }
477 let market_subset = market.extract_subset(&component_ids);
478 drop(market);
479 market_subset
480 };
481
482 let mut paths_simulated = 0usize;
483 let mut simulation_failures = 0usize;
484
485 let mut best: Option<RouteResult> = None;
487 let timeout_ms = self.timeout.as_millis() as u64;
488
489 for (edge_path, _) in scored_paths {
490 let elapsed_ms = start.elapsed().as_millis() as u64;
492 if elapsed_ms > timeout_ms {
493 break;
494 }
495
496 let result = match Self::simulate_path(
497 &edge_path,
498 &market,
499 token_prices.as_ref(),
500 amount_in.clone(),
501 ) {
502 Ok(r) => r,
503 Err(e) => {
504 trace!(error = %e, "simulation failed for path");
505 simulation_failures += 1;
506 continue;
507 }
508 };
509
510 if best
512 .as_ref()
513 .map(|best| result.net_amount_out() > best.net_amount_out())
514 .unwrap_or(true)
515 {
516 best = Some(result);
517 }
518
519 paths_simulated += 1;
520 }
521
522 let solve_time_ms = start.elapsed().as_millis() as u64;
524 let block_number = market
525 .last_updated()
526 .map(|b| b.number());
527 let coverage_pct = if paths_to_simulate == 0 {
529 100.0
530 } else {
531 (paths_simulated as f64 / paths_to_simulate as f64) * 100.0
532 };
533
534 counter!("algorithm.scoring_failures").increment(scoring_failures as u64);
536 counter!("algorithm.simulation_failures").increment(simulation_failures as u64);
537 histogram!("algorithm.simulation_coverage_pct").record(coverage_pct);
538
539 match &best {
540 Some(result) => {
541 let tokens = market.token_registry_ref();
542 let path_desc = result.route().path_description(tokens);
543 let protocols = result
544 .route()
545 .swaps()
546 .iter()
547 .map(|s| s.protocol())
548 .collect::<Vec<_>>();
549
550 let price = amount_in
551 .to_f64()
552 .filter(|&v| v > 0.0)
553 .and_then(|amt_in| {
554 result
555 .net_amount_out()
556 .to_f64()
557 .map(|amt_out| amt_out / amt_in)
558 })
559 .unwrap_or(f64::NAN);
560
561 debug!(
562 solve_time_ms,
563 block_number,
564 paths_candidates,
565 paths_to_simulate,
566 paths_simulated,
567 simulation_failures,
568 simulation_coverage_pct = coverage_pct,
569 components_considered = component_ids.len(),
570 tokens_considered = market.token_registry_ref().len(),
571 path = %path_desc,
572 amount_in = %amount_in,
573 net_amount_out = %result.net_amount_out(),
574 price_out_per_in = price,
575 hop_count = result.route().swaps().len(),
576 protocols = ?protocols,
577 "route found"
578 );
579 }
580 None => {
581 debug!(
582 solve_time_ms,
583 block_number,
584 paths_candidates,
585 paths_to_simulate,
586 paths_simulated,
587 simulation_failures,
588 simulation_coverage_pct = coverage_pct,
589 components_considered = component_ids.len(),
590 tokens_considered = market.token_registry_ref().len(),
591 "no viable route"
592 );
593 }
594 }
595
596 best.ok_or({
597 if solve_time_ms > timeout_ms {
598 AlgorithmError::Timeout { elapsed_ms: solve_time_ms }
599 } else {
600 AlgorithmError::InsufficientLiquidity
601 }
602 })
603 }
604
605 fn computation_requirements(&self) -> ComputationRequirements {
606 ComputationRequirements::none()
613 .allow_stale("token_prices")
614 .expect("Conflicting Computation Requirements")
615 }
616
617 fn timeout(&self) -> Duration {
618 self.timeout
619 }
620}
621
622#[cfg(test)]
623mod tests {
624 use std::{collections::HashSet, sync::Arc};
625
626 use rstest::rstest;
627 use tokio::sync::RwLock;
628 use tycho_simulation::{
629 tycho_core::simulation::protocol_sim::Price,
630 tycho_ethereum::gas::{BlockGasPrice, GasPrice},
631 };
632
633 use super::*;
634 use crate::{
635 algorithm::test_utils::{
636 addr, component,
637 fixtures::{addrs, diamond_graph, linear_graph, parallel_graph},
638 market_read, order, setup_market, token, MockProtocolSim, ONE_ETH,
639 },
640 derived::{types::TokenGasPrices, DerivedData},
641 graph::GraphManager,
642 types::OrderSide,
643 };
644
645 fn wrap_market(market: SharedMarketData) -> SharedMarketDataRef {
646 Arc::new(RwLock::new(market))
647 }
648
649 fn setup_derived_with_token_prices(token_addresses: &[Address]) -> SharedDerivedDataRef {
654 let mut token_prices: TokenGasPrices = HashMap::new();
655 for addr in token_addresses {
656 token_prices.insert(
658 addr.clone(),
659 Price { numerator: BigUint::from(1u64), denominator: BigUint::from(1u64) },
660 );
661 }
662
663 let mut derived_data = DerivedData::new();
664 derived_data.set_token_prices(token_prices, 1);
665 Arc::new(RwLock::new(derived_data))
666 }
667 #[test]
670 fn test_try_score_path_calculates_correctly() {
671 let (a, b, c, _) = addrs();
672 let mut m = linear_graph();
673
674 m.set_edge_weight(&"ab".to_string(), &a, &b, DepthAndPrice::new(2.0, 1000.0), false)
676 .unwrap();
677 m.set_edge_weight(&"bc".to_string(), &b, &c, DepthAndPrice::new(0.5, 500.0), false)
678 .unwrap();
679
680 let graph = m.graph();
682 let paths = MostLiquidAlgorithm::find_paths(graph, &a, &c, 2, 2).unwrap();
683 assert_eq!(paths.len(), 1);
684 let path = &paths[0];
685
686 let expected = 2.0 * 0.5 * 500.0;
688 let score = MostLiquidAlgorithm::try_score_path(path).unwrap();
689 assert_eq!(score, expected, "expected {expected}, got {score}");
690 }
691
692 #[test]
693 fn test_try_score_path_empty_returns_none() {
694 let path: Path<DepthAndPrice> = Path::new();
695 assert_eq!(MostLiquidAlgorithm::try_score_path(&path), None);
696 }
697
698 #[test]
699 fn test_try_score_path_missing_weight_returns_none() {
700 let (a, b, _, _) = addrs();
701 let m = linear_graph();
702 let graph = m.graph();
703 let paths = MostLiquidAlgorithm::find_paths(graph, &a, &b, 1, 1).unwrap();
704 assert_eq!(paths.len(), 1);
705 assert!(MostLiquidAlgorithm::try_score_path(&paths[0]).is_none());
706 }
707
708 #[test]
709 fn test_try_score_path_circular_route() {
710 let (a, b, _, _) = addrs();
712 let mut m = linear_graph();
713
714 m.set_edge_weight(&"ab".to_string(), &a, &b, DepthAndPrice::new(2.0, 1000.0), false)
718 .unwrap();
719 m.set_edge_weight(&"ab".to_string(), &b, &a, DepthAndPrice::new(0.6, 800.0), false)
720 .unwrap();
721
722 let graph = m.graph();
723 let paths = MostLiquidAlgorithm::find_paths(graph, &a, &a, 2, 2).unwrap();
725
726 assert_eq!(paths.len(), 1);
728
729 let score = MostLiquidAlgorithm::try_score_path(&paths[0]).unwrap();
734 let expected = 2.0 * 0.6 * 800.0;
735 assert_eq!(score, expected, "expected {expected}, got {score}");
736 }
737
738 fn all_ids(paths: Vec<Path<'_, DepthAndPrice>>) -> HashSet<Vec<&str>> {
741 paths
742 .iter()
743 .map(|p| {
744 p.iter()
745 .map(|(_, e, _)| e.component_id.as_str())
746 .collect()
747 })
748 .collect()
749 }
750
751 #[test]
752 fn test_find_paths_linear_forward_and_reverse() {
753 let (a, b, c, d) = addrs();
754 let m = linear_graph();
755 let g = m.graph();
756
757 let p = MostLiquidAlgorithm::find_paths(g, &a, &b, 1, 1).unwrap();
759 assert_eq!(all_ids(p), HashSet::from([vec!["ab"]]));
760
761 let p = MostLiquidAlgorithm::find_paths(g, &a, &c, 1, 2).unwrap();
762 assert_eq!(all_ids(p), HashSet::from([vec!["ab", "bc"]]));
763
764 let p = MostLiquidAlgorithm::find_paths(g, &a, &d, 1, 3).unwrap();
765 assert_eq!(all_ids(p), HashSet::from([vec!["ab", "bc", "cd"]]));
766
767 let p = MostLiquidAlgorithm::find_paths(g, &d, &a, 1, 3).unwrap();
769 assert_eq!(all_ids(p), HashSet::from([vec!["cd", "bc", "ab"]]));
770 }
771
772 #[test]
773 fn test_find_paths_respects_hop_bounds() {
774 let (a, _, c, d) = addrs();
775 let m = linear_graph();
776 let g = m.graph();
777
778 assert!(MostLiquidAlgorithm::find_paths(g, &a, &d, 1, 2)
780 .unwrap()
781 .is_empty());
782
783 assert!(MostLiquidAlgorithm::find_paths(g, &a, &c, 3, 3)
785 .unwrap()
786 .is_empty());
787 }
788
789 #[test]
790 fn test_find_paths_parallel_pools() {
791 let (a, b, c, _) = addrs();
792 let m = parallel_graph();
793 let g = m.graph();
794
795 let p = MostLiquidAlgorithm::find_paths(g, &a, &b, 1, 1).unwrap();
797 assert_eq!(all_ids(p), HashSet::from([vec!["ab1"], vec!["ab2"], vec!["ab3"]]));
798
799 let p = MostLiquidAlgorithm::find_paths(g, &a, &c, 1, 2).unwrap();
801 assert_eq!(
802 all_ids(p),
803 HashSet::from([
804 vec!["ab1", "bc1"],
805 vec!["ab1", "bc2"],
806 vec!["ab2", "bc1"],
807 vec!["ab2", "bc2"],
808 vec!["ab3", "bc1"],
809 vec!["ab3", "bc2"],
810 ])
811 );
812 }
813
814 #[test]
815 fn test_find_paths_diamond_multiple_routes() {
816 let (a, _, _, d) = addrs();
817 let m = diamond_graph();
818 let g = m.graph();
819
820 let p = MostLiquidAlgorithm::find_paths(g, &a, &d, 1, 2).unwrap();
822 assert_eq!(all_ids(p), HashSet::from([vec!["ab", "bd"], vec!["ac", "cd"]]));
823 }
824
825 #[test]
826 fn test_find_paths_no_intermediate_cycles() {
827 let (a, b, _, _) = addrs();
828 let m = linear_graph();
829 let g = m.graph();
830
831 let p = MostLiquidAlgorithm::find_paths(g, &a, &b, 1, 3).unwrap();
836 assert_eq!(all_ids(p), HashSet::from([vec!["ab"]]));
837 }
838
839 #[test]
840 fn test_find_paths_cyclic_same_source_dest() {
841 let (a, _, _, _) = addrs();
842 let m = parallel_graph();
844 let g = m.graph();
845
846 let p = MostLiquidAlgorithm::find_paths(g, &a, &a, 2, 2).unwrap();
849 assert_eq!(
850 all_ids(p),
851 HashSet::from([
852 vec!["ab1", "ab1"],
853 vec!["ab1", "ab2"],
854 vec!["ab1", "ab3"],
855 vec!["ab2", "ab1"],
856 vec!["ab2", "ab2"],
857 vec!["ab2", "ab3"],
858 vec!["ab3", "ab1"],
859 vec!["ab3", "ab2"],
860 vec!["ab3", "ab3"],
861 ])
862 );
863 }
864
865 #[rstest]
866 #[case::source_not_in_graph(false, true)]
867 #[case::dest_not_in_graph(true, false)]
868 fn test_find_paths_token_not_in_graph(#[case] from_exists: bool, #[case] to_exists: bool) {
869 let (a, b, _, _) = addrs();
871 let non_existent = addr(0x99);
872 let m = linear_graph();
873 let g = m.graph();
874
875 let from = if from_exists { a } else { non_existent.clone() };
876 let to = if to_exists { b } else { non_existent };
877
878 let result = MostLiquidAlgorithm::find_paths(g, &from, &to, 1, 3);
879
880 assert!(matches!(result, Err(AlgorithmError::NoPath { .. })));
881 }
882
883 #[rstest]
884 #[case::min_greater_than_max(3, 1)]
885 #[case::min_hops_zero(0, 1)]
886 fn test_find_paths_invalid_configuration(#[case] min_hops: usize, #[case] max_hops: usize) {
887 let (a, b, _, _) = addrs();
888 let m = linear_graph();
889 let g = m.graph();
890
891 assert!(matches!(
892 MostLiquidAlgorithm::find_paths(g, &a, &b, min_hops, max_hops)
893 .err()
894 .unwrap(),
895 AlgorithmError::InvalidConfiguration { reason: _ }
896 ));
897 }
898
899 #[test]
900 fn test_find_paths_bfs_ordering() {
901 let (a, b, c, d) = addrs();
906 let e = addr(0x0E);
907 let mut m = PetgraphStableDiGraphManager::<DepthAndPrice>::new();
908 let mut t = HashMap::new();
909 t.insert("ae".into(), vec![a.clone(), e.clone()]);
910 t.insert("ab".into(), vec![a.clone(), b.clone()]);
911 t.insert("be".into(), vec![b, e.clone()]);
912 t.insert("ac".into(), vec![a.clone(), c.clone()]);
913 t.insert("cd".into(), vec![c, d.clone()]);
914 t.insert("de".into(), vec![d, e.clone()]);
915 m.initialize_graph(&t);
916 let g = m.graph();
917
918 let p = MostLiquidAlgorithm::find_paths(g, &a, &e, 1, 3).unwrap();
919
920 assert_eq!(p.len(), 3, "Expected 3 paths total");
922 assert_eq!(p[0].len(), 1, "First path should be 1-hop");
923 assert_eq!(p[1].len(), 2, "Second path should be 2-hop");
924 assert_eq!(p[2].len(), 3, "Third path should be 3-hop");
925 }
926
927 #[test]
935 fn test_simulate_path_single_hop() {
936 let token_a = token(0x01, "A");
937 let token_b = token(0x02, "B");
938
939 let (market, manager) =
940 setup_market(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
941
942 let paths = MostLiquidAlgorithm::find_paths(
943 manager.graph(),
944 &token_a.address,
945 &token_b.address,
946 1,
947 1,
948 )
949 .unwrap();
950 let path = paths.into_iter().next().unwrap();
951
952 let result = MostLiquidAlgorithm::simulate_path(
953 &path,
954 &market_read(&market),
955 None,
956 BigUint::from(100u64),
957 )
958 .unwrap();
959
960 assert_eq!(result.route().swaps().len(), 1);
961 assert_eq!(*result.route().swaps()[0].amount_in(), BigUint::from(100u64));
962 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(200u64)); assert_eq!(result.route().swaps()[0].component_id(), "pool1");
964 }
965
966 #[test]
967 fn test_simulate_path_multi_hop_chains_amounts() {
968 let token_a = token(0x01, "A");
969 let token_b = token(0x02, "B");
970 let token_c = token(0x03, "C");
971
972 let (market, manager) = setup_market(vec![
973 ("pool1", &token_a, &token_b, MockProtocolSim::new(2.0)),
974 ("pool2", &token_b, &token_c, MockProtocolSim::new(3.0)),
975 ]);
976
977 let paths = MostLiquidAlgorithm::find_paths(
978 manager.graph(),
979 &token_a.address,
980 &token_c.address,
981 2,
982 2,
983 )
984 .unwrap();
985 let path = paths.into_iter().next().unwrap();
986
987 let result = MostLiquidAlgorithm::simulate_path(
988 &path,
989 &market_read(&market),
990 None,
991 BigUint::from(10u64),
992 )
993 .unwrap();
994
995 assert_eq!(result.route().swaps().len(), 2);
996 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(20u64));
998 assert_eq!(*result.route().swaps()[1].amount_in(), BigUint::from(20u64));
1000 assert_eq!(*result.route().swaps()[1].amount_out(), BigUint::from(60u64));
1001 }
1002
1003 #[test]
1004 fn test_simulate_path_same_pool_twice_uses_updated_state() {
1005 let token_a = token(0x01, "A");
1008 let token_b = token(0x02, "B");
1009
1010 let (market, manager) =
1011 setup_market(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1012
1013 let paths = MostLiquidAlgorithm::find_paths(
1016 manager.graph(),
1017 &token_a.address,
1018 &token_a.address,
1019 2,
1020 2,
1021 )
1022 .unwrap();
1023
1024 assert_eq!(paths.len(), 1);
1026 let path = paths[0].clone();
1027
1028 let result = MostLiquidAlgorithm::simulate_path(
1029 &path,
1030 &market_read(&market),
1031 None,
1032 BigUint::from(10u64),
1033 )
1034 .unwrap();
1035
1036 assert_eq!(result.route().swaps().len(), 2);
1037 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(20u64));
1039 assert_eq!(*result.route().swaps()[1].amount_out(), BigUint::from(6u64));
1041 }
1042
1043 #[test]
1044 fn test_simulate_path_missing_token_returns_data_not_found() {
1045 let token_a = token(0x01, "A");
1046 let token_b = token(0x02, "B");
1047 let token_c = token(0x03, "C");
1048
1049 let (market, _) =
1050 setup_market(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1051 let market = market_read(&market);
1052
1053 let mut topology = market.component_topology();
1055 topology
1056 .insert("pool2".to_string(), vec![token_b.address.clone(), token_c.address.clone()]);
1057 let mut manager = PetgraphStableDiGraphManager::default();
1058 manager.initialize_graph(&topology);
1059
1060 let graph = manager.graph();
1061 let paths =
1062 MostLiquidAlgorithm::find_paths(graph, &token_a.address, &token_c.address, 2, 2)
1063 .unwrap();
1064 let path = paths.into_iter().next().unwrap();
1065
1066 let result =
1067 MostLiquidAlgorithm::simulate_path(&path, &market, None, BigUint::from(100u64));
1068 assert!(matches!(result, Err(AlgorithmError::DataNotFound { kind: "token", .. })));
1069 }
1070
1071 #[test]
1072 fn test_simulate_path_missing_component_returns_data_not_found() {
1073 let token_a = token(0x01, "A");
1074 let token_b = token(0x02, "B");
1075 let (market, manager) =
1076 setup_market(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1077
1078 let mut market_write = market.try_write().unwrap();
1080 market_write.remove_components([&"pool1".to_string()]);
1081 drop(market_write);
1082
1083 let graph = manager.graph();
1084 let paths =
1085 MostLiquidAlgorithm::find_paths(graph, &token_a.address, &token_b.address, 1, 1)
1086 .unwrap();
1087 let path = paths.into_iter().next().unwrap();
1088
1089 let result = MostLiquidAlgorithm::simulate_path(
1090 &path,
1091 &market_read(&market),
1092 None,
1093 BigUint::from(100u64),
1094 );
1095 assert!(matches!(result, Err(AlgorithmError::DataNotFound { kind: "component", .. })));
1096 }
1097
1098 #[tokio::test]
1101 async fn test_find_best_route_single_path() {
1102 let token_a = token(0x01, "A");
1103 let token_b = token(0x02, "B");
1104
1105 let (market, manager) =
1106 setup_market(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1107
1108 let algorithm = MostLiquidAlgorithm::with_config(
1109 AlgorithmConfig::new(1, 1, Duration::from_millis(100), None).unwrap(),
1110 )
1111 .unwrap();
1112 let order = order(&token_a, &token_b, ONE_ETH, OrderSide::Sell);
1113 let result = algorithm
1114 .find_best_route(manager.graph(), market, None, &order)
1115 .await
1116 .unwrap();
1117
1118 assert_eq!(result.route().swaps().len(), 1);
1119 assert_eq!(*result.route().swaps()[0].amount_in(), BigUint::from(ONE_ETH));
1120 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(ONE_ETH * 2));
1121 }
1122
1123 #[tokio::test]
1124 async fn test_find_best_route_ranks_by_net_amount_out() {
1125 let token_a = token(0x01, "A");
1136 let token_b = token(0x02, "B");
1137
1138 let (market, manager) = setup_market(vec![
1139 ("best", &token_a, &token_b, MockProtocolSim::new(3.0).with_gas(10)),
1140 ("low_out", &token_a, &token_b, MockProtocolSim::new(2.0).with_gas(5)),
1141 ("high_gas", &token_a, &token_b, MockProtocolSim::new(4.0).with_gas(30)),
1142 ]);
1143
1144 let algorithm = MostLiquidAlgorithm::with_config(
1145 AlgorithmConfig::new(1, 1, Duration::from_millis(100), None).unwrap(),
1146 )
1147 .unwrap();
1148 let order = order(&token_a, &token_b, 1000, OrderSide::Sell);
1149
1150 let derived = setup_derived_with_token_prices(std::slice::from_ref(&token_b.address));
1152
1153 let result = algorithm
1154 .find_best_route(manager.graph(), market, Some(derived), &order)
1155 .await
1156 .unwrap();
1157
1158 assert_eq!(result.route().swaps().len(), 1);
1160 assert_eq!(result.route().swaps()[0].component_id(), "best");
1161 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(3000u64));
1162 assert_eq!(result.net_amount_out(), &BigInt::from(2000)); }
1164
1165 #[tokio::test]
1166 async fn test_find_best_route_no_path_returns_error() {
1167 let token_a = token(0x01, "A");
1168 let token_b = token(0x02, "B");
1169 let token_c = token(0x03, "C"); let (market, manager) =
1172 setup_market(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1173
1174 let algorithm = MostLiquidAlgorithm::new();
1175 let order = order(&token_a, &token_c, ONE_ETH, OrderSide::Sell);
1176
1177 let result = algorithm
1178 .find_best_route(manager.graph(), market, None, &order)
1179 .await;
1180 assert!(matches!(result, Err(AlgorithmError::NoPath { .. })));
1181 }
1182
1183 #[tokio::test]
1184 async fn test_find_best_route_multi_hop() {
1185 let token_a = token(0x01, "A");
1186 let token_b = token(0x02, "B");
1187 let token_c = token(0x03, "C");
1188
1189 let (market, manager) = setup_market(vec![
1190 ("pool1", &token_a, &token_b, MockProtocolSim::new(2.0)),
1191 ("pool2", &token_b, &token_c, MockProtocolSim::new(3.0)),
1192 ]);
1193
1194 let algorithm = MostLiquidAlgorithm::with_config(
1195 AlgorithmConfig::new(1, 2, Duration::from_millis(100), None).unwrap(),
1196 )
1197 .unwrap();
1198 let order = order(&token_a, &token_c, ONE_ETH, OrderSide::Sell);
1199
1200 let result = algorithm
1201 .find_best_route(manager.graph(), market, None, &order)
1202 .await
1203 .unwrap();
1204
1205 assert_eq!(result.route().swaps().len(), 2);
1207 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(ONE_ETH * 2));
1208 assert_eq!(result.route().swaps()[0].component_id(), "pool1".to_string());
1209 assert_eq!(*result.route().swaps()[1].amount_out(), BigUint::from(ONE_ETH * 2 * 3));
1210 assert_eq!(result.route().swaps()[1].component_id(), "pool2".to_string());
1211 }
1212
1213 #[tokio::test]
1214 async fn test_find_best_route_skips_paths_without_edge_weights() {
1215 let token_a = token(0x01, "A");
1217 let token_b = token(0x02, "B");
1218
1219 let mut market = SharedMarketData::new();
1221 let pool1_state = MockProtocolSim::new(2.0);
1222 let pool2_state = MockProtocolSim::new(3.0); let pool1_comp = component("pool1", &[token_a.clone(), token_b.clone()]);
1225 let pool2_comp = component("pool2", &[token_a.clone(), token_b.clone()]);
1226
1227 market.update_gas_price(BlockGasPrice {
1229 block_number: 1,
1230 block_hash: Default::default(),
1231 block_timestamp: 0,
1232 pricing: GasPrice::Legacy { gas_price: BigUint::from(1u64) },
1233 });
1234
1235 market.upsert_components(vec![pool1_comp, pool2_comp]);
1237
1238 market.update_states(vec![
1240 ("pool1".to_string(), Box::new(pool1_state.clone()) as Box<dyn ProtocolSim>),
1241 ("pool2".to_string(), Box::new(pool2_state) as Box<dyn ProtocolSim>),
1242 ]);
1243
1244 market.upsert_tokens(vec![token_a.clone(), token_b.clone()]);
1246
1247 let mut manager = PetgraphStableDiGraphManager::default();
1249 manager.initialize_graph(&market.component_topology());
1250
1251 let weight = DepthAndPrice::from_protocol_sim(&pool1_state, &token_a, &token_b).unwrap();
1253 manager
1254 .set_edge_weight(
1255 &"pool1".to_string(),
1256 &token_a.address,
1257 &token_b.address,
1258 weight,
1259 false,
1260 )
1261 .unwrap();
1262
1263 let algorithm = MostLiquidAlgorithm::with_config(
1265 AlgorithmConfig::new(1, 1, Duration::from_millis(100), None).unwrap(),
1266 )
1267 .unwrap();
1268 let order = order(&token_a, &token_b, ONE_ETH, OrderSide::Sell);
1269 let market = wrap_market(market);
1270 let result = algorithm
1271 .find_best_route(manager.graph(), market, None, &order)
1272 .await
1273 .unwrap();
1274
1275 assert_eq!(result.route().swaps().len(), 1);
1277 assert_eq!(result.route().swaps()[0].component_id(), "pool1");
1278 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(ONE_ETH * 2));
1279 }
1280
1281 #[tokio::test]
1282 async fn test_find_best_route_no_scorable_paths() {
1283 let token_a = token(0x01, "A");
1285 let token_b = token(0x02, "B");
1286
1287 let mut market = SharedMarketData::new();
1288 let pool_state = MockProtocolSim::new(2.0);
1289 let pool_comp = component("pool1", &[token_a.clone(), token_b.clone()]);
1290
1291 market.update_gas_price(BlockGasPrice {
1293 block_number: 1,
1294 block_hash: Default::default(),
1295 block_timestamp: 0,
1296 pricing: GasPrice::Eip1559 {
1297 base_fee_per_gas: BigUint::from(1u64),
1298 max_priority_fee_per_gas: BigUint::from(0u64),
1299 },
1300 });
1301
1302 market.upsert_components(vec![pool_comp]);
1303 market.update_states(vec![(
1304 "pool1".to_string(),
1305 Box::new(pool_state) as Box<dyn ProtocolSim>,
1306 )]);
1307 market.upsert_tokens(vec![token_a.clone(), token_b.clone()]);
1308
1309 let mut manager = PetgraphStableDiGraphManager::default();
1311 manager.initialize_graph(&market.component_topology());
1312
1313 let algorithm = MostLiquidAlgorithm::new();
1314 let order = order(&token_a, &token_b, ONE_ETH, OrderSide::Sell);
1315 let market = wrap_market(market);
1316
1317 let result = algorithm
1318 .find_best_route(manager.graph(), market, None, &order)
1319 .await;
1320 assert!(matches!(
1321 result,
1322 Err(AlgorithmError::NoPath { reason: NoPathReason::NoScorablePaths, .. })
1323 ));
1324 }
1325
1326 #[tokio::test]
1327 async fn test_find_best_route_gas_exceeds_output_returns_negative_net() {
1328 let token_a = token(0x01, "A");
1329 let token_b = token(0x02, "B");
1330
1331 let (market, manager) =
1332 setup_market(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1333 let mut market_write = market.try_write().unwrap();
1334
1335 market_write.update_gas_price(BlockGasPrice {
1338 block_number: 1,
1339 block_hash: Default::default(),
1340 block_timestamp: 0,
1341 pricing: GasPrice::Eip1559 {
1342 base_fee_per_gas: BigUint::from(1_000_000u64),
1343 max_priority_fee_per_gas: BigUint::from(1_000_000u64),
1344 },
1345 });
1346 drop(market_write); let algorithm = MostLiquidAlgorithm::new();
1349 let order = order(&token_a, &token_b, 1, OrderSide::Sell); let derived = setup_derived_with_token_prices(std::slice::from_ref(&token_b.address));
1353
1354 let result = algorithm
1356 .find_best_route(manager.graph(), market, Some(derived), &order)
1357 .await
1358 .expect("should return route even with negative net_amount_out");
1359
1360 assert_eq!(result.route().swaps().len(), 1);
1362 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(2u64)); let expected_net = BigInt::from(2) - BigInt::from(100_000_000_000u64);
1366 assert_eq!(result.net_amount_out(), &expected_net);
1367 }
1368
1369 #[tokio::test]
1370 async fn test_find_best_route_insufficient_liquidity() {
1371 let token_a = token(0x01, "A");
1373 let token_b = token(0x02, "B");
1374
1375 let (market, manager) = setup_market(vec![(
1376 "pool1",
1377 &token_a,
1378 &token_b,
1379 MockProtocolSim::new(2.0).with_liquidity(1000),
1380 )]);
1381
1382 let algorithm = MostLiquidAlgorithm::new();
1383 let order = order(&token_a, &token_b, ONE_ETH, OrderSide::Sell); let result = algorithm
1386 .find_best_route(manager.graph(), market, None, &order)
1387 .await;
1388 assert!(matches!(result, Err(AlgorithmError::InsufficientLiquidity)));
1389 }
1390
1391 #[tokio::test]
1392 async fn test_find_best_route_missing_gas_price_returns_error() {
1393 let token_a = token(0x01, "A");
1395 let token_b = token(0x02, "B");
1396
1397 let mut market = SharedMarketData::new();
1398 let pool_state = MockProtocolSim::new(2.0);
1399 let pool_comp = component("pool1", &[token_a.clone(), token_b.clone()]);
1400
1401 market.upsert_components(vec![pool_comp]);
1403 market.update_states(vec![(
1404 "pool1".to_string(),
1405 Box::new(pool_state.clone()) as Box<dyn ProtocolSim>,
1406 )]);
1407 market.upsert_tokens(vec![token_a.clone(), token_b.clone()]);
1408
1409 let mut manager = PetgraphStableDiGraphManager::default();
1411 manager.initialize_graph(&market.component_topology());
1412 let weight = DepthAndPrice::from_protocol_sim(&pool_state, &token_a, &token_b).unwrap();
1413 manager
1414 .set_edge_weight(
1415 &"pool1".to_string(),
1416 &token_a.address,
1417 &token_b.address,
1418 weight,
1419 false,
1420 )
1421 .unwrap();
1422
1423 let algorithm = MostLiquidAlgorithm::new();
1424 let order = order(&token_a, &token_b, ONE_ETH, OrderSide::Sell);
1425 let market = wrap_market(market);
1426
1427 let result = algorithm
1428 .find_best_route(manager.graph(), market, None, &order)
1429 .await;
1430
1431 assert!(matches!(result, Err(AlgorithmError::DataNotFound { kind: "gas price", .. })));
1433 }
1434
1435 #[tokio::test]
1436 async fn test_find_best_route_circular_arbitrage() {
1437 let token_a = token(0x01, "A");
1438 let token_b = token(0x02, "B");
1439
1440 let (market, manager) =
1443 setup_market(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1444
1445 let algorithm = MostLiquidAlgorithm::with_config(
1447 AlgorithmConfig::new(2, 2, Duration::from_millis(100), None).unwrap(),
1448 )
1449 .unwrap();
1450
1451 let order = order(&token_a, &token_a, 100, OrderSide::Sell);
1453
1454 let result = algorithm
1455 .find_best_route(manager.graph(), market, None, &order)
1456 .await
1457 .unwrap();
1458
1459 assert_eq!(result.route().swaps().len(), 2, "Should have 2 swaps for circular route");
1461
1462 assert_eq!(*result.route().swaps()[0].token_in(), token_a.address);
1464 assert_eq!(*result.route().swaps()[0].token_out(), token_b.address);
1465 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(200u64));
1466
1467 assert_eq!(*result.route().swaps()[1].token_in(), token_b.address);
1469 assert_eq!(*result.route().swaps()[1].token_out(), token_a.address);
1470 assert_eq!(*result.route().swaps()[1].amount_out(), BigUint::from(66u64));
1471
1472 assert_eq!(result.route().swaps()[0].token_in(), result.route().swaps()[1].token_out());
1474 }
1475
1476 #[tokio::test]
1477 async fn test_find_best_route_respects_min_hops() {
1478 let token_a = token(0x01, "A");
1481 let token_b = token(0x02, "B");
1482 let token_c = token(0x03, "C");
1483
1484 let (market, manager) = setup_market(vec![
1485 ("pool_ab", &token_a, &token_b, MockProtocolSim::new(10.0)), ("pool_ac", &token_a, &token_c, MockProtocolSim::new(2.0)), ("pool_cb", &token_c, &token_b, MockProtocolSim::new(3.0)), ]);
1490
1491 let algorithm = MostLiquidAlgorithm::with_config(
1493 AlgorithmConfig::new(2, 3, Duration::from_millis(100), None).unwrap(),
1494 )
1495 .unwrap();
1496 let order = order(&token_a, &token_b, 100, OrderSide::Sell);
1497
1498 let derived = setup_derived_with_token_prices(std::slice::from_ref(&token_b.address));
1501
1502 let result = algorithm
1503 .find_best_route(manager.graph(), market, Some(derived), &order)
1504 .await
1505 .unwrap();
1506
1507 assert_eq!(result.route().swaps().len(), 2, "Should use 2-hop path due to min_hops=2");
1509 assert_eq!(result.route().swaps()[0].component_id(), "pool_ac");
1510 assert_eq!(result.route().swaps()[1].component_id(), "pool_cb");
1511 }
1512
1513 #[tokio::test]
1514 async fn test_find_best_route_respects_max_hops() {
1515 let token_a = token(0x01, "A");
1518 let token_b = token(0x02, "B");
1519 let token_c = token(0x03, "C");
1520
1521 let (market, manager) = setup_market(vec![
1522 ("pool_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
1523 ("pool_bc", &token_b, &token_c, MockProtocolSim::new(3.0)),
1524 ]);
1525
1526 let algorithm = MostLiquidAlgorithm::with_config(
1528 AlgorithmConfig::new(1, 1, Duration::from_millis(100), None).unwrap(),
1529 )
1530 .unwrap();
1531 let order = order(&token_a, &token_c, 100, OrderSide::Sell);
1532
1533 let result = algorithm
1534 .find_best_route(manager.graph(), market, None, &order)
1535 .await;
1536 assert!(
1537 matches!(result, Err(AlgorithmError::NoPath { .. })),
1538 "Should return NoPath when max_hops is insufficient"
1539 );
1540 }
1541
1542 #[tokio::test]
1543 async fn test_find_best_route_timeout_returns_best_so_far() {
1544 let token_a = token(0x01, "A");
1548 let token_b = token(0x02, "B");
1549
1550 let (market, manager) = setup_market(vec![
1552 ("pool1", &token_a, &token_b, MockProtocolSim::new(1.0)),
1553 ("pool2", &token_a, &token_b, MockProtocolSim::new(2.0)),
1554 ("pool3", &token_a, &token_b, MockProtocolSim::new(3.0)),
1555 ("pool4", &token_a, &token_b, MockProtocolSim::new(4.0)),
1556 ("pool5", &token_a, &token_b, MockProtocolSim::new(5.0)),
1557 ]);
1558
1559 let algorithm = MostLiquidAlgorithm::with_config(
1561 AlgorithmConfig::new(1, 1, Duration::from_millis(0), None).unwrap(),
1562 )
1563 .unwrap();
1564 let order = order(&token_a, &token_b, 100, OrderSide::Sell);
1565
1566 let result = algorithm
1567 .find_best_route(manager.graph(), market, None, &order)
1568 .await;
1569
1570 match result {
1575 Ok(r) => {
1576 assert_eq!(r.route().swaps().len(), 1);
1578 }
1579 Err(AlgorithmError::Timeout { .. }) => {
1580 }
1582 Err(e) => panic!("Unexpected error: {:?}", e),
1583 }
1584 }
1585
1586 #[rstest::rstest]
1589 #[case::default_config(1, 3, 50)]
1590 #[case::single_hop_only(1, 1, 100)]
1591 #[case::multi_hop_min(2, 5, 200)]
1592 #[case::zero_timeout(1, 3, 0)]
1593 #[case::large_values(10, 100, 10000)]
1594 fn test_algorithm_config_getters(
1595 #[case] min_hops: usize,
1596 #[case] max_hops: usize,
1597 #[case] timeout_ms: u64,
1598 ) {
1599 use crate::algorithm::Algorithm;
1600
1601 let algorithm = MostLiquidAlgorithm::with_config(
1602 AlgorithmConfig::new(min_hops, max_hops, Duration::from_millis(timeout_ms), None)
1603 .unwrap(),
1604 )
1605 .unwrap();
1606
1607 assert_eq!(algorithm.max_hops, max_hops);
1608 assert_eq!(algorithm.timeout, Duration::from_millis(timeout_ms));
1609 assert_eq!(algorithm.name(), "most_liquid");
1610 }
1611
1612 #[test]
1613 fn test_algorithm_default_config() {
1614 use crate::algorithm::Algorithm;
1615
1616 let algorithm = MostLiquidAlgorithm::new();
1617
1618 assert_eq!(algorithm.max_hops, 3);
1619 assert_eq!(algorithm.timeout, Duration::from_millis(500));
1620 assert_eq!(algorithm.name(), "most_liquid");
1621 }
1622
1623 #[tokio::test]
1626 async fn test_find_best_route_respects_max_routes_cap() {
1627 let token_a = token(0x01, "A");
1640 let token_b = token(0x02, "B");
1641
1642 let (market, manager) = setup_market(vec![
1643 ("pool1", &token_a, &token_b, MockProtocolSim::new(4.0).with_liquidity(1_000_000)),
1644 ("pool2", &token_a, &token_b, MockProtocolSim::new(3.0).with_liquidity(2_000_000)),
1645 ("pool3", &token_a, &token_b, MockProtocolSim::new(2.0).with_liquidity(3_000_000)),
1646 ("pool4", &token_a, &token_b, MockProtocolSim::new(1.0).with_liquidity(4_000_000)),
1647 ]);
1648
1649 let algorithm = MostLiquidAlgorithm::with_config(
1651 AlgorithmConfig::new(1, 1, Duration::from_millis(100), Some(2)).unwrap(),
1652 )
1653 .unwrap();
1654 let order = order(&token_a, &token_b, 1000, OrderSide::Sell);
1655 let result = algorithm
1656 .find_best_route(manager.graph(), market, None, &order)
1657 .await
1658 .unwrap();
1659
1660 assert_eq!(result.route().swaps().len(), 1);
1664 assert_eq!(result.route().swaps()[0].component_id(), "pool3");
1665 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(2000u64));
1666 }
1667
1668 #[tokio::test]
1669 async fn test_find_best_route_no_cap_when_max_routes_is_none() {
1670 let token_a = token(0x01, "A");
1672 let token_b = token(0x02, "B");
1673
1674 let (market, manager) = setup_market(vec![
1675 ("pool1", &token_a, &token_b, MockProtocolSim::new(4.0).with_liquidity(1_000_000)),
1676 ("pool2", &token_a, &token_b, MockProtocolSim::new(3.0).with_liquidity(2_000_000)),
1677 ("pool3", &token_a, &token_b, MockProtocolSim::new(2.0).with_liquidity(3_000_000)),
1678 ("pool4", &token_a, &token_b, MockProtocolSim::new(1.0).with_liquidity(4_000_000)),
1679 ]);
1680
1681 let algorithm = MostLiquidAlgorithm::with_config(
1682 AlgorithmConfig::new(1, 1, Duration::from_millis(100), None).unwrap(),
1683 )
1684 .unwrap();
1685 let order = order(&token_a, &token_b, 1000, OrderSide::Sell);
1686 let result = algorithm
1687 .find_best_route(manager.graph(), market, None, &order)
1688 .await
1689 .unwrap();
1690
1691 assert_eq!(result.route().swaps().len(), 1);
1693 assert_eq!(result.route().swaps()[0].component_id(), "pool1");
1694 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(4000u64));
1695 }
1696
1697 #[test]
1698 fn test_algorithm_config_rejects_zero_max_routes() {
1699 let result = AlgorithmConfig::new(1, 3, Duration::from_millis(100), Some(0));
1700 assert!(matches!(
1701 result,
1702 Err(AlgorithmError::InvalidConfiguration { reason }) if reason.contains("max_routes must be at least 1")
1703 ));
1704 }
1705
1706 #[test]
1707 fn test_algorithm_config_rejects_zero_min_hops() {
1708 let result = AlgorithmConfig::new(0, 3, Duration::from_millis(100), None);
1709 assert!(matches!(
1710 result,
1711 Err(AlgorithmError::InvalidConfiguration { reason }) if reason.contains("min_hops must be at least 1")
1712 ));
1713 }
1714
1715 #[test]
1716 fn test_algorithm_config_rejects_min_greater_than_max() {
1717 let result = AlgorithmConfig::new(5, 3, Duration::from_millis(100), None);
1718 assert!(matches!(
1719 result,
1720 Err(AlgorithmError::InvalidConfiguration { reason }) if reason.contains("cannot exceed")
1721 ));
1722 }
1723}