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 raw_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 let depth = match derived
127 .token_prices()
128 .and_then(|p| p.get(&token_in.address))
129 {
130 Some(price) => {
131 let num = match price.numerator.to_f64() {
132 Some(v) if v > 0.0 => v,
133 Some(_) => {
134 trace!(
135 component_id = %component_id,
136 token_in = %token_in.address,
137 "token price numerator is zero, skipping edge"
138 );
139 return None;
140 }
141 None => {
142 trace!(
143 component_id = %component_id,
144 token_in = %token_in.address,
145 "token price numerator overflows f64, skipping edge"
146 );
147 return None;
148 }
149 };
150 let den = match price.denominator.to_f64() {
151 Some(v) if v > 0.0 => v,
152 Some(_) => {
153 trace!(
154 component_id = %component_id,
155 token_in = %token_in.address,
156 "token price denominator is zero, skipping edge"
157 );
158 return None;
159 }
160 None => {
161 trace!(
162 component_id = %component_id,
163 token_in = %token_in.address,
164 "token price denominator overflows f64, skipping edge"
165 );
166 return None;
167 }
168 };
169 raw_depth * den / num
170 }
171 None => {
172 trace!(
173 component_id = %component_id,
174 token_in = %token_in.address,
175 "token price not found, skipping edge"
176 );
177 return None;
178 }
179 };
180
181 Some(Self { spot_price, depth })
182 }
183}
184
185impl MostLiquidAlgorithm {
186 pub fn new() -> Self {
188 Self { min_hops: 1, max_hops: 3, timeout: Duration::from_millis(500), max_routes: None }
189 }
190
191 pub fn with_config(config: AlgorithmConfig) -> Result<Self, AlgorithmError> {
193 Ok(Self {
194 min_hops: config.min_hops(),
195 max_hops: config.max_hops(),
196 timeout: config.timeout(),
197 max_routes: config.max_routes(),
198 })
199 }
200
201 #[instrument(level = "debug", skip(graph))]
212 fn find_paths<'a>(
213 graph: &'a StableDiGraph<DepthAndPrice>,
214 from: &Address,
215 to: &Address,
216 min_hops: usize,
217 max_hops: usize,
218 ) -> Result<Vec<Path<'a, DepthAndPrice>>, AlgorithmError> {
219 if min_hops == 0 || min_hops > max_hops {
220 return Err(AlgorithmError::InvalidConfiguration {
221 reason: format!(
222 "invalid hop configuration: min_hops={min_hops} max_hops={max_hops}",
223 ),
224 });
225 }
226
227 let from_idx = graph
230 .node_indices()
231 .find(|&n| &graph[n] == from)
232 .ok_or(AlgorithmError::NoPath {
233 from: from.clone(),
234 to: to.clone(),
235 reason: NoPathReason::SourceTokenNotInGraph,
236 })?;
237 let to_idx = graph
238 .node_indices()
239 .find(|&n| &graph[n] == to)
240 .ok_or(AlgorithmError::NoPath {
241 from: from.clone(),
242 to: to.clone(),
243 reason: NoPathReason::DestinationTokenNotInGraph,
244 })?;
245
246 let mut paths = Vec::new();
247 let mut queue = VecDeque::new();
248 queue.push_back((from_idx, Path::new()));
249
250 while let Some((current_node, current_path)) = queue.pop_front() {
251 if current_path.len() >= max_hops {
252 continue;
253 }
254
255 for edge in graph.edges(current_node) {
256 let next_node = edge.target();
257 let next_addr = &graph[next_node];
258
259 let already_visited = current_path.tokens.contains(&next_addr);
264 let is_closing_circular_route = from_idx == to_idx && next_node == to_idx;
265 if already_visited && !is_closing_circular_route {
266 continue;
267 }
268
269 let mut new_path = current_path.clone();
270 new_path.add_hop(&graph[current_node], edge.weight(), next_addr);
271
272 if next_node == to_idx && new_path.len() >= min_hops {
273 paths.push(new_path.clone());
274 }
275
276 queue.push_back((next_node, new_path));
277 }
278 }
279
280 Ok(paths)
281 }
282
283 fn try_score_path(path: &Path<DepthAndPrice>) -> Option<f64> {
297 if path.is_empty() {
298 trace!("cannot score empty path");
299 return None;
300 }
301
302 let mut price = 1.0;
303 let mut min_depth = f64::MAX;
304
305 for edge in path.edge_iter() {
306 let Some(data) = edge.data.as_ref() else {
307 debug!(component_id = %edge.component_id, "edge missing weight data, path cannot be scored");
308 return None;
309 };
310
311 price *= data.spot_price;
312 min_depth = min_depth.min(data.depth);
313 }
314
315 Some(price * min_depth)
316 }
317
318 #[instrument(level = "trace", skip(path, market, token_prices), fields(hop_count = path.len()))]
331 pub(crate) fn simulate_path<D>(
332 path: &Path<D>,
333 market: &SharedMarketData,
334 token_prices: Option<&TokenGasPrices>,
335 amount_in: BigUint,
336 ) -> Result<RouteResult, AlgorithmError> {
337 let mut current_amount = amount_in.clone();
338 let mut swaps = Vec::with_capacity(path.len());
339
340 let mut state_overrides: HashMap<&ComponentId, Box<dyn ProtocolSim>> = HashMap::new();
342
343 for (address_in, edge_data, address_out) in path.iter() {
344 let token_in = market
346 .get_token(address_in)
347 .ok_or_else(|| AlgorithmError::DataNotFound {
348 kind: "token",
349 id: Some(format!("{:?}", address_in)),
350 })?;
351 let token_out = market
352 .get_token(address_out)
353 .ok_or_else(|| AlgorithmError::DataNotFound {
354 kind: "token",
355 id: Some(format!("{:?}", address_out)),
356 })?;
357
358 let component_id = &edge_data.component_id;
359 let component = market
360 .get_component(component_id)
361 .ok_or_else(|| AlgorithmError::DataNotFound {
362 kind: "component",
363 id: Some(component_id.clone()),
364 })?;
365 let component_state = market
366 .get_simulation_state(component_id)
367 .ok_or_else(|| AlgorithmError::DataNotFound {
368 kind: "simulation state",
369 id: Some(component_id.clone()),
370 })?;
371
372 let state = state_overrides
373 .get(component_id)
374 .map(Box::as_ref)
375 .unwrap_or(component_state);
376
377 let result = state
379 .get_amount_out(current_amount.clone(), token_in, token_out)
380 .map_err(|e| AlgorithmError::Other(format!("simulation error: {:?}", e)))?;
381
382 swaps.push(Swap::new(
384 component_id.clone(),
385 component.protocol_system.clone(),
386 token_in.address.clone(),
387 token_out.address.clone(),
388 current_amount.clone(),
389 result.amount.clone(),
390 result.gas,
391 component.clone(),
392 state.clone_box(),
393 ));
394
395 state_overrides.insert(component_id, result.new_state);
396 current_amount = result.amount;
397 }
398
399 let route = Route::new(swaps);
401 let output_amount = route
402 .swaps()
403 .last()
404 .map(|s| s.amount_out().clone())
405 .unwrap_or_else(|| BigUint::ZERO);
406
407 let gas_price = market
408 .gas_price()
409 .ok_or(AlgorithmError::DataNotFound { kind: "gas price", id: None })?
410 .effective_gas_price()
411 .clone();
412
413 let net_amount_out = if let Some(last_swap) = route.swaps().last() {
414 let total_gas = route.total_gas();
415 let gas_cost_wei = &total_gas * &gas_price;
416
417 let gas_cost_in_output_token: Option<BigUint> = token_prices
419 .and_then(|prices| prices.get(last_swap.token_out()))
420 .map(|price| {
421 &gas_cost_wei * &price.numerator / &price.denominator
424 });
425
426 match gas_cost_in_output_token {
427 Some(gas_cost) => BigInt::from(output_amount) - BigInt::from(gas_cost),
428 None => {
429 BigInt::from(output_amount)
432 }
433 }
434 } else {
435 BigInt::from(output_amount)
436 };
437
438 Ok(RouteResult::new(route, net_amount_out, gas_price))
439 }
440}
441
442impl Default for MostLiquidAlgorithm {
443 fn default() -> Self {
444 Self::new()
445 }
446}
447
448impl Algorithm for MostLiquidAlgorithm {
449 type GraphType = StableDiGraph<DepthAndPrice>;
450 type GraphManager = PetgraphStableDiGraphManager<DepthAndPrice>;
451
452 fn name(&self) -> &str {
453 "most_liquid"
454 }
455
456 #[instrument(level = "debug", skip_all, fields(order_id = %order.id()))]
458 async fn find_best_route(
459 &self,
460 graph: &Self::GraphType,
461 market: SharedMarketDataRef,
462 derived: Option<SharedDerivedDataRef>,
463 order: &Order,
464 ) -> Result<RouteResult, AlgorithmError> {
465 let start = Instant::now();
466
467 if !order.is_sell() {
469 return Err(AlgorithmError::ExactOutNotSupported);
470 }
471
472 let token_prices = if let Some(ref derived) = derived {
474 derived
475 .read()
476 .await
477 .token_prices()
478 .cloned()
479 } else {
480 None
481 };
482
483 let amount_in = order.amount().clone();
484
485 let all_paths = Self::find_paths(
487 graph,
488 order.token_in(),
489 order.token_out(),
490 self.min_hops,
491 self.max_hops,
492 )?;
493
494 let paths_candidates = all_paths.len();
495 if paths_candidates == 0 {
496 return Err(AlgorithmError::NoPath {
497 from: order.token_in().clone(),
498 to: order.token_out().clone(),
499 reason: NoPathReason::NoGraphPath,
500 });
501 }
502
503 let mut scored_paths: Vec<(Path<DepthAndPrice>, f64)> = all_paths
506 .into_iter()
507 .filter_map(|path| {
508 let score = Self::try_score_path(&path)?;
509 Some((path, score))
510 })
511 .collect();
512
513 scored_paths.sort_by(|(_, a_score), (_, b_score)| {
514 b_score
516 .partial_cmp(a_score)
517 .unwrap_or(std::cmp::Ordering::Equal)
518 });
519
520 if let Some(max_routes) = self.max_routes {
521 scored_paths.truncate(max_routes);
522 }
523
524 let paths_to_simulate = scored_paths.len();
525 let scoring_failures = paths_candidates - paths_to_simulate;
526 if paths_to_simulate == 0 {
527 return Err(AlgorithmError::NoPath {
528 from: order.token_in().clone(),
529 to: order.token_out().clone(),
530 reason: NoPathReason::NoScorablePaths,
531 });
532 }
533
534 let component_ids: HashSet<ComponentId> = scored_paths
536 .iter()
537 .flat_map(|(path, _)| {
538 path.edge_iter()
539 .iter()
540 .map(|e| e.component_id.clone())
541 })
542 .collect();
543
544 let market = {
546 let market = market.read().await;
547 if market.gas_price().is_none() {
548 return Err(AlgorithmError::DataNotFound { kind: "gas price", id: None });
549 }
550 let market_subset = market.extract_subset(&component_ids);
551 drop(market);
552 market_subset
553 };
554
555 let mut paths_simulated = 0usize;
556 let mut simulation_failures = 0usize;
557
558 let mut best: Option<RouteResult> = None;
560 let timeout_ms = self.timeout.as_millis() as u64;
561
562 for (edge_path, _) in scored_paths {
563 let elapsed_ms = start.elapsed().as_millis() as u64;
565 if elapsed_ms > timeout_ms {
566 break;
567 }
568
569 let result = match Self::simulate_path(
570 &edge_path,
571 &market,
572 token_prices.as_ref(),
573 amount_in.clone(),
574 ) {
575 Ok(r) => r,
576 Err(e) => {
577 trace!(error = %e, "simulation failed for path");
578 simulation_failures += 1;
579 continue;
580 }
581 };
582
583 if best
585 .as_ref()
586 .map(|best| result.net_amount_out() > best.net_amount_out())
587 .unwrap_or(true)
588 {
589 best = Some(result);
590 }
591
592 paths_simulated += 1;
593 }
594
595 let solve_time_ms = start.elapsed().as_millis() as u64;
597 let block_number = market
598 .last_updated()
599 .map(|b| b.number());
600 let coverage_pct = if paths_to_simulate == 0 {
602 100.0
603 } else {
604 (paths_simulated as f64 / paths_to_simulate as f64) * 100.0
605 };
606
607 counter!("algorithm.scoring_failures").increment(scoring_failures as u64);
609 counter!("algorithm.simulation_failures").increment(simulation_failures as u64);
610 histogram!("algorithm.simulation_coverage_pct").record(coverage_pct);
611
612 match &best {
613 Some(result) => {
614 let tokens = market.token_registry_ref();
615 let path_desc = result.route().path_description(tokens);
616 let protocols = result
617 .route()
618 .swaps()
619 .iter()
620 .map(|s| s.protocol())
621 .collect::<Vec<_>>();
622
623 let price = amount_in
624 .to_f64()
625 .filter(|&v| v > 0.0)
626 .and_then(|amt_in| {
627 result
628 .net_amount_out()
629 .to_f64()
630 .map(|amt_out| amt_out / amt_in)
631 })
632 .unwrap_or(f64::NAN);
633
634 debug!(
635 solve_time_ms,
636 block_number,
637 paths_candidates,
638 paths_to_simulate,
639 paths_simulated,
640 simulation_failures,
641 simulation_coverage_pct = coverage_pct,
642 components_considered = component_ids.len(),
643 tokens_considered = market.token_registry_ref().len(),
644 path = %path_desc,
645 amount_in = %amount_in,
646 net_amount_out = %result.net_amount_out(),
647 price_out_per_in = price,
648 hop_count = result.route().swaps().len(),
649 protocols = ?protocols,
650 "route found"
651 );
652 }
653 None => {
654 debug!(
655 solve_time_ms,
656 block_number,
657 paths_candidates,
658 paths_to_simulate,
659 paths_simulated,
660 simulation_failures,
661 simulation_coverage_pct = coverage_pct,
662 components_considered = component_ids.len(),
663 tokens_considered = market.token_registry_ref().len(),
664 "no viable route"
665 );
666 }
667 }
668
669 best.ok_or({
670 if solve_time_ms > timeout_ms {
671 AlgorithmError::Timeout { elapsed_ms: solve_time_ms }
672 } else {
673 AlgorithmError::InsufficientLiquidity
674 }
675 })
676 }
677
678 fn computation_requirements(&self) -> ComputationRequirements {
679 ComputationRequirements::none()
687 .allow_stale("token_prices")
688 .expect("Conflicting Computation Requirements")
689 }
690
691 fn timeout(&self) -> Duration {
692 self.timeout
693 }
694}
695
696#[cfg(test)]
697mod tests {
698 use std::{collections::HashSet, sync::Arc};
699
700 use rstest::rstest;
701 use tokio::sync::RwLock;
702 use tycho_simulation::{
703 tycho_core::simulation::protocol_sim::Price,
704 tycho_ethereum::gas::{BlockGasPrice, GasPrice},
705 };
706
707 use super::*;
708 use crate::{
709 algorithm::test_utils::{
710 addr, component,
711 fixtures::{addrs, diamond_graph, linear_graph, parallel_graph},
712 market_read, order, setup_market, token, MockProtocolSim, ONE_ETH,
713 },
714 derived::{
715 computation::{FailedItem, FailedItemError},
716 types::TokenGasPrices,
717 DerivedData,
718 },
719 graph::GraphManager,
720 types::OrderSide,
721 };
722
723 fn wrap_market(market: SharedMarketData) -> SharedMarketDataRef {
724 Arc::new(RwLock::new(market))
725 }
726
727 fn setup_derived_with_token_prices(token_addresses: &[Address]) -> SharedDerivedDataRef {
732 let mut token_prices: TokenGasPrices = HashMap::new();
733 for addr in token_addresses {
734 token_prices.insert(
736 addr.clone(),
737 Price { numerator: BigUint::from(1u64), denominator: BigUint::from(1u64) },
738 );
739 }
740
741 let mut derived_data = DerivedData::new();
742 derived_data.set_token_prices(token_prices, vec![], 1, true);
743 Arc::new(RwLock::new(derived_data))
744 }
745 #[test]
748 fn test_try_score_path_calculates_correctly() {
749 let (a, b, c, _) = addrs();
750 let mut m = linear_graph();
751
752 m.set_edge_weight(&"ab".to_string(), &a, &b, DepthAndPrice::new(2.0, 1000.0), false)
754 .unwrap();
755 m.set_edge_weight(&"bc".to_string(), &b, &c, DepthAndPrice::new(0.5, 500.0), false)
756 .unwrap();
757
758 let graph = m.graph();
760 let paths = MostLiquidAlgorithm::find_paths(graph, &a, &c, 2, 2).unwrap();
761 assert_eq!(paths.len(), 1);
762 let path = &paths[0];
763
764 let expected = 2.0 * 0.5 * 500.0;
766 let score = MostLiquidAlgorithm::try_score_path(path).unwrap();
767 assert_eq!(score, expected, "expected {expected}, got {score}");
768 }
769
770 #[test]
771 fn test_try_score_path_empty_returns_none() {
772 let path: Path<DepthAndPrice> = Path::new();
773 assert_eq!(MostLiquidAlgorithm::try_score_path(&path), None);
774 }
775
776 #[test]
777 fn test_try_score_path_missing_weight_returns_none() {
778 let (a, b, _, _) = addrs();
779 let m = linear_graph();
780 let graph = m.graph();
781 let paths = MostLiquidAlgorithm::find_paths(graph, &a, &b, 1, 1).unwrap();
782 assert_eq!(paths.len(), 1);
783 assert!(MostLiquidAlgorithm::try_score_path(&paths[0]).is_none());
784 }
785
786 #[test]
787 fn test_try_score_path_circular_route() {
788 let (a, b, _, _) = addrs();
790 let mut m = linear_graph();
791
792 m.set_edge_weight(&"ab".to_string(), &a, &b, DepthAndPrice::new(2.0, 1000.0), false)
796 .unwrap();
797 m.set_edge_weight(&"ab".to_string(), &b, &a, DepthAndPrice::new(0.6, 800.0), false)
798 .unwrap();
799
800 let graph = m.graph();
801 let paths = MostLiquidAlgorithm::find_paths(graph, &a, &a, 2, 2).unwrap();
803
804 assert_eq!(paths.len(), 1);
806
807 let score = MostLiquidAlgorithm::try_score_path(&paths[0]).unwrap();
812 let expected = 2.0 * 0.6 * 800.0;
813 assert_eq!(score, expected, "expected {expected}, got {score}");
814 }
815
816 fn make_mock_sim() -> MockProtocolSim {
817 MockProtocolSim::new(2.0)
818 }
819
820 fn pair_key(comp: &str, b_in: u8, b_out: u8) -> (String, Address, Address) {
821 (comp.to_string(), addr(b_in), addr(b_out))
822 }
823
824 fn pair_key_str(comp: &str, b_in: u8, b_out: u8) -> String {
825 format!("{comp}/{}/{}", addr(b_in), addr(b_out))
826 }
827
828 fn make_token_prices(addresses: &[Address]) -> TokenGasPrices {
829 let mut prices = TokenGasPrices::new();
830 for addr in addresses {
831 prices.insert(
833 addr.clone(),
834 Price { numerator: BigUint::from(1u64), denominator: BigUint::from(1u64) },
835 );
836 }
837 prices
838 }
839
840 #[test]
841 fn test_from_sim_and_derived_failed_spot_price_returns_none() {
842 let key = pair_key("pool1", 0x01, 0x02);
843 let key_str = pair_key_str("pool1", 0x01, 0x02);
844 let tok_in = token(0x01, "A");
845 let tok_out = token(0x02, "B");
846
847 let mut derived = DerivedData::new();
848 derived.set_spot_prices(
850 Default::default(),
851 vec![FailedItem {
852 key: key_str,
853 error: FailedItemError::SimulationFailed("sim error".into()),
854 }],
855 10,
856 true,
857 );
858 derived.set_pool_depths(Default::default(), vec![], 10, true);
859 derived.set_token_prices(
860 make_token_prices(&[tok_in.address.clone(), tok_out.address.clone()]),
861 vec![],
862 10,
863 true,
864 );
865
866 let sim = make_mock_sim();
867 let result =
868 <DepthAndPrice as crate::graph::EdgeWeightFromSimAndDerived>::from_sim_and_derived(
869 &sim, &key.0, &tok_in, &tok_out, &derived,
870 );
871
872 assert!(result.is_none());
873 }
874
875 #[test]
876 fn test_from_sim_and_derived_failed_pool_depth_returns_none() {
877 let key = pair_key("pool1", 0x01, 0x02);
878 let key_str = pair_key_str("pool1", 0x01, 0x02);
879 let tok_in = token(0x01, "A");
880 let tok_out = token(0x02, "B");
881
882 let mut derived = DerivedData::new();
883 let mut prices = crate::derived::types::SpotPrices::default();
885 prices.insert(key.clone(), 1.5);
886 derived.set_spot_prices(prices, vec![], 10, true);
887 derived.set_pool_depths(
889 Default::default(),
890 vec![FailedItem {
891 key: key_str,
892 error: FailedItemError::SimulationFailed("depth error".into()),
893 }],
894 10,
895 true,
896 );
897 derived.set_token_prices(
898 make_token_prices(&[tok_in.address.clone(), tok_out.address.clone()]),
899 vec![],
900 10,
901 true,
902 );
903
904 let sim = make_mock_sim();
905 let result =
906 <DepthAndPrice as crate::graph::EdgeWeightFromSimAndDerived>::from_sim_and_derived(
907 &sim, &key.0, &tok_in, &tok_out, &derived,
908 );
909
910 assert!(result.is_none());
911 }
912
913 #[test]
914 fn test_from_sim_and_derived_both_failed_returns_none() {
915 let key = pair_key("pool1", 0x01, 0x02);
916 let key_str = pair_key_str("pool1", 0x01, 0x02);
917 let tok_in = token(0x01, "A");
918 let tok_out = token(0x02, "B");
919
920 let mut derived = DerivedData::new();
921 derived.set_spot_prices(
922 Default::default(),
923 vec![FailedItem {
924 key: key_str.clone(),
925 error: FailedItemError::SimulationFailed("spot error".into()),
926 }],
927 10,
928 true,
929 );
930 derived.set_pool_depths(
931 Default::default(),
932 vec![FailedItem {
933 key: key_str,
934 error: FailedItemError::SimulationFailed("depth error".into()),
935 }],
936 10,
937 true,
938 );
939 derived.set_token_prices(
940 make_token_prices(&[tok_in.address.clone(), tok_out.address.clone()]),
941 vec![],
942 10,
943 true,
944 );
945
946 let sim = make_mock_sim();
947 let result =
948 <DepthAndPrice as crate::graph::EdgeWeightFromSimAndDerived>::from_sim_and_derived(
949 &sim, &key.0, &tok_in, &tok_out, &derived,
950 );
951
952 assert!(result.is_none());
953 }
954
955 #[test]
956 fn test_from_sim_and_derived_missing_token_price_returns_none() {
957 let key = pair_key("pool1", 0x01, 0x02);
958 let tok_in = token(0x01, "A");
959 let tok_out = token(0x02, "B");
960
961 let mut derived = DerivedData::new();
962 let mut prices = crate::derived::types::SpotPrices::default();
964 prices.insert(key.clone(), 1.5);
965 derived.set_spot_prices(prices, vec![], 10, true);
966
967 let mut depths = crate::derived::types::PoolDepths::default();
968 depths.insert(key.clone(), BigUint::from(1000u64));
969 derived.set_pool_depths(depths, vec![], 10, true);
970
971 let sim = make_mock_sim();
974 let result =
975 <DepthAndPrice as crate::graph::EdgeWeightFromSimAndDerived>::from_sim_and_derived(
976 &sim, &key.0, &tok_in, &tok_out, &derived,
977 );
978
979 assert!(
980 result.is_none(),
981 "should return None when token price is missing for depth normalization"
982 );
983 }
984
985 #[test]
986 fn test_from_sim_and_derived_normalizes_depth_to_eth() {
987 let key = pair_key("pool1", 0x01, 0x02);
988 let tok_in = token(0x01, "A");
989 let tok_out = token(0x02, "B");
990
991 let mut derived = DerivedData::new();
992
993 let mut spot = crate::derived::types::SpotPrices::default();
995 spot.insert(key.clone(), 2.0);
996 derived.set_spot_prices(spot, vec![], 10, true);
997
998 let mut depths = crate::derived::types::PoolDepths::default();
1000 depths.insert(key.clone(), BigUint::from(2_000_000u64));
1001 derived.set_pool_depths(depths, vec![], 10, true);
1002
1003 let mut token_prices = TokenGasPrices::new();
1006 token_prices.insert(
1007 tok_in.address.clone(),
1008 Price { numerator: BigUint::from(2000u64), denominator: BigUint::from(1u64) },
1009 );
1010 derived.set_token_prices(token_prices, vec![], 10, true);
1011
1012 let sim = make_mock_sim();
1013 let result =
1014 <DepthAndPrice as crate::graph::EdgeWeightFromSimAndDerived>::from_sim_and_derived(
1015 &sim, &key.0, &tok_in, &tok_out, &derived,
1016 );
1017
1018 let data = result.expect("should return Some when all data present");
1019 assert!((data.spot_price - 2.0).abs() < f64::EPSILON, "spot price should be 2.0");
1020 assert!(
1022 (data.depth - 1000.0).abs() < f64::EPSILON,
1023 "depth should be 1000.0 ETH, got {}",
1024 data.depth
1025 );
1026 }
1027
1028 #[test]
1029 fn test_from_sim_and_derived_normalizes_depth_fractional_price() {
1030 let key = pair_key("pool1", 0x01, 0x02);
1031 let tok_in = token(0x01, "A");
1032 let tok_out = token(0x02, "B");
1033
1034 let mut derived = DerivedData::new();
1035
1036 let mut spot = crate::derived::types::SpotPrices::default();
1037 spot.insert(key.clone(), 0.5);
1038 derived.set_spot_prices(spot, vec![], 10, true);
1039
1040 let mut depths = crate::derived::types::PoolDepths::default();
1042 depths.insert(key.clone(), BigUint::from(500u64));
1043 derived.set_pool_depths(depths, vec![], 10, true);
1044
1045 let mut token_prices = TokenGasPrices::new();
1048 token_prices.insert(
1049 tok_in.address.clone(),
1050 Price { numerator: BigUint::from(3u64), denominator: BigUint::from(2u64) },
1051 );
1052 derived.set_token_prices(token_prices, vec![], 10, true);
1053
1054 let sim = make_mock_sim();
1055 let result =
1056 <DepthAndPrice as crate::graph::EdgeWeightFromSimAndDerived>::from_sim_and_derived(
1057 &sim, &key.0, &tok_in, &tok_out, &derived,
1058 );
1059
1060 let data = result.expect("should return Some when all data present");
1061 let expected_depth = 500.0 * 2.0 / 3.0;
1062 assert!(
1063 (data.depth - expected_depth).abs() < 1e-10,
1064 "depth should be {expected_depth}, got {}",
1065 data.depth
1066 );
1067 }
1068
1069 fn all_ids(paths: Vec<Path<'_, DepthAndPrice>>) -> HashSet<Vec<&str>> {
1072 paths
1073 .iter()
1074 .map(|p| {
1075 p.iter()
1076 .map(|(_, e, _)| e.component_id.as_str())
1077 .collect()
1078 })
1079 .collect()
1080 }
1081
1082 #[test]
1083 fn test_find_paths_linear_forward_and_reverse() {
1084 let (a, b, c, d) = addrs();
1085 let m = linear_graph();
1086 let g = m.graph();
1087
1088 let p = MostLiquidAlgorithm::find_paths(g, &a, &b, 1, 1).unwrap();
1090 assert_eq!(all_ids(p), HashSet::from([vec!["ab"]]));
1091
1092 let p = MostLiquidAlgorithm::find_paths(g, &a, &c, 1, 2).unwrap();
1093 assert_eq!(all_ids(p), HashSet::from([vec!["ab", "bc"]]));
1094
1095 let p = MostLiquidAlgorithm::find_paths(g, &a, &d, 1, 3).unwrap();
1096 assert_eq!(all_ids(p), HashSet::from([vec!["ab", "bc", "cd"]]));
1097
1098 let p = MostLiquidAlgorithm::find_paths(g, &d, &a, 1, 3).unwrap();
1100 assert_eq!(all_ids(p), HashSet::from([vec!["cd", "bc", "ab"]]));
1101 }
1102
1103 #[test]
1104 fn test_find_paths_respects_hop_bounds() {
1105 let (a, _, c, d) = addrs();
1106 let m = linear_graph();
1107 let g = m.graph();
1108
1109 assert!(MostLiquidAlgorithm::find_paths(g, &a, &d, 1, 2)
1111 .unwrap()
1112 .is_empty());
1113
1114 assert!(MostLiquidAlgorithm::find_paths(g, &a, &c, 3, 3)
1116 .unwrap()
1117 .is_empty());
1118 }
1119
1120 #[test]
1121 fn test_find_paths_parallel_pools() {
1122 let (a, b, c, _) = addrs();
1123 let m = parallel_graph();
1124 let g = m.graph();
1125
1126 let p = MostLiquidAlgorithm::find_paths(g, &a, &b, 1, 1).unwrap();
1128 assert_eq!(all_ids(p), HashSet::from([vec!["ab1"], vec!["ab2"], vec!["ab3"]]));
1129
1130 let p = MostLiquidAlgorithm::find_paths(g, &a, &c, 1, 2).unwrap();
1132 assert_eq!(
1133 all_ids(p),
1134 HashSet::from([
1135 vec!["ab1", "bc1"],
1136 vec!["ab1", "bc2"],
1137 vec!["ab2", "bc1"],
1138 vec!["ab2", "bc2"],
1139 vec!["ab3", "bc1"],
1140 vec!["ab3", "bc2"],
1141 ])
1142 );
1143 }
1144
1145 #[test]
1146 fn test_find_paths_diamond_multiple_routes() {
1147 let (a, _, _, d) = addrs();
1148 let m = diamond_graph();
1149 let g = m.graph();
1150
1151 let p = MostLiquidAlgorithm::find_paths(g, &a, &d, 1, 2).unwrap();
1153 assert_eq!(all_ids(p), HashSet::from([vec!["ab", "bd"], vec!["ac", "cd"]]));
1154 }
1155
1156 #[test]
1157 fn test_find_paths_no_intermediate_cycles() {
1158 let (a, b, _, _) = addrs();
1159 let m = linear_graph();
1160 let g = m.graph();
1161
1162 let p = MostLiquidAlgorithm::find_paths(g, &a, &b, 1, 3).unwrap();
1167 assert_eq!(all_ids(p), HashSet::from([vec!["ab"]]));
1168 }
1169
1170 #[test]
1171 fn test_find_paths_cyclic_same_source_dest() {
1172 let (a, _, _, _) = addrs();
1173 let m = parallel_graph();
1175 let g = m.graph();
1176
1177 let p = MostLiquidAlgorithm::find_paths(g, &a, &a, 2, 2).unwrap();
1180 assert_eq!(
1181 all_ids(p),
1182 HashSet::from([
1183 vec!["ab1", "ab1"],
1184 vec!["ab1", "ab2"],
1185 vec!["ab1", "ab3"],
1186 vec!["ab2", "ab1"],
1187 vec!["ab2", "ab2"],
1188 vec!["ab2", "ab3"],
1189 vec!["ab3", "ab1"],
1190 vec!["ab3", "ab2"],
1191 vec!["ab3", "ab3"],
1192 ])
1193 );
1194 }
1195
1196 #[rstest]
1197 #[case::source_not_in_graph(false, true)]
1198 #[case::dest_not_in_graph(true, false)]
1199 fn test_find_paths_token_not_in_graph(#[case] from_exists: bool, #[case] to_exists: bool) {
1200 let (a, b, _, _) = addrs();
1202 let non_existent = addr(0x99);
1203 let m = linear_graph();
1204 let g = m.graph();
1205
1206 let from = if from_exists { a } else { non_existent.clone() };
1207 let to = if to_exists { b } else { non_existent };
1208
1209 let result = MostLiquidAlgorithm::find_paths(g, &from, &to, 1, 3);
1210
1211 assert!(matches!(result, Err(AlgorithmError::NoPath { .. })));
1212 }
1213
1214 #[rstest]
1215 #[case::min_greater_than_max(3, 1)]
1216 #[case::min_hops_zero(0, 1)]
1217 fn test_find_paths_invalid_configuration(#[case] min_hops: usize, #[case] max_hops: usize) {
1218 let (a, b, _, _) = addrs();
1219 let m = linear_graph();
1220 let g = m.graph();
1221
1222 assert!(matches!(
1223 MostLiquidAlgorithm::find_paths(g, &a, &b, min_hops, max_hops)
1224 .err()
1225 .unwrap(),
1226 AlgorithmError::InvalidConfiguration { reason: _ }
1227 ));
1228 }
1229
1230 #[test]
1231 fn test_find_paths_bfs_ordering() {
1232 let (a, b, c, d) = addrs();
1237 let e = addr(0x0E);
1238 let mut m = PetgraphStableDiGraphManager::<DepthAndPrice>::new();
1239 let mut t = HashMap::new();
1240 t.insert("ae".into(), vec![a.clone(), e.clone()]);
1241 t.insert("ab".into(), vec![a.clone(), b.clone()]);
1242 t.insert("be".into(), vec![b, e.clone()]);
1243 t.insert("ac".into(), vec![a.clone(), c.clone()]);
1244 t.insert("cd".into(), vec![c, d.clone()]);
1245 t.insert("de".into(), vec![d, e.clone()]);
1246 m.initialize_graph(&t);
1247 let g = m.graph();
1248
1249 let p = MostLiquidAlgorithm::find_paths(g, &a, &e, 1, 3).unwrap();
1250
1251 assert_eq!(p.len(), 3, "Expected 3 paths total");
1253 assert_eq!(p[0].len(), 1, "First path should be 1-hop");
1254 assert_eq!(p[1].len(), 2, "Second path should be 2-hop");
1255 assert_eq!(p[2].len(), 3, "Third path should be 3-hop");
1256 }
1257
1258 #[test]
1266 fn test_simulate_path_single_hop() {
1267 let token_a = token(0x01, "A");
1268 let token_b = token(0x02, "B");
1269
1270 let (market, manager) =
1271 setup_market(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1272
1273 let paths = MostLiquidAlgorithm::find_paths(
1274 manager.graph(),
1275 &token_a.address,
1276 &token_b.address,
1277 1,
1278 1,
1279 )
1280 .unwrap();
1281 let path = paths.into_iter().next().unwrap();
1282
1283 let result = MostLiquidAlgorithm::simulate_path(
1284 &path,
1285 &market_read(&market),
1286 None,
1287 BigUint::from(100u64),
1288 )
1289 .unwrap();
1290
1291 assert_eq!(result.route().swaps().len(), 1);
1292 assert_eq!(*result.route().swaps()[0].amount_in(), BigUint::from(100u64));
1293 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(200u64)); assert_eq!(result.route().swaps()[0].component_id(), "pool1");
1295 }
1296
1297 #[test]
1298 fn test_simulate_path_multi_hop_chains_amounts() {
1299 let token_a = token(0x01, "A");
1300 let token_b = token(0x02, "B");
1301 let token_c = token(0x03, "C");
1302
1303 let (market, manager) = setup_market(vec![
1304 ("pool1", &token_a, &token_b, MockProtocolSim::new(2.0)),
1305 ("pool2", &token_b, &token_c, MockProtocolSim::new(3.0)),
1306 ]);
1307
1308 let paths = MostLiquidAlgorithm::find_paths(
1309 manager.graph(),
1310 &token_a.address,
1311 &token_c.address,
1312 2,
1313 2,
1314 )
1315 .unwrap();
1316 let path = paths.into_iter().next().unwrap();
1317
1318 let result = MostLiquidAlgorithm::simulate_path(
1319 &path,
1320 &market_read(&market),
1321 None,
1322 BigUint::from(10u64),
1323 )
1324 .unwrap();
1325
1326 assert_eq!(result.route().swaps().len(), 2);
1327 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(20u64));
1329 assert_eq!(*result.route().swaps()[1].amount_in(), BigUint::from(20u64));
1331 assert_eq!(*result.route().swaps()[1].amount_out(), BigUint::from(60u64));
1332 }
1333
1334 #[test]
1335 fn test_simulate_path_same_pool_twice_uses_updated_state() {
1336 let token_a = token(0x01, "A");
1339 let token_b = token(0x02, "B");
1340
1341 let (market, manager) =
1342 setup_market(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1343
1344 let paths = MostLiquidAlgorithm::find_paths(
1347 manager.graph(),
1348 &token_a.address,
1349 &token_a.address,
1350 2,
1351 2,
1352 )
1353 .unwrap();
1354
1355 assert_eq!(paths.len(), 1);
1357 let path = paths[0].clone();
1358
1359 let result = MostLiquidAlgorithm::simulate_path(
1360 &path,
1361 &market_read(&market),
1362 None,
1363 BigUint::from(10u64),
1364 )
1365 .unwrap();
1366
1367 assert_eq!(result.route().swaps().len(), 2);
1368 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(20u64));
1370 assert_eq!(*result.route().swaps()[1].amount_out(), BigUint::from(6u64));
1372 }
1373
1374 #[test]
1375 fn test_simulate_path_missing_token_returns_data_not_found() {
1376 let token_a = token(0x01, "A");
1377 let token_b = token(0x02, "B");
1378 let token_c = token(0x03, "C");
1379
1380 let (market, _) =
1381 setup_market(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1382 let market = market_read(&market);
1383
1384 let mut topology = market.component_topology();
1386 topology
1387 .insert("pool2".to_string(), vec![token_b.address.clone(), token_c.address.clone()]);
1388 let mut manager = PetgraphStableDiGraphManager::default();
1389 manager.initialize_graph(&topology);
1390
1391 let graph = manager.graph();
1392 let paths =
1393 MostLiquidAlgorithm::find_paths(graph, &token_a.address, &token_c.address, 2, 2)
1394 .unwrap();
1395 let path = paths.into_iter().next().unwrap();
1396
1397 let result =
1398 MostLiquidAlgorithm::simulate_path(&path, &market, None, BigUint::from(100u64));
1399 assert!(matches!(result, Err(AlgorithmError::DataNotFound { kind: "token", .. })));
1400 }
1401
1402 #[test]
1403 fn test_simulate_path_missing_component_returns_data_not_found() {
1404 let token_a = token(0x01, "A");
1405 let token_b = token(0x02, "B");
1406 let (market, manager) =
1407 setup_market(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1408
1409 let mut market_write = market.try_write().unwrap();
1411 market_write.remove_components([&"pool1".to_string()]);
1412 drop(market_write);
1413
1414 let graph = manager.graph();
1415 let paths =
1416 MostLiquidAlgorithm::find_paths(graph, &token_a.address, &token_b.address, 1, 1)
1417 .unwrap();
1418 let path = paths.into_iter().next().unwrap();
1419
1420 let result = MostLiquidAlgorithm::simulate_path(
1421 &path,
1422 &market_read(&market),
1423 None,
1424 BigUint::from(100u64),
1425 );
1426 assert!(matches!(result, Err(AlgorithmError::DataNotFound { kind: "component", .. })));
1427 }
1428
1429 #[tokio::test]
1432 async fn test_find_best_route_single_path() {
1433 let token_a = token(0x01, "A");
1434 let token_b = token(0x02, "B");
1435
1436 let (market, manager) =
1437 setup_market(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1438
1439 let algorithm = MostLiquidAlgorithm::with_config(
1440 AlgorithmConfig::new(1, 1, Duration::from_millis(100), None).unwrap(),
1441 )
1442 .unwrap();
1443 let order = order(&token_a, &token_b, ONE_ETH, OrderSide::Sell);
1444 let result = algorithm
1445 .find_best_route(manager.graph(), market, None, &order)
1446 .await
1447 .unwrap();
1448
1449 assert_eq!(result.route().swaps().len(), 1);
1450 assert_eq!(*result.route().swaps()[0].amount_in(), BigUint::from(ONE_ETH));
1451 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(ONE_ETH * 2));
1452 }
1453
1454 #[tokio::test]
1455 async fn test_find_best_route_ranks_by_net_amount_out() {
1456 let token_a = token(0x01, "A");
1467 let token_b = token(0x02, "B");
1468
1469 let (market, manager) = setup_market(vec![
1470 ("best", &token_a, &token_b, MockProtocolSim::new(3.0).with_gas(10)),
1471 ("low_out", &token_a, &token_b, MockProtocolSim::new(2.0).with_gas(5)),
1472 ("high_gas", &token_a, &token_b, MockProtocolSim::new(4.0).with_gas(30)),
1473 ]);
1474
1475 let algorithm = MostLiquidAlgorithm::with_config(
1476 AlgorithmConfig::new(1, 1, Duration::from_millis(100), None).unwrap(),
1477 )
1478 .unwrap();
1479 let order = order(&token_a, &token_b, 1000, OrderSide::Sell);
1480
1481 let derived = setup_derived_with_token_prices(std::slice::from_ref(&token_b.address));
1483
1484 let result = algorithm
1485 .find_best_route(manager.graph(), market, Some(derived), &order)
1486 .await
1487 .unwrap();
1488
1489 assert_eq!(result.route().swaps().len(), 1);
1491 assert_eq!(result.route().swaps()[0].component_id(), "best");
1492 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(3000u64));
1493 assert_eq!(result.net_amount_out(), &BigInt::from(2000)); }
1495
1496 #[tokio::test]
1497 async fn test_find_best_route_no_path_returns_error() {
1498 let token_a = token(0x01, "A");
1499 let token_b = token(0x02, "B");
1500 let token_c = token(0x03, "C"); let (market, manager) =
1503 setup_market(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1504
1505 let algorithm = MostLiquidAlgorithm::new();
1506 let order = order(&token_a, &token_c, ONE_ETH, OrderSide::Sell);
1507
1508 let result = algorithm
1509 .find_best_route(manager.graph(), market, None, &order)
1510 .await;
1511 assert!(matches!(result, Err(AlgorithmError::NoPath { .. })));
1512 }
1513
1514 #[tokio::test]
1515 async fn test_find_best_route_multi_hop() {
1516 let token_a = token(0x01, "A");
1517 let token_b = token(0x02, "B");
1518 let token_c = token(0x03, "C");
1519
1520 let (market, manager) = setup_market(vec![
1521 ("pool1", &token_a, &token_b, MockProtocolSim::new(2.0)),
1522 ("pool2", &token_b, &token_c, MockProtocolSim::new(3.0)),
1523 ]);
1524
1525 let algorithm = MostLiquidAlgorithm::with_config(
1526 AlgorithmConfig::new(1, 2, Duration::from_millis(100), None).unwrap(),
1527 )
1528 .unwrap();
1529 let order = order(&token_a, &token_c, ONE_ETH, OrderSide::Sell);
1530
1531 let result = algorithm
1532 .find_best_route(manager.graph(), market, None, &order)
1533 .await
1534 .unwrap();
1535
1536 assert_eq!(result.route().swaps().len(), 2);
1538 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(ONE_ETH * 2));
1539 assert_eq!(result.route().swaps()[0].component_id(), "pool1".to_string());
1540 assert_eq!(*result.route().swaps()[1].amount_out(), BigUint::from(ONE_ETH * 2 * 3));
1541 assert_eq!(result.route().swaps()[1].component_id(), "pool2".to_string());
1542 }
1543
1544 #[tokio::test]
1545 async fn test_find_best_route_skips_paths_without_edge_weights() {
1546 let token_a = token(0x01, "A");
1548 let token_b = token(0x02, "B");
1549
1550 let mut market = SharedMarketData::new();
1552 let pool1_state = MockProtocolSim::new(2.0);
1553 let pool2_state = MockProtocolSim::new(3.0); let pool1_comp = component("pool1", &[token_a.clone(), token_b.clone()]);
1556 let pool2_comp = component("pool2", &[token_a.clone(), token_b.clone()]);
1557
1558 market.update_gas_price(BlockGasPrice {
1560 block_number: 1,
1561 block_hash: Default::default(),
1562 block_timestamp: 0,
1563 pricing: GasPrice::Legacy { gas_price: BigUint::from(1u64) },
1564 });
1565
1566 market.upsert_components(vec![pool1_comp, pool2_comp]);
1568
1569 market.update_states(vec![
1571 ("pool1".to_string(), Box::new(pool1_state.clone()) as Box<dyn ProtocolSim>),
1572 ("pool2".to_string(), Box::new(pool2_state) as Box<dyn ProtocolSim>),
1573 ]);
1574
1575 market.upsert_tokens(vec![token_a.clone(), token_b.clone()]);
1577
1578 let mut manager = PetgraphStableDiGraphManager::default();
1580 manager.initialize_graph(&market.component_topology());
1581
1582 let weight = DepthAndPrice::from_protocol_sim(&pool1_state, &token_a, &token_b).unwrap();
1584 manager
1585 .set_edge_weight(
1586 &"pool1".to_string(),
1587 &token_a.address,
1588 &token_b.address,
1589 weight,
1590 false,
1591 )
1592 .unwrap();
1593
1594 let algorithm = MostLiquidAlgorithm::with_config(
1596 AlgorithmConfig::new(1, 1, Duration::from_millis(100), None).unwrap(),
1597 )
1598 .unwrap();
1599 let order = order(&token_a, &token_b, ONE_ETH, OrderSide::Sell);
1600 let market = wrap_market(market);
1601 let result = algorithm
1602 .find_best_route(manager.graph(), market, None, &order)
1603 .await
1604 .unwrap();
1605
1606 assert_eq!(result.route().swaps().len(), 1);
1608 assert_eq!(result.route().swaps()[0].component_id(), "pool1");
1609 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(ONE_ETH * 2));
1610 }
1611
1612 #[tokio::test]
1613 async fn test_find_best_route_no_scorable_paths() {
1614 let token_a = token(0x01, "A");
1616 let token_b = token(0x02, "B");
1617
1618 let mut market = SharedMarketData::new();
1619 let pool_state = MockProtocolSim::new(2.0);
1620 let pool_comp = component("pool1", &[token_a.clone(), token_b.clone()]);
1621
1622 market.update_gas_price(BlockGasPrice {
1624 block_number: 1,
1625 block_hash: Default::default(),
1626 block_timestamp: 0,
1627 pricing: GasPrice::Eip1559 {
1628 base_fee_per_gas: BigUint::from(1u64),
1629 max_priority_fee_per_gas: BigUint::from(0u64),
1630 },
1631 });
1632
1633 market.upsert_components(vec![pool_comp]);
1634 market.update_states(vec![(
1635 "pool1".to_string(),
1636 Box::new(pool_state) as Box<dyn ProtocolSim>,
1637 )]);
1638 market.upsert_tokens(vec![token_a.clone(), token_b.clone()]);
1639
1640 let mut manager = PetgraphStableDiGraphManager::default();
1642 manager.initialize_graph(&market.component_topology());
1643
1644 let algorithm = MostLiquidAlgorithm::new();
1645 let order = order(&token_a, &token_b, ONE_ETH, OrderSide::Sell);
1646 let market = wrap_market(market);
1647
1648 let result = algorithm
1649 .find_best_route(manager.graph(), market, None, &order)
1650 .await;
1651 assert!(matches!(
1652 result,
1653 Err(AlgorithmError::NoPath { reason: NoPathReason::NoScorablePaths, .. })
1654 ));
1655 }
1656
1657 #[tokio::test]
1658 async fn test_find_best_route_gas_exceeds_output_returns_negative_net() {
1659 let token_a = token(0x01, "A");
1660 let token_b = token(0x02, "B");
1661
1662 let (market, manager) =
1663 setup_market(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1664 let mut market_write = market.try_write().unwrap();
1665
1666 market_write.update_gas_price(BlockGasPrice {
1669 block_number: 1,
1670 block_hash: Default::default(),
1671 block_timestamp: 0,
1672 pricing: GasPrice::Eip1559 {
1673 base_fee_per_gas: BigUint::from(1_000_000u64),
1674 max_priority_fee_per_gas: BigUint::from(1_000_000u64),
1675 },
1676 });
1677 drop(market_write); let algorithm = MostLiquidAlgorithm::new();
1680 let order = order(&token_a, &token_b, 1, OrderSide::Sell); let derived = setup_derived_with_token_prices(std::slice::from_ref(&token_b.address));
1684
1685 let result = algorithm
1687 .find_best_route(manager.graph(), market, Some(derived), &order)
1688 .await
1689 .expect("should return route even with negative net_amount_out");
1690
1691 assert_eq!(result.route().swaps().len(), 1);
1693 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(2u64)); let expected_net = BigInt::from(2) - BigInt::from(100_000_000_000u64);
1697 assert_eq!(result.net_amount_out(), &expected_net);
1698 }
1699
1700 #[tokio::test]
1701 async fn test_find_best_route_insufficient_liquidity() {
1702 let token_a = token(0x01, "A");
1704 let token_b = token(0x02, "B");
1705
1706 let (market, manager) = setup_market(vec![(
1707 "pool1",
1708 &token_a,
1709 &token_b,
1710 MockProtocolSim::new(2.0).with_liquidity(1000),
1711 )]);
1712
1713 let algorithm = MostLiquidAlgorithm::new();
1714 let order = order(&token_a, &token_b, ONE_ETH, OrderSide::Sell); let result = algorithm
1717 .find_best_route(manager.graph(), market, None, &order)
1718 .await;
1719 assert!(matches!(result, Err(AlgorithmError::InsufficientLiquidity)));
1720 }
1721
1722 #[tokio::test]
1723 async fn test_find_best_route_missing_gas_price_returns_error() {
1724 let token_a = token(0x01, "A");
1726 let token_b = token(0x02, "B");
1727
1728 let mut market = SharedMarketData::new();
1729 let pool_state = MockProtocolSim::new(2.0);
1730 let pool_comp = component("pool1", &[token_a.clone(), token_b.clone()]);
1731
1732 market.upsert_components(vec![pool_comp]);
1734 market.update_states(vec![(
1735 "pool1".to_string(),
1736 Box::new(pool_state.clone()) as Box<dyn ProtocolSim>,
1737 )]);
1738 market.upsert_tokens(vec![token_a.clone(), token_b.clone()]);
1739
1740 let mut manager = PetgraphStableDiGraphManager::default();
1742 manager.initialize_graph(&market.component_topology());
1743 let weight = DepthAndPrice::from_protocol_sim(&pool_state, &token_a, &token_b).unwrap();
1744 manager
1745 .set_edge_weight(
1746 &"pool1".to_string(),
1747 &token_a.address,
1748 &token_b.address,
1749 weight,
1750 false,
1751 )
1752 .unwrap();
1753
1754 let algorithm = MostLiquidAlgorithm::new();
1755 let order = order(&token_a, &token_b, ONE_ETH, OrderSide::Sell);
1756 let market = wrap_market(market);
1757
1758 let result = algorithm
1759 .find_best_route(manager.graph(), market, None, &order)
1760 .await;
1761
1762 assert!(matches!(result, Err(AlgorithmError::DataNotFound { kind: "gas price", .. })));
1764 }
1765
1766 #[tokio::test]
1767 async fn test_find_best_route_circular_arbitrage() {
1768 let token_a = token(0x01, "A");
1769 let token_b = token(0x02, "B");
1770
1771 let (market, manager) =
1774 setup_market(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1775
1776 let algorithm = MostLiquidAlgorithm::with_config(
1778 AlgorithmConfig::new(2, 2, Duration::from_millis(100), None).unwrap(),
1779 )
1780 .unwrap();
1781
1782 let order = order(&token_a, &token_a, 100, OrderSide::Sell);
1784
1785 let result = algorithm
1786 .find_best_route(manager.graph(), market, None, &order)
1787 .await
1788 .unwrap();
1789
1790 assert_eq!(result.route().swaps().len(), 2, "Should have 2 swaps for circular route");
1792
1793 assert_eq!(*result.route().swaps()[0].token_in(), token_a.address);
1795 assert_eq!(*result.route().swaps()[0].token_out(), token_b.address);
1796 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(200u64));
1797
1798 assert_eq!(*result.route().swaps()[1].token_in(), token_b.address);
1800 assert_eq!(*result.route().swaps()[1].token_out(), token_a.address);
1801 assert_eq!(*result.route().swaps()[1].amount_out(), BigUint::from(66u64));
1802
1803 assert_eq!(result.route().swaps()[0].token_in(), result.route().swaps()[1].token_out());
1805 }
1806
1807 #[tokio::test]
1808 async fn test_find_best_route_respects_min_hops() {
1809 let token_a = token(0x01, "A");
1812 let token_b = token(0x02, "B");
1813 let token_c = token(0x03, "C");
1814
1815 let (market, manager) = setup_market(vec![
1816 ("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)), ]);
1821
1822 let algorithm = MostLiquidAlgorithm::with_config(
1824 AlgorithmConfig::new(2, 3, Duration::from_millis(100), None).unwrap(),
1825 )
1826 .unwrap();
1827 let order = order(&token_a, &token_b, 100, OrderSide::Sell);
1828
1829 let derived = setup_derived_with_token_prices(std::slice::from_ref(&token_b.address));
1832
1833 let result = algorithm
1834 .find_best_route(manager.graph(), market, Some(derived), &order)
1835 .await
1836 .unwrap();
1837
1838 assert_eq!(result.route().swaps().len(), 2, "Should use 2-hop path due to min_hops=2");
1840 assert_eq!(result.route().swaps()[0].component_id(), "pool_ac");
1841 assert_eq!(result.route().swaps()[1].component_id(), "pool_cb");
1842 }
1843
1844 #[tokio::test]
1845 async fn test_find_best_route_respects_max_hops() {
1846 let token_a = token(0x01, "A");
1849 let token_b = token(0x02, "B");
1850 let token_c = token(0x03, "C");
1851
1852 let (market, manager) = setup_market(vec![
1853 ("pool_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
1854 ("pool_bc", &token_b, &token_c, MockProtocolSim::new(3.0)),
1855 ]);
1856
1857 let algorithm = MostLiquidAlgorithm::with_config(
1859 AlgorithmConfig::new(1, 1, Duration::from_millis(100), None).unwrap(),
1860 )
1861 .unwrap();
1862 let order = order(&token_a, &token_c, 100, OrderSide::Sell);
1863
1864 let result = algorithm
1865 .find_best_route(manager.graph(), market, None, &order)
1866 .await;
1867 assert!(
1868 matches!(result, Err(AlgorithmError::NoPath { .. })),
1869 "Should return NoPath when max_hops is insufficient"
1870 );
1871 }
1872
1873 #[tokio::test]
1874 async fn test_find_best_route_timeout_returns_best_so_far() {
1875 let token_a = token(0x01, "A");
1879 let token_b = token(0x02, "B");
1880
1881 let (market, manager) = setup_market(vec![
1883 ("pool1", &token_a, &token_b, MockProtocolSim::new(1.0)),
1884 ("pool2", &token_a, &token_b, MockProtocolSim::new(2.0)),
1885 ("pool3", &token_a, &token_b, MockProtocolSim::new(3.0)),
1886 ("pool4", &token_a, &token_b, MockProtocolSim::new(4.0)),
1887 ("pool5", &token_a, &token_b, MockProtocolSim::new(5.0)),
1888 ]);
1889
1890 let algorithm = MostLiquidAlgorithm::with_config(
1892 AlgorithmConfig::new(1, 1, Duration::from_millis(0), None).unwrap(),
1893 )
1894 .unwrap();
1895 let order = order(&token_a, &token_b, 100, OrderSide::Sell);
1896
1897 let result = algorithm
1898 .find_best_route(manager.graph(), market, None, &order)
1899 .await;
1900
1901 match result {
1906 Ok(r) => {
1907 assert_eq!(r.route().swaps().len(), 1);
1909 }
1910 Err(AlgorithmError::Timeout { .. }) => {
1911 }
1913 Err(e) => panic!("Unexpected error: {:?}", e),
1914 }
1915 }
1916
1917 #[rstest::rstest]
1920 #[case::default_config(1, 3, 50)]
1921 #[case::single_hop_only(1, 1, 100)]
1922 #[case::multi_hop_min(2, 5, 200)]
1923 #[case::zero_timeout(1, 3, 0)]
1924 #[case::large_values(10, 100, 10000)]
1925 fn test_algorithm_config_getters(
1926 #[case] min_hops: usize,
1927 #[case] max_hops: usize,
1928 #[case] timeout_ms: u64,
1929 ) {
1930 use crate::algorithm::Algorithm;
1931
1932 let algorithm = MostLiquidAlgorithm::with_config(
1933 AlgorithmConfig::new(min_hops, max_hops, Duration::from_millis(timeout_ms), None)
1934 .unwrap(),
1935 )
1936 .unwrap();
1937
1938 assert_eq!(algorithm.max_hops, max_hops);
1939 assert_eq!(algorithm.timeout, Duration::from_millis(timeout_ms));
1940 assert_eq!(algorithm.name(), "most_liquid");
1941 }
1942
1943 #[test]
1944 fn test_algorithm_default_config() {
1945 use crate::algorithm::Algorithm;
1946
1947 let algorithm = MostLiquidAlgorithm::new();
1948
1949 assert_eq!(algorithm.max_hops, 3);
1950 assert_eq!(algorithm.timeout, Duration::from_millis(500));
1951 assert_eq!(algorithm.name(), "most_liquid");
1952 }
1953
1954 #[tokio::test]
1957 async fn test_find_best_route_respects_max_routes_cap() {
1958 let token_a = token(0x01, "A");
1971 let token_b = token(0x02, "B");
1972
1973 let (market, manager) = setup_market(vec![
1974 ("pool1", &token_a, &token_b, MockProtocolSim::new(4.0).with_liquidity(1_000_000)),
1975 ("pool2", &token_a, &token_b, MockProtocolSim::new(3.0).with_liquidity(2_000_000)),
1976 ("pool3", &token_a, &token_b, MockProtocolSim::new(2.0).with_liquidity(3_000_000)),
1977 ("pool4", &token_a, &token_b, MockProtocolSim::new(1.0).with_liquidity(4_000_000)),
1978 ]);
1979
1980 let algorithm = MostLiquidAlgorithm::with_config(
1982 AlgorithmConfig::new(1, 1, Duration::from_millis(100), Some(2)).unwrap(),
1983 )
1984 .unwrap();
1985 let order = order(&token_a, &token_b, 1000, OrderSide::Sell);
1986 let result = algorithm
1987 .find_best_route(manager.graph(), market, None, &order)
1988 .await
1989 .unwrap();
1990
1991 assert_eq!(result.route().swaps().len(), 1);
1995 assert_eq!(result.route().swaps()[0].component_id(), "pool3");
1996 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(2000u64));
1997 }
1998
1999 #[tokio::test]
2000 async fn test_find_best_route_no_cap_when_max_routes_is_none() {
2001 let token_a = token(0x01, "A");
2003 let token_b = token(0x02, "B");
2004
2005 let (market, manager) = setup_market(vec![
2006 ("pool1", &token_a, &token_b, MockProtocolSim::new(4.0).with_liquidity(1_000_000)),
2007 ("pool2", &token_a, &token_b, MockProtocolSim::new(3.0).with_liquidity(2_000_000)),
2008 ("pool3", &token_a, &token_b, MockProtocolSim::new(2.0).with_liquidity(3_000_000)),
2009 ("pool4", &token_a, &token_b, MockProtocolSim::new(1.0).with_liquidity(4_000_000)),
2010 ]);
2011
2012 let algorithm = MostLiquidAlgorithm::with_config(
2013 AlgorithmConfig::new(1, 1, Duration::from_millis(100), None).unwrap(),
2014 )
2015 .unwrap();
2016 let order = order(&token_a, &token_b, 1000, OrderSide::Sell);
2017 let result = algorithm
2018 .find_best_route(manager.graph(), market, None, &order)
2019 .await
2020 .unwrap();
2021
2022 assert_eq!(result.route().swaps().len(), 1);
2024 assert_eq!(result.route().swaps()[0].component_id(), "pool1");
2025 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(4000u64));
2026 }
2027
2028 #[test]
2029 fn test_algorithm_config_rejects_zero_max_routes() {
2030 let result = AlgorithmConfig::new(1, 3, Duration::from_millis(100), Some(0));
2031 assert!(matches!(
2032 result,
2033 Err(AlgorithmError::InvalidConfiguration { reason }) if reason.contains("max_routes must be at least 1")
2034 ));
2035 }
2036
2037 #[test]
2038 fn test_algorithm_config_rejects_zero_min_hops() {
2039 let result = AlgorithmConfig::new(0, 3, Duration::from_millis(100), None);
2040 assert!(matches!(
2041 result,
2042 Err(AlgorithmError::InvalidConfiguration { reason }) if reason.contains("min_hops must be at least 1")
2043 ));
2044 }
2045
2046 #[test]
2047 fn test_algorithm_config_rejects_min_greater_than_max() {
2048 let result = AlgorithmConfig::new(5, 3, Duration::from_millis(100), None);
2049 assert!(matches!(
2050 result,
2051 Err(AlgorithmError::InvalidConfiguration { reason }) if reason.contains("cannot exceed")
2052 ));
2053 }
2054}