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 algorithm::sim_guard::GuardedProtocolSim,
28 derived::{computation::ComputationRequirements, types::TokenGasPrices, SharedDerivedDataRef},
29 feed::market_data::{MarketData, MarketState, StateLabel},
30 graph::{petgraph::StableDiGraph, Path, PetgraphStableDiGraphManager},
31 types::{ComponentId, Order, Route, RouteResult, Swap},
32 AlgorithmError,
33};
34pub struct MostLiquidAlgorithm {
36 min_hops: usize,
37 max_hops: usize,
38 timeout: Duration,
39 max_routes: Option<usize>,
40 connector_tokens: Option<HashSet<Address>>,
41}
42
43#[derive(Debug, Clone, Default)]
49pub struct DepthAndPrice {
50 pub spot_price: f64,
52 pub depth: f64,
54}
55
56impl DepthAndPrice {
57 #[cfg(test)]
59 pub fn new(spot_price: f64, depth: f64) -> Self {
60 Self { spot_price, depth }
61 }
62
63 #[cfg(test)]
65 pub fn from_protocol_sim<S: ProtocolSim + ?Sized>(
66 sim: &S,
67 token_in: &Token,
68 token_out: &Token,
69 ) -> Result<Self, AlgorithmError> {
70 Ok(Self {
71 spot_price: sim
72 .spot_price(token_in, token_out)
73 .map_err(|e| {
74 AlgorithmError::Other(format!("missing spot price for DepthAndPrice: {:?}", e))
75 })?,
76 depth: sim
77 .get_limits(token_in.address.clone(), token_out.address.clone())
78 .map_err(|e| {
79 AlgorithmError::Other(format!("missing depth for DepthAndPrice: {:?}", e))
80 })?
81 .0
82 .to_f64()
83 .ok_or_else(|| {
84 AlgorithmError::Other("depth conversion to f64 failed".to_string())
85 })?,
86 })
87 }
88}
89
90impl crate::graph::EdgeWeightFromSimAndDerived for DepthAndPrice {
91 fn from_sim_and_derived(
92 _sim: &dyn ProtocolSim,
93 component_id: &ComponentId,
94 token_in: &Token,
95 token_out: &Token,
96 derived: &crate::derived::DerivedData,
97 ) -> Option<Self> {
98 let key = (component_id.clone(), token_in.address.clone(), token_out.address.clone());
99
100 let spot_price = match derived
102 .spot_prices()
103 .and_then(|p| p.get(&key).copied())
104 {
105 Some(p) => p,
106 None => {
107 trace!(component_id = %component_id, "spot price not found, skipping edge");
108 return None;
109 }
110 };
111
112 let raw_depth = match derived
114 .component_depths()
115 .and_then(|d| d.get(&key))
116 {
117 Some(d) => d.to_f64().unwrap_or(0.0),
118 None => {
119 trace!(component_id = %component_id, "component depth not found, skipping edge");
120 return None;
121 }
122 };
123
124 let depth = match derived
129 .token_prices()
130 .and_then(|p| p.get(&token_in.address))
131 {
132 Some(price) => {
133 let num = match price.numerator.to_f64() {
134 Some(v) if v > 0.0 => v,
135 Some(_) => {
136 trace!(
137 component_id = %component_id,
138 token_in = %token_in.address,
139 "token price numerator is zero, skipping edge"
140 );
141 return None;
142 }
143 None => {
144 trace!(
145 component_id = %component_id,
146 token_in = %token_in.address,
147 "token price numerator overflows f64, skipping edge"
148 );
149 return None;
150 }
151 };
152 let den = match price.denominator.to_f64() {
153 Some(v) if v > 0.0 => v,
154 Some(_) => {
155 trace!(
156 component_id = %component_id,
157 token_in = %token_in.address,
158 "token price denominator is zero, skipping edge"
159 );
160 return None;
161 }
162 None => {
163 trace!(
164 component_id = %component_id,
165 token_in = %token_in.address,
166 "token price denominator overflows f64, skipping edge"
167 );
168 return None;
169 }
170 };
171 raw_depth * den / num
172 }
173 None => {
174 trace!(
175 component_id = %component_id,
176 token_in = %token_in.address,
177 "token price not found, skipping edge"
178 );
179 return None;
180 }
181 };
182
183 Some(Self { spot_price, depth })
184 }
185}
186
187impl MostLiquidAlgorithm {
188 pub fn new() -> Self {
190 Self {
191 min_hops: 1,
192 max_hops: 3,
193 timeout: Duration::from_millis(500),
194 max_routes: None,
195 connector_tokens: None,
196 }
197 }
198
199 pub fn with_config(config: AlgorithmConfig) -> Result<Self, AlgorithmError> {
201 Ok(Self {
202 min_hops: config.min_hops(),
203 max_hops: config.max_hops(),
204 timeout: config.timeout(),
205 max_routes: config.max_routes(),
206 connector_tokens: config.connector_tokens().cloned(),
207 })
208 }
209
210 #[instrument(level = "debug", skip(graph, connector_tokens))]
221 pub(crate) fn find_paths<'a>(
222 graph: &'a StableDiGraph<DepthAndPrice>,
223 from: &Address,
224 to: &Address,
225 min_hops: usize,
226 max_hops: usize,
227 connector_tokens: Option<&HashSet<Address>>,
228 ) -> Result<Vec<Path<'a, DepthAndPrice>>, AlgorithmError> {
229 if min_hops == 0 || min_hops > max_hops {
230 return Err(AlgorithmError::InvalidConfiguration {
231 reason: format!(
232 "invalid hop configuration: min_hops={min_hops} max_hops={max_hops}",
233 ),
234 });
235 }
236
237 let from_idx = graph
240 .node_indices()
241 .find(|&n| &graph[n] == from)
242 .ok_or(AlgorithmError::NoPath {
243 from: from.clone(),
244 to: to.clone(),
245 reason: NoPathReason::SourceTokenNotInGraph,
246 })?;
247 let to_idx = graph
248 .node_indices()
249 .find(|&n| &graph[n] == to)
250 .ok_or(AlgorithmError::NoPath {
251 from: from.clone(),
252 to: to.clone(),
253 reason: NoPathReason::DestinationTokenNotInGraph,
254 })?;
255
256 let mut paths = Vec::new();
257 let mut queue = VecDeque::new();
258 queue.push_back((from_idx, Path::new()));
259
260 while let Some((current_node, current_path)) = queue.pop_front() {
261 if current_path.len() >= max_hops {
262 continue;
263 }
264
265 for edge in graph.edges(current_node) {
266 let next_node = edge.target();
267 let next_addr = &graph[next_node];
268
269 let already_visited = current_path.tokens.contains(&next_addr);
274 let is_closing_circular_route = from_idx == to_idx && next_node == to_idx;
275 if already_visited && !is_closing_circular_route {
276 continue;
277 }
278
279 let is_destination = next_node == to_idx;
281 if !is_destination {
282 if let Some(tokens) = connector_tokens {
283 if !tokens.contains(next_addr) {
284 continue;
285 }
286 }
287 }
288
289 let mut new_path = current_path.clone();
290 new_path.add_hop(&graph[current_node], edge.weight(), next_addr);
291
292 if next_node == to_idx && new_path.len() >= min_hops {
293 paths.push(new_path.clone());
294 }
295
296 queue.push_back((next_node, new_path));
297 }
298 }
299
300 Ok(paths)
301 }
302
303 pub(crate) fn try_score_path(path: &Path<DepthAndPrice>) -> Option<f64> {
317 if path.is_empty() {
318 trace!("cannot score empty path");
319 return None;
320 }
321
322 let mut price = 1.0;
323 let mut min_depth = f64::MAX;
324
325 for edge in path.edge_iter() {
326 let Some(data) = edge.data.as_ref() else {
327 debug!(component_id = %edge.component_id, "edge missing weight data, path cannot be scored");
328 return None;
329 };
330
331 price *= data.spot_price;
332 min_depth = min_depth.min(data.depth);
333 }
334
335 Some(price * min_depth)
336 }
337
338 #[instrument(level = "trace", skip(path, market, token_prices), fields(hop_count = path.len()))]
351 pub(crate) fn simulate_path<D>(
352 path: &Path<D>,
353 market: &MarketState,
354 token_prices: Option<&TokenGasPrices>,
355 amount_in: BigUint,
356 ) -> Result<RouteResult, AlgorithmError> {
357 let mut current_amount = amount_in.clone();
358 let mut swaps = Vec::with_capacity(path.len());
359
360 let mut state_overrides: HashMap<&ComponentId, Box<dyn ProtocolSim>> = HashMap::new();
362 let mut tokens: HashMap<Address, Token> = HashMap::new();
363
364 for (address_in, edge_data, address_out) in path.iter() {
365 let token_in = market
367 .get_token(address_in)
368 .ok_or_else(|| AlgorithmError::DataNotFound {
369 kind: "token",
370 id: Some(format!("{:?}", address_in)),
371 })?;
372 let token_out = market
373 .get_token(address_out)
374 .ok_or_else(|| AlgorithmError::DataNotFound {
375 kind: "token",
376 id: Some(format!("{:?}", address_out)),
377 })?;
378
379 let component_id = &edge_data.component_id;
380 let component = market
381 .get_component(component_id)
382 .ok_or_else(|| AlgorithmError::DataNotFound {
383 kind: "component",
384 id: Some(component_id.clone()),
385 })?;
386 let component_state = market
387 .get_simulation_state(component_id)
388 .ok_or_else(|| AlgorithmError::DataNotFound {
389 kind: "simulation state",
390 id: Some(component_id.clone()),
391 })?;
392
393 let state = state_overrides
394 .get(component_id)
395 .map(Box::as_ref)
396 .unwrap_or(component_state);
397
398 let result = state
400 .get_amount_out_guarded(current_amount.clone(), token_in, token_out)
401 .map_err(|e| AlgorithmError::Other(format!("simulation error: {:?}", e)))?;
402
403 swaps.push(Swap::new(
405 component_id.clone(),
406 component.protocol_system.clone(),
407 token_in.address.clone(),
408 token_out.address.clone(),
409 current_amount.clone(),
410 result.amount.clone(),
411 result.gas,
412 component.clone(),
413 state.clone_box(),
414 ));
415 tokens
416 .entry(token_in.address.clone())
417 .or_insert_with(|| token_in.clone());
418 tokens
419 .entry(token_out.address.clone())
420 .or_insert_with(|| token_out.clone());
421
422 state_overrides.insert(component_id, result.new_state);
423 current_amount = result.amount;
424 }
425
426 let route = Route::new(swaps, tokens)?;
428 let output_amount = route
429 .swaps()
430 .last()
431 .map(|s| s.amount_out().clone())
432 .unwrap_or_else(|| BigUint::ZERO);
433
434 let gas_price = market
435 .gas_price()
436 .ok_or(AlgorithmError::DataNotFound { kind: "gas price", id: None })?
437 .effective_gas_price()
438 .clone();
439
440 let net_amount_out = if let Some(last_swap) = route.swaps().last() {
441 let total_gas = route.total_gas();
442 let gas_cost_wei = &total_gas * &gas_price;
443
444 let gas_cost_in_output_token: Option<BigUint> = token_prices
446 .and_then(|prices| prices.get(last_swap.token_out()))
447 .map(|price| {
448 &gas_cost_wei * &price.numerator / &price.denominator
451 });
452
453 match gas_cost_in_output_token {
454 Some(gas_cost) => BigInt::from(output_amount) - BigInt::from(gas_cost),
455 None => {
456 BigInt::from(output_amount)
459 }
460 }
461 } else {
462 BigInt::from(output_amount)
463 };
464
465 Ok(RouteResult::new(route, net_amount_out, gas_price))
466 }
467}
468
469impl Default for MostLiquidAlgorithm {
470 fn default() -> Self {
471 Self::new()
472 }
473}
474
475impl Algorithm for MostLiquidAlgorithm {
476 type GraphType = StableDiGraph<DepthAndPrice>;
477 type GraphManager = PetgraphStableDiGraphManager<DepthAndPrice>;
478
479 fn name(&self) -> &str {
480 "most_liquid"
481 }
482
483 #[instrument(level = "debug", skip_all, fields(order_id = %order.id()))]
485 async fn find_best_route(
486 &self,
487 graph: &Self::GraphType,
488 market: MarketData,
489 label: Option<StateLabel>,
490 derived: Option<SharedDerivedDataRef>,
491 order: &Order,
492 ) -> Result<RouteResult, AlgorithmError> {
493 let start = Instant::now();
494
495 if !order.is_sell() {
497 return Err(AlgorithmError::ExactOutNotSupported);
498 }
499
500 let token_prices = if let Some(ref derived) = derived {
502 derived
503 .read()
504 .await
505 .token_prices()
506 .cloned()
507 } else {
508 None
509 };
510
511 let amount_in = order.amount().clone();
512
513 let all_paths = Self::find_paths(
515 graph,
516 order.token_in(),
517 order.token_out(),
518 self.min_hops,
519 self.max_hops,
520 self.connector_tokens.as_ref(),
521 )?;
522
523 let paths_candidates = all_paths.len();
524 if paths_candidates == 0 {
525 return Err(AlgorithmError::NoPath {
526 from: order.token_in().clone(),
527 to: order.token_out().clone(),
528 reason: NoPathReason::NoGraphPath,
529 });
530 }
531
532 let mut scored_paths: Vec<(Path<DepthAndPrice>, f64)> = all_paths
535 .into_iter()
536 .filter_map(|path| {
537 let score = Self::try_score_path(&path)?;
538 Some((path, score))
539 })
540 .collect();
541
542 scored_paths.sort_by(|(_, a_score), (_, b_score)| {
543 b_score
545 .partial_cmp(a_score)
546 .unwrap_or(std::cmp::Ordering::Equal)
547 });
548
549 if let Some(max_routes) = self.max_routes {
550 scored_paths.truncate(max_routes);
551 }
552
553 let paths_to_simulate = scored_paths.len();
554 let scoring_failures = paths_candidates - paths_to_simulate;
555 if paths_to_simulate == 0 {
556 return Err(AlgorithmError::NoPath {
557 from: order.token_in().clone(),
558 to: order.token_out().clone(),
559 reason: NoPathReason::NoScorablePaths,
560 });
561 }
562
563 let component_ids: HashSet<ComponentId> = scored_paths
565 .iter()
566 .flat_map(|(path, _)| {
567 path.edge_iter()
568 .iter()
569 .map(|e| e.component_id.clone())
570 })
571 .collect();
572
573 let market = {
575 let market = match label.as_ref() {
576 Some(l) => market
577 .read_labeled(l)
578 .await
579 .map_err(|e| AlgorithmError::Other(e.to_string()))?,
580 None => market.read().await,
581 };
582 if market.gas_price().is_none() {
583 return Err(AlgorithmError::DataNotFound { kind: "gas price", id: None });
584 }
585 let market_subset = market.extract_subset_with_overlay(&component_ids);
586 drop(market);
587 market_subset
588 };
589
590 let mut paths_simulated = 0usize;
591 let mut simulation_failures = 0usize;
592 let mut validation_failures = 0usize;
593
594 let mut best: Option<RouteResult> = None;
596 let timeout_ms = self.timeout.as_millis() as u64;
597
598 for (edge_path, _) in scored_paths {
599 let elapsed_ms = start.elapsed().as_millis() as u64;
601 if elapsed_ms > timeout_ms {
602 break;
603 }
604
605 let result = match Self::simulate_path(
606 &edge_path,
607 &market,
608 token_prices.as_ref(),
609 amount_in.clone(),
610 ) {
611 Ok(r) => r,
612 Err(e) => {
613 trace!(error = %e, "simulation failed for path");
614 simulation_failures += 1;
615 continue;
616 }
617 };
618
619 if let Err(e) = result.route().validate() {
621 trace!(error = %e, "skipping invalid route");
622 validation_failures += 1;
623 continue;
624 }
625
626 if best
628 .as_ref()
629 .map(|best| result.net_amount_out() > best.net_amount_out())
630 .unwrap_or(true)
631 {
632 best = Some(result);
633 }
634
635 paths_simulated += 1;
636 }
637
638 let solve_time_ms = start.elapsed().as_millis() as u64;
640 let block_number = market
641 .last_updated()
642 .map(|b| b.number());
643 let coverage_pct = if paths_to_simulate == 0 {
645 100.0
646 } else {
647 (paths_simulated as f64 / paths_to_simulate as f64) * 100.0
648 };
649
650 counter!("algorithm.scoring_failures").increment(scoring_failures as u64);
652 counter!("algorithm.simulation_failures").increment(simulation_failures as u64);
653 counter!("algorithm.validation_failures").increment(validation_failures as u64);
654 histogram!("algorithm.simulation_coverage_pct").record(coverage_pct);
655
656 match &best {
657 Some(result) => {
658 let tokens = market.token_registry_ref();
659 let path_desc = result.route().path_description(tokens);
660 let protocols = result
661 .route()
662 .swaps()
663 .iter()
664 .map(|s| s.protocol())
665 .collect::<Vec<_>>();
666
667 let price = amount_in
668 .to_f64()
669 .filter(|&v| v > 0.0)
670 .and_then(|amt_in| {
671 result
672 .net_amount_out()
673 .to_f64()
674 .map(|amt_out| amt_out / amt_in)
675 })
676 .unwrap_or(f64::NAN);
677
678 debug!(
679 solve_time_ms,
680 block_number,
681 paths_candidates,
682 paths_to_simulate,
683 paths_simulated,
684 simulation_failures,
685 validation_failures,
686 simulation_coverage_pct = coverage_pct,
687 components_considered = component_ids.len(),
688 tokens_considered = market.token_registry_ref().len(),
689 path = %path_desc,
690 amount_in = %amount_in,
691 net_amount_out = %result.net_amount_out(),
692 price_out_per_in = price,
693 hop_count = result.route().swaps().len(),
694 protocols = ?protocols,
695 "route found"
696 );
697 }
698 None => {
699 debug!(
700 solve_time_ms,
701 block_number,
702 paths_candidates,
703 paths_to_simulate,
704 paths_simulated,
705 simulation_failures,
706 validation_failures,
707 simulation_coverage_pct = coverage_pct,
708 components_considered = component_ids.len(),
709 tokens_considered = market.token_registry_ref().len(),
710 "no viable route"
711 );
712 }
713 }
714
715 best.ok_or({
716 if solve_time_ms > timeout_ms {
717 AlgorithmError::Timeout { elapsed_ms: solve_time_ms }
718 } else {
719 AlgorithmError::InsufficientLiquidity
720 }
721 })
722 }
723
724 fn computation_requirements(&self) -> ComputationRequirements {
725 ComputationRequirements::none()
733 .allow_stale("token_prices")
734 .expect("Conflicting Computation Requirements")
735 }
736
737 fn timeout(&self) -> Duration {
738 self.timeout
739 }
740}
741
742#[cfg(test)]
743mod tests {
744 use std::collections::HashSet;
745
746 use rstest::rstest;
747 use tycho_simulation::{
748 tycho_core::simulation::protocol_sim::Price,
749 tycho_ethereum::gas::{BlockGasPrice, GasPrice},
750 };
751
752 use super::*;
753 use crate::{
754 algorithm::test_utils::{
755 addr, component,
756 fixtures::{addrs, diamond_graph, linear_graph, parallel_graph},
757 market_read, order, setup_market_weighted, token, MockProtocolSim, ONE_ETH,
758 },
759 derived::{
760 computation::{FailedItem, FailedItemError},
761 types::TokenGasPrices,
762 DerivedData,
763 },
764 graph::GraphManager,
765 types::OrderSide,
766 };
767
768 fn wrap_market(market: MarketState) -> MarketData {
769 MarketData::new(std::sync::Arc::new(tokio::sync::RwLock::new(market)))
770 }
771
772 fn setup_derived_with_token_prices(token_addresses: &[Address]) -> SharedDerivedDataRef {
777 let mut token_prices: TokenGasPrices = HashMap::new();
778 for addr in token_addresses {
779 token_prices.insert(
781 addr.clone(),
782 Price { numerator: BigUint::from(1u64), denominator: BigUint::from(1u64) },
783 );
784 }
785
786 let mut derived_data = DerivedData::new();
787 derived_data.set_token_prices(token_prices, vec![], 1, true);
788 std::sync::Arc::new(tokio::sync::RwLock::new(derived_data))
789 }
790 #[test]
793 fn test_try_score_path_calculates_correctly() {
794 let (a, b, c, _) = addrs();
795 let mut m = linear_graph();
796
797 m.set_edge_weight(&"ab".to_string(), &a, &b, DepthAndPrice::new(2.0, 1000.0), false)
799 .unwrap();
800 m.set_edge_weight(&"bc".to_string(), &b, &c, DepthAndPrice::new(0.5, 500.0), false)
801 .unwrap();
802
803 let graph = m.graph();
805 let paths = MostLiquidAlgorithm::find_paths(graph, &a, &c, 2, 2, None).unwrap();
806 assert_eq!(paths.len(), 1);
807 let path = &paths[0];
808
809 let expected = 2.0 * 0.5 * 500.0;
811 let score = MostLiquidAlgorithm::try_score_path(path).unwrap();
812 assert_eq!(score, expected, "expected {expected}, got {score}");
813 }
814
815 #[test]
816 fn test_try_score_path_empty_returns_none() {
817 let path: Path<DepthAndPrice> = Path::new();
818 assert_eq!(MostLiquidAlgorithm::try_score_path(&path), None);
819 }
820
821 #[test]
822 fn test_try_score_path_missing_weight_returns_none() {
823 let (a, b, _, _) = addrs();
824 let m = linear_graph();
825 let graph = m.graph();
826 let paths = MostLiquidAlgorithm::find_paths(graph, &a, &b, 1, 1, None).unwrap();
827 assert_eq!(paths.len(), 1);
828 assert!(MostLiquidAlgorithm::try_score_path(&paths[0]).is_none());
829 }
830
831 #[test]
832 fn test_try_score_path_circular_route() {
833 let (a, b, _, _) = addrs();
835 let mut m = linear_graph();
836
837 m.set_edge_weight(&"ab".to_string(), &a, &b, DepthAndPrice::new(2.0, 1000.0), false)
841 .unwrap();
842 m.set_edge_weight(&"ab".to_string(), &b, &a, DepthAndPrice::new(0.6, 800.0), false)
843 .unwrap();
844
845 let graph = m.graph();
846 let paths = MostLiquidAlgorithm::find_paths(graph, &a, &a, 2, 2, None).unwrap();
848
849 assert_eq!(paths.len(), 1);
851
852 let score = MostLiquidAlgorithm::try_score_path(&paths[0]).unwrap();
857 let expected = 2.0 * 0.6 * 800.0;
858 assert_eq!(score, expected, "expected {expected}, got {score}");
859 }
860
861 fn make_mock_sim() -> MockProtocolSim {
862 MockProtocolSim::new(2.0)
863 }
864
865 fn pair_key(comp: &str, b_in: u8, b_out: u8) -> (String, Address, Address) {
866 (comp.to_string(), addr(b_in), addr(b_out))
867 }
868
869 fn pair_key_str(comp: &str, b_in: u8, b_out: u8) -> String {
870 format!("{comp}/{}/{}", addr(b_in), addr(b_out))
871 }
872
873 fn make_token_prices(addresses: &[Address]) -> TokenGasPrices {
874 let mut prices = TokenGasPrices::new();
875 for addr in addresses {
876 prices.insert(
878 addr.clone(),
879 Price { numerator: BigUint::from(1u64), denominator: BigUint::from(1u64) },
880 );
881 }
882 prices
883 }
884
885 #[test]
886 fn test_from_sim_and_derived_failed_spot_price_returns_none() {
887 let key = pair_key("component1", 0x01, 0x02);
888 let key_str = pair_key_str("component1", 0x01, 0x02);
889 let tok_in = token(0x01, "A");
890 let tok_out = token(0x02, "B");
891
892 let mut derived = DerivedData::new();
893 derived.set_spot_prices(
895 Default::default(),
896 vec![FailedItem {
897 key: key_str,
898 error: FailedItemError::SimulationFailed("sim error".into()),
899 }],
900 10,
901 true,
902 );
903 derived.set_component_depths(Default::default(), vec![], 10, true);
904 derived.set_token_prices(
905 make_token_prices(&[tok_in.address.clone(), tok_out.address.clone()]),
906 vec![],
907 10,
908 true,
909 );
910
911 let sim = make_mock_sim();
912 let result =
913 <DepthAndPrice as crate::graph::EdgeWeightFromSimAndDerived>::from_sim_and_derived(
914 &sim, &key.0, &tok_in, &tok_out, &derived,
915 );
916
917 assert!(result.is_none());
918 }
919
920 #[test]
921 fn test_from_sim_and_derived_failed_component_depth_returns_none() {
922 let key = pair_key("component1", 0x01, 0x02);
923 let key_str = pair_key_str("component1", 0x01, 0x02);
924 let tok_in = token(0x01, "A");
925 let tok_out = token(0x02, "B");
926
927 let mut derived = DerivedData::new();
928 let mut prices = crate::derived::types::SpotPrices::default();
930 prices.insert(key.clone(), 1.5);
931 derived.set_spot_prices(prices, vec![], 10, true);
932 derived.set_component_depths(
934 Default::default(),
935 vec![FailedItem {
936 key: key_str,
937 error: FailedItemError::SimulationFailed("depth error".into()),
938 }],
939 10,
940 true,
941 );
942 derived.set_token_prices(
943 make_token_prices(&[tok_in.address.clone(), tok_out.address.clone()]),
944 vec![],
945 10,
946 true,
947 );
948
949 let sim = make_mock_sim();
950 let result =
951 <DepthAndPrice as crate::graph::EdgeWeightFromSimAndDerived>::from_sim_and_derived(
952 &sim, &key.0, &tok_in, &tok_out, &derived,
953 );
954
955 assert!(result.is_none());
956 }
957
958 #[test]
959 fn test_from_sim_and_derived_both_failed_returns_none() {
960 let key = pair_key("component1", 0x01, 0x02);
961 let key_str = pair_key_str("component1", 0x01, 0x02);
962 let tok_in = token(0x01, "A");
963 let tok_out = token(0x02, "B");
964
965 let mut derived = DerivedData::new();
966 derived.set_spot_prices(
967 Default::default(),
968 vec![FailedItem {
969 key: key_str.clone(),
970 error: FailedItemError::SimulationFailed("spot error".into()),
971 }],
972 10,
973 true,
974 );
975 derived.set_component_depths(
976 Default::default(),
977 vec![FailedItem {
978 key: key_str,
979 error: FailedItemError::SimulationFailed("depth error".into()),
980 }],
981 10,
982 true,
983 );
984 derived.set_token_prices(
985 make_token_prices(&[tok_in.address.clone(), tok_out.address.clone()]),
986 vec![],
987 10,
988 true,
989 );
990
991 let sim = make_mock_sim();
992 let result =
993 <DepthAndPrice as crate::graph::EdgeWeightFromSimAndDerived>::from_sim_and_derived(
994 &sim, &key.0, &tok_in, &tok_out, &derived,
995 );
996
997 assert!(result.is_none());
998 }
999
1000 #[test]
1001 fn test_from_sim_and_derived_missing_token_price_returns_none() {
1002 let key = pair_key("component1", 0x01, 0x02);
1003 let tok_in = token(0x01, "A");
1004 let tok_out = token(0x02, "B");
1005
1006 let mut derived = DerivedData::new();
1007 let mut prices = crate::derived::types::SpotPrices::default();
1009 prices.insert(key.clone(), 1.5);
1010 derived.set_spot_prices(prices, vec![], 10, true);
1011
1012 let mut depths = crate::derived::types::ComponentDepths::default();
1013 depths.insert(key.clone(), BigUint::from(1000u64));
1014 derived.set_component_depths(depths, vec![], 10, true);
1015
1016 let sim = make_mock_sim();
1019 let result =
1020 <DepthAndPrice as crate::graph::EdgeWeightFromSimAndDerived>::from_sim_and_derived(
1021 &sim, &key.0, &tok_in, &tok_out, &derived,
1022 );
1023
1024 assert!(
1025 result.is_none(),
1026 "should return None when token price is missing for depth normalization"
1027 );
1028 }
1029
1030 #[test]
1031 fn test_from_sim_and_derived_normalizes_depth_to_eth() {
1032 let key = pair_key("component1", 0x01, 0x02);
1033 let tok_in = token(0x01, "A");
1034 let tok_out = token(0x02, "B");
1035
1036 let mut derived = DerivedData::new();
1037
1038 let mut spot = crate::derived::types::SpotPrices::default();
1040 spot.insert(key.clone(), 2.0);
1041 derived.set_spot_prices(spot, vec![], 10, true);
1042
1043 let mut depths = crate::derived::types::ComponentDepths::default();
1045 depths.insert(key.clone(), BigUint::from(2_000_000u64));
1046 derived.set_component_depths(depths, vec![], 10, true);
1047
1048 let mut token_prices = TokenGasPrices::new();
1051 token_prices.insert(
1052 tok_in.address.clone(),
1053 Price { numerator: BigUint::from(2000u64), denominator: BigUint::from(1u64) },
1054 );
1055 derived.set_token_prices(token_prices, vec![], 10, true);
1056
1057 let sim = make_mock_sim();
1058 let result =
1059 <DepthAndPrice as crate::graph::EdgeWeightFromSimAndDerived>::from_sim_and_derived(
1060 &sim, &key.0, &tok_in, &tok_out, &derived,
1061 );
1062
1063 let data = result.expect("should return Some when all data present");
1064 assert!((data.spot_price - 2.0).abs() < f64::EPSILON, "spot price should be 2.0");
1065 assert!(
1067 (data.depth - 1000.0).abs() < f64::EPSILON,
1068 "depth should be 1000.0 ETH, got {}",
1069 data.depth
1070 );
1071 }
1072
1073 #[test]
1074 fn test_from_sim_and_derived_normalizes_depth_fractional_price() {
1075 let key = pair_key("component1", 0x01, 0x02);
1076 let tok_in = token(0x01, "A");
1077 let tok_out = token(0x02, "B");
1078
1079 let mut derived = DerivedData::new();
1080
1081 let mut spot = crate::derived::types::SpotPrices::default();
1082 spot.insert(key.clone(), 0.5);
1083 derived.set_spot_prices(spot, vec![], 10, true);
1084
1085 let mut depths = crate::derived::types::ComponentDepths::default();
1087 depths.insert(key.clone(), BigUint::from(500u64));
1088 derived.set_component_depths(depths, vec![], 10, true);
1089
1090 let mut token_prices = TokenGasPrices::new();
1093 token_prices.insert(
1094 tok_in.address.clone(),
1095 Price { numerator: BigUint::from(3u64), denominator: BigUint::from(2u64) },
1096 );
1097 derived.set_token_prices(token_prices, vec![], 10, true);
1098
1099 let sim = make_mock_sim();
1100 let result =
1101 <DepthAndPrice as crate::graph::EdgeWeightFromSimAndDerived>::from_sim_and_derived(
1102 &sim, &key.0, &tok_in, &tok_out, &derived,
1103 );
1104
1105 let data = result.expect("should return Some when all data present");
1106 let expected_depth = 500.0 * 2.0 / 3.0;
1107 assert!(
1108 (data.depth - expected_depth).abs() < 1e-10,
1109 "depth should be {expected_depth}, got {}",
1110 data.depth
1111 );
1112 }
1113
1114 fn all_ids(paths: Vec<Path<'_, DepthAndPrice>>) -> HashSet<Vec<&str>> {
1117 paths
1118 .iter()
1119 .map(|p| {
1120 p.iter()
1121 .map(|(_, e, _)| e.component_id.as_str())
1122 .collect()
1123 })
1124 .collect()
1125 }
1126
1127 #[test]
1128 fn test_find_paths_linear_forward_and_reverse() {
1129 let (a, b, c, d) = addrs();
1130 let m = linear_graph();
1131 let g = m.graph();
1132
1133 let p = MostLiquidAlgorithm::find_paths(g, &a, &b, 1, 1, None).unwrap();
1135 assert_eq!(all_ids(p), HashSet::from([vec!["ab"]]));
1136
1137 let p = MostLiquidAlgorithm::find_paths(g, &a, &c, 1, 2, None).unwrap();
1138 assert_eq!(all_ids(p), HashSet::from([vec!["ab", "bc"]]));
1139
1140 let p = MostLiquidAlgorithm::find_paths(g, &a, &d, 1, 3, None).unwrap();
1141 assert_eq!(all_ids(p), HashSet::from([vec!["ab", "bc", "cd"]]));
1142
1143 let p = MostLiquidAlgorithm::find_paths(g, &d, &a, 1, 3, None).unwrap();
1145 assert_eq!(all_ids(p), HashSet::from([vec!["cd", "bc", "ab"]]));
1146 }
1147
1148 #[test]
1149 fn test_find_paths_respects_hop_bounds() {
1150 let (a, _, c, d) = addrs();
1151 let m = linear_graph();
1152 let g = m.graph();
1153
1154 assert!(MostLiquidAlgorithm::find_paths(g, &a, &d, 1, 2, None)
1156 .unwrap()
1157 .is_empty());
1158
1159 assert!(MostLiquidAlgorithm::find_paths(g, &a, &c, 3, 3, None)
1161 .unwrap()
1162 .is_empty());
1163 }
1164
1165 #[test]
1166 fn test_find_paths_parallel_components() {
1167 let (a, b, c, _) = addrs();
1168 let m = parallel_graph();
1169 let g = m.graph();
1170
1171 let p = MostLiquidAlgorithm::find_paths(g, &a, &b, 1, 1, None).unwrap();
1173 assert_eq!(all_ids(p), HashSet::from([vec!["ab1"], vec!["ab2"], vec!["ab3"]]));
1174
1175 let p = MostLiquidAlgorithm::find_paths(g, &a, &c, 1, 2, None).unwrap();
1177 assert_eq!(
1178 all_ids(p),
1179 HashSet::from([
1180 vec!["ab1", "bc1"],
1181 vec!["ab1", "bc2"],
1182 vec!["ab2", "bc1"],
1183 vec!["ab2", "bc2"],
1184 vec!["ab3", "bc1"],
1185 vec!["ab3", "bc2"],
1186 ])
1187 );
1188 }
1189
1190 #[test]
1191 fn test_find_paths_diamond_multiple_routes() {
1192 let (a, _, _, d) = addrs();
1193 let m = diamond_graph();
1194 let g = m.graph();
1195
1196 let p = MostLiquidAlgorithm::find_paths(g, &a, &d, 1, 2, None).unwrap();
1198 assert_eq!(all_ids(p), HashSet::from([vec!["ab", "bd"], vec!["ac", "cd"]]));
1199 }
1200
1201 #[test]
1202 fn test_find_paths_no_intermediate_cycles() {
1203 let (a, b, _, _) = addrs();
1204 let m = linear_graph();
1205 let g = m.graph();
1206
1207 let p = MostLiquidAlgorithm::find_paths(g, &a, &b, 1, 3, None).unwrap();
1212 assert_eq!(all_ids(p), HashSet::from([vec!["ab"]]));
1213 }
1214
1215 #[test]
1216 fn test_find_paths_cyclic_same_source_dest() {
1217 let (a, _, _, _) = addrs();
1218 let m = parallel_graph();
1220 let g = m.graph();
1221
1222 let p = MostLiquidAlgorithm::find_paths(g, &a, &a, 2, 2, None).unwrap();
1225 assert_eq!(
1226 all_ids(p),
1227 HashSet::from([
1228 vec!["ab1", "ab1"],
1229 vec!["ab1", "ab2"],
1230 vec!["ab1", "ab3"],
1231 vec!["ab2", "ab1"],
1232 vec!["ab2", "ab2"],
1233 vec!["ab2", "ab3"],
1234 vec!["ab3", "ab1"],
1235 vec!["ab3", "ab2"],
1236 vec!["ab3", "ab3"],
1237 ])
1238 );
1239 }
1240
1241 #[rstest]
1242 #[case::source_not_in_graph(false, true)]
1243 #[case::dest_not_in_graph(true, false)]
1244 fn test_find_paths_token_not_in_graph(#[case] from_exists: bool, #[case] to_exists: bool) {
1245 let (a, b, _, _) = addrs();
1247 let non_existent = addr(0x99);
1248 let m = linear_graph();
1249 let g = m.graph();
1250
1251 let from = if from_exists { a } else { non_existent.clone() };
1252 let to = if to_exists { b } else { non_existent };
1253
1254 let result = MostLiquidAlgorithm::find_paths(g, &from, &to, 1, 3, None);
1255
1256 assert!(matches!(result, Err(AlgorithmError::NoPath { .. })));
1257 }
1258
1259 #[rstest]
1260 #[case::min_greater_than_max(3, 1)]
1261 #[case::min_hops_zero(0, 1)]
1262 fn test_find_paths_invalid_configuration(#[case] min_hops: usize, #[case] max_hops: usize) {
1263 let (a, b, _, _) = addrs();
1264 let m = linear_graph();
1265 let g = m.graph();
1266
1267 assert!(matches!(
1268 MostLiquidAlgorithm::find_paths(g, &a, &b, min_hops, max_hops, None)
1269 .err()
1270 .unwrap(),
1271 AlgorithmError::InvalidConfiguration { reason: _ }
1272 ));
1273 }
1274
1275 #[test]
1276 fn test_find_paths_bfs_ordering() {
1277 let (a, b, c, d) = addrs();
1282 let e = addr(0x0E);
1283 let mut m = PetgraphStableDiGraphManager::<DepthAndPrice>::new();
1284 let mut t = HashMap::new();
1285 t.insert("ae".into(), vec![a.clone(), e.clone()]);
1286 t.insert("ab".into(), vec![a.clone(), b.clone()]);
1287 t.insert("be".into(), vec![b, e.clone()]);
1288 t.insert("ac".into(), vec![a.clone(), c.clone()]);
1289 t.insert("cd".into(), vec![c, d.clone()]);
1290 t.insert("de".into(), vec![d, e.clone()]);
1291 m.initialize_graph(&t);
1292 let g = m.graph();
1293
1294 let p = MostLiquidAlgorithm::find_paths(g, &a, &e, 1, 3, None).unwrap();
1295
1296 assert_eq!(p.len(), 3, "Expected 3 paths total");
1298 assert_eq!(p[0].len(), 1, "First path should be 1-hop");
1299 assert_eq!(p[1].len(), 2, "Second path should be 2-hop");
1300 assert_eq!(p[2].len(), 3, "Third path should be 3-hop");
1301 }
1302
1303 #[test]
1311 fn test_simulate_path_single_hop() {
1312 let token_a = token(0x01, "A");
1313 let token_b = token(0x02, "B");
1314
1315 let (market, manager) = setup_market_weighted(vec![(
1316 "component1",
1317 &token_a,
1318 &token_b,
1319 MockProtocolSim::new(2.0),
1320 )]);
1321
1322 let paths = MostLiquidAlgorithm::find_paths(
1323 manager.graph(),
1324 &token_a.address,
1325 &token_b.address,
1326 1,
1327 1,
1328 None,
1329 )
1330 .unwrap();
1331 let path = paths.into_iter().next().unwrap();
1332
1333 let result = MostLiquidAlgorithm::simulate_path(
1334 &path,
1335 market_read(&market).base_market_state(),
1336 None,
1337 BigUint::from(100u64),
1338 )
1339 .unwrap();
1340
1341 assert_eq!(result.route().swaps().len(), 1);
1342 assert_eq!(*result.route().swaps()[0].amount_in(), BigUint::from(100u64));
1343 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(200u64)); assert_eq!(result.route().swaps()[0].component_id(), "component1");
1345 }
1346
1347 #[test]
1348 fn test_simulate_path_multi_hop_chains_amounts() {
1349 let token_a = token(0x01, "A");
1350 let token_b = token(0x02, "B");
1351 let token_c = token(0x03, "C");
1352
1353 let (market, manager) = setup_market_weighted(vec![
1354 ("component1", &token_a, &token_b, MockProtocolSim::new(2.0)),
1355 ("component2", &token_b, &token_c, MockProtocolSim::new(3.0)),
1356 ]);
1357
1358 let paths = MostLiquidAlgorithm::find_paths(
1359 manager.graph(),
1360 &token_a.address,
1361 &token_c.address,
1362 2,
1363 2,
1364 None,
1365 )
1366 .unwrap();
1367 let path = paths.into_iter().next().unwrap();
1368
1369 let result = MostLiquidAlgorithm::simulate_path(
1370 &path,
1371 market_read(&market).base_market_state(),
1372 None,
1373 BigUint::from(10u64),
1374 )
1375 .unwrap();
1376
1377 assert_eq!(result.route().swaps().len(), 2);
1378 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(20u64));
1380 assert_eq!(*result.route().swaps()[1].amount_in(), BigUint::from(20u64));
1382 assert_eq!(*result.route().swaps()[1].amount_out(), BigUint::from(60u64));
1383 }
1384
1385 #[test]
1386 fn test_simulate_path_same_component_twice_uses_updated_state() {
1387 let token_a = token(0x01, "A");
1390 let token_b = token(0x02, "B");
1391
1392 let (market, manager) = setup_market_weighted(vec![(
1393 "component1",
1394 &token_a,
1395 &token_b,
1396 MockProtocolSim::new(2.0),
1397 )]);
1398
1399 let paths = MostLiquidAlgorithm::find_paths(
1402 manager.graph(),
1403 &token_a.address,
1404 &token_a.address,
1405 2,
1406 2,
1407 None,
1408 )
1409 .unwrap();
1410
1411 assert_eq!(paths.len(), 1);
1413 let path = paths[0].clone();
1414
1415 let result = MostLiquidAlgorithm::simulate_path(
1416 &path,
1417 market_read(&market).base_market_state(),
1418 None,
1419 BigUint::from(10u64),
1420 )
1421 .unwrap();
1422
1423 assert_eq!(result.route().swaps().len(), 2);
1424 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(20u64));
1426 assert_eq!(*result.route().swaps()[1].amount_out(), BigUint::from(6u64));
1428 }
1429
1430 #[test]
1431 fn test_simulate_path_missing_token_returns_data_not_found() {
1432 let token_a = token(0x01, "A");
1433 let token_b = token(0x02, "B");
1434 let token_c = token(0x03, "C");
1435
1436 let (market, _) = setup_market_weighted(vec![(
1437 "component1",
1438 &token_a,
1439 &token_b,
1440 MockProtocolSim::new(2.0),
1441 )]);
1442 let market = market_read(&market);
1443
1444 let mut topology = market.component_topology();
1446 topology.insert(
1447 "component2".to_string(),
1448 vec![token_b.address.clone(), token_c.address.clone()],
1449 );
1450 let mut manager = PetgraphStableDiGraphManager::default();
1451 manager.initialize_graph(&topology);
1452
1453 let graph = manager.graph();
1454 let paths =
1455 MostLiquidAlgorithm::find_paths(graph, &token_a.address, &token_c.address, 2, 2, None)
1456 .unwrap();
1457 let path = paths.into_iter().next().unwrap();
1458
1459 let result = MostLiquidAlgorithm::simulate_path(
1460 &path,
1461 market.base_market_state(),
1462 None,
1463 BigUint::from(100u64),
1464 );
1465 assert!(matches!(result, Err(AlgorithmError::DataNotFound { kind: "token", .. })));
1466 }
1467
1468 #[test]
1469 fn test_simulate_path_missing_component_returns_data_not_found() {
1470 let token_a = token(0x01, "A");
1471 let token_b = token(0x02, "B");
1472 let (market, manager) = setup_market_weighted(vec![(
1473 "component1",
1474 &token_a,
1475 &token_b,
1476 MockProtocolSim::new(2.0),
1477 )]);
1478
1479 let mut market_write = market.try_write().unwrap();
1481 market_write.remove_components([&"component1".to_string()]);
1482 drop(market_write);
1483
1484 let graph = manager.graph();
1485 let paths =
1486 MostLiquidAlgorithm::find_paths(graph, &token_a.address, &token_b.address, 1, 1, None)
1487 .unwrap();
1488 let path = paths.into_iter().next().unwrap();
1489
1490 let result = MostLiquidAlgorithm::simulate_path(
1491 &path,
1492 market_read(&market).base_market_state(),
1493 None,
1494 BigUint::from(100u64),
1495 );
1496 assert!(matches!(result, Err(AlgorithmError::DataNotFound { kind: "component", .. })));
1497 }
1498
1499 #[test]
1502 fn test_connector_tokens_blocks_disallowed_intermediate() {
1503 let (a, b, c, d) = addrs();
1505 let m = diamond_graph();
1506 let g = m.graph();
1507 let allowed: HashSet<Address> = [c.clone()].into();
1508 let paths = MostLiquidAlgorithm::find_paths(g, &a, &d, 1, 2, Some(&allowed)).unwrap();
1509 let intermediates: HashSet<&Address> = paths
1510 .iter()
1511 .flat_map(|p| p.iter().map(|(node, _, _)| node))
1512 .filter(|addr| *addr != &a && *addr != &d)
1513 .collect();
1514 assert!(!intermediates.contains(&b), "B should be blocked");
1516 assert!(intermediates.contains(&c), "C should be allowed");
1517 }
1518
1519 #[test]
1520 fn test_connector_tokens_allows_endpoints_even_if_not_listed() {
1521 let (a, b, _, _) = addrs();
1523 let m = linear_graph();
1524 let g = m.graph();
1525 let allowed: HashSet<Address> = HashSet::new(); let paths = MostLiquidAlgorithm::find_paths(g, &a, &b, 1, 1, Some(&allowed)).unwrap();
1528 assert!(!paths.is_empty(), "1-hop direct route should survive empty allowlist");
1529 }
1530
1531 #[test]
1532 fn test_connector_tokens_none_is_unrestricted() {
1533 let (a, b, c, d) = addrs();
1535 let m = diamond_graph();
1536 let g = m.graph();
1537 let paths = MostLiquidAlgorithm::find_paths(g, &a, &d, 1, 2, None).unwrap();
1538 let intermediates: HashSet<&Address> = paths
1539 .iter()
1540 .flat_map(|p| p.iter().map(|(node, _, _)| node))
1541 .filter(|addr| *addr != &a && *addr != &d)
1542 .collect();
1543 assert!(intermediates.contains(&b), "B should appear with no restriction");
1544 assert!(intermediates.contains(&c), "C should appear with no restriction");
1545 }
1546
1547 #[tokio::test]
1550 async fn test_find_best_route_single_path() {
1551 let token_a = token(0x01, "A");
1552 let token_b = token(0x02, "B");
1553
1554 let (market, manager) = setup_market_weighted(vec![(
1555 "component1",
1556 &token_a,
1557 &token_b,
1558 MockProtocolSim::new(2.0),
1559 )]);
1560
1561 let algorithm = MostLiquidAlgorithm::with_config(
1562 AlgorithmConfig::new(1, 1, Duration::from_millis(100), None).unwrap(),
1563 )
1564 .unwrap();
1565 let order = order(&token_a, &token_b, ONE_ETH, OrderSide::Sell);
1566 let result = algorithm
1567 .find_best_route(manager.graph(), market, None, None, &order)
1568 .await
1569 .unwrap();
1570
1571 assert_eq!(result.route().swaps().len(), 1);
1572 assert_eq!(*result.route().swaps()[0].amount_in(), BigUint::from(ONE_ETH));
1573 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(ONE_ETH * 2));
1574 }
1575
1576 #[tokio::test]
1577 async fn test_find_best_route_ranks_by_net_amount_out() {
1578 let token_a = token(0x01, "A");
1589 let token_b = token(0x02, "B");
1590
1591 let (market, manager) = setup_market_weighted(vec![
1592 ("best", &token_a, &token_b, MockProtocolSim::new(3.0).with_gas(10)),
1593 ("low_out", &token_a, &token_b, MockProtocolSim::new(2.0).with_gas(5)),
1594 ("high_gas", &token_a, &token_b, MockProtocolSim::new(4.0).with_gas(30)),
1595 ]);
1596
1597 let algorithm = MostLiquidAlgorithm::with_config(
1598 AlgorithmConfig::new(1, 1, Duration::from_millis(100), None).unwrap(),
1599 )
1600 .unwrap();
1601 let order = order(&token_a, &token_b, 1000, OrderSide::Sell);
1602
1603 let derived = setup_derived_with_token_prices(std::slice::from_ref(&token_b.address));
1605
1606 let result = algorithm
1607 .find_best_route(manager.graph(), market, None, Some(derived), &order)
1608 .await
1609 .unwrap();
1610
1611 assert_eq!(result.route().swaps().len(), 1);
1613 assert_eq!(result.route().swaps()[0].component_id(), "best");
1614 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(3000u64));
1615 assert_eq!(result.net_amount_out(), &BigInt::from(2000)); }
1617
1618 #[tokio::test]
1619 async fn test_find_best_route_no_path_returns_error() {
1620 let token_a = token(0x01, "A");
1621 let token_b = token(0x02, "B");
1622 let token_c = token(0x03, "C"); let (market, manager) = setup_market_weighted(vec![(
1625 "component1",
1626 &token_a,
1627 &token_b,
1628 MockProtocolSim::new(2.0),
1629 )]);
1630
1631 let algorithm = MostLiquidAlgorithm::new();
1632 let order = order(&token_a, &token_c, ONE_ETH, OrderSide::Sell);
1633
1634 let result = algorithm
1635 .find_best_route(manager.graph(), market, None, None, &order)
1636 .await;
1637 assert!(matches!(result, Err(AlgorithmError::NoPath { .. })));
1638 }
1639
1640 #[tokio::test]
1641 async fn test_find_best_route_multi_hop() {
1642 let token_a = token(0x01, "A");
1643 let token_b = token(0x02, "B");
1644 let token_c = token(0x03, "C");
1645
1646 let (market, manager) = setup_market_weighted(vec![
1647 ("component1", &token_a, &token_b, MockProtocolSim::new(2.0)),
1648 ("component2", &token_b, &token_c, MockProtocolSim::new(3.0)),
1649 ]);
1650
1651 let algorithm = MostLiquidAlgorithm::with_config(
1652 AlgorithmConfig::new(1, 2, Duration::from_millis(100), None).unwrap(),
1653 )
1654 .unwrap();
1655 let order = order(&token_a, &token_c, ONE_ETH, OrderSide::Sell);
1656
1657 let result = algorithm
1658 .find_best_route(manager.graph(), market, None, None, &order)
1659 .await
1660 .unwrap();
1661
1662 assert_eq!(result.route().swaps().len(), 2);
1664 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(ONE_ETH * 2));
1665 assert_eq!(result.route().swaps()[0].component_id(), "component1".to_string());
1666 assert_eq!(*result.route().swaps()[1].amount_out(), BigUint::from(ONE_ETH * 2 * 3));
1667 assert_eq!(result.route().swaps()[1].component_id(), "component2".to_string());
1668 }
1669
1670 #[tokio::test]
1671 async fn test_find_best_route_skips_paths_without_edge_weights() {
1672 let token_a = token(0x01, "A");
1674 let token_b = token(0x02, "B");
1675
1676 let mut market = MarketState::new();
1678 let component1_state = MockProtocolSim::new(2.0);
1679 let component2_state = MockProtocolSim::new(3.0); let component1_comp = component("component1", &[token_a.clone(), token_b.clone()]);
1682 let component2_comp = component("component2", &[token_a.clone(), token_b.clone()]);
1683
1684 market.update_gas_price(BlockGasPrice {
1686 block_number: 1,
1687 block_hash: Default::default(),
1688 block_timestamp: 0,
1689 pricing: GasPrice::Legacy { gas_price: BigUint::from(1u64) },
1690 });
1691
1692 market.upsert_components(vec![component1_comp, component2_comp]);
1694
1695 market.update_states(vec![
1697 ("component1".to_string(), Box::new(component1_state.clone()) as Box<dyn ProtocolSim>),
1698 ("component2".to_string(), Box::new(component2_state) as Box<dyn ProtocolSim>),
1699 ]);
1700
1701 market.upsert_tokens(vec![token_a.clone(), token_b.clone()]);
1703
1704 let mut manager = PetgraphStableDiGraphManager::default();
1706 manager.initialize_graph(&market.component_topology());
1707
1708 let weight =
1710 DepthAndPrice::from_protocol_sim(&component1_state, &token_a, &token_b).unwrap();
1711 manager
1712 .set_edge_weight(
1713 &"component1".to_string(),
1714 &token_a.address,
1715 &token_b.address,
1716 weight,
1717 false,
1718 )
1719 .unwrap();
1720
1721 let algorithm = MostLiquidAlgorithm::with_config(
1723 AlgorithmConfig::new(1, 1, Duration::from_millis(100), None).unwrap(),
1724 )
1725 .unwrap();
1726 let order = order(&token_a, &token_b, ONE_ETH, OrderSide::Sell);
1727 let market = wrap_market(market);
1728 let result = algorithm
1729 .find_best_route(manager.graph(), market, None, None, &order)
1730 .await
1731 .unwrap();
1732
1733 assert_eq!(result.route().swaps().len(), 1);
1735 assert_eq!(result.route().swaps()[0].component_id(), "component1");
1736 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(ONE_ETH * 2));
1737 }
1738
1739 #[tokio::test]
1740 async fn test_find_best_route_no_scorable_paths() {
1741 let token_a = token(0x01, "A");
1743 let token_b = token(0x02, "B");
1744
1745 let mut market = MarketState::new();
1746 let component_state = MockProtocolSim::new(2.0);
1747 let comp = component("component1", &[token_a.clone(), token_b.clone()]);
1748
1749 market.update_gas_price(BlockGasPrice {
1751 block_number: 1,
1752 block_hash: Default::default(),
1753 block_timestamp: 0,
1754 pricing: GasPrice::Eip1559 {
1755 base_fee_per_gas: BigUint::from(1u64),
1756 max_priority_fee_per_gas: BigUint::from(0u64),
1757 },
1758 });
1759
1760 market.upsert_components(vec![comp]);
1761 market.update_states(vec![(
1762 "component1".to_string(),
1763 Box::new(component_state) as Box<dyn ProtocolSim>,
1764 )]);
1765 market.upsert_tokens(vec![token_a.clone(), token_b.clone()]);
1766
1767 let mut manager = PetgraphStableDiGraphManager::default();
1769 manager.initialize_graph(&market.component_topology());
1770
1771 let algorithm = MostLiquidAlgorithm::new();
1772 let order = order(&token_a, &token_b, ONE_ETH, OrderSide::Sell);
1773 let market = wrap_market(market);
1774
1775 let result = algorithm
1776 .find_best_route(manager.graph(), market, None, None, &order)
1777 .await;
1778 assert!(matches!(
1779 result,
1780 Err(AlgorithmError::NoPath { reason: NoPathReason::NoScorablePaths, .. })
1781 ));
1782 }
1783
1784 #[tokio::test]
1785 async fn test_find_best_route_gas_exceeds_output_returns_negative_net() {
1786 let token_a = token(0x01, "A");
1787 let token_b = token(0x02, "B");
1788
1789 let (market, manager) = setup_market_weighted(vec![(
1790 "component1",
1791 &token_a,
1792 &token_b,
1793 MockProtocolSim::new(2.0),
1794 )]);
1795 let mut market_write = market.try_write().unwrap();
1796
1797 market_write.update_gas_price(BlockGasPrice {
1800 block_number: 1,
1801 block_hash: Default::default(),
1802 block_timestamp: 0,
1803 pricing: GasPrice::Eip1559 {
1804 base_fee_per_gas: BigUint::from(1_000_000u64),
1805 max_priority_fee_per_gas: BigUint::from(1_000_000u64),
1806 },
1807 });
1808 drop(market_write); let algorithm = MostLiquidAlgorithm::new();
1811 let order = order(&token_a, &token_b, 1, OrderSide::Sell); let derived = setup_derived_with_token_prices(std::slice::from_ref(&token_b.address));
1815
1816 let result = algorithm
1818 .find_best_route(manager.graph(), market, None, Some(derived), &order)
1819 .await
1820 .expect("should return route even with negative net_amount_out");
1821
1822 assert_eq!(result.route().swaps().len(), 1);
1824 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(2u64)); let expected_net = BigInt::from(2) - BigInt::from(100_000_000_000u64);
1828 assert_eq!(result.net_amount_out(), &expected_net);
1829 }
1830
1831 #[tokio::test]
1832 async fn test_find_best_route_insufficient_liquidity() {
1833 let token_a = token(0x01, "A");
1835 let token_b = token(0x02, "B");
1836
1837 let (market, manager) = setup_market_weighted(vec![(
1838 "component1",
1839 &token_a,
1840 &token_b,
1841 MockProtocolSim::new(2.0).with_liquidity(1000),
1842 )]);
1843
1844 let algorithm = MostLiquidAlgorithm::new();
1845 let order = order(&token_a, &token_b, ONE_ETH, OrderSide::Sell); let result = algorithm
1848 .find_best_route(manager.graph(), market, None, None, &order)
1849 .await;
1850 assert!(matches!(result, Err(AlgorithmError::InsufficientLiquidity)));
1851 }
1852
1853 #[tokio::test]
1854 async fn test_find_best_route_missing_gas_price_returns_error() {
1855 let token_a = token(0x01, "A");
1857 let token_b = token(0x02, "B");
1858
1859 let mut market = MarketState::new();
1860 let component_state = MockProtocolSim::new(2.0);
1861 let comp = component("component1", &[token_a.clone(), token_b.clone()]);
1862
1863 market.upsert_components(vec![comp]);
1865 market.update_states(vec![(
1866 "component1".to_string(),
1867 Box::new(component_state.clone()) as Box<dyn ProtocolSim>,
1868 )]);
1869 market.upsert_tokens(vec![token_a.clone(), token_b.clone()]);
1870
1871 let mut manager = PetgraphStableDiGraphManager::default();
1873 manager.initialize_graph(&market.component_topology());
1874 let weight =
1875 DepthAndPrice::from_protocol_sim(&component_state, &token_a, &token_b).unwrap();
1876 manager
1877 .set_edge_weight(
1878 &"component1".to_string(),
1879 &token_a.address,
1880 &token_b.address,
1881 weight,
1882 false,
1883 )
1884 .unwrap();
1885
1886 let algorithm = MostLiquidAlgorithm::new();
1887 let order = order(&token_a, &token_b, ONE_ETH, OrderSide::Sell);
1888 let market = wrap_market(market);
1889
1890 let result = algorithm
1891 .find_best_route(manager.graph(), market, None, None, &order)
1892 .await;
1893
1894 assert!(matches!(result, Err(AlgorithmError::DataNotFound { kind: "gas price", .. })));
1896 }
1897
1898 #[tokio::test]
1899 async fn test_find_best_route_circular_arbitrage() {
1900 let token_a = token(0x01, "A");
1901 let token_b = token(0x02, "B");
1902
1903 let (market, manager) = setup_market_weighted(vec![(
1906 "component1",
1907 &token_a,
1908 &token_b,
1909 MockProtocolSim::new(2.0),
1910 )]);
1911
1912 let algorithm = MostLiquidAlgorithm::with_config(
1914 AlgorithmConfig::new(2, 2, Duration::from_millis(100), None).unwrap(),
1915 )
1916 .unwrap();
1917
1918 let order = order(&token_a, &token_a, 100, OrderSide::Sell);
1920
1921 let result = algorithm
1922 .find_best_route(manager.graph(), market, None, None, &order)
1923 .await
1924 .unwrap();
1925
1926 assert_eq!(result.route().swaps().len(), 2, "Should have 2 swaps for circular route");
1928
1929 assert_eq!(*result.route().swaps()[0].token_in(), token_a.address);
1931 assert_eq!(*result.route().swaps()[0].token_out(), token_b.address);
1932 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(200u64));
1933
1934 assert_eq!(*result.route().swaps()[1].token_in(), token_b.address);
1936 assert_eq!(*result.route().swaps()[1].token_out(), token_a.address);
1937 assert_eq!(*result.route().swaps()[1].amount_out(), BigUint::from(66u64));
1938
1939 assert_eq!(result.route().swaps()[0].token_in(), result.route().swaps()[1].token_out());
1941 }
1942
1943 #[tokio::test]
1944 async fn test_find_best_route_respects_min_hops() {
1945 let token_a = token(0x01, "A");
1948 let token_b = token(0x02, "B");
1949 let token_c = token(0x03, "C");
1950
1951 let (market, manager) = setup_market_weighted(vec![
1952 ("component_ab", &token_a, &token_b, MockProtocolSim::new(10.0)), ("component_ac", &token_a, &token_c, MockProtocolSim::new(2.0)), ("component_cb", &token_c, &token_b, MockProtocolSim::new(3.0)), ]);
1958
1959 let algorithm = MostLiquidAlgorithm::with_config(
1961 AlgorithmConfig::new(2, 3, Duration::from_millis(100), None).unwrap(),
1962 )
1963 .unwrap();
1964 let order = order(&token_a, &token_b, 100, OrderSide::Sell);
1965
1966 let derived = setup_derived_with_token_prices(std::slice::from_ref(&token_b.address));
1969
1970 let result = algorithm
1971 .find_best_route(manager.graph(), market, None, Some(derived), &order)
1972 .await
1973 .unwrap();
1974
1975 assert_eq!(result.route().swaps().len(), 2, "Should use 2-hop path due to min_hops=2");
1977 assert_eq!(result.route().swaps()[0].component_id(), "component_ac");
1978 assert_eq!(result.route().swaps()[1].component_id(), "component_cb");
1979 }
1980
1981 #[tokio::test]
1982 async fn test_find_best_route_respects_max_hops() {
1983 let token_a = token(0x01, "A");
1986 let token_b = token(0x02, "B");
1987 let token_c = token(0x03, "C");
1988
1989 let (market, manager) = setup_market_weighted(vec![
1990 ("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
1991 ("component_bc", &token_b, &token_c, MockProtocolSim::new(3.0)),
1992 ]);
1993
1994 let algorithm = MostLiquidAlgorithm::with_config(
1996 AlgorithmConfig::new(1, 1, Duration::from_millis(100), None).unwrap(),
1997 )
1998 .unwrap();
1999 let order = order(&token_a, &token_c, 100, OrderSide::Sell);
2000
2001 let result = algorithm
2002 .find_best_route(manager.graph(), market, None, None, &order)
2003 .await;
2004 assert!(
2005 matches!(result, Err(AlgorithmError::NoPath { .. })),
2006 "Should return NoPath when max_hops is insufficient"
2007 );
2008 }
2009
2010 #[tokio::test]
2011 async fn test_find_best_route_timeout_returns_best_so_far() {
2012 let token_a = token(0x01, "A");
2016 let token_b = token(0x02, "B");
2017
2018 let (market, manager) = setup_market_weighted(vec![
2020 ("component1", &token_a, &token_b, MockProtocolSim::new(1.0)),
2021 ("component2", &token_a, &token_b, MockProtocolSim::new(2.0)),
2022 ("component3", &token_a, &token_b, MockProtocolSim::new(3.0)),
2023 ("component4", &token_a, &token_b, MockProtocolSim::new(4.0)),
2024 ("component5", &token_a, &token_b, MockProtocolSim::new(5.0)),
2025 ]);
2026
2027 let algorithm = MostLiquidAlgorithm::with_config(
2029 AlgorithmConfig::new(1, 1, Duration::from_millis(0), None).unwrap(),
2030 )
2031 .unwrap();
2032 let order = order(&token_a, &token_b, 100, OrderSide::Sell);
2033
2034 let result = algorithm
2035 .find_best_route(manager.graph(), market, None, None, &order)
2036 .await;
2037
2038 match result {
2043 Ok(r) => {
2044 assert_eq!(r.route().swaps().len(), 1);
2046 }
2047 Err(AlgorithmError::Timeout { .. }) => {
2048 }
2050 Err(e) => panic!("Unexpected error: {:?}", e),
2051 }
2052 }
2053
2054 #[rstest::rstest]
2057 #[case::default_config(1, 3, 50)]
2058 #[case::single_hop_only(1, 1, 100)]
2059 #[case::multi_hop_min(2, 5, 200)]
2060 #[case::zero_timeout(1, 3, 0)]
2061 #[case::large_values(10, 100, 10000)]
2062 fn test_algorithm_config_getters(
2063 #[case] min_hops: usize,
2064 #[case] max_hops: usize,
2065 #[case] timeout_ms: u64,
2066 ) {
2067 use crate::algorithm::Algorithm;
2068
2069 let algorithm = MostLiquidAlgorithm::with_config(
2070 AlgorithmConfig::new(min_hops, max_hops, Duration::from_millis(timeout_ms), None)
2071 .unwrap(),
2072 )
2073 .unwrap();
2074
2075 assert_eq!(algorithm.max_hops, max_hops);
2076 assert_eq!(algorithm.timeout, Duration::from_millis(timeout_ms));
2077 assert_eq!(algorithm.name(), "most_liquid");
2078 }
2079
2080 #[test]
2081 fn test_algorithm_default_config() {
2082 use crate::algorithm::Algorithm;
2083
2084 let algorithm = MostLiquidAlgorithm::new();
2085
2086 assert_eq!(algorithm.max_hops, 3);
2087 assert_eq!(algorithm.timeout, Duration::from_millis(500));
2088 assert_eq!(algorithm.name(), "most_liquid");
2089 }
2090
2091 #[tokio::test]
2094 async fn test_find_best_route_respects_max_routes_cap() {
2095 let token_a = token(0x01, "A");
2108 let token_b = token(0x02, "B");
2109
2110 let (market, manager) = setup_market_weighted(vec![
2111 ("component1", &token_a, &token_b, MockProtocolSim::new(4.0).with_liquidity(1_000_000)),
2112 ("component2", &token_a, &token_b, MockProtocolSim::new(3.0).with_liquidity(2_000_000)),
2113 ("component3", &token_a, &token_b, MockProtocolSim::new(2.0).with_liquidity(3_000_000)),
2114 ("component4", &token_a, &token_b, MockProtocolSim::new(1.0).with_liquidity(4_000_000)),
2115 ]);
2116
2117 let algorithm = MostLiquidAlgorithm::with_config(
2119 AlgorithmConfig::new(1, 1, Duration::from_millis(100), Some(2)).unwrap(),
2120 )
2121 .unwrap();
2122 let order = order(&token_a, &token_b, 1000, OrderSide::Sell);
2123 let result = algorithm
2124 .find_best_route(manager.graph(), market, None, None, &order)
2125 .await
2126 .unwrap();
2127
2128 assert_eq!(result.route().swaps().len(), 1);
2132 assert_eq!(result.route().swaps()[0].component_id(), "component3");
2133 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(2000u64));
2134 }
2135
2136 #[tokio::test]
2137 async fn test_find_best_route_no_cap_when_max_routes_is_none() {
2138 let token_a = token(0x01, "A");
2140 let token_b = token(0x02, "B");
2141
2142 let (market, manager) = setup_market_weighted(vec![
2143 ("component1", &token_a, &token_b, MockProtocolSim::new(4.0).with_liquidity(1_000_000)),
2144 ("component2", &token_a, &token_b, MockProtocolSim::new(3.0).with_liquidity(2_000_000)),
2145 ("component3", &token_a, &token_b, MockProtocolSim::new(2.0).with_liquidity(3_000_000)),
2146 ("component4", &token_a, &token_b, MockProtocolSim::new(1.0).with_liquidity(4_000_000)),
2147 ]);
2148
2149 let algorithm = MostLiquidAlgorithm::with_config(
2150 AlgorithmConfig::new(1, 1, Duration::from_millis(100), None).unwrap(),
2151 )
2152 .unwrap();
2153 let order = order(&token_a, &token_b, 1000, OrderSide::Sell);
2154 let result = algorithm
2155 .find_best_route(manager.graph(), market, None, None, &order)
2156 .await
2157 .unwrap();
2158
2159 assert_eq!(result.route().swaps().len(), 1);
2161 assert_eq!(result.route().swaps()[0].component_id(), "component1");
2162 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(4000u64));
2163 }
2164
2165 #[test]
2166 fn test_algorithm_config_rejects_zero_max_routes() {
2167 let result = AlgorithmConfig::new(1, 3, Duration::from_millis(100), Some(0));
2168 assert!(matches!(
2169 result,
2170 Err(AlgorithmError::InvalidConfiguration { reason }) if reason.contains("max_routes must be at least 1")
2171 ));
2172 }
2173
2174 #[test]
2175 fn test_algorithm_config_rejects_zero_min_hops() {
2176 let result = AlgorithmConfig::new(0, 3, Duration::from_millis(100), None);
2177 assert!(matches!(
2178 result,
2179 Err(AlgorithmError::InvalidConfiguration { reason }) if reason.contains("min_hops must be at least 1")
2180 ));
2181 }
2182
2183 #[test]
2184 fn test_algorithm_config_rejects_min_greater_than_max() {
2185 let result = AlgorithmConfig::new(5, 3, Duration::from_millis(100), None);
2186 assert!(matches!(
2187 result,
2188 Err(AlgorithmError::InvalidConfiguration { reason }) if reason.contains("cannot exceed")
2189 ));
2190 }
2191}