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 = match derived
99 .spot_prices()
100 .and_then(|p| p.get(&key).copied())
101 {
102 Some(p) => p,
103 None => {
104 trace!(component_id = %component_id, "spot price not found, skipping edge");
105 return None;
106 }
107 };
108
109 let depth = match derived
111 .pool_depths()
112 .and_then(|d| d.get(&key))
113 {
114 Some(d) => d.to_f64().unwrap_or(0.0),
115 None => {
116 trace!(component_id = %component_id, "pool depth not found, skipping edge");
117 return None;
118 }
119 };
120
121 Some(Self { spot_price, depth })
122 }
123}
124
125impl MostLiquidAlgorithm {
126 pub fn new() -> Self {
128 Self { min_hops: 1, max_hops: 3, timeout: Duration::from_millis(500), max_routes: None }
129 }
130
131 pub fn with_config(config: AlgorithmConfig) -> Result<Self, AlgorithmError> {
133 Ok(Self {
134 min_hops: config.min_hops(),
135 max_hops: config.max_hops(),
136 timeout: config.timeout(),
137 max_routes: config.max_routes(),
138 })
139 }
140
141 #[instrument(level = "debug", skip(graph))]
152 fn find_paths<'a>(
153 graph: &'a StableDiGraph<DepthAndPrice>,
154 from: &Address,
155 to: &Address,
156 min_hops: usize,
157 max_hops: usize,
158 ) -> Result<Vec<Path<'a, DepthAndPrice>>, AlgorithmError> {
159 if min_hops == 0 || min_hops > max_hops {
160 return Err(AlgorithmError::InvalidConfiguration {
161 reason: format!(
162 "invalid hop configuration: min_hops={min_hops} max_hops={max_hops}",
163 ),
164 });
165 }
166
167 let from_idx = graph
170 .node_indices()
171 .find(|&n| &graph[n] == from)
172 .ok_or(AlgorithmError::NoPath {
173 from: from.clone(),
174 to: to.clone(),
175 reason: NoPathReason::SourceTokenNotInGraph,
176 })?;
177 let to_idx = graph
178 .node_indices()
179 .find(|&n| &graph[n] == to)
180 .ok_or(AlgorithmError::NoPath {
181 from: from.clone(),
182 to: to.clone(),
183 reason: NoPathReason::DestinationTokenNotInGraph,
184 })?;
185
186 let mut paths = Vec::new();
187 let mut queue = VecDeque::new();
188 queue.push_back((from_idx, Path::new()));
189
190 while let Some((current_node, current_path)) = queue.pop_front() {
191 if current_path.len() >= max_hops {
192 continue;
193 }
194
195 for edge in graph.edges(current_node) {
196 let next_node = edge.target();
197 let next_addr = &graph[next_node];
198
199 let already_visited = current_path.tokens.contains(&next_addr);
204 let is_closing_circular_route = from_idx == to_idx && next_node == to_idx;
205 if already_visited && !is_closing_circular_route {
206 continue;
207 }
208
209 let mut new_path = current_path.clone();
210 new_path.add_hop(&graph[current_node], edge.weight(), next_addr);
211
212 if next_node == to_idx && new_path.len() >= min_hops {
213 paths.push(new_path.clone());
214 }
215
216 queue.push_back((next_node, new_path));
217 }
218 }
219
220 Ok(paths)
221 }
222
223 fn try_score_path(path: &Path<DepthAndPrice>) -> Option<f64> {
237 if path.is_empty() {
238 trace!("cannot score empty path");
239 return None;
240 }
241
242 let mut price = 1.0;
243 let mut min_depth = f64::MAX;
244
245 for edge in path.edge_iter() {
246 let Some(data) = edge.data.as_ref() else {
247 debug!(component_id = %edge.component_id, "edge missing weight data, path cannot be scored");
248 return None;
249 };
250
251 price *= data.spot_price;
252 min_depth = min_depth.min(data.depth);
253 }
254
255 Some(price * min_depth)
256 }
257
258 #[instrument(level = "trace", skip(path, market, token_prices), fields(hop_count = path.len()))]
271 pub(crate) fn simulate_path<D>(
272 path: &Path<D>,
273 market: &SharedMarketData,
274 token_prices: Option<&TokenGasPrices>,
275 amount_in: BigUint,
276 ) -> Result<RouteResult, AlgorithmError> {
277 let mut current_amount = amount_in.clone();
278 let mut swaps = Vec::with_capacity(path.len());
279
280 let mut state_overrides: HashMap<&ComponentId, Box<dyn ProtocolSim>> = HashMap::new();
282
283 for (address_in, edge_data, address_out) in path.iter() {
284 let token_in = market
286 .get_token(address_in)
287 .ok_or_else(|| AlgorithmError::DataNotFound {
288 kind: "token",
289 id: Some(format!("{:?}", address_in)),
290 })?;
291 let token_out = market
292 .get_token(address_out)
293 .ok_or_else(|| AlgorithmError::DataNotFound {
294 kind: "token",
295 id: Some(format!("{:?}", address_out)),
296 })?;
297
298 let component_id = &edge_data.component_id;
299 let component = market
300 .get_component(component_id)
301 .ok_or_else(|| AlgorithmError::DataNotFound {
302 kind: "component",
303 id: Some(component_id.clone()),
304 })?;
305 let component_state = market
306 .get_simulation_state(component_id)
307 .ok_or_else(|| AlgorithmError::DataNotFound {
308 kind: "simulation state",
309 id: Some(component_id.clone()),
310 })?;
311
312 let state = state_overrides
313 .get(component_id)
314 .map(Box::as_ref)
315 .unwrap_or(component_state);
316
317 let result = state
319 .get_amount_out(current_amount.clone(), token_in, token_out)
320 .map_err(|e| AlgorithmError::Other(format!("simulation error: {:?}", e)))?;
321
322 swaps.push(Swap::new(
324 component_id.clone(),
325 component.protocol_system.clone(),
326 token_in.address.clone(),
327 token_out.address.clone(),
328 current_amount.clone(),
329 result.amount.clone(),
330 result.gas,
331 component.clone(),
332 state.clone_box(),
333 ));
334
335 state_overrides.insert(component_id, result.new_state);
336 current_amount = result.amount;
337 }
338
339 let route = Route::new(swaps);
341 let output_amount = route
342 .swaps()
343 .last()
344 .map(|s| s.amount_out().clone())
345 .unwrap_or_else(|| BigUint::ZERO);
346
347 let gas_price = market
348 .gas_price()
349 .ok_or(AlgorithmError::DataNotFound { kind: "gas price", id: None })?
350 .effective_gas_price()
351 .clone();
352
353 let net_amount_out = if let Some(last_swap) = route.swaps().last() {
354 let total_gas = route.total_gas();
355 let gas_cost_wei = &total_gas * &gas_price;
356
357 let gas_cost_in_output_token: Option<BigUint> = token_prices
359 .and_then(|prices| prices.get(last_swap.token_out()))
360 .map(|price| {
361 &gas_cost_wei * &price.numerator / &price.denominator
364 });
365
366 match gas_cost_in_output_token {
367 Some(gas_cost) => BigInt::from(output_amount) - BigInt::from(gas_cost),
368 None => {
369 BigInt::from(output_amount)
372 }
373 }
374 } else {
375 BigInt::from(output_amount)
376 };
377
378 Ok(RouteResult::new(route, net_amount_out, gas_price))
379 }
380}
381
382impl Default for MostLiquidAlgorithm {
383 fn default() -> Self {
384 Self::new()
385 }
386}
387
388impl Algorithm for MostLiquidAlgorithm {
389 type GraphType = StableDiGraph<DepthAndPrice>;
390 type GraphManager = PetgraphStableDiGraphManager<DepthAndPrice>;
391
392 fn name(&self) -> &str {
393 "most_liquid"
394 }
395
396 #[instrument(level = "debug", skip_all, fields(order_id = %order.id()))]
398 async fn find_best_route(
399 &self,
400 graph: &Self::GraphType,
401 market: SharedMarketDataRef,
402 derived: Option<SharedDerivedDataRef>,
403 order: &Order,
404 ) -> Result<RouteResult, AlgorithmError> {
405 let start = Instant::now();
406
407 if !order.is_sell() {
409 return Err(AlgorithmError::ExactOutNotSupported);
410 }
411
412 let token_prices = if let Some(ref derived) = derived {
414 derived
415 .read()
416 .await
417 .token_prices()
418 .cloned()
419 } else {
420 None
421 };
422
423 let amount_in = order.amount().clone();
424
425 let all_paths = Self::find_paths(
427 graph,
428 order.token_in(),
429 order.token_out(),
430 self.min_hops,
431 self.max_hops,
432 )?;
433
434 let paths_candidates = all_paths.len();
435 if paths_candidates == 0 {
436 return Err(AlgorithmError::NoPath {
437 from: order.token_in().clone(),
438 to: order.token_out().clone(),
439 reason: NoPathReason::NoGraphPath,
440 });
441 }
442
443 let mut scored_paths: Vec<(Path<DepthAndPrice>, f64)> = all_paths
446 .into_iter()
447 .filter_map(|path| {
448 let score = Self::try_score_path(&path)?;
449 Some((path, score))
450 })
451 .collect();
452
453 scored_paths.sort_by(|(_, a_score), (_, b_score)| {
454 b_score
456 .partial_cmp(a_score)
457 .unwrap_or(std::cmp::Ordering::Equal)
458 });
459
460 if let Some(max_routes) = self.max_routes {
461 scored_paths.truncate(max_routes);
462 }
463
464 let paths_to_simulate = scored_paths.len();
465 let scoring_failures = paths_candidates - paths_to_simulate;
466 if paths_to_simulate == 0 {
467 return Err(AlgorithmError::NoPath {
468 from: order.token_in().clone(),
469 to: order.token_out().clone(),
470 reason: NoPathReason::NoScorablePaths,
471 });
472 }
473
474 let component_ids: HashSet<ComponentId> = scored_paths
476 .iter()
477 .flat_map(|(path, _)| {
478 path.edge_iter()
479 .iter()
480 .map(|e| e.component_id.clone())
481 })
482 .collect();
483
484 let market = {
486 let market = market.read().await;
487 if market.gas_price().is_none() {
488 return Err(AlgorithmError::DataNotFound { kind: "gas price", id: None });
489 }
490 let market_subset = market.extract_subset(&component_ids);
491 drop(market);
492 market_subset
493 };
494
495 let mut paths_simulated = 0usize;
496 let mut simulation_failures = 0usize;
497
498 let mut best: Option<RouteResult> = None;
500 let timeout_ms = self.timeout.as_millis() as u64;
501
502 for (edge_path, _) in scored_paths {
503 let elapsed_ms = start.elapsed().as_millis() as u64;
505 if elapsed_ms > timeout_ms {
506 break;
507 }
508
509 let result = match Self::simulate_path(
510 &edge_path,
511 &market,
512 token_prices.as_ref(),
513 amount_in.clone(),
514 ) {
515 Ok(r) => r,
516 Err(e) => {
517 trace!(error = %e, "simulation failed for path");
518 simulation_failures += 1;
519 continue;
520 }
521 };
522
523 if best
525 .as_ref()
526 .map(|best| result.net_amount_out() > best.net_amount_out())
527 .unwrap_or(true)
528 {
529 best = Some(result);
530 }
531
532 paths_simulated += 1;
533 }
534
535 let solve_time_ms = start.elapsed().as_millis() as u64;
537 let block_number = market
538 .last_updated()
539 .map(|b| b.number());
540 let coverage_pct = if paths_to_simulate == 0 {
542 100.0
543 } else {
544 (paths_simulated as f64 / paths_to_simulate as f64) * 100.0
545 };
546
547 counter!("algorithm.scoring_failures").increment(scoring_failures as u64);
549 counter!("algorithm.simulation_failures").increment(simulation_failures as u64);
550 histogram!("algorithm.simulation_coverage_pct").record(coverage_pct);
551
552 match &best {
553 Some(result) => {
554 let tokens = market.token_registry_ref();
555 let path_desc = result.route().path_description(tokens);
556 let protocols = result
557 .route()
558 .swaps()
559 .iter()
560 .map(|s| s.protocol())
561 .collect::<Vec<_>>();
562
563 let price = amount_in
564 .to_f64()
565 .filter(|&v| v > 0.0)
566 .and_then(|amt_in| {
567 result
568 .net_amount_out()
569 .to_f64()
570 .map(|amt_out| amt_out / amt_in)
571 })
572 .unwrap_or(f64::NAN);
573
574 debug!(
575 solve_time_ms,
576 block_number,
577 paths_candidates,
578 paths_to_simulate,
579 paths_simulated,
580 simulation_failures,
581 simulation_coverage_pct = coverage_pct,
582 components_considered = component_ids.len(),
583 tokens_considered = market.token_registry_ref().len(),
584 path = %path_desc,
585 amount_in = %amount_in,
586 net_amount_out = %result.net_amount_out(),
587 price_out_per_in = price,
588 hop_count = result.route().swaps().len(),
589 protocols = ?protocols,
590 "route found"
591 );
592 }
593 None => {
594 debug!(
595 solve_time_ms,
596 block_number,
597 paths_candidates,
598 paths_to_simulate,
599 paths_simulated,
600 simulation_failures,
601 simulation_coverage_pct = coverage_pct,
602 components_considered = component_ids.len(),
603 tokens_considered = market.token_registry_ref().len(),
604 "no viable route"
605 );
606 }
607 }
608
609 best.ok_or({
610 if solve_time_ms > timeout_ms {
611 AlgorithmError::Timeout { elapsed_ms: solve_time_ms }
612 } else {
613 AlgorithmError::InsufficientLiquidity
614 }
615 })
616 }
617
618 fn computation_requirements(&self) -> ComputationRequirements {
619 ComputationRequirements::none()
626 .allow_stale("token_prices")
627 .expect("Conflicting Computation Requirements")
628 }
629
630 fn timeout(&self) -> Duration {
631 self.timeout
632 }
633}
634
635#[cfg(test)]
636mod tests {
637 use std::{collections::HashSet, sync::Arc};
638
639 use rstest::rstest;
640 use tokio::sync::RwLock;
641 use tycho_simulation::{
642 tycho_core::simulation::protocol_sim::Price,
643 tycho_ethereum::gas::{BlockGasPrice, GasPrice},
644 };
645
646 use super::*;
647 use crate::{
648 algorithm::test_utils::{
649 addr, component,
650 fixtures::{addrs, diamond_graph, linear_graph, parallel_graph},
651 market_read, order, setup_market, token, MockProtocolSim, ONE_ETH,
652 },
653 derived::{
654 computation::{FailedItem, FailedItemError},
655 types::TokenGasPrices,
656 DerivedData,
657 },
658 graph::GraphManager,
659 types::OrderSide,
660 };
661
662 fn wrap_market(market: SharedMarketData) -> SharedMarketDataRef {
663 Arc::new(RwLock::new(market))
664 }
665
666 fn setup_derived_with_token_prices(token_addresses: &[Address]) -> SharedDerivedDataRef {
671 let mut token_prices: TokenGasPrices = HashMap::new();
672 for addr in token_addresses {
673 token_prices.insert(
675 addr.clone(),
676 Price { numerator: BigUint::from(1u64), denominator: BigUint::from(1u64) },
677 );
678 }
679
680 let mut derived_data = DerivedData::new();
681 derived_data.set_token_prices(token_prices, vec![], 1, true);
682 Arc::new(RwLock::new(derived_data))
683 }
684 #[test]
687 fn test_try_score_path_calculates_correctly() {
688 let (a, b, c, _) = addrs();
689 let mut m = linear_graph();
690
691 m.set_edge_weight(&"ab".to_string(), &a, &b, DepthAndPrice::new(2.0, 1000.0), false)
693 .unwrap();
694 m.set_edge_weight(&"bc".to_string(), &b, &c, DepthAndPrice::new(0.5, 500.0), false)
695 .unwrap();
696
697 let graph = m.graph();
699 let paths = MostLiquidAlgorithm::find_paths(graph, &a, &c, 2, 2).unwrap();
700 assert_eq!(paths.len(), 1);
701 let path = &paths[0];
702
703 let expected = 2.0 * 0.5 * 500.0;
705 let score = MostLiquidAlgorithm::try_score_path(path).unwrap();
706 assert_eq!(score, expected, "expected {expected}, got {score}");
707 }
708
709 #[test]
710 fn test_try_score_path_empty_returns_none() {
711 let path: Path<DepthAndPrice> = Path::new();
712 assert_eq!(MostLiquidAlgorithm::try_score_path(&path), None);
713 }
714
715 #[test]
716 fn test_try_score_path_missing_weight_returns_none() {
717 let (a, b, _, _) = addrs();
718 let m = linear_graph();
719 let graph = m.graph();
720 let paths = MostLiquidAlgorithm::find_paths(graph, &a, &b, 1, 1).unwrap();
721 assert_eq!(paths.len(), 1);
722 assert!(MostLiquidAlgorithm::try_score_path(&paths[0]).is_none());
723 }
724
725 #[test]
726 fn test_try_score_path_circular_route() {
727 let (a, b, _, _) = addrs();
729 let mut m = linear_graph();
730
731 m.set_edge_weight(&"ab".to_string(), &a, &b, DepthAndPrice::new(2.0, 1000.0), false)
735 .unwrap();
736 m.set_edge_weight(&"ab".to_string(), &b, &a, DepthAndPrice::new(0.6, 800.0), false)
737 .unwrap();
738
739 let graph = m.graph();
740 let paths = MostLiquidAlgorithm::find_paths(graph, &a, &a, 2, 2).unwrap();
742
743 assert_eq!(paths.len(), 1);
745
746 let score = MostLiquidAlgorithm::try_score_path(&paths[0]).unwrap();
751 let expected = 2.0 * 0.6 * 800.0;
752 assert_eq!(score, expected, "expected {expected}, got {score}");
753 }
754
755 fn make_mock_sim() -> MockProtocolSim {
756 MockProtocolSim::new(2.0)
757 }
758
759 fn pair_key(comp: &str, b_in: u8, b_out: u8) -> (String, Address, Address) {
760 (comp.to_string(), addr(b_in), addr(b_out))
761 }
762
763 fn pair_key_str(comp: &str, b_in: u8, b_out: u8) -> String {
764 format!("{comp}/{}/{}", addr(b_in), addr(b_out))
765 }
766
767 #[test]
768 fn test_from_sim_and_derived_failed_spot_price_returns_none() {
769 let key = pair_key("pool1", 0x01, 0x02);
770 let key_str = pair_key_str("pool1", 0x01, 0x02);
771 let tok_in = token(0x01, "A");
772 let tok_out = token(0x02, "B");
773
774 let mut derived = DerivedData::new();
775 derived.set_spot_prices(
777 Default::default(),
778 vec![FailedItem {
779 key: key_str,
780 error: FailedItemError::SimulationFailed("sim error".into()),
781 }],
782 10,
783 true,
784 );
785 derived.set_pool_depths(Default::default(), vec![], 10, true);
786
787 let sim = make_mock_sim();
788 let result =
789 <DepthAndPrice as crate::graph::EdgeWeightFromSimAndDerived>::from_sim_and_derived(
790 &sim, &key.0, &tok_in, &tok_out, &derived,
791 );
792
793 assert!(result.is_none());
794 }
795
796 #[test]
797 fn test_from_sim_and_derived_failed_pool_depth_returns_none() {
798 let key = pair_key("pool1", 0x01, 0x02);
799 let key_str = pair_key_str("pool1", 0x01, 0x02);
800 let tok_in = token(0x01, "A");
801 let tok_out = token(0x02, "B");
802
803 let mut derived = DerivedData::new();
804 let mut prices = crate::derived::types::SpotPrices::default();
806 prices.insert(key.clone(), 1.5);
807 derived.set_spot_prices(prices, vec![], 10, true);
808 derived.set_pool_depths(
810 Default::default(),
811 vec![FailedItem {
812 key: key_str,
813 error: FailedItemError::SimulationFailed("depth error".into()),
814 }],
815 10,
816 true,
817 );
818
819 let sim = make_mock_sim();
820 let result =
821 <DepthAndPrice as crate::graph::EdgeWeightFromSimAndDerived>::from_sim_and_derived(
822 &sim, &key.0, &tok_in, &tok_out, &derived,
823 );
824
825 assert!(result.is_none());
826 }
827
828 #[test]
829 fn test_from_sim_and_derived_both_failed_returns_none() {
830 let key = pair_key("pool1", 0x01, 0x02);
831 let key_str = pair_key_str("pool1", 0x01, 0x02);
832 let tok_in = token(0x01, "A");
833 let tok_out = token(0x02, "B");
834
835 let mut derived = DerivedData::new();
836 derived.set_spot_prices(
837 Default::default(),
838 vec![FailedItem {
839 key: key_str.clone(),
840 error: FailedItemError::SimulationFailed("spot error".into()),
841 }],
842 10,
843 true,
844 );
845 derived.set_pool_depths(
846 Default::default(),
847 vec![FailedItem {
848 key: key_str,
849 error: FailedItemError::SimulationFailed("depth error".into()),
850 }],
851 10,
852 true,
853 );
854
855 let sim = make_mock_sim();
856 let result =
857 <DepthAndPrice as crate::graph::EdgeWeightFromSimAndDerived>::from_sim_and_derived(
858 &sim, &key.0, &tok_in, &tok_out, &derived,
859 );
860
861 assert!(result.is_none());
862 }
863
864 fn all_ids(paths: Vec<Path<'_, DepthAndPrice>>) -> HashSet<Vec<&str>> {
867 paths
868 .iter()
869 .map(|p| {
870 p.iter()
871 .map(|(_, e, _)| e.component_id.as_str())
872 .collect()
873 })
874 .collect()
875 }
876
877 #[test]
878 fn test_find_paths_linear_forward_and_reverse() {
879 let (a, b, c, d) = addrs();
880 let m = linear_graph();
881 let g = m.graph();
882
883 let p = MostLiquidAlgorithm::find_paths(g, &a, &b, 1, 1).unwrap();
885 assert_eq!(all_ids(p), HashSet::from([vec!["ab"]]));
886
887 let p = MostLiquidAlgorithm::find_paths(g, &a, &c, 1, 2).unwrap();
888 assert_eq!(all_ids(p), HashSet::from([vec!["ab", "bc"]]));
889
890 let p = MostLiquidAlgorithm::find_paths(g, &a, &d, 1, 3).unwrap();
891 assert_eq!(all_ids(p), HashSet::from([vec!["ab", "bc", "cd"]]));
892
893 let p = MostLiquidAlgorithm::find_paths(g, &d, &a, 1, 3).unwrap();
895 assert_eq!(all_ids(p), HashSet::from([vec!["cd", "bc", "ab"]]));
896 }
897
898 #[test]
899 fn test_find_paths_respects_hop_bounds() {
900 let (a, _, c, d) = addrs();
901 let m = linear_graph();
902 let g = m.graph();
903
904 assert!(MostLiquidAlgorithm::find_paths(g, &a, &d, 1, 2)
906 .unwrap()
907 .is_empty());
908
909 assert!(MostLiquidAlgorithm::find_paths(g, &a, &c, 3, 3)
911 .unwrap()
912 .is_empty());
913 }
914
915 #[test]
916 fn test_find_paths_parallel_pools() {
917 let (a, b, c, _) = addrs();
918 let m = parallel_graph();
919 let g = m.graph();
920
921 let p = MostLiquidAlgorithm::find_paths(g, &a, &b, 1, 1).unwrap();
923 assert_eq!(all_ids(p), HashSet::from([vec!["ab1"], vec!["ab2"], vec!["ab3"]]));
924
925 let p = MostLiquidAlgorithm::find_paths(g, &a, &c, 1, 2).unwrap();
927 assert_eq!(
928 all_ids(p),
929 HashSet::from([
930 vec!["ab1", "bc1"],
931 vec!["ab1", "bc2"],
932 vec!["ab2", "bc1"],
933 vec!["ab2", "bc2"],
934 vec!["ab3", "bc1"],
935 vec!["ab3", "bc2"],
936 ])
937 );
938 }
939
940 #[test]
941 fn test_find_paths_diamond_multiple_routes() {
942 let (a, _, _, d) = addrs();
943 let m = diamond_graph();
944 let g = m.graph();
945
946 let p = MostLiquidAlgorithm::find_paths(g, &a, &d, 1, 2).unwrap();
948 assert_eq!(all_ids(p), HashSet::from([vec!["ab", "bd"], vec!["ac", "cd"]]));
949 }
950
951 #[test]
952 fn test_find_paths_no_intermediate_cycles() {
953 let (a, b, _, _) = addrs();
954 let m = linear_graph();
955 let g = m.graph();
956
957 let p = MostLiquidAlgorithm::find_paths(g, &a, &b, 1, 3).unwrap();
962 assert_eq!(all_ids(p), HashSet::from([vec!["ab"]]));
963 }
964
965 #[test]
966 fn test_find_paths_cyclic_same_source_dest() {
967 let (a, _, _, _) = addrs();
968 let m = parallel_graph();
970 let g = m.graph();
971
972 let p = MostLiquidAlgorithm::find_paths(g, &a, &a, 2, 2).unwrap();
975 assert_eq!(
976 all_ids(p),
977 HashSet::from([
978 vec!["ab1", "ab1"],
979 vec!["ab1", "ab2"],
980 vec!["ab1", "ab3"],
981 vec!["ab2", "ab1"],
982 vec!["ab2", "ab2"],
983 vec!["ab2", "ab3"],
984 vec!["ab3", "ab1"],
985 vec!["ab3", "ab2"],
986 vec!["ab3", "ab3"],
987 ])
988 );
989 }
990
991 #[rstest]
992 #[case::source_not_in_graph(false, true)]
993 #[case::dest_not_in_graph(true, false)]
994 fn test_find_paths_token_not_in_graph(#[case] from_exists: bool, #[case] to_exists: bool) {
995 let (a, b, _, _) = addrs();
997 let non_existent = addr(0x99);
998 let m = linear_graph();
999 let g = m.graph();
1000
1001 let from = if from_exists { a } else { non_existent.clone() };
1002 let to = if to_exists { b } else { non_existent };
1003
1004 let result = MostLiquidAlgorithm::find_paths(g, &from, &to, 1, 3);
1005
1006 assert!(matches!(result, Err(AlgorithmError::NoPath { .. })));
1007 }
1008
1009 #[rstest]
1010 #[case::min_greater_than_max(3, 1)]
1011 #[case::min_hops_zero(0, 1)]
1012 fn test_find_paths_invalid_configuration(#[case] min_hops: usize, #[case] max_hops: usize) {
1013 let (a, b, _, _) = addrs();
1014 let m = linear_graph();
1015 let g = m.graph();
1016
1017 assert!(matches!(
1018 MostLiquidAlgorithm::find_paths(g, &a, &b, min_hops, max_hops)
1019 .err()
1020 .unwrap(),
1021 AlgorithmError::InvalidConfiguration { reason: _ }
1022 ));
1023 }
1024
1025 #[test]
1026 fn test_find_paths_bfs_ordering() {
1027 let (a, b, c, d) = addrs();
1032 let e = addr(0x0E);
1033 let mut m = PetgraphStableDiGraphManager::<DepthAndPrice>::new();
1034 let mut t = HashMap::new();
1035 t.insert("ae".into(), vec![a.clone(), e.clone()]);
1036 t.insert("ab".into(), vec![a.clone(), b.clone()]);
1037 t.insert("be".into(), vec![b, e.clone()]);
1038 t.insert("ac".into(), vec![a.clone(), c.clone()]);
1039 t.insert("cd".into(), vec![c, d.clone()]);
1040 t.insert("de".into(), vec![d, e.clone()]);
1041 m.initialize_graph(&t);
1042 let g = m.graph();
1043
1044 let p = MostLiquidAlgorithm::find_paths(g, &a, &e, 1, 3).unwrap();
1045
1046 assert_eq!(p.len(), 3, "Expected 3 paths total");
1048 assert_eq!(p[0].len(), 1, "First path should be 1-hop");
1049 assert_eq!(p[1].len(), 2, "Second path should be 2-hop");
1050 assert_eq!(p[2].len(), 3, "Third path should be 3-hop");
1051 }
1052
1053 #[test]
1061 fn test_simulate_path_single_hop() {
1062 let token_a = token(0x01, "A");
1063 let token_b = token(0x02, "B");
1064
1065 let (market, manager) =
1066 setup_market(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1067
1068 let paths = MostLiquidAlgorithm::find_paths(
1069 manager.graph(),
1070 &token_a.address,
1071 &token_b.address,
1072 1,
1073 1,
1074 )
1075 .unwrap();
1076 let path = paths.into_iter().next().unwrap();
1077
1078 let result = MostLiquidAlgorithm::simulate_path(
1079 &path,
1080 &market_read(&market),
1081 None,
1082 BigUint::from(100u64),
1083 )
1084 .unwrap();
1085
1086 assert_eq!(result.route().swaps().len(), 1);
1087 assert_eq!(*result.route().swaps()[0].amount_in(), BigUint::from(100u64));
1088 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(200u64)); assert_eq!(result.route().swaps()[0].component_id(), "pool1");
1090 }
1091
1092 #[test]
1093 fn test_simulate_path_multi_hop_chains_amounts() {
1094 let token_a = token(0x01, "A");
1095 let token_b = token(0x02, "B");
1096 let token_c = token(0x03, "C");
1097
1098 let (market, manager) = setup_market(vec![
1099 ("pool1", &token_a, &token_b, MockProtocolSim::new(2.0)),
1100 ("pool2", &token_b, &token_c, MockProtocolSim::new(3.0)),
1101 ]);
1102
1103 let paths = MostLiquidAlgorithm::find_paths(
1104 manager.graph(),
1105 &token_a.address,
1106 &token_c.address,
1107 2,
1108 2,
1109 )
1110 .unwrap();
1111 let path = paths.into_iter().next().unwrap();
1112
1113 let result = MostLiquidAlgorithm::simulate_path(
1114 &path,
1115 &market_read(&market),
1116 None,
1117 BigUint::from(10u64),
1118 )
1119 .unwrap();
1120
1121 assert_eq!(result.route().swaps().len(), 2);
1122 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(20u64));
1124 assert_eq!(*result.route().swaps()[1].amount_in(), BigUint::from(20u64));
1126 assert_eq!(*result.route().swaps()[1].amount_out(), BigUint::from(60u64));
1127 }
1128
1129 #[test]
1130 fn test_simulate_path_same_pool_twice_uses_updated_state() {
1131 let token_a = token(0x01, "A");
1134 let token_b = token(0x02, "B");
1135
1136 let (market, manager) =
1137 setup_market(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1138
1139 let paths = MostLiquidAlgorithm::find_paths(
1142 manager.graph(),
1143 &token_a.address,
1144 &token_a.address,
1145 2,
1146 2,
1147 )
1148 .unwrap();
1149
1150 assert_eq!(paths.len(), 1);
1152 let path = paths[0].clone();
1153
1154 let result = MostLiquidAlgorithm::simulate_path(
1155 &path,
1156 &market_read(&market),
1157 None,
1158 BigUint::from(10u64),
1159 )
1160 .unwrap();
1161
1162 assert_eq!(result.route().swaps().len(), 2);
1163 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(20u64));
1165 assert_eq!(*result.route().swaps()[1].amount_out(), BigUint::from(6u64));
1167 }
1168
1169 #[test]
1170 fn test_simulate_path_missing_token_returns_data_not_found() {
1171 let token_a = token(0x01, "A");
1172 let token_b = token(0x02, "B");
1173 let token_c = token(0x03, "C");
1174
1175 let (market, _) =
1176 setup_market(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1177 let market = market_read(&market);
1178
1179 let mut topology = market.component_topology();
1181 topology
1182 .insert("pool2".to_string(), vec![token_b.address.clone(), token_c.address.clone()]);
1183 let mut manager = PetgraphStableDiGraphManager::default();
1184 manager.initialize_graph(&topology);
1185
1186 let graph = manager.graph();
1187 let paths =
1188 MostLiquidAlgorithm::find_paths(graph, &token_a.address, &token_c.address, 2, 2)
1189 .unwrap();
1190 let path = paths.into_iter().next().unwrap();
1191
1192 let result =
1193 MostLiquidAlgorithm::simulate_path(&path, &market, None, BigUint::from(100u64));
1194 assert!(matches!(result, Err(AlgorithmError::DataNotFound { kind: "token", .. })));
1195 }
1196
1197 #[test]
1198 fn test_simulate_path_missing_component_returns_data_not_found() {
1199 let token_a = token(0x01, "A");
1200 let token_b = token(0x02, "B");
1201 let (market, manager) =
1202 setup_market(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1203
1204 let mut market_write = market.try_write().unwrap();
1206 market_write.remove_components([&"pool1".to_string()]);
1207 drop(market_write);
1208
1209 let graph = manager.graph();
1210 let paths =
1211 MostLiquidAlgorithm::find_paths(graph, &token_a.address, &token_b.address, 1, 1)
1212 .unwrap();
1213 let path = paths.into_iter().next().unwrap();
1214
1215 let result = MostLiquidAlgorithm::simulate_path(
1216 &path,
1217 &market_read(&market),
1218 None,
1219 BigUint::from(100u64),
1220 );
1221 assert!(matches!(result, Err(AlgorithmError::DataNotFound { kind: "component", .. })));
1222 }
1223
1224 #[tokio::test]
1227 async fn test_find_best_route_single_path() {
1228 let token_a = token(0x01, "A");
1229 let token_b = token(0x02, "B");
1230
1231 let (market, manager) =
1232 setup_market(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1233
1234 let algorithm = MostLiquidAlgorithm::with_config(
1235 AlgorithmConfig::new(1, 1, Duration::from_millis(100), None).unwrap(),
1236 )
1237 .unwrap();
1238 let order = order(&token_a, &token_b, ONE_ETH, OrderSide::Sell);
1239 let result = algorithm
1240 .find_best_route(manager.graph(), market, None, &order)
1241 .await
1242 .unwrap();
1243
1244 assert_eq!(result.route().swaps().len(), 1);
1245 assert_eq!(*result.route().swaps()[0].amount_in(), BigUint::from(ONE_ETH));
1246 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(ONE_ETH * 2));
1247 }
1248
1249 #[tokio::test]
1250 async fn test_find_best_route_ranks_by_net_amount_out() {
1251 let token_a = token(0x01, "A");
1262 let token_b = token(0x02, "B");
1263
1264 let (market, manager) = setup_market(vec![
1265 ("best", &token_a, &token_b, MockProtocolSim::new(3.0).with_gas(10)),
1266 ("low_out", &token_a, &token_b, MockProtocolSim::new(2.0).with_gas(5)),
1267 ("high_gas", &token_a, &token_b, MockProtocolSim::new(4.0).with_gas(30)),
1268 ]);
1269
1270 let algorithm = MostLiquidAlgorithm::with_config(
1271 AlgorithmConfig::new(1, 1, Duration::from_millis(100), None).unwrap(),
1272 )
1273 .unwrap();
1274 let order = order(&token_a, &token_b, 1000, OrderSide::Sell);
1275
1276 let derived = setup_derived_with_token_prices(std::slice::from_ref(&token_b.address));
1278
1279 let result = algorithm
1280 .find_best_route(manager.graph(), market, Some(derived), &order)
1281 .await
1282 .unwrap();
1283
1284 assert_eq!(result.route().swaps().len(), 1);
1286 assert_eq!(result.route().swaps()[0].component_id(), "best");
1287 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(3000u64));
1288 assert_eq!(result.net_amount_out(), &BigInt::from(2000)); }
1290
1291 #[tokio::test]
1292 async fn test_find_best_route_no_path_returns_error() {
1293 let token_a = token(0x01, "A");
1294 let token_b = token(0x02, "B");
1295 let token_c = token(0x03, "C"); let (market, manager) =
1298 setup_market(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1299
1300 let algorithm = MostLiquidAlgorithm::new();
1301 let order = order(&token_a, &token_c, ONE_ETH, OrderSide::Sell);
1302
1303 let result = algorithm
1304 .find_best_route(manager.graph(), market, None, &order)
1305 .await;
1306 assert!(matches!(result, Err(AlgorithmError::NoPath { .. })));
1307 }
1308
1309 #[tokio::test]
1310 async fn test_find_best_route_multi_hop() {
1311 let token_a = token(0x01, "A");
1312 let token_b = token(0x02, "B");
1313 let token_c = token(0x03, "C");
1314
1315 let (market, manager) = setup_market(vec![
1316 ("pool1", &token_a, &token_b, MockProtocolSim::new(2.0)),
1317 ("pool2", &token_b, &token_c, MockProtocolSim::new(3.0)),
1318 ]);
1319
1320 let algorithm = MostLiquidAlgorithm::with_config(
1321 AlgorithmConfig::new(1, 2, Duration::from_millis(100), None).unwrap(),
1322 )
1323 .unwrap();
1324 let order = order(&token_a, &token_c, ONE_ETH, OrderSide::Sell);
1325
1326 let result = algorithm
1327 .find_best_route(manager.graph(), market, None, &order)
1328 .await
1329 .unwrap();
1330
1331 assert_eq!(result.route().swaps().len(), 2);
1333 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(ONE_ETH * 2));
1334 assert_eq!(result.route().swaps()[0].component_id(), "pool1".to_string());
1335 assert_eq!(*result.route().swaps()[1].amount_out(), BigUint::from(ONE_ETH * 2 * 3));
1336 assert_eq!(result.route().swaps()[1].component_id(), "pool2".to_string());
1337 }
1338
1339 #[tokio::test]
1340 async fn test_find_best_route_skips_paths_without_edge_weights() {
1341 let token_a = token(0x01, "A");
1343 let token_b = token(0x02, "B");
1344
1345 let mut market = SharedMarketData::new();
1347 let pool1_state = MockProtocolSim::new(2.0);
1348 let pool2_state = MockProtocolSim::new(3.0); let pool1_comp = component("pool1", &[token_a.clone(), token_b.clone()]);
1351 let pool2_comp = component("pool2", &[token_a.clone(), token_b.clone()]);
1352
1353 market.update_gas_price(BlockGasPrice {
1355 block_number: 1,
1356 block_hash: Default::default(),
1357 block_timestamp: 0,
1358 pricing: GasPrice::Legacy { gas_price: BigUint::from(1u64) },
1359 });
1360
1361 market.upsert_components(vec![pool1_comp, pool2_comp]);
1363
1364 market.update_states(vec![
1366 ("pool1".to_string(), Box::new(pool1_state.clone()) as Box<dyn ProtocolSim>),
1367 ("pool2".to_string(), Box::new(pool2_state) as Box<dyn ProtocolSim>),
1368 ]);
1369
1370 market.upsert_tokens(vec![token_a.clone(), token_b.clone()]);
1372
1373 let mut manager = PetgraphStableDiGraphManager::default();
1375 manager.initialize_graph(&market.component_topology());
1376
1377 let weight = DepthAndPrice::from_protocol_sim(&pool1_state, &token_a, &token_b).unwrap();
1379 manager
1380 .set_edge_weight(
1381 &"pool1".to_string(),
1382 &token_a.address,
1383 &token_b.address,
1384 weight,
1385 false,
1386 )
1387 .unwrap();
1388
1389 let algorithm = MostLiquidAlgorithm::with_config(
1391 AlgorithmConfig::new(1, 1, Duration::from_millis(100), None).unwrap(),
1392 )
1393 .unwrap();
1394 let order = order(&token_a, &token_b, ONE_ETH, OrderSide::Sell);
1395 let market = wrap_market(market);
1396 let result = algorithm
1397 .find_best_route(manager.graph(), market, None, &order)
1398 .await
1399 .unwrap();
1400
1401 assert_eq!(result.route().swaps().len(), 1);
1403 assert_eq!(result.route().swaps()[0].component_id(), "pool1");
1404 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(ONE_ETH * 2));
1405 }
1406
1407 #[tokio::test]
1408 async fn test_find_best_route_no_scorable_paths() {
1409 let token_a = token(0x01, "A");
1411 let token_b = token(0x02, "B");
1412
1413 let mut market = SharedMarketData::new();
1414 let pool_state = MockProtocolSim::new(2.0);
1415 let pool_comp = component("pool1", &[token_a.clone(), token_b.clone()]);
1416
1417 market.update_gas_price(BlockGasPrice {
1419 block_number: 1,
1420 block_hash: Default::default(),
1421 block_timestamp: 0,
1422 pricing: GasPrice::Eip1559 {
1423 base_fee_per_gas: BigUint::from(1u64),
1424 max_priority_fee_per_gas: BigUint::from(0u64),
1425 },
1426 });
1427
1428 market.upsert_components(vec![pool_comp]);
1429 market.update_states(vec![(
1430 "pool1".to_string(),
1431 Box::new(pool_state) as Box<dyn ProtocolSim>,
1432 )]);
1433 market.upsert_tokens(vec![token_a.clone(), token_b.clone()]);
1434
1435 let mut manager = PetgraphStableDiGraphManager::default();
1437 manager.initialize_graph(&market.component_topology());
1438
1439 let algorithm = MostLiquidAlgorithm::new();
1440 let order = order(&token_a, &token_b, ONE_ETH, OrderSide::Sell);
1441 let market = wrap_market(market);
1442
1443 let result = algorithm
1444 .find_best_route(manager.graph(), market, None, &order)
1445 .await;
1446 assert!(matches!(
1447 result,
1448 Err(AlgorithmError::NoPath { reason: NoPathReason::NoScorablePaths, .. })
1449 ));
1450 }
1451
1452 #[tokio::test]
1453 async fn test_find_best_route_gas_exceeds_output_returns_negative_net() {
1454 let token_a = token(0x01, "A");
1455 let token_b = token(0x02, "B");
1456
1457 let (market, manager) =
1458 setup_market(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1459 let mut market_write = market.try_write().unwrap();
1460
1461 market_write.update_gas_price(BlockGasPrice {
1464 block_number: 1,
1465 block_hash: Default::default(),
1466 block_timestamp: 0,
1467 pricing: GasPrice::Eip1559 {
1468 base_fee_per_gas: BigUint::from(1_000_000u64),
1469 max_priority_fee_per_gas: BigUint::from(1_000_000u64),
1470 },
1471 });
1472 drop(market_write); let algorithm = MostLiquidAlgorithm::new();
1475 let order = order(&token_a, &token_b, 1, OrderSide::Sell); let derived = setup_derived_with_token_prices(std::slice::from_ref(&token_b.address));
1479
1480 let result = algorithm
1482 .find_best_route(manager.graph(), market, Some(derived), &order)
1483 .await
1484 .expect("should return route even with negative net_amount_out");
1485
1486 assert_eq!(result.route().swaps().len(), 1);
1488 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(2u64)); let expected_net = BigInt::from(2) - BigInt::from(100_000_000_000u64);
1492 assert_eq!(result.net_amount_out(), &expected_net);
1493 }
1494
1495 #[tokio::test]
1496 async fn test_find_best_route_insufficient_liquidity() {
1497 let token_a = token(0x01, "A");
1499 let token_b = token(0x02, "B");
1500
1501 let (market, manager) = setup_market(vec![(
1502 "pool1",
1503 &token_a,
1504 &token_b,
1505 MockProtocolSim::new(2.0).with_liquidity(1000),
1506 )]);
1507
1508 let algorithm = MostLiquidAlgorithm::new();
1509 let order = order(&token_a, &token_b, ONE_ETH, OrderSide::Sell); let result = algorithm
1512 .find_best_route(manager.graph(), market, None, &order)
1513 .await;
1514 assert!(matches!(result, Err(AlgorithmError::InsufficientLiquidity)));
1515 }
1516
1517 #[tokio::test]
1518 async fn test_find_best_route_missing_gas_price_returns_error() {
1519 let token_a = token(0x01, "A");
1521 let token_b = token(0x02, "B");
1522
1523 let mut market = SharedMarketData::new();
1524 let pool_state = MockProtocolSim::new(2.0);
1525 let pool_comp = component("pool1", &[token_a.clone(), token_b.clone()]);
1526
1527 market.upsert_components(vec![pool_comp]);
1529 market.update_states(vec![(
1530 "pool1".to_string(),
1531 Box::new(pool_state.clone()) as Box<dyn ProtocolSim>,
1532 )]);
1533 market.upsert_tokens(vec![token_a.clone(), token_b.clone()]);
1534
1535 let mut manager = PetgraphStableDiGraphManager::default();
1537 manager.initialize_graph(&market.component_topology());
1538 let weight = DepthAndPrice::from_protocol_sim(&pool_state, &token_a, &token_b).unwrap();
1539 manager
1540 .set_edge_weight(
1541 &"pool1".to_string(),
1542 &token_a.address,
1543 &token_b.address,
1544 weight,
1545 false,
1546 )
1547 .unwrap();
1548
1549 let algorithm = MostLiquidAlgorithm::new();
1550 let order = order(&token_a, &token_b, ONE_ETH, OrderSide::Sell);
1551 let market = wrap_market(market);
1552
1553 let result = algorithm
1554 .find_best_route(manager.graph(), market, None, &order)
1555 .await;
1556
1557 assert!(matches!(result, Err(AlgorithmError::DataNotFound { kind: "gas price", .. })));
1559 }
1560
1561 #[tokio::test]
1562 async fn test_find_best_route_circular_arbitrage() {
1563 let token_a = token(0x01, "A");
1564 let token_b = token(0x02, "B");
1565
1566 let (market, manager) =
1569 setup_market(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1570
1571 let algorithm = MostLiquidAlgorithm::with_config(
1573 AlgorithmConfig::new(2, 2, Duration::from_millis(100), None).unwrap(),
1574 )
1575 .unwrap();
1576
1577 let order = order(&token_a, &token_a, 100, OrderSide::Sell);
1579
1580 let result = algorithm
1581 .find_best_route(manager.graph(), market, None, &order)
1582 .await
1583 .unwrap();
1584
1585 assert_eq!(result.route().swaps().len(), 2, "Should have 2 swaps for circular route");
1587
1588 assert_eq!(*result.route().swaps()[0].token_in(), token_a.address);
1590 assert_eq!(*result.route().swaps()[0].token_out(), token_b.address);
1591 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(200u64));
1592
1593 assert_eq!(*result.route().swaps()[1].token_in(), token_b.address);
1595 assert_eq!(*result.route().swaps()[1].token_out(), token_a.address);
1596 assert_eq!(*result.route().swaps()[1].amount_out(), BigUint::from(66u64));
1597
1598 assert_eq!(result.route().swaps()[0].token_in(), result.route().swaps()[1].token_out());
1600 }
1601
1602 #[tokio::test]
1603 async fn test_find_best_route_respects_min_hops() {
1604 let token_a = token(0x01, "A");
1607 let token_b = token(0x02, "B");
1608 let token_c = token(0x03, "C");
1609
1610 let (market, manager) = setup_market(vec![
1611 ("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)), ]);
1616
1617 let algorithm = MostLiquidAlgorithm::with_config(
1619 AlgorithmConfig::new(2, 3, Duration::from_millis(100), None).unwrap(),
1620 )
1621 .unwrap();
1622 let order = order(&token_a, &token_b, 100, OrderSide::Sell);
1623
1624 let derived = setup_derived_with_token_prices(std::slice::from_ref(&token_b.address));
1627
1628 let result = algorithm
1629 .find_best_route(manager.graph(), market, Some(derived), &order)
1630 .await
1631 .unwrap();
1632
1633 assert_eq!(result.route().swaps().len(), 2, "Should use 2-hop path due to min_hops=2");
1635 assert_eq!(result.route().swaps()[0].component_id(), "pool_ac");
1636 assert_eq!(result.route().swaps()[1].component_id(), "pool_cb");
1637 }
1638
1639 #[tokio::test]
1640 async fn test_find_best_route_respects_max_hops() {
1641 let token_a = token(0x01, "A");
1644 let token_b = token(0x02, "B");
1645 let token_c = token(0x03, "C");
1646
1647 let (market, manager) = setup_market(vec![
1648 ("pool_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
1649 ("pool_bc", &token_b, &token_c, MockProtocolSim::new(3.0)),
1650 ]);
1651
1652 let algorithm = MostLiquidAlgorithm::with_config(
1654 AlgorithmConfig::new(1, 1, Duration::from_millis(100), None).unwrap(),
1655 )
1656 .unwrap();
1657 let order = order(&token_a, &token_c, 100, OrderSide::Sell);
1658
1659 let result = algorithm
1660 .find_best_route(manager.graph(), market, None, &order)
1661 .await;
1662 assert!(
1663 matches!(result, Err(AlgorithmError::NoPath { .. })),
1664 "Should return NoPath when max_hops is insufficient"
1665 );
1666 }
1667
1668 #[tokio::test]
1669 async fn test_find_best_route_timeout_returns_best_so_far() {
1670 let token_a = token(0x01, "A");
1674 let token_b = token(0x02, "B");
1675
1676 let (market, manager) = setup_market(vec![
1678 ("pool1", &token_a, &token_b, MockProtocolSim::new(1.0)),
1679 ("pool2", &token_a, &token_b, MockProtocolSim::new(2.0)),
1680 ("pool3", &token_a, &token_b, MockProtocolSim::new(3.0)),
1681 ("pool4", &token_a, &token_b, MockProtocolSim::new(4.0)),
1682 ("pool5", &token_a, &token_b, MockProtocolSim::new(5.0)),
1683 ]);
1684
1685 let algorithm = MostLiquidAlgorithm::with_config(
1687 AlgorithmConfig::new(1, 1, Duration::from_millis(0), None).unwrap(),
1688 )
1689 .unwrap();
1690 let order = order(&token_a, &token_b, 100, OrderSide::Sell);
1691
1692 let result = algorithm
1693 .find_best_route(manager.graph(), market, None, &order)
1694 .await;
1695
1696 match result {
1701 Ok(r) => {
1702 assert_eq!(r.route().swaps().len(), 1);
1704 }
1705 Err(AlgorithmError::Timeout { .. }) => {
1706 }
1708 Err(e) => panic!("Unexpected error: {:?}", e),
1709 }
1710 }
1711
1712 #[rstest::rstest]
1715 #[case::default_config(1, 3, 50)]
1716 #[case::single_hop_only(1, 1, 100)]
1717 #[case::multi_hop_min(2, 5, 200)]
1718 #[case::zero_timeout(1, 3, 0)]
1719 #[case::large_values(10, 100, 10000)]
1720 fn test_algorithm_config_getters(
1721 #[case] min_hops: usize,
1722 #[case] max_hops: usize,
1723 #[case] timeout_ms: u64,
1724 ) {
1725 use crate::algorithm::Algorithm;
1726
1727 let algorithm = MostLiquidAlgorithm::with_config(
1728 AlgorithmConfig::new(min_hops, max_hops, Duration::from_millis(timeout_ms), None)
1729 .unwrap(),
1730 )
1731 .unwrap();
1732
1733 assert_eq!(algorithm.max_hops, max_hops);
1734 assert_eq!(algorithm.timeout, Duration::from_millis(timeout_ms));
1735 assert_eq!(algorithm.name(), "most_liquid");
1736 }
1737
1738 #[test]
1739 fn test_algorithm_default_config() {
1740 use crate::algorithm::Algorithm;
1741
1742 let algorithm = MostLiquidAlgorithm::new();
1743
1744 assert_eq!(algorithm.max_hops, 3);
1745 assert_eq!(algorithm.timeout, Duration::from_millis(500));
1746 assert_eq!(algorithm.name(), "most_liquid");
1747 }
1748
1749 #[tokio::test]
1752 async fn test_find_best_route_respects_max_routes_cap() {
1753 let token_a = token(0x01, "A");
1766 let token_b = token(0x02, "B");
1767
1768 let (market, manager) = setup_market(vec![
1769 ("pool1", &token_a, &token_b, MockProtocolSim::new(4.0).with_liquidity(1_000_000)),
1770 ("pool2", &token_a, &token_b, MockProtocolSim::new(3.0).with_liquidity(2_000_000)),
1771 ("pool3", &token_a, &token_b, MockProtocolSim::new(2.0).with_liquidity(3_000_000)),
1772 ("pool4", &token_a, &token_b, MockProtocolSim::new(1.0).with_liquidity(4_000_000)),
1773 ]);
1774
1775 let algorithm = MostLiquidAlgorithm::with_config(
1777 AlgorithmConfig::new(1, 1, Duration::from_millis(100), Some(2)).unwrap(),
1778 )
1779 .unwrap();
1780 let order = order(&token_a, &token_b, 1000, OrderSide::Sell);
1781 let result = algorithm
1782 .find_best_route(manager.graph(), market, None, &order)
1783 .await
1784 .unwrap();
1785
1786 assert_eq!(result.route().swaps().len(), 1);
1790 assert_eq!(result.route().swaps()[0].component_id(), "pool3");
1791 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(2000u64));
1792 }
1793
1794 #[tokio::test]
1795 async fn test_find_best_route_no_cap_when_max_routes_is_none() {
1796 let token_a = token(0x01, "A");
1798 let token_b = token(0x02, "B");
1799
1800 let (market, manager) = setup_market(vec![
1801 ("pool1", &token_a, &token_b, MockProtocolSim::new(4.0).with_liquidity(1_000_000)),
1802 ("pool2", &token_a, &token_b, MockProtocolSim::new(3.0).with_liquidity(2_000_000)),
1803 ("pool3", &token_a, &token_b, MockProtocolSim::new(2.0).with_liquidity(3_000_000)),
1804 ("pool4", &token_a, &token_b, MockProtocolSim::new(1.0).with_liquidity(4_000_000)),
1805 ]);
1806
1807 let algorithm = MostLiquidAlgorithm::with_config(
1808 AlgorithmConfig::new(1, 1, Duration::from_millis(100), None).unwrap(),
1809 )
1810 .unwrap();
1811 let order = order(&token_a, &token_b, 1000, OrderSide::Sell);
1812 let result = algorithm
1813 .find_best_route(manager.graph(), market, None, &order)
1814 .await
1815 .unwrap();
1816
1817 assert_eq!(result.route().swaps().len(), 1);
1819 assert_eq!(result.route().swaps()[0].component_id(), "pool1");
1820 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(4000u64));
1821 }
1822
1823 #[test]
1824 fn test_algorithm_config_rejects_zero_max_routes() {
1825 let result = AlgorithmConfig::new(1, 3, Duration::from_millis(100), Some(0));
1826 assert!(matches!(
1827 result,
1828 Err(AlgorithmError::InvalidConfiguration { reason }) if reason.contains("max_routes must be at least 1")
1829 ));
1830 }
1831
1832 #[test]
1833 fn test_algorithm_config_rejects_zero_min_hops() {
1834 let result = AlgorithmConfig::new(0, 3, Duration::from_millis(100), None);
1835 assert!(matches!(
1836 result,
1837 Err(AlgorithmError::InvalidConfiguration { reason }) if reason.contains("min_hops must be at least 1")
1838 ));
1839 }
1840
1841 #[test]
1842 fn test_algorithm_config_rejects_min_greater_than_max() {
1843 let result = AlgorithmConfig::new(5, 3, Duration::from_millis(100), None);
1844 assert!(matches!(
1845 result,
1846 Err(AlgorithmError::InvalidConfiguration { reason }) if reason.contains("cannot exceed")
1847 ));
1848 }
1849}