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