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 .pool_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, "pool 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("pool1", 0x01, 0x02);
888 let key_str = pair_key_str("pool1", 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_pool_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_pool_depth_returns_none() {
922 let key = pair_key("pool1", 0x01, 0x02);
923 let key_str = pair_key_str("pool1", 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_pool_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("pool1", 0x01, 0x02);
961 let key_str = pair_key_str("pool1", 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_pool_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("pool1", 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::PoolDepths::default();
1013 depths.insert(key.clone(), BigUint::from(1000u64));
1014 derived.set_pool_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("pool1", 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::PoolDepths::default();
1045 depths.insert(key.clone(), BigUint::from(2_000_000u64));
1046 derived.set_pool_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("pool1", 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::PoolDepths::default();
1087 depths.insert(key.clone(), BigUint::from(500u64));
1088 derived.set_pool_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_pools() {
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) =
1316 setup_market_weighted(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1317
1318 let paths = MostLiquidAlgorithm::find_paths(
1319 manager.graph(),
1320 &token_a.address,
1321 &token_b.address,
1322 1,
1323 1,
1324 None,
1325 )
1326 .unwrap();
1327 let path = paths.into_iter().next().unwrap();
1328
1329 let result = MostLiquidAlgorithm::simulate_path(
1330 &path,
1331 market_read(&market).base_market_state(),
1332 None,
1333 BigUint::from(100u64),
1334 )
1335 .unwrap();
1336
1337 assert_eq!(result.route().swaps().len(), 1);
1338 assert_eq!(*result.route().swaps()[0].amount_in(), BigUint::from(100u64));
1339 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(200u64)); assert_eq!(result.route().swaps()[0].component_id(), "pool1");
1341 }
1342
1343 #[test]
1344 fn test_simulate_path_multi_hop_chains_amounts() {
1345 let token_a = token(0x01, "A");
1346 let token_b = token(0x02, "B");
1347 let token_c = token(0x03, "C");
1348
1349 let (market, manager) = setup_market_weighted(vec![
1350 ("pool1", &token_a, &token_b, MockProtocolSim::new(2.0)),
1351 ("pool2", &token_b, &token_c, MockProtocolSim::new(3.0)),
1352 ]);
1353
1354 let paths = MostLiquidAlgorithm::find_paths(
1355 manager.graph(),
1356 &token_a.address,
1357 &token_c.address,
1358 2,
1359 2,
1360 None,
1361 )
1362 .unwrap();
1363 let path = paths.into_iter().next().unwrap();
1364
1365 let result = MostLiquidAlgorithm::simulate_path(
1366 &path,
1367 market_read(&market).base_market_state(),
1368 None,
1369 BigUint::from(10u64),
1370 )
1371 .unwrap();
1372
1373 assert_eq!(result.route().swaps().len(), 2);
1374 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(20u64));
1376 assert_eq!(*result.route().swaps()[1].amount_in(), BigUint::from(20u64));
1378 assert_eq!(*result.route().swaps()[1].amount_out(), BigUint::from(60u64));
1379 }
1380
1381 #[test]
1382 fn test_simulate_path_same_pool_twice_uses_updated_state() {
1383 let token_a = token(0x01, "A");
1386 let token_b = token(0x02, "B");
1387
1388 let (market, manager) =
1389 setup_market_weighted(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1390
1391 let paths = MostLiquidAlgorithm::find_paths(
1394 manager.graph(),
1395 &token_a.address,
1396 &token_a.address,
1397 2,
1398 2,
1399 None,
1400 )
1401 .unwrap();
1402
1403 assert_eq!(paths.len(), 1);
1405 let path = paths[0].clone();
1406
1407 let result = MostLiquidAlgorithm::simulate_path(
1408 &path,
1409 market_read(&market).base_market_state(),
1410 None,
1411 BigUint::from(10u64),
1412 )
1413 .unwrap();
1414
1415 assert_eq!(result.route().swaps().len(), 2);
1416 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(20u64));
1418 assert_eq!(*result.route().swaps()[1].amount_out(), BigUint::from(6u64));
1420 }
1421
1422 #[test]
1423 fn test_simulate_path_missing_token_returns_data_not_found() {
1424 let token_a = token(0x01, "A");
1425 let token_b = token(0x02, "B");
1426 let token_c = token(0x03, "C");
1427
1428 let (market, _) =
1429 setup_market_weighted(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1430 let market = market_read(&market);
1431
1432 let mut topology = market.component_topology();
1434 topology
1435 .insert("pool2".to_string(), vec![token_b.address.clone(), token_c.address.clone()]);
1436 let mut manager = PetgraphStableDiGraphManager::default();
1437 manager.initialize_graph(&topology);
1438
1439 let graph = manager.graph();
1440 let paths =
1441 MostLiquidAlgorithm::find_paths(graph, &token_a.address, &token_c.address, 2, 2, None)
1442 .unwrap();
1443 let path = paths.into_iter().next().unwrap();
1444
1445 let result = MostLiquidAlgorithm::simulate_path(
1446 &path,
1447 market.base_market_state(),
1448 None,
1449 BigUint::from(100u64),
1450 );
1451 assert!(matches!(result, Err(AlgorithmError::DataNotFound { kind: "token", .. })));
1452 }
1453
1454 #[test]
1455 fn test_simulate_path_missing_component_returns_data_not_found() {
1456 let token_a = token(0x01, "A");
1457 let token_b = token(0x02, "B");
1458 let (market, manager) =
1459 setup_market_weighted(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1460
1461 let mut market_write = market.try_write().unwrap();
1463 market_write.remove_components([&"pool1".to_string()]);
1464 drop(market_write);
1465
1466 let graph = manager.graph();
1467 let paths =
1468 MostLiquidAlgorithm::find_paths(graph, &token_a.address, &token_b.address, 1, 1, None)
1469 .unwrap();
1470 let path = paths.into_iter().next().unwrap();
1471
1472 let result = MostLiquidAlgorithm::simulate_path(
1473 &path,
1474 market_read(&market).base_market_state(),
1475 None,
1476 BigUint::from(100u64),
1477 );
1478 assert!(matches!(result, Err(AlgorithmError::DataNotFound { kind: "component", .. })));
1479 }
1480
1481 #[test]
1484 fn test_connector_tokens_blocks_disallowed_intermediate() {
1485 let (a, b, c, d) = addrs();
1487 let m = diamond_graph();
1488 let g = m.graph();
1489 let allowed: HashSet<Address> = [c.clone()].into();
1490 let paths = MostLiquidAlgorithm::find_paths(g, &a, &d, 1, 2, Some(&allowed)).unwrap();
1491 let intermediates: HashSet<&Address> = paths
1492 .iter()
1493 .flat_map(|p| p.iter().map(|(node, _, _)| node))
1494 .filter(|addr| *addr != &a && *addr != &d)
1495 .collect();
1496 assert!(!intermediates.contains(&b), "B should be blocked");
1498 assert!(intermediates.contains(&c), "C should be allowed");
1499 }
1500
1501 #[test]
1502 fn test_connector_tokens_allows_endpoints_even_if_not_listed() {
1503 let (a, b, _, _) = addrs();
1505 let m = linear_graph();
1506 let g = m.graph();
1507 let allowed: HashSet<Address> = HashSet::new(); let paths = MostLiquidAlgorithm::find_paths(g, &a, &b, 1, 1, Some(&allowed)).unwrap();
1510 assert!(!paths.is_empty(), "1-hop direct route should survive empty allowlist");
1511 }
1512
1513 #[test]
1514 fn test_connector_tokens_none_is_unrestricted() {
1515 let (a, b, c, d) = addrs();
1517 let m = diamond_graph();
1518 let g = m.graph();
1519 let paths = MostLiquidAlgorithm::find_paths(g, &a, &d, 1, 2, None).unwrap();
1520 let intermediates: HashSet<&Address> = paths
1521 .iter()
1522 .flat_map(|p| p.iter().map(|(node, _, _)| node))
1523 .filter(|addr| *addr != &a && *addr != &d)
1524 .collect();
1525 assert!(intermediates.contains(&b), "B should appear with no restriction");
1526 assert!(intermediates.contains(&c), "C should appear with no restriction");
1527 }
1528
1529 #[tokio::test]
1532 async fn test_find_best_route_single_path() {
1533 let token_a = token(0x01, "A");
1534 let token_b = token(0x02, "B");
1535
1536 let (market, manager) =
1537 setup_market_weighted(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1538
1539 let algorithm = MostLiquidAlgorithm::with_config(
1540 AlgorithmConfig::new(1, 1, Duration::from_millis(100), None).unwrap(),
1541 )
1542 .unwrap();
1543 let order = order(&token_a, &token_b, ONE_ETH, OrderSide::Sell);
1544 let result = algorithm
1545 .find_best_route(manager.graph(), market, None, None, &order)
1546 .await
1547 .unwrap();
1548
1549 assert_eq!(result.route().swaps().len(), 1);
1550 assert_eq!(*result.route().swaps()[0].amount_in(), BigUint::from(ONE_ETH));
1551 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(ONE_ETH * 2));
1552 }
1553
1554 #[tokio::test]
1555 async fn test_find_best_route_ranks_by_net_amount_out() {
1556 let token_a = token(0x01, "A");
1567 let token_b = token(0x02, "B");
1568
1569 let (market, manager) = setup_market_weighted(vec![
1570 ("best", &token_a, &token_b, MockProtocolSim::new(3.0).with_gas(10)),
1571 ("low_out", &token_a, &token_b, MockProtocolSim::new(2.0).with_gas(5)),
1572 ("high_gas", &token_a, &token_b, MockProtocolSim::new(4.0).with_gas(30)),
1573 ]);
1574
1575 let algorithm = MostLiquidAlgorithm::with_config(
1576 AlgorithmConfig::new(1, 1, Duration::from_millis(100), None).unwrap(),
1577 )
1578 .unwrap();
1579 let order = order(&token_a, &token_b, 1000, OrderSide::Sell);
1580
1581 let derived = setup_derived_with_token_prices(std::slice::from_ref(&token_b.address));
1583
1584 let result = algorithm
1585 .find_best_route(manager.graph(), market, None, Some(derived), &order)
1586 .await
1587 .unwrap();
1588
1589 assert_eq!(result.route().swaps().len(), 1);
1591 assert_eq!(result.route().swaps()[0].component_id(), "best");
1592 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(3000u64));
1593 assert_eq!(result.net_amount_out(), &BigInt::from(2000)); }
1595
1596 #[tokio::test]
1597 async fn test_find_best_route_no_path_returns_error() {
1598 let token_a = token(0x01, "A");
1599 let token_b = token(0x02, "B");
1600 let token_c = token(0x03, "C"); let (market, manager) =
1603 setup_market_weighted(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1604
1605 let algorithm = MostLiquidAlgorithm::new();
1606 let order = order(&token_a, &token_c, ONE_ETH, OrderSide::Sell);
1607
1608 let result = algorithm
1609 .find_best_route(manager.graph(), market, None, None, &order)
1610 .await;
1611 assert!(matches!(result, Err(AlgorithmError::NoPath { .. })));
1612 }
1613
1614 #[tokio::test]
1615 async fn test_find_best_route_multi_hop() {
1616 let token_a = token(0x01, "A");
1617 let token_b = token(0x02, "B");
1618 let token_c = token(0x03, "C");
1619
1620 let (market, manager) = setup_market_weighted(vec![
1621 ("pool1", &token_a, &token_b, MockProtocolSim::new(2.0)),
1622 ("pool2", &token_b, &token_c, MockProtocolSim::new(3.0)),
1623 ]);
1624
1625 let algorithm = MostLiquidAlgorithm::with_config(
1626 AlgorithmConfig::new(1, 2, Duration::from_millis(100), None).unwrap(),
1627 )
1628 .unwrap();
1629 let order = order(&token_a, &token_c, ONE_ETH, OrderSide::Sell);
1630
1631 let result = algorithm
1632 .find_best_route(manager.graph(), market, None, None, &order)
1633 .await
1634 .unwrap();
1635
1636 assert_eq!(result.route().swaps().len(), 2);
1638 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(ONE_ETH * 2));
1639 assert_eq!(result.route().swaps()[0].component_id(), "pool1".to_string());
1640 assert_eq!(*result.route().swaps()[1].amount_out(), BigUint::from(ONE_ETH * 2 * 3));
1641 assert_eq!(result.route().swaps()[1].component_id(), "pool2".to_string());
1642 }
1643
1644 #[tokio::test]
1645 async fn test_find_best_route_skips_paths_without_edge_weights() {
1646 let token_a = token(0x01, "A");
1648 let token_b = token(0x02, "B");
1649
1650 let mut market = MarketState::new();
1652 let pool1_state = MockProtocolSim::new(2.0);
1653 let pool2_state = MockProtocolSim::new(3.0); let pool1_comp = component("pool1", &[token_a.clone(), token_b.clone()]);
1656 let pool2_comp = component("pool2", &[token_a.clone(), token_b.clone()]);
1657
1658 market.update_gas_price(BlockGasPrice {
1660 block_number: 1,
1661 block_hash: Default::default(),
1662 block_timestamp: 0,
1663 pricing: GasPrice::Legacy { gas_price: BigUint::from(1u64) },
1664 });
1665
1666 market.upsert_components(vec![pool1_comp, pool2_comp]);
1668
1669 market.update_states(vec![
1671 ("pool1".to_string(), Box::new(pool1_state.clone()) as Box<dyn ProtocolSim>),
1672 ("pool2".to_string(), Box::new(pool2_state) as Box<dyn ProtocolSim>),
1673 ]);
1674
1675 market.upsert_tokens(vec![token_a.clone(), token_b.clone()]);
1677
1678 let mut manager = PetgraphStableDiGraphManager::default();
1680 manager.initialize_graph(&market.component_topology());
1681
1682 let weight = DepthAndPrice::from_protocol_sim(&pool1_state, &token_a, &token_b).unwrap();
1684 manager
1685 .set_edge_weight(
1686 &"pool1".to_string(),
1687 &token_a.address,
1688 &token_b.address,
1689 weight,
1690 false,
1691 )
1692 .unwrap();
1693
1694 let algorithm = MostLiquidAlgorithm::with_config(
1696 AlgorithmConfig::new(1, 1, Duration::from_millis(100), None).unwrap(),
1697 )
1698 .unwrap();
1699 let order = order(&token_a, &token_b, ONE_ETH, OrderSide::Sell);
1700 let market = wrap_market(market);
1701 let result = algorithm
1702 .find_best_route(manager.graph(), market, None, None, &order)
1703 .await
1704 .unwrap();
1705
1706 assert_eq!(result.route().swaps().len(), 1);
1708 assert_eq!(result.route().swaps()[0].component_id(), "pool1");
1709 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(ONE_ETH * 2));
1710 }
1711
1712 #[tokio::test]
1713 async fn test_find_best_route_no_scorable_paths() {
1714 let token_a = token(0x01, "A");
1716 let token_b = token(0x02, "B");
1717
1718 let mut market = MarketState::new();
1719 let pool_state = MockProtocolSim::new(2.0);
1720 let pool_comp = component("pool1", &[token_a.clone(), token_b.clone()]);
1721
1722 market.update_gas_price(BlockGasPrice {
1724 block_number: 1,
1725 block_hash: Default::default(),
1726 block_timestamp: 0,
1727 pricing: GasPrice::Eip1559 {
1728 base_fee_per_gas: BigUint::from(1u64),
1729 max_priority_fee_per_gas: BigUint::from(0u64),
1730 },
1731 });
1732
1733 market.upsert_components(vec![pool_comp]);
1734 market.update_states(vec![(
1735 "pool1".to_string(),
1736 Box::new(pool_state) 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
1744 let algorithm = MostLiquidAlgorithm::new();
1745 let order = order(&token_a, &token_b, ONE_ETH, OrderSide::Sell);
1746 let market = wrap_market(market);
1747
1748 let result = algorithm
1749 .find_best_route(manager.graph(), market, None, None, &order)
1750 .await;
1751 assert!(matches!(
1752 result,
1753 Err(AlgorithmError::NoPath { reason: NoPathReason::NoScorablePaths, .. })
1754 ));
1755 }
1756
1757 #[tokio::test]
1758 async fn test_find_best_route_gas_exceeds_output_returns_negative_net() {
1759 let token_a = token(0x01, "A");
1760 let token_b = token(0x02, "B");
1761
1762 let (market, manager) =
1763 setup_market_weighted(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1764 let mut market_write = market.try_write().unwrap();
1765
1766 market_write.update_gas_price(BlockGasPrice {
1769 block_number: 1,
1770 block_hash: Default::default(),
1771 block_timestamp: 0,
1772 pricing: GasPrice::Eip1559 {
1773 base_fee_per_gas: BigUint::from(1_000_000u64),
1774 max_priority_fee_per_gas: BigUint::from(1_000_000u64),
1775 },
1776 });
1777 drop(market_write); let algorithm = MostLiquidAlgorithm::new();
1780 let order = order(&token_a, &token_b, 1, OrderSide::Sell); let derived = setup_derived_with_token_prices(std::slice::from_ref(&token_b.address));
1784
1785 let result = algorithm
1787 .find_best_route(manager.graph(), market, None, Some(derived), &order)
1788 .await
1789 .expect("should return route even with negative net_amount_out");
1790
1791 assert_eq!(result.route().swaps().len(), 1);
1793 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(2u64)); let expected_net = BigInt::from(2) - BigInt::from(100_000_000_000u64);
1797 assert_eq!(result.net_amount_out(), &expected_net);
1798 }
1799
1800 #[tokio::test]
1801 async fn test_find_best_route_insufficient_liquidity() {
1802 let token_a = token(0x01, "A");
1804 let token_b = token(0x02, "B");
1805
1806 let (market, manager) = setup_market_weighted(vec![(
1807 "pool1",
1808 &token_a,
1809 &token_b,
1810 MockProtocolSim::new(2.0).with_liquidity(1000),
1811 )]);
1812
1813 let algorithm = MostLiquidAlgorithm::new();
1814 let order = order(&token_a, &token_b, ONE_ETH, OrderSide::Sell); let result = algorithm
1817 .find_best_route(manager.graph(), market, None, None, &order)
1818 .await;
1819 assert!(matches!(result, Err(AlgorithmError::InsufficientLiquidity)));
1820 }
1821
1822 #[tokio::test]
1823 async fn test_find_best_route_missing_gas_price_returns_error() {
1824 let token_a = token(0x01, "A");
1826 let token_b = token(0x02, "B");
1827
1828 let mut market = MarketState::new();
1829 let pool_state = MockProtocolSim::new(2.0);
1830 let pool_comp = component("pool1", &[token_a.clone(), token_b.clone()]);
1831
1832 market.upsert_components(vec![pool_comp]);
1834 market.update_states(vec![(
1835 "pool1".to_string(),
1836 Box::new(pool_state.clone()) as Box<dyn ProtocolSim>,
1837 )]);
1838 market.upsert_tokens(vec![token_a.clone(), token_b.clone()]);
1839
1840 let mut manager = PetgraphStableDiGraphManager::default();
1842 manager.initialize_graph(&market.component_topology());
1843 let weight = DepthAndPrice::from_protocol_sim(&pool_state, &token_a, &token_b).unwrap();
1844 manager
1845 .set_edge_weight(
1846 &"pool1".to_string(),
1847 &token_a.address,
1848 &token_b.address,
1849 weight,
1850 false,
1851 )
1852 .unwrap();
1853
1854 let algorithm = MostLiquidAlgorithm::new();
1855 let order = order(&token_a, &token_b, ONE_ETH, OrderSide::Sell);
1856 let market = wrap_market(market);
1857
1858 let result = algorithm
1859 .find_best_route(manager.graph(), market, None, None, &order)
1860 .await;
1861
1862 assert!(matches!(result, Err(AlgorithmError::DataNotFound { kind: "gas price", .. })));
1864 }
1865
1866 #[tokio::test]
1867 async fn test_find_best_route_circular_arbitrage() {
1868 let token_a = token(0x01, "A");
1869 let token_b = token(0x02, "B");
1870
1871 let (market, manager) =
1874 setup_market_weighted(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1875
1876 let algorithm = MostLiquidAlgorithm::with_config(
1878 AlgorithmConfig::new(2, 2, Duration::from_millis(100), None).unwrap(),
1879 )
1880 .unwrap();
1881
1882 let order = order(&token_a, &token_a, 100, OrderSide::Sell);
1884
1885 let result = algorithm
1886 .find_best_route(manager.graph(), market, None, None, &order)
1887 .await
1888 .unwrap();
1889
1890 assert_eq!(result.route().swaps().len(), 2, "Should have 2 swaps for circular route");
1892
1893 assert_eq!(*result.route().swaps()[0].token_in(), token_a.address);
1895 assert_eq!(*result.route().swaps()[0].token_out(), token_b.address);
1896 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(200u64));
1897
1898 assert_eq!(*result.route().swaps()[1].token_in(), token_b.address);
1900 assert_eq!(*result.route().swaps()[1].token_out(), token_a.address);
1901 assert_eq!(*result.route().swaps()[1].amount_out(), BigUint::from(66u64));
1902
1903 assert_eq!(result.route().swaps()[0].token_in(), result.route().swaps()[1].token_out());
1905 }
1906
1907 #[tokio::test]
1908 async fn test_find_best_route_respects_min_hops() {
1909 let token_a = token(0x01, "A");
1912 let token_b = token(0x02, "B");
1913 let token_c = token(0x03, "C");
1914
1915 let (market, manager) = setup_market_weighted(vec![
1916 ("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)), ]);
1921
1922 let algorithm = MostLiquidAlgorithm::with_config(
1924 AlgorithmConfig::new(2, 3, Duration::from_millis(100), None).unwrap(),
1925 )
1926 .unwrap();
1927 let order = order(&token_a, &token_b, 100, OrderSide::Sell);
1928
1929 let derived = setup_derived_with_token_prices(std::slice::from_ref(&token_b.address));
1932
1933 let result = algorithm
1934 .find_best_route(manager.graph(), market, None, Some(derived), &order)
1935 .await
1936 .unwrap();
1937
1938 assert_eq!(result.route().swaps().len(), 2, "Should use 2-hop path due to min_hops=2");
1940 assert_eq!(result.route().swaps()[0].component_id(), "pool_ac");
1941 assert_eq!(result.route().swaps()[1].component_id(), "pool_cb");
1942 }
1943
1944 #[tokio::test]
1945 async fn test_find_best_route_respects_max_hops() {
1946 let token_a = token(0x01, "A");
1949 let token_b = token(0x02, "B");
1950 let token_c = token(0x03, "C");
1951
1952 let (market, manager) = setup_market_weighted(vec![
1953 ("pool_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
1954 ("pool_bc", &token_b, &token_c, MockProtocolSim::new(3.0)),
1955 ]);
1956
1957 let algorithm = MostLiquidAlgorithm::with_config(
1959 AlgorithmConfig::new(1, 1, Duration::from_millis(100), None).unwrap(),
1960 )
1961 .unwrap();
1962 let order = order(&token_a, &token_c, 100, OrderSide::Sell);
1963
1964 let result = algorithm
1965 .find_best_route(manager.graph(), market, None, None, &order)
1966 .await;
1967 assert!(
1968 matches!(result, Err(AlgorithmError::NoPath { .. })),
1969 "Should return NoPath when max_hops is insufficient"
1970 );
1971 }
1972
1973 #[tokio::test]
1974 async fn test_find_best_route_timeout_returns_best_so_far() {
1975 let token_a = token(0x01, "A");
1979 let token_b = token(0x02, "B");
1980
1981 let (market, manager) = setup_market_weighted(vec![
1983 ("pool1", &token_a, &token_b, MockProtocolSim::new(1.0)),
1984 ("pool2", &token_a, &token_b, MockProtocolSim::new(2.0)),
1985 ("pool3", &token_a, &token_b, MockProtocolSim::new(3.0)),
1986 ("pool4", &token_a, &token_b, MockProtocolSim::new(4.0)),
1987 ("pool5", &token_a, &token_b, MockProtocolSim::new(5.0)),
1988 ]);
1989
1990 let algorithm = MostLiquidAlgorithm::with_config(
1992 AlgorithmConfig::new(1, 1, Duration::from_millis(0), None).unwrap(),
1993 )
1994 .unwrap();
1995 let order = order(&token_a, &token_b, 100, OrderSide::Sell);
1996
1997 let result = algorithm
1998 .find_best_route(manager.graph(), market, None, None, &order)
1999 .await;
2000
2001 match result {
2006 Ok(r) => {
2007 assert_eq!(r.route().swaps().len(), 1);
2009 }
2010 Err(AlgorithmError::Timeout { .. }) => {
2011 }
2013 Err(e) => panic!("Unexpected error: {:?}", e),
2014 }
2015 }
2016
2017 #[rstest::rstest]
2020 #[case::default_config(1, 3, 50)]
2021 #[case::single_hop_only(1, 1, 100)]
2022 #[case::multi_hop_min(2, 5, 200)]
2023 #[case::zero_timeout(1, 3, 0)]
2024 #[case::large_values(10, 100, 10000)]
2025 fn test_algorithm_config_getters(
2026 #[case] min_hops: usize,
2027 #[case] max_hops: usize,
2028 #[case] timeout_ms: u64,
2029 ) {
2030 use crate::algorithm::Algorithm;
2031
2032 let algorithm = MostLiquidAlgorithm::with_config(
2033 AlgorithmConfig::new(min_hops, max_hops, Duration::from_millis(timeout_ms), None)
2034 .unwrap(),
2035 )
2036 .unwrap();
2037
2038 assert_eq!(algorithm.max_hops, max_hops);
2039 assert_eq!(algorithm.timeout, Duration::from_millis(timeout_ms));
2040 assert_eq!(algorithm.name(), "most_liquid");
2041 }
2042
2043 #[test]
2044 fn test_algorithm_default_config() {
2045 use crate::algorithm::Algorithm;
2046
2047 let algorithm = MostLiquidAlgorithm::new();
2048
2049 assert_eq!(algorithm.max_hops, 3);
2050 assert_eq!(algorithm.timeout, Duration::from_millis(500));
2051 assert_eq!(algorithm.name(), "most_liquid");
2052 }
2053
2054 #[tokio::test]
2057 async fn test_find_best_route_respects_max_routes_cap() {
2058 let token_a = token(0x01, "A");
2071 let token_b = token(0x02, "B");
2072
2073 let (market, manager) = setup_market_weighted(vec![
2074 ("pool1", &token_a, &token_b, MockProtocolSim::new(4.0).with_liquidity(1_000_000)),
2075 ("pool2", &token_a, &token_b, MockProtocolSim::new(3.0).with_liquidity(2_000_000)),
2076 ("pool3", &token_a, &token_b, MockProtocolSim::new(2.0).with_liquidity(3_000_000)),
2077 ("pool4", &token_a, &token_b, MockProtocolSim::new(1.0).with_liquidity(4_000_000)),
2078 ]);
2079
2080 let algorithm = MostLiquidAlgorithm::with_config(
2082 AlgorithmConfig::new(1, 1, Duration::from_millis(100), Some(2)).unwrap(),
2083 )
2084 .unwrap();
2085 let order = order(&token_a, &token_b, 1000, OrderSide::Sell);
2086 let result = algorithm
2087 .find_best_route(manager.graph(), market, None, None, &order)
2088 .await
2089 .unwrap();
2090
2091 assert_eq!(result.route().swaps().len(), 1);
2095 assert_eq!(result.route().swaps()[0].component_id(), "pool3");
2096 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(2000u64));
2097 }
2098
2099 #[tokio::test]
2100 async fn test_find_best_route_no_cap_when_max_routes_is_none() {
2101 let token_a = token(0x01, "A");
2103 let token_b = token(0x02, "B");
2104
2105 let (market, manager) = setup_market_weighted(vec![
2106 ("pool1", &token_a, &token_b, MockProtocolSim::new(4.0).with_liquidity(1_000_000)),
2107 ("pool2", &token_a, &token_b, MockProtocolSim::new(3.0).with_liquidity(2_000_000)),
2108 ("pool3", &token_a, &token_b, MockProtocolSim::new(2.0).with_liquidity(3_000_000)),
2109 ("pool4", &token_a, &token_b, MockProtocolSim::new(1.0).with_liquidity(4_000_000)),
2110 ]);
2111
2112 let algorithm = MostLiquidAlgorithm::with_config(
2113 AlgorithmConfig::new(1, 1, Duration::from_millis(100), None).unwrap(),
2114 )
2115 .unwrap();
2116 let order = order(&token_a, &token_b, 1000, OrderSide::Sell);
2117 let result = algorithm
2118 .find_best_route(manager.graph(), market, None, None, &order)
2119 .await
2120 .unwrap();
2121
2122 assert_eq!(result.route().swaps().len(), 1);
2124 assert_eq!(result.route().swaps()[0].component_id(), "pool1");
2125 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(4000u64));
2126 }
2127
2128 #[test]
2129 fn test_algorithm_config_rejects_zero_max_routes() {
2130 let result = AlgorithmConfig::new(1, 3, Duration::from_millis(100), Some(0));
2131 assert!(matches!(
2132 result,
2133 Err(AlgorithmError::InvalidConfiguration { reason }) if reason.contains("max_routes must be at least 1")
2134 ));
2135 }
2136
2137 #[test]
2138 fn test_algorithm_config_rejects_zero_min_hops() {
2139 let result = AlgorithmConfig::new(0, 3, Duration::from_millis(100), None);
2140 assert!(matches!(
2141 result,
2142 Err(AlgorithmError::InvalidConfiguration { reason }) if reason.contains("min_hops must be at least 1")
2143 ));
2144 }
2145
2146 #[test]
2147 fn test_algorithm_config_rejects_min_greater_than_max() {
2148 let result = AlgorithmConfig::new(5, 3, Duration::from_millis(100), None);
2149 assert!(matches!(
2150 result,
2151 Err(AlgorithmError::InvalidConfiguration { reason }) if reason.contains("cannot exceed")
2152 ));
2153 }
2154}