1use std::{
11 collections::{HashMap, HashSet, VecDeque},
12 time::{Duration, Instant},
13};
14
15use metrics::{counter, histogram};
16use num_bigint::{BigInt, BigUint};
17use num_traits::ToPrimitive;
18use petgraph::prelude::EdgeRef;
19use tracing::{debug, instrument, trace};
20use tycho_simulation::{
21 tycho_common::simulation::protocol_sim::ProtocolSim,
22 tycho_core::models::{token::Token, Address},
23};
24
25use super::{Algorithm, AlgorithmConfig, NoPathReason};
26use crate::{
27 derived::{computation::ComputationRequirements, types::TokenGasPrices, SharedDerivedDataRef},
28 feed::market_data::{MarketData, MarketState, StateLabel},
29 graph::{petgraph::StableDiGraph, Path, PetgraphStableDiGraphManager},
30 types::{ComponentId, Order, Route, RouteResult, Swap},
31 AlgorithmError,
32};
33pub struct MostLiquidAlgorithm {
35 min_hops: usize,
36 max_hops: usize,
37 timeout: Duration,
38 max_routes: Option<usize>,
39 connector_tokens: Option<HashSet<Address>>,
40}
41
42#[derive(Debug, Clone, Default)]
48pub struct DepthAndPrice {
49 pub spot_price: f64,
51 pub depth: f64,
53}
54
55impl DepthAndPrice {
56 #[cfg(test)]
58 pub fn new(spot_price: f64, depth: f64) -> Self {
59 Self { spot_price, depth }
60 }
61
62 #[cfg(test)]
64 pub fn from_protocol_sim(
65 sim: &impl ProtocolSim,
66 token_in: &Token,
67 token_out: &Token,
68 ) -> Result<Self, AlgorithmError> {
69 Ok(Self {
70 spot_price: sim
71 .spot_price(token_in, token_out)
72 .map_err(|e| {
73 AlgorithmError::Other(format!("missing spot price for DepthAndPrice: {:?}", e))
74 })?,
75 depth: sim
76 .get_limits(token_in.address.clone(), token_out.address.clone())
77 .map_err(|e| {
78 AlgorithmError::Other(format!("missing depth for DepthAndPrice: {:?}", e))
79 })?
80 .0
81 .to_f64()
82 .ok_or_else(|| {
83 AlgorithmError::Other("depth conversion to f64 failed".to_string())
84 })?,
85 })
86 }
87}
88
89impl crate::graph::EdgeWeightFromSimAndDerived for DepthAndPrice {
90 fn from_sim_and_derived(
91 _sim: &dyn ProtocolSim,
92 component_id: &ComponentId,
93 token_in: &Token,
94 token_out: &Token,
95 derived: &crate::derived::DerivedData,
96 ) -> Option<Self> {
97 let key = (component_id.clone(), token_in.address.clone(), token_out.address.clone());
98
99 let spot_price = match derived
101 .spot_prices()
102 .and_then(|p| p.get(&key).copied())
103 {
104 Some(p) => p,
105 None => {
106 trace!(component_id = %component_id, "spot price not found, skipping edge");
107 return None;
108 }
109 };
110
111 let raw_depth = match derived
113 .pool_depths()
114 .and_then(|d| d.get(&key))
115 {
116 Some(d) => d.to_f64().unwrap_or(0.0),
117 None => {
118 trace!(component_id = %component_id, "pool depth not found, skipping edge");
119 return None;
120 }
121 };
122
123 let depth = match derived
128 .token_prices()
129 .and_then(|p| p.get(&token_in.address))
130 {
131 Some(price) => {
132 let num = match price.numerator.to_f64() {
133 Some(v) if v > 0.0 => v,
134 Some(_) => {
135 trace!(
136 component_id = %component_id,
137 token_in = %token_in.address,
138 "token price numerator is zero, skipping edge"
139 );
140 return None;
141 }
142 None => {
143 trace!(
144 component_id = %component_id,
145 token_in = %token_in.address,
146 "token price numerator overflows f64, skipping edge"
147 );
148 return None;
149 }
150 };
151 let den = match price.denominator.to_f64() {
152 Some(v) if v > 0.0 => v,
153 Some(_) => {
154 trace!(
155 component_id = %component_id,
156 token_in = %token_in.address,
157 "token price denominator is zero, skipping edge"
158 );
159 return None;
160 }
161 None => {
162 trace!(
163 component_id = %component_id,
164 token_in = %token_in.address,
165 "token price denominator overflows f64, skipping edge"
166 );
167 return None;
168 }
169 };
170 raw_depth * den / num
171 }
172 None => {
173 trace!(
174 component_id = %component_id,
175 token_in = %token_in.address,
176 "token price not found, skipping edge"
177 );
178 return None;
179 }
180 };
181
182 Some(Self { spot_price, depth })
183 }
184}
185
186impl MostLiquidAlgorithm {
187 pub fn new() -> Self {
189 Self {
190 min_hops: 1,
191 max_hops: 3,
192 timeout: Duration::from_millis(500),
193 max_routes: None,
194 connector_tokens: None,
195 }
196 }
197
198 pub fn with_config(config: AlgorithmConfig) -> Result<Self, AlgorithmError> {
200 Ok(Self {
201 min_hops: config.min_hops(),
202 max_hops: config.max_hops(),
203 timeout: config.timeout(),
204 max_routes: config.max_routes(),
205 connector_tokens: config.connector_tokens().cloned(),
206 })
207 }
208
209 #[instrument(level = "debug", skip(graph, connector_tokens))]
220 fn find_paths<'a>(
221 graph: &'a StableDiGraph<DepthAndPrice>,
222 from: &Address,
223 to: &Address,
224 min_hops: usize,
225 max_hops: usize,
226 connector_tokens: Option<&HashSet<Address>>,
227 ) -> Result<Vec<Path<'a, DepthAndPrice>>, AlgorithmError> {
228 if min_hops == 0 || min_hops > max_hops {
229 return Err(AlgorithmError::InvalidConfiguration {
230 reason: format!(
231 "invalid hop configuration: min_hops={min_hops} max_hops={max_hops}",
232 ),
233 });
234 }
235
236 let from_idx = graph
239 .node_indices()
240 .find(|&n| &graph[n] == from)
241 .ok_or(AlgorithmError::NoPath {
242 from: from.clone(),
243 to: to.clone(),
244 reason: NoPathReason::SourceTokenNotInGraph,
245 })?;
246 let to_idx = graph
247 .node_indices()
248 .find(|&n| &graph[n] == to)
249 .ok_or(AlgorithmError::NoPath {
250 from: from.clone(),
251 to: to.clone(),
252 reason: NoPathReason::DestinationTokenNotInGraph,
253 })?;
254
255 let mut paths = Vec::new();
256 let mut queue = VecDeque::new();
257 queue.push_back((from_idx, Path::new()));
258
259 while let Some((current_node, current_path)) = queue.pop_front() {
260 if current_path.len() >= max_hops {
261 continue;
262 }
263
264 for edge in graph.edges(current_node) {
265 let next_node = edge.target();
266 let next_addr = &graph[next_node];
267
268 let already_visited = current_path.tokens.contains(&next_addr);
273 let is_closing_circular_route = from_idx == to_idx && next_node == to_idx;
274 if already_visited && !is_closing_circular_route {
275 continue;
276 }
277
278 let is_destination = next_node == to_idx;
280 if !is_destination {
281 if let Some(tokens) = connector_tokens {
282 if !tokens.contains(next_addr) {
283 continue;
284 }
285 }
286 }
287
288 let mut new_path = current_path.clone();
289 new_path.add_hop(&graph[current_node], edge.weight(), next_addr);
290
291 if next_node == to_idx && new_path.len() >= min_hops {
292 paths.push(new_path.clone());
293 }
294
295 queue.push_back((next_node, new_path));
296 }
297 }
298
299 Ok(paths)
300 }
301
302 fn try_score_path(path: &Path<DepthAndPrice>) -> Option<f64> {
316 if path.is_empty() {
317 trace!("cannot score empty path");
318 return None;
319 }
320
321 let mut price = 1.0;
322 let mut min_depth = f64::MAX;
323
324 for edge in path.edge_iter() {
325 let Some(data) = edge.data.as_ref() else {
326 debug!(component_id = %edge.component_id, "edge missing weight data, path cannot be scored");
327 return None;
328 };
329
330 price *= data.spot_price;
331 min_depth = min_depth.min(data.depth);
332 }
333
334 Some(price * min_depth)
335 }
336
337 #[instrument(level = "trace", skip(path, market, token_prices), fields(hop_count = path.len()))]
350 pub(crate) fn simulate_path<D>(
351 path: &Path<D>,
352 market: &MarketState,
353 token_prices: Option<&TokenGasPrices>,
354 amount_in: BigUint,
355 ) -> Result<RouteResult, AlgorithmError> {
356 let mut current_amount = amount_in.clone();
357 let mut swaps = Vec::with_capacity(path.len());
358
359 let mut state_overrides: HashMap<&ComponentId, Box<dyn ProtocolSim>> = HashMap::new();
361
362 for (address_in, edge_data, address_out) in path.iter() {
363 let token_in = market
365 .get_token(address_in)
366 .ok_or_else(|| AlgorithmError::DataNotFound {
367 kind: "token",
368 id: Some(format!("{:?}", address_in)),
369 })?;
370 let token_out = market
371 .get_token(address_out)
372 .ok_or_else(|| AlgorithmError::DataNotFound {
373 kind: "token",
374 id: Some(format!("{:?}", address_out)),
375 })?;
376
377 let component_id = &edge_data.component_id;
378 let component = market
379 .get_component(component_id)
380 .ok_or_else(|| AlgorithmError::DataNotFound {
381 kind: "component",
382 id: Some(component_id.clone()),
383 })?;
384 let component_state = market
385 .get_simulation_state(component_id)
386 .ok_or_else(|| AlgorithmError::DataNotFound {
387 kind: "simulation state",
388 id: Some(component_id.clone()),
389 })?;
390
391 let state = state_overrides
392 .get(component_id)
393 .map(Box::as_ref)
394 .unwrap_or(component_state);
395
396 let result = state
398 .get_amount_out(current_amount.clone(), token_in, token_out)
399 .map_err(|e| AlgorithmError::Other(format!("simulation error: {:?}", e)))?;
400
401 swaps.push(Swap::new(
403 component_id.clone(),
404 component.protocol_system.clone(),
405 token_in.address.clone(),
406 token_out.address.clone(),
407 current_amount.clone(),
408 result.amount.clone(),
409 result.gas,
410 component.clone(),
411 state.clone_box(),
412 ));
413
414 state_overrides.insert(component_id, result.new_state);
415 current_amount = result.amount;
416 }
417
418 let route = Route::new(swaps);
420 let output_amount = route
421 .swaps()
422 .last()
423 .map(|s| s.amount_out().clone())
424 .unwrap_or_else(|| BigUint::ZERO);
425
426 let gas_price = market
427 .gas_price()
428 .ok_or(AlgorithmError::DataNotFound { kind: "gas price", id: None })?
429 .effective_gas_price()
430 .clone();
431
432 let net_amount_out = if let Some(last_swap) = route.swaps().last() {
433 let total_gas = route.total_gas();
434 let gas_cost_wei = &total_gas * &gas_price;
435
436 let gas_cost_in_output_token: Option<BigUint> = token_prices
438 .and_then(|prices| prices.get(last_swap.token_out()))
439 .map(|price| {
440 &gas_cost_wei * &price.numerator / &price.denominator
443 });
444
445 match gas_cost_in_output_token {
446 Some(gas_cost) => BigInt::from(output_amount) - BigInt::from(gas_cost),
447 None => {
448 BigInt::from(output_amount)
451 }
452 }
453 } else {
454 BigInt::from(output_amount)
455 };
456
457 Ok(RouteResult::new(route, net_amount_out, gas_price))
458 }
459}
460
461impl Default for MostLiquidAlgorithm {
462 fn default() -> Self {
463 Self::new()
464 }
465}
466
467impl Algorithm for MostLiquidAlgorithm {
468 type GraphType = StableDiGraph<DepthAndPrice>;
469 type GraphManager = PetgraphStableDiGraphManager<DepthAndPrice>;
470
471 fn name(&self) -> &str {
472 "most_liquid"
473 }
474
475 #[instrument(level = "debug", skip_all, fields(order_id = %order.id()))]
477 async fn find_best_route(
478 &self,
479 graph: &Self::GraphType,
480 market: MarketData,
481 label: Option<StateLabel>,
482 derived: Option<SharedDerivedDataRef>,
483 order: &Order,
484 ) -> Result<RouteResult, AlgorithmError> {
485 let start = Instant::now();
486
487 if !order.is_sell() {
489 return Err(AlgorithmError::ExactOutNotSupported);
490 }
491
492 let token_prices = if let Some(ref derived) = derived {
494 derived
495 .read()
496 .await
497 .token_prices()
498 .cloned()
499 } else {
500 None
501 };
502
503 let amount_in = order.amount().clone();
504
505 let all_paths = Self::find_paths(
507 graph,
508 order.token_in(),
509 order.token_out(),
510 self.min_hops,
511 self.max_hops,
512 self.connector_tokens.as_ref(),
513 )?;
514
515 let paths_candidates = all_paths.len();
516 if paths_candidates == 0 {
517 return Err(AlgorithmError::NoPath {
518 from: order.token_in().clone(),
519 to: order.token_out().clone(),
520 reason: NoPathReason::NoGraphPath,
521 });
522 }
523
524 let mut scored_paths: Vec<(Path<DepthAndPrice>, f64)> = all_paths
527 .into_iter()
528 .filter_map(|path| {
529 let score = Self::try_score_path(&path)?;
530 Some((path, score))
531 })
532 .collect();
533
534 scored_paths.sort_by(|(_, a_score), (_, b_score)| {
535 b_score
537 .partial_cmp(a_score)
538 .unwrap_or(std::cmp::Ordering::Equal)
539 });
540
541 if let Some(max_routes) = self.max_routes {
542 scored_paths.truncate(max_routes);
543 }
544
545 let paths_to_simulate = scored_paths.len();
546 let scoring_failures = paths_candidates - paths_to_simulate;
547 if paths_to_simulate == 0 {
548 return Err(AlgorithmError::NoPath {
549 from: order.token_in().clone(),
550 to: order.token_out().clone(),
551 reason: NoPathReason::NoScorablePaths,
552 });
553 }
554
555 let component_ids: HashSet<ComponentId> = scored_paths
557 .iter()
558 .flat_map(|(path, _)| {
559 path.edge_iter()
560 .iter()
561 .map(|e| e.component_id.clone())
562 })
563 .collect();
564
565 let market = {
567 let market = match label.as_ref() {
568 Some(l) => market
569 .read_labeled(l)
570 .await
571 .map_err(|e| AlgorithmError::Other(e.to_string()))?,
572 None => market.read().await,
573 };
574 if market.gas_price().is_none() {
575 return Err(AlgorithmError::DataNotFound { kind: "gas price", id: None });
576 }
577 let market_subset = market.extract_subset_with_overlay(&component_ids);
578 drop(market);
579 market_subset
580 };
581
582 let mut paths_simulated = 0usize;
583 let mut simulation_failures = 0usize;
584
585 let mut best: Option<RouteResult> = None;
587 let timeout_ms = self.timeout.as_millis() as u64;
588
589 for (edge_path, _) in scored_paths {
590 let elapsed_ms = start.elapsed().as_millis() as u64;
592 if elapsed_ms > timeout_ms {
593 break;
594 }
595
596 let result = match Self::simulate_path(
597 &edge_path,
598 &market,
599 token_prices.as_ref(),
600 amount_in.clone(),
601 ) {
602 Ok(r) => r,
603 Err(e) => {
604 trace!(error = %e, "simulation failed for path");
605 simulation_failures += 1;
606 continue;
607 }
608 };
609
610 if best
612 .as_ref()
613 .map(|best| result.net_amount_out() > best.net_amount_out())
614 .unwrap_or(true)
615 {
616 best = Some(result);
617 }
618
619 paths_simulated += 1;
620 }
621
622 let solve_time_ms = start.elapsed().as_millis() as u64;
624 let block_number = market
625 .last_updated()
626 .map(|b| b.number());
627 let coverage_pct = if paths_to_simulate == 0 {
629 100.0
630 } else {
631 (paths_simulated as f64 / paths_to_simulate as f64) * 100.0
632 };
633
634 counter!("algorithm.scoring_failures").increment(scoring_failures as u64);
636 counter!("algorithm.simulation_failures").increment(simulation_failures as u64);
637 histogram!("algorithm.simulation_coverage_pct").record(coverage_pct);
638
639 match &best {
640 Some(result) => {
641 let tokens = market.token_registry_ref();
642 let path_desc = result.route().path_description(tokens);
643 let protocols = result
644 .route()
645 .swaps()
646 .iter()
647 .map(|s| s.protocol())
648 .collect::<Vec<_>>();
649
650 let price = amount_in
651 .to_f64()
652 .filter(|&v| v > 0.0)
653 .and_then(|amt_in| {
654 result
655 .net_amount_out()
656 .to_f64()
657 .map(|amt_out| amt_out / amt_in)
658 })
659 .unwrap_or(f64::NAN);
660
661 debug!(
662 solve_time_ms,
663 block_number,
664 paths_candidates,
665 paths_to_simulate,
666 paths_simulated,
667 simulation_failures,
668 simulation_coverage_pct = coverage_pct,
669 components_considered = component_ids.len(),
670 tokens_considered = market.token_registry_ref().len(),
671 path = %path_desc,
672 amount_in = %amount_in,
673 net_amount_out = %result.net_amount_out(),
674 price_out_per_in = price,
675 hop_count = result.route().swaps().len(),
676 protocols = ?protocols,
677 "route found"
678 );
679 }
680 None => {
681 debug!(
682 solve_time_ms,
683 block_number,
684 paths_candidates,
685 paths_to_simulate,
686 paths_simulated,
687 simulation_failures,
688 simulation_coverage_pct = coverage_pct,
689 components_considered = component_ids.len(),
690 tokens_considered = market.token_registry_ref().len(),
691 "no viable route"
692 );
693 }
694 }
695
696 best.ok_or({
697 if solve_time_ms > timeout_ms {
698 AlgorithmError::Timeout { elapsed_ms: solve_time_ms }
699 } else {
700 AlgorithmError::InsufficientLiquidity
701 }
702 })
703 }
704
705 fn computation_requirements(&self) -> ComputationRequirements {
706 ComputationRequirements::none()
714 .allow_stale("token_prices")
715 .expect("Conflicting Computation Requirements")
716 }
717
718 fn timeout(&self) -> Duration {
719 self.timeout
720 }
721}
722
723#[cfg(test)]
724mod tests {
725 use std::collections::HashSet;
726
727 use rstest::rstest;
728 use tycho_simulation::{
729 tycho_core::simulation::protocol_sim::Price,
730 tycho_ethereum::gas::{BlockGasPrice, GasPrice},
731 };
732
733 use super::*;
734 use crate::{
735 algorithm::test_utils::{
736 addr, component,
737 fixtures::{addrs, diamond_graph, linear_graph, parallel_graph},
738 market_read, order, setup_market, token, MockProtocolSim, ONE_ETH,
739 },
740 derived::{
741 computation::{FailedItem, FailedItemError},
742 types::TokenGasPrices,
743 DerivedData,
744 },
745 graph::GraphManager,
746 types::OrderSide,
747 };
748
749 fn wrap_market(market: MarketState) -> MarketData {
750 MarketData::new(std::sync::Arc::new(tokio::sync::RwLock::new(market)))
751 }
752
753 fn setup_derived_with_token_prices(token_addresses: &[Address]) -> SharedDerivedDataRef {
758 let mut token_prices: TokenGasPrices = HashMap::new();
759 for addr in token_addresses {
760 token_prices.insert(
762 addr.clone(),
763 Price { numerator: BigUint::from(1u64), denominator: BigUint::from(1u64) },
764 );
765 }
766
767 let mut derived_data = DerivedData::new();
768 derived_data.set_token_prices(token_prices, vec![], 1, true);
769 std::sync::Arc::new(tokio::sync::RwLock::new(derived_data))
770 }
771 #[test]
774 fn test_try_score_path_calculates_correctly() {
775 let (a, b, c, _) = addrs();
776 let mut m = linear_graph();
777
778 m.set_edge_weight(&"ab".to_string(), &a, &b, DepthAndPrice::new(2.0, 1000.0), false)
780 .unwrap();
781 m.set_edge_weight(&"bc".to_string(), &b, &c, DepthAndPrice::new(0.5, 500.0), false)
782 .unwrap();
783
784 let graph = m.graph();
786 let paths = MostLiquidAlgorithm::find_paths(graph, &a, &c, 2, 2, None).unwrap();
787 assert_eq!(paths.len(), 1);
788 let path = &paths[0];
789
790 let expected = 2.0 * 0.5 * 500.0;
792 let score = MostLiquidAlgorithm::try_score_path(path).unwrap();
793 assert_eq!(score, expected, "expected {expected}, got {score}");
794 }
795
796 #[test]
797 fn test_try_score_path_empty_returns_none() {
798 let path: Path<DepthAndPrice> = Path::new();
799 assert_eq!(MostLiquidAlgorithm::try_score_path(&path), None);
800 }
801
802 #[test]
803 fn test_try_score_path_missing_weight_returns_none() {
804 let (a, b, _, _) = addrs();
805 let m = linear_graph();
806 let graph = m.graph();
807 let paths = MostLiquidAlgorithm::find_paths(graph, &a, &b, 1, 1, None).unwrap();
808 assert_eq!(paths.len(), 1);
809 assert!(MostLiquidAlgorithm::try_score_path(&paths[0]).is_none());
810 }
811
812 #[test]
813 fn test_try_score_path_circular_route() {
814 let (a, b, _, _) = addrs();
816 let mut m = linear_graph();
817
818 m.set_edge_weight(&"ab".to_string(), &a, &b, DepthAndPrice::new(2.0, 1000.0), false)
822 .unwrap();
823 m.set_edge_weight(&"ab".to_string(), &b, &a, DepthAndPrice::new(0.6, 800.0), false)
824 .unwrap();
825
826 let graph = m.graph();
827 let paths = MostLiquidAlgorithm::find_paths(graph, &a, &a, 2, 2, None).unwrap();
829
830 assert_eq!(paths.len(), 1);
832
833 let score = MostLiquidAlgorithm::try_score_path(&paths[0]).unwrap();
838 let expected = 2.0 * 0.6 * 800.0;
839 assert_eq!(score, expected, "expected {expected}, got {score}");
840 }
841
842 fn make_mock_sim() -> MockProtocolSim {
843 MockProtocolSim::new(2.0)
844 }
845
846 fn pair_key(comp: &str, b_in: u8, b_out: u8) -> (String, Address, Address) {
847 (comp.to_string(), addr(b_in), addr(b_out))
848 }
849
850 fn pair_key_str(comp: &str, b_in: u8, b_out: u8) -> String {
851 format!("{comp}/{}/{}", addr(b_in), addr(b_out))
852 }
853
854 fn make_token_prices(addresses: &[Address]) -> TokenGasPrices {
855 let mut prices = TokenGasPrices::new();
856 for addr in addresses {
857 prices.insert(
859 addr.clone(),
860 Price { numerator: BigUint::from(1u64), denominator: BigUint::from(1u64) },
861 );
862 }
863 prices
864 }
865
866 #[test]
867 fn test_from_sim_and_derived_failed_spot_price_returns_none() {
868 let key = pair_key("pool1", 0x01, 0x02);
869 let key_str = pair_key_str("pool1", 0x01, 0x02);
870 let tok_in = token(0x01, "A");
871 let tok_out = token(0x02, "B");
872
873 let mut derived = DerivedData::new();
874 derived.set_spot_prices(
876 Default::default(),
877 vec![FailedItem {
878 key: key_str,
879 error: FailedItemError::SimulationFailed("sim error".into()),
880 }],
881 10,
882 true,
883 );
884 derived.set_pool_depths(Default::default(), vec![], 10, true);
885 derived.set_token_prices(
886 make_token_prices(&[tok_in.address.clone(), tok_out.address.clone()]),
887 vec![],
888 10,
889 true,
890 );
891
892 let sim = make_mock_sim();
893 let result =
894 <DepthAndPrice as crate::graph::EdgeWeightFromSimAndDerived>::from_sim_and_derived(
895 &sim, &key.0, &tok_in, &tok_out, &derived,
896 );
897
898 assert!(result.is_none());
899 }
900
901 #[test]
902 fn test_from_sim_and_derived_failed_pool_depth_returns_none() {
903 let key = pair_key("pool1", 0x01, 0x02);
904 let key_str = pair_key_str("pool1", 0x01, 0x02);
905 let tok_in = token(0x01, "A");
906 let tok_out = token(0x02, "B");
907
908 let mut derived = DerivedData::new();
909 let mut prices = crate::derived::types::SpotPrices::default();
911 prices.insert(key.clone(), 1.5);
912 derived.set_spot_prices(prices, vec![], 10, true);
913 derived.set_pool_depths(
915 Default::default(),
916 vec![FailedItem {
917 key: key_str,
918 error: FailedItemError::SimulationFailed("depth error".into()),
919 }],
920 10,
921 true,
922 );
923 derived.set_token_prices(
924 make_token_prices(&[tok_in.address.clone(), tok_out.address.clone()]),
925 vec![],
926 10,
927 true,
928 );
929
930 let sim = make_mock_sim();
931 let result =
932 <DepthAndPrice as crate::graph::EdgeWeightFromSimAndDerived>::from_sim_and_derived(
933 &sim, &key.0, &tok_in, &tok_out, &derived,
934 );
935
936 assert!(result.is_none());
937 }
938
939 #[test]
940 fn test_from_sim_and_derived_both_failed_returns_none() {
941 let key = pair_key("pool1", 0x01, 0x02);
942 let key_str = pair_key_str("pool1", 0x01, 0x02);
943 let tok_in = token(0x01, "A");
944 let tok_out = token(0x02, "B");
945
946 let mut derived = DerivedData::new();
947 derived.set_spot_prices(
948 Default::default(),
949 vec![FailedItem {
950 key: key_str.clone(),
951 error: FailedItemError::SimulationFailed("spot error".into()),
952 }],
953 10,
954 true,
955 );
956 derived.set_pool_depths(
957 Default::default(),
958 vec![FailedItem {
959 key: key_str,
960 error: FailedItemError::SimulationFailed("depth error".into()),
961 }],
962 10,
963 true,
964 );
965 derived.set_token_prices(
966 make_token_prices(&[tok_in.address.clone(), tok_out.address.clone()]),
967 vec![],
968 10,
969 true,
970 );
971
972 let sim = make_mock_sim();
973 let result =
974 <DepthAndPrice as crate::graph::EdgeWeightFromSimAndDerived>::from_sim_and_derived(
975 &sim, &key.0, &tok_in, &tok_out, &derived,
976 );
977
978 assert!(result.is_none());
979 }
980
981 #[test]
982 fn test_from_sim_and_derived_missing_token_price_returns_none() {
983 let key = pair_key("pool1", 0x01, 0x02);
984 let tok_in = token(0x01, "A");
985 let tok_out = token(0x02, "B");
986
987 let mut derived = DerivedData::new();
988 let mut prices = crate::derived::types::SpotPrices::default();
990 prices.insert(key.clone(), 1.5);
991 derived.set_spot_prices(prices, vec![], 10, true);
992
993 let mut depths = crate::derived::types::PoolDepths::default();
994 depths.insert(key.clone(), BigUint::from(1000u64));
995 derived.set_pool_depths(depths, vec![], 10, true);
996
997 let sim = make_mock_sim();
1000 let result =
1001 <DepthAndPrice as crate::graph::EdgeWeightFromSimAndDerived>::from_sim_and_derived(
1002 &sim, &key.0, &tok_in, &tok_out, &derived,
1003 );
1004
1005 assert!(
1006 result.is_none(),
1007 "should return None when token price is missing for depth normalization"
1008 );
1009 }
1010
1011 #[test]
1012 fn test_from_sim_and_derived_normalizes_depth_to_eth() {
1013 let key = pair_key("pool1", 0x01, 0x02);
1014 let tok_in = token(0x01, "A");
1015 let tok_out = token(0x02, "B");
1016
1017 let mut derived = DerivedData::new();
1018
1019 let mut spot = crate::derived::types::SpotPrices::default();
1021 spot.insert(key.clone(), 2.0);
1022 derived.set_spot_prices(spot, vec![], 10, true);
1023
1024 let mut depths = crate::derived::types::PoolDepths::default();
1026 depths.insert(key.clone(), BigUint::from(2_000_000u64));
1027 derived.set_pool_depths(depths, vec![], 10, true);
1028
1029 let mut token_prices = TokenGasPrices::new();
1032 token_prices.insert(
1033 tok_in.address.clone(),
1034 Price { numerator: BigUint::from(2000u64), denominator: BigUint::from(1u64) },
1035 );
1036 derived.set_token_prices(token_prices, vec![], 10, true);
1037
1038 let sim = make_mock_sim();
1039 let result =
1040 <DepthAndPrice as crate::graph::EdgeWeightFromSimAndDerived>::from_sim_and_derived(
1041 &sim, &key.0, &tok_in, &tok_out, &derived,
1042 );
1043
1044 let data = result.expect("should return Some when all data present");
1045 assert!((data.spot_price - 2.0).abs() < f64::EPSILON, "spot price should be 2.0");
1046 assert!(
1048 (data.depth - 1000.0).abs() < f64::EPSILON,
1049 "depth should be 1000.0 ETH, got {}",
1050 data.depth
1051 );
1052 }
1053
1054 #[test]
1055 fn test_from_sim_and_derived_normalizes_depth_fractional_price() {
1056 let key = pair_key("pool1", 0x01, 0x02);
1057 let tok_in = token(0x01, "A");
1058 let tok_out = token(0x02, "B");
1059
1060 let mut derived = DerivedData::new();
1061
1062 let mut spot = crate::derived::types::SpotPrices::default();
1063 spot.insert(key.clone(), 0.5);
1064 derived.set_spot_prices(spot, vec![], 10, true);
1065
1066 let mut depths = crate::derived::types::PoolDepths::default();
1068 depths.insert(key.clone(), BigUint::from(500u64));
1069 derived.set_pool_depths(depths, vec![], 10, true);
1070
1071 let mut token_prices = TokenGasPrices::new();
1074 token_prices.insert(
1075 tok_in.address.clone(),
1076 Price { numerator: BigUint::from(3u64), denominator: BigUint::from(2u64) },
1077 );
1078 derived.set_token_prices(token_prices, vec![], 10, true);
1079
1080 let sim = make_mock_sim();
1081 let result =
1082 <DepthAndPrice as crate::graph::EdgeWeightFromSimAndDerived>::from_sim_and_derived(
1083 &sim, &key.0, &tok_in, &tok_out, &derived,
1084 );
1085
1086 let data = result.expect("should return Some when all data present");
1087 let expected_depth = 500.0 * 2.0 / 3.0;
1088 assert!(
1089 (data.depth - expected_depth).abs() < 1e-10,
1090 "depth should be {expected_depth}, got {}",
1091 data.depth
1092 );
1093 }
1094
1095 fn all_ids(paths: Vec<Path<'_, DepthAndPrice>>) -> HashSet<Vec<&str>> {
1098 paths
1099 .iter()
1100 .map(|p| {
1101 p.iter()
1102 .map(|(_, e, _)| e.component_id.as_str())
1103 .collect()
1104 })
1105 .collect()
1106 }
1107
1108 #[test]
1109 fn test_find_paths_linear_forward_and_reverse() {
1110 let (a, b, c, d) = addrs();
1111 let m = linear_graph();
1112 let g = m.graph();
1113
1114 let p = MostLiquidAlgorithm::find_paths(g, &a, &b, 1, 1, None).unwrap();
1116 assert_eq!(all_ids(p), HashSet::from([vec!["ab"]]));
1117
1118 let p = MostLiquidAlgorithm::find_paths(g, &a, &c, 1, 2, None).unwrap();
1119 assert_eq!(all_ids(p), HashSet::from([vec!["ab", "bc"]]));
1120
1121 let p = MostLiquidAlgorithm::find_paths(g, &a, &d, 1, 3, None).unwrap();
1122 assert_eq!(all_ids(p), HashSet::from([vec!["ab", "bc", "cd"]]));
1123
1124 let p = MostLiquidAlgorithm::find_paths(g, &d, &a, 1, 3, None).unwrap();
1126 assert_eq!(all_ids(p), HashSet::from([vec!["cd", "bc", "ab"]]));
1127 }
1128
1129 #[test]
1130 fn test_find_paths_respects_hop_bounds() {
1131 let (a, _, c, d) = addrs();
1132 let m = linear_graph();
1133 let g = m.graph();
1134
1135 assert!(MostLiquidAlgorithm::find_paths(g, &a, &d, 1, 2, None)
1137 .unwrap()
1138 .is_empty());
1139
1140 assert!(MostLiquidAlgorithm::find_paths(g, &a, &c, 3, 3, None)
1142 .unwrap()
1143 .is_empty());
1144 }
1145
1146 #[test]
1147 fn test_find_paths_parallel_pools() {
1148 let (a, b, c, _) = addrs();
1149 let m = parallel_graph();
1150 let g = m.graph();
1151
1152 let p = MostLiquidAlgorithm::find_paths(g, &a, &b, 1, 1, None).unwrap();
1154 assert_eq!(all_ids(p), HashSet::from([vec!["ab1"], vec!["ab2"], vec!["ab3"]]));
1155
1156 let p = MostLiquidAlgorithm::find_paths(g, &a, &c, 1, 2, None).unwrap();
1158 assert_eq!(
1159 all_ids(p),
1160 HashSet::from([
1161 vec!["ab1", "bc1"],
1162 vec!["ab1", "bc2"],
1163 vec!["ab2", "bc1"],
1164 vec!["ab2", "bc2"],
1165 vec!["ab3", "bc1"],
1166 vec!["ab3", "bc2"],
1167 ])
1168 );
1169 }
1170
1171 #[test]
1172 fn test_find_paths_diamond_multiple_routes() {
1173 let (a, _, _, d) = addrs();
1174 let m = diamond_graph();
1175 let g = m.graph();
1176
1177 let p = MostLiquidAlgorithm::find_paths(g, &a, &d, 1, 2, None).unwrap();
1179 assert_eq!(all_ids(p), HashSet::from([vec!["ab", "bd"], vec!["ac", "cd"]]));
1180 }
1181
1182 #[test]
1183 fn test_find_paths_no_intermediate_cycles() {
1184 let (a, b, _, _) = addrs();
1185 let m = linear_graph();
1186 let g = m.graph();
1187
1188 let p = MostLiquidAlgorithm::find_paths(g, &a, &b, 1, 3, None).unwrap();
1193 assert_eq!(all_ids(p), HashSet::from([vec!["ab"]]));
1194 }
1195
1196 #[test]
1197 fn test_find_paths_cyclic_same_source_dest() {
1198 let (a, _, _, _) = addrs();
1199 let m = parallel_graph();
1201 let g = m.graph();
1202
1203 let p = MostLiquidAlgorithm::find_paths(g, &a, &a, 2, 2, None).unwrap();
1206 assert_eq!(
1207 all_ids(p),
1208 HashSet::from([
1209 vec!["ab1", "ab1"],
1210 vec!["ab1", "ab2"],
1211 vec!["ab1", "ab3"],
1212 vec!["ab2", "ab1"],
1213 vec!["ab2", "ab2"],
1214 vec!["ab2", "ab3"],
1215 vec!["ab3", "ab1"],
1216 vec!["ab3", "ab2"],
1217 vec!["ab3", "ab3"],
1218 ])
1219 );
1220 }
1221
1222 #[rstest]
1223 #[case::source_not_in_graph(false, true)]
1224 #[case::dest_not_in_graph(true, false)]
1225 fn test_find_paths_token_not_in_graph(#[case] from_exists: bool, #[case] to_exists: bool) {
1226 let (a, b, _, _) = addrs();
1228 let non_existent = addr(0x99);
1229 let m = linear_graph();
1230 let g = m.graph();
1231
1232 let from = if from_exists { a } else { non_existent.clone() };
1233 let to = if to_exists { b } else { non_existent };
1234
1235 let result = MostLiquidAlgorithm::find_paths(g, &from, &to, 1, 3, None);
1236
1237 assert!(matches!(result, Err(AlgorithmError::NoPath { .. })));
1238 }
1239
1240 #[rstest]
1241 #[case::min_greater_than_max(3, 1)]
1242 #[case::min_hops_zero(0, 1)]
1243 fn test_find_paths_invalid_configuration(#[case] min_hops: usize, #[case] max_hops: usize) {
1244 let (a, b, _, _) = addrs();
1245 let m = linear_graph();
1246 let g = m.graph();
1247
1248 assert!(matches!(
1249 MostLiquidAlgorithm::find_paths(g, &a, &b, min_hops, max_hops, None)
1250 .err()
1251 .unwrap(),
1252 AlgorithmError::InvalidConfiguration { reason: _ }
1253 ));
1254 }
1255
1256 #[test]
1257 fn test_find_paths_bfs_ordering() {
1258 let (a, b, c, d) = addrs();
1263 let e = addr(0x0E);
1264 let mut m = PetgraphStableDiGraphManager::<DepthAndPrice>::new();
1265 let mut t = HashMap::new();
1266 t.insert("ae".into(), vec![a.clone(), e.clone()]);
1267 t.insert("ab".into(), vec![a.clone(), b.clone()]);
1268 t.insert("be".into(), vec![b, e.clone()]);
1269 t.insert("ac".into(), vec![a.clone(), c.clone()]);
1270 t.insert("cd".into(), vec![c, d.clone()]);
1271 t.insert("de".into(), vec![d, e.clone()]);
1272 m.initialize_graph(&t);
1273 let g = m.graph();
1274
1275 let p = MostLiquidAlgorithm::find_paths(g, &a, &e, 1, 3, None).unwrap();
1276
1277 assert_eq!(p.len(), 3, "Expected 3 paths total");
1279 assert_eq!(p[0].len(), 1, "First path should be 1-hop");
1280 assert_eq!(p[1].len(), 2, "Second path should be 2-hop");
1281 assert_eq!(p[2].len(), 3, "Third path should be 3-hop");
1282 }
1283
1284 #[test]
1292 fn test_simulate_path_single_hop() {
1293 let token_a = token(0x01, "A");
1294 let token_b = token(0x02, "B");
1295
1296 let (market, manager) =
1297 setup_market(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1298
1299 let paths = MostLiquidAlgorithm::find_paths(
1300 manager.graph(),
1301 &token_a.address,
1302 &token_b.address,
1303 1,
1304 1,
1305 None,
1306 )
1307 .unwrap();
1308 let path = paths.into_iter().next().unwrap();
1309
1310 let result = MostLiquidAlgorithm::simulate_path(
1311 &path,
1312 market_read(&market).base_market_state(),
1313 None,
1314 BigUint::from(100u64),
1315 )
1316 .unwrap();
1317
1318 assert_eq!(result.route().swaps().len(), 1);
1319 assert_eq!(*result.route().swaps()[0].amount_in(), BigUint::from(100u64));
1320 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(200u64)); assert_eq!(result.route().swaps()[0].component_id(), "pool1");
1322 }
1323
1324 #[test]
1325 fn test_simulate_path_multi_hop_chains_amounts() {
1326 let token_a = token(0x01, "A");
1327 let token_b = token(0x02, "B");
1328 let token_c = token(0x03, "C");
1329
1330 let (market, manager) = setup_market(vec![
1331 ("pool1", &token_a, &token_b, MockProtocolSim::new(2.0)),
1332 ("pool2", &token_b, &token_c, MockProtocolSim::new(3.0)),
1333 ]);
1334
1335 let paths = MostLiquidAlgorithm::find_paths(
1336 manager.graph(),
1337 &token_a.address,
1338 &token_c.address,
1339 2,
1340 2,
1341 None,
1342 )
1343 .unwrap();
1344 let path = paths.into_iter().next().unwrap();
1345
1346 let result = MostLiquidAlgorithm::simulate_path(
1347 &path,
1348 market_read(&market).base_market_state(),
1349 None,
1350 BigUint::from(10u64),
1351 )
1352 .unwrap();
1353
1354 assert_eq!(result.route().swaps().len(), 2);
1355 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(20u64));
1357 assert_eq!(*result.route().swaps()[1].amount_in(), BigUint::from(20u64));
1359 assert_eq!(*result.route().swaps()[1].amount_out(), BigUint::from(60u64));
1360 }
1361
1362 #[test]
1363 fn test_simulate_path_same_pool_twice_uses_updated_state() {
1364 let token_a = token(0x01, "A");
1367 let token_b = token(0x02, "B");
1368
1369 let (market, manager) =
1370 setup_market(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1371
1372 let paths = MostLiquidAlgorithm::find_paths(
1375 manager.graph(),
1376 &token_a.address,
1377 &token_a.address,
1378 2,
1379 2,
1380 None,
1381 )
1382 .unwrap();
1383
1384 assert_eq!(paths.len(), 1);
1386 let path = paths[0].clone();
1387
1388 let result = MostLiquidAlgorithm::simulate_path(
1389 &path,
1390 market_read(&market).base_market_state(),
1391 None,
1392 BigUint::from(10u64),
1393 )
1394 .unwrap();
1395
1396 assert_eq!(result.route().swaps().len(), 2);
1397 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(20u64));
1399 assert_eq!(*result.route().swaps()[1].amount_out(), BigUint::from(6u64));
1401 }
1402
1403 #[test]
1404 fn test_simulate_path_missing_token_returns_data_not_found() {
1405 let token_a = token(0x01, "A");
1406 let token_b = token(0x02, "B");
1407 let token_c = token(0x03, "C");
1408
1409 let (market, _) =
1410 setup_market(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1411 let market = market_read(&market);
1412
1413 let mut topology = market.component_topology();
1415 topology
1416 .insert("pool2".to_string(), vec![token_b.address.clone(), token_c.address.clone()]);
1417 let mut manager = PetgraphStableDiGraphManager::default();
1418 manager.initialize_graph(&topology);
1419
1420 let graph = manager.graph();
1421 let paths =
1422 MostLiquidAlgorithm::find_paths(graph, &token_a.address, &token_c.address, 2, 2, None)
1423 .unwrap();
1424 let path = paths.into_iter().next().unwrap();
1425
1426 let result = MostLiquidAlgorithm::simulate_path(
1427 &path,
1428 market.base_market_state(),
1429 None,
1430 BigUint::from(100u64),
1431 );
1432 assert!(matches!(result, Err(AlgorithmError::DataNotFound { kind: "token", .. })));
1433 }
1434
1435 #[test]
1436 fn test_simulate_path_missing_component_returns_data_not_found() {
1437 let token_a = token(0x01, "A");
1438 let token_b = token(0x02, "B");
1439 let (market, manager) =
1440 setup_market(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1441
1442 let mut market_write = market.try_write().unwrap();
1444 market_write.remove_components([&"pool1".to_string()]);
1445 drop(market_write);
1446
1447 let graph = manager.graph();
1448 let paths =
1449 MostLiquidAlgorithm::find_paths(graph, &token_a.address, &token_b.address, 1, 1, None)
1450 .unwrap();
1451 let path = paths.into_iter().next().unwrap();
1452
1453 let result = MostLiquidAlgorithm::simulate_path(
1454 &path,
1455 market_read(&market).base_market_state(),
1456 None,
1457 BigUint::from(100u64),
1458 );
1459 assert!(matches!(result, Err(AlgorithmError::DataNotFound { kind: "component", .. })));
1460 }
1461
1462 #[test]
1465 fn test_connector_tokens_blocks_disallowed_intermediate() {
1466 let (a, b, c, d) = addrs();
1468 let m = diamond_graph();
1469 let g = m.graph();
1470 let allowed: HashSet<Address> = [c.clone()].into();
1471 let paths = MostLiquidAlgorithm::find_paths(g, &a, &d, 1, 2, Some(&allowed)).unwrap();
1472 let intermediates: HashSet<&Address> = paths
1473 .iter()
1474 .flat_map(|p| p.iter().map(|(node, _, _)| node))
1475 .filter(|addr| *addr != &a && *addr != &d)
1476 .collect();
1477 assert!(!intermediates.contains(&b), "B should be blocked");
1479 assert!(intermediates.contains(&c), "C should be allowed");
1480 }
1481
1482 #[test]
1483 fn test_connector_tokens_allows_endpoints_even_if_not_listed() {
1484 let (a, b, _, _) = addrs();
1486 let m = linear_graph();
1487 let g = m.graph();
1488 let allowed: HashSet<Address> = HashSet::new(); let paths = MostLiquidAlgorithm::find_paths(g, &a, &b, 1, 1, Some(&allowed)).unwrap();
1491 assert!(!paths.is_empty(), "1-hop direct route should survive empty allowlist");
1492 }
1493
1494 #[test]
1495 fn test_connector_tokens_none_is_unrestricted() {
1496 let (a, b, c, d) = addrs();
1498 let m = diamond_graph();
1499 let g = m.graph();
1500 let paths = MostLiquidAlgorithm::find_paths(g, &a, &d, 1, 2, None).unwrap();
1501 let intermediates: HashSet<&Address> = paths
1502 .iter()
1503 .flat_map(|p| p.iter().map(|(node, _, _)| node))
1504 .filter(|addr| *addr != &a && *addr != &d)
1505 .collect();
1506 assert!(intermediates.contains(&b), "B should appear with no restriction");
1507 assert!(intermediates.contains(&c), "C should appear with no restriction");
1508 }
1509
1510 #[tokio::test]
1513 async fn test_find_best_route_single_path() {
1514 let token_a = token(0x01, "A");
1515 let token_b = token(0x02, "B");
1516
1517 let (market, manager) =
1518 setup_market(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1519
1520 let algorithm = MostLiquidAlgorithm::with_config(
1521 AlgorithmConfig::new(1, 1, Duration::from_millis(100), None).unwrap(),
1522 )
1523 .unwrap();
1524 let order = order(&token_a, &token_b, ONE_ETH, OrderSide::Sell);
1525 let result = algorithm
1526 .find_best_route(manager.graph(), market, None, None, &order)
1527 .await
1528 .unwrap();
1529
1530 assert_eq!(result.route().swaps().len(), 1);
1531 assert_eq!(*result.route().swaps()[0].amount_in(), BigUint::from(ONE_ETH));
1532 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(ONE_ETH * 2));
1533 }
1534
1535 #[tokio::test]
1536 async fn test_find_best_route_ranks_by_net_amount_out() {
1537 let token_a = token(0x01, "A");
1548 let token_b = token(0x02, "B");
1549
1550 let (market, manager) = setup_market(vec![
1551 ("best", &token_a, &token_b, MockProtocolSim::new(3.0).with_gas(10)),
1552 ("low_out", &token_a, &token_b, MockProtocolSim::new(2.0).with_gas(5)),
1553 ("high_gas", &token_a, &token_b, MockProtocolSim::new(4.0).with_gas(30)),
1554 ]);
1555
1556 let algorithm = MostLiquidAlgorithm::with_config(
1557 AlgorithmConfig::new(1, 1, Duration::from_millis(100), None).unwrap(),
1558 )
1559 .unwrap();
1560 let order = order(&token_a, &token_b, 1000, OrderSide::Sell);
1561
1562 let derived = setup_derived_with_token_prices(std::slice::from_ref(&token_b.address));
1564
1565 let result = algorithm
1566 .find_best_route(manager.graph(), market, None, Some(derived), &order)
1567 .await
1568 .unwrap();
1569
1570 assert_eq!(result.route().swaps().len(), 1);
1572 assert_eq!(result.route().swaps()[0].component_id(), "best");
1573 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(3000u64));
1574 assert_eq!(result.net_amount_out(), &BigInt::from(2000)); }
1576
1577 #[tokio::test]
1578 async fn test_find_best_route_no_path_returns_error() {
1579 let token_a = token(0x01, "A");
1580 let token_b = token(0x02, "B");
1581 let token_c = token(0x03, "C"); let (market, manager) =
1584 setup_market(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1585
1586 let algorithm = MostLiquidAlgorithm::new();
1587 let order = order(&token_a, &token_c, ONE_ETH, OrderSide::Sell);
1588
1589 let result = algorithm
1590 .find_best_route(manager.graph(), market, None, None, &order)
1591 .await;
1592 assert!(matches!(result, Err(AlgorithmError::NoPath { .. })));
1593 }
1594
1595 #[tokio::test]
1596 async fn test_find_best_route_multi_hop() {
1597 let token_a = token(0x01, "A");
1598 let token_b = token(0x02, "B");
1599 let token_c = token(0x03, "C");
1600
1601 let (market, manager) = setup_market(vec![
1602 ("pool1", &token_a, &token_b, MockProtocolSim::new(2.0)),
1603 ("pool2", &token_b, &token_c, MockProtocolSim::new(3.0)),
1604 ]);
1605
1606 let algorithm = MostLiquidAlgorithm::with_config(
1607 AlgorithmConfig::new(1, 2, Duration::from_millis(100), None).unwrap(),
1608 )
1609 .unwrap();
1610 let order = order(&token_a, &token_c, ONE_ETH, OrderSide::Sell);
1611
1612 let result = algorithm
1613 .find_best_route(manager.graph(), market, None, None, &order)
1614 .await
1615 .unwrap();
1616
1617 assert_eq!(result.route().swaps().len(), 2);
1619 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(ONE_ETH * 2));
1620 assert_eq!(result.route().swaps()[0].component_id(), "pool1".to_string());
1621 assert_eq!(*result.route().swaps()[1].amount_out(), BigUint::from(ONE_ETH * 2 * 3));
1622 assert_eq!(result.route().swaps()[1].component_id(), "pool2".to_string());
1623 }
1624
1625 #[tokio::test]
1626 async fn test_find_best_route_skips_paths_without_edge_weights() {
1627 let token_a = token(0x01, "A");
1629 let token_b = token(0x02, "B");
1630
1631 let mut market = MarketState::new();
1633 let pool1_state = MockProtocolSim::new(2.0);
1634 let pool2_state = MockProtocolSim::new(3.0); let pool1_comp = component("pool1", &[token_a.clone(), token_b.clone()]);
1637 let pool2_comp = component("pool2", &[token_a.clone(), token_b.clone()]);
1638
1639 market.update_gas_price(BlockGasPrice {
1641 block_number: 1,
1642 block_hash: Default::default(),
1643 block_timestamp: 0,
1644 pricing: GasPrice::Legacy { gas_price: BigUint::from(1u64) },
1645 });
1646
1647 market.upsert_components(vec![pool1_comp, pool2_comp]);
1649
1650 market.update_states(vec![
1652 ("pool1".to_string(), Box::new(pool1_state.clone()) as Box<dyn ProtocolSim>),
1653 ("pool2".to_string(), Box::new(pool2_state) as Box<dyn ProtocolSim>),
1654 ]);
1655
1656 market.upsert_tokens(vec![token_a.clone(), token_b.clone()]);
1658
1659 let mut manager = PetgraphStableDiGraphManager::default();
1661 manager.initialize_graph(&market.component_topology());
1662
1663 let weight = DepthAndPrice::from_protocol_sim(&pool1_state, &token_a, &token_b).unwrap();
1665 manager
1666 .set_edge_weight(
1667 &"pool1".to_string(),
1668 &token_a.address,
1669 &token_b.address,
1670 weight,
1671 false,
1672 )
1673 .unwrap();
1674
1675 let algorithm = MostLiquidAlgorithm::with_config(
1677 AlgorithmConfig::new(1, 1, Duration::from_millis(100), None).unwrap(),
1678 )
1679 .unwrap();
1680 let order = order(&token_a, &token_b, ONE_ETH, OrderSide::Sell);
1681 let market = wrap_market(market);
1682 let result = algorithm
1683 .find_best_route(manager.graph(), market, None, None, &order)
1684 .await
1685 .unwrap();
1686
1687 assert_eq!(result.route().swaps().len(), 1);
1689 assert_eq!(result.route().swaps()[0].component_id(), "pool1");
1690 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(ONE_ETH * 2));
1691 }
1692
1693 #[tokio::test]
1694 async fn test_find_best_route_no_scorable_paths() {
1695 let token_a = token(0x01, "A");
1697 let token_b = token(0x02, "B");
1698
1699 let mut market = MarketState::new();
1700 let pool_state = MockProtocolSim::new(2.0);
1701 let pool_comp = component("pool1", &[token_a.clone(), token_b.clone()]);
1702
1703 market.update_gas_price(BlockGasPrice {
1705 block_number: 1,
1706 block_hash: Default::default(),
1707 block_timestamp: 0,
1708 pricing: GasPrice::Eip1559 {
1709 base_fee_per_gas: BigUint::from(1u64),
1710 max_priority_fee_per_gas: BigUint::from(0u64),
1711 },
1712 });
1713
1714 market.upsert_components(vec![pool_comp]);
1715 market.update_states(vec![(
1716 "pool1".to_string(),
1717 Box::new(pool_state) as Box<dyn ProtocolSim>,
1718 )]);
1719 market.upsert_tokens(vec![token_a.clone(), token_b.clone()]);
1720
1721 let mut manager = PetgraphStableDiGraphManager::default();
1723 manager.initialize_graph(&market.component_topology());
1724
1725 let algorithm = MostLiquidAlgorithm::new();
1726 let order = order(&token_a, &token_b, ONE_ETH, OrderSide::Sell);
1727 let market = wrap_market(market);
1728
1729 let result = algorithm
1730 .find_best_route(manager.graph(), market, None, None, &order)
1731 .await;
1732 assert!(matches!(
1733 result,
1734 Err(AlgorithmError::NoPath { reason: NoPathReason::NoScorablePaths, .. })
1735 ));
1736 }
1737
1738 #[tokio::test]
1739 async fn test_find_best_route_gas_exceeds_output_returns_negative_net() {
1740 let token_a = token(0x01, "A");
1741 let token_b = token(0x02, "B");
1742
1743 let (market, manager) =
1744 setup_market(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1745 let mut market_write = market.try_write().unwrap();
1746
1747 market_write.update_gas_price(BlockGasPrice {
1750 block_number: 1,
1751 block_hash: Default::default(),
1752 block_timestamp: 0,
1753 pricing: GasPrice::Eip1559 {
1754 base_fee_per_gas: BigUint::from(1_000_000u64),
1755 max_priority_fee_per_gas: BigUint::from(1_000_000u64),
1756 },
1757 });
1758 drop(market_write); let algorithm = MostLiquidAlgorithm::new();
1761 let order = order(&token_a, &token_b, 1, OrderSide::Sell); let derived = setup_derived_with_token_prices(std::slice::from_ref(&token_b.address));
1765
1766 let result = algorithm
1768 .find_best_route(manager.graph(), market, None, Some(derived), &order)
1769 .await
1770 .expect("should return route even with negative net_amount_out");
1771
1772 assert_eq!(result.route().swaps().len(), 1);
1774 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(2u64)); let expected_net = BigInt::from(2) - BigInt::from(100_000_000_000u64);
1778 assert_eq!(result.net_amount_out(), &expected_net);
1779 }
1780
1781 #[tokio::test]
1782 async fn test_find_best_route_insufficient_liquidity() {
1783 let token_a = token(0x01, "A");
1785 let token_b = token(0x02, "B");
1786
1787 let (market, manager) = setup_market(vec![(
1788 "pool1",
1789 &token_a,
1790 &token_b,
1791 MockProtocolSim::new(2.0).with_liquidity(1000),
1792 )]);
1793
1794 let algorithm = MostLiquidAlgorithm::new();
1795 let order = order(&token_a, &token_b, ONE_ETH, OrderSide::Sell); let result = algorithm
1798 .find_best_route(manager.graph(), market, None, None, &order)
1799 .await;
1800 assert!(matches!(result, Err(AlgorithmError::InsufficientLiquidity)));
1801 }
1802
1803 #[tokio::test]
1804 async fn test_find_best_route_missing_gas_price_returns_error() {
1805 let token_a = token(0x01, "A");
1807 let token_b = token(0x02, "B");
1808
1809 let mut market = MarketState::new();
1810 let pool_state = MockProtocolSim::new(2.0);
1811 let pool_comp = component("pool1", &[token_a.clone(), token_b.clone()]);
1812
1813 market.upsert_components(vec![pool_comp]);
1815 market.update_states(vec![(
1816 "pool1".to_string(),
1817 Box::new(pool_state.clone()) as Box<dyn ProtocolSim>,
1818 )]);
1819 market.upsert_tokens(vec![token_a.clone(), token_b.clone()]);
1820
1821 let mut manager = PetgraphStableDiGraphManager::default();
1823 manager.initialize_graph(&market.component_topology());
1824 let weight = DepthAndPrice::from_protocol_sim(&pool_state, &token_a, &token_b).unwrap();
1825 manager
1826 .set_edge_weight(
1827 &"pool1".to_string(),
1828 &token_a.address,
1829 &token_b.address,
1830 weight,
1831 false,
1832 )
1833 .unwrap();
1834
1835 let algorithm = MostLiquidAlgorithm::new();
1836 let order = order(&token_a, &token_b, ONE_ETH, OrderSide::Sell);
1837 let market = wrap_market(market);
1838
1839 let result = algorithm
1840 .find_best_route(manager.graph(), market, None, None, &order)
1841 .await;
1842
1843 assert!(matches!(result, Err(AlgorithmError::DataNotFound { kind: "gas price", .. })));
1845 }
1846
1847 #[tokio::test]
1848 async fn test_find_best_route_circular_arbitrage() {
1849 let token_a = token(0x01, "A");
1850 let token_b = token(0x02, "B");
1851
1852 let (market, manager) =
1855 setup_market(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1856
1857 let algorithm = MostLiquidAlgorithm::with_config(
1859 AlgorithmConfig::new(2, 2, Duration::from_millis(100), None).unwrap(),
1860 )
1861 .unwrap();
1862
1863 let order = order(&token_a, &token_a, 100, OrderSide::Sell);
1865
1866 let result = algorithm
1867 .find_best_route(manager.graph(), market, None, None, &order)
1868 .await
1869 .unwrap();
1870
1871 assert_eq!(result.route().swaps().len(), 2, "Should have 2 swaps for circular route");
1873
1874 assert_eq!(*result.route().swaps()[0].token_in(), token_a.address);
1876 assert_eq!(*result.route().swaps()[0].token_out(), token_b.address);
1877 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(200u64));
1878
1879 assert_eq!(*result.route().swaps()[1].token_in(), token_b.address);
1881 assert_eq!(*result.route().swaps()[1].token_out(), token_a.address);
1882 assert_eq!(*result.route().swaps()[1].amount_out(), BigUint::from(66u64));
1883
1884 assert_eq!(result.route().swaps()[0].token_in(), result.route().swaps()[1].token_out());
1886 }
1887
1888 #[tokio::test]
1889 async fn test_find_best_route_respects_min_hops() {
1890 let token_a = token(0x01, "A");
1893 let token_b = token(0x02, "B");
1894 let token_c = token(0x03, "C");
1895
1896 let (market, manager) = setup_market(vec![
1897 ("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)), ]);
1902
1903 let algorithm = MostLiquidAlgorithm::with_config(
1905 AlgorithmConfig::new(2, 3, Duration::from_millis(100), None).unwrap(),
1906 )
1907 .unwrap();
1908 let order = order(&token_a, &token_b, 100, OrderSide::Sell);
1909
1910 let derived = setup_derived_with_token_prices(std::slice::from_ref(&token_b.address));
1913
1914 let result = algorithm
1915 .find_best_route(manager.graph(), market, None, Some(derived), &order)
1916 .await
1917 .unwrap();
1918
1919 assert_eq!(result.route().swaps().len(), 2, "Should use 2-hop path due to min_hops=2");
1921 assert_eq!(result.route().swaps()[0].component_id(), "pool_ac");
1922 assert_eq!(result.route().swaps()[1].component_id(), "pool_cb");
1923 }
1924
1925 #[tokio::test]
1926 async fn test_find_best_route_respects_max_hops() {
1927 let token_a = token(0x01, "A");
1930 let token_b = token(0x02, "B");
1931 let token_c = token(0x03, "C");
1932
1933 let (market, manager) = setup_market(vec![
1934 ("pool_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
1935 ("pool_bc", &token_b, &token_c, MockProtocolSim::new(3.0)),
1936 ]);
1937
1938 let algorithm = MostLiquidAlgorithm::with_config(
1940 AlgorithmConfig::new(1, 1, Duration::from_millis(100), None).unwrap(),
1941 )
1942 .unwrap();
1943 let order = order(&token_a, &token_c, 100, OrderSide::Sell);
1944
1945 let result = algorithm
1946 .find_best_route(manager.graph(), market, None, None, &order)
1947 .await;
1948 assert!(
1949 matches!(result, Err(AlgorithmError::NoPath { .. })),
1950 "Should return NoPath when max_hops is insufficient"
1951 );
1952 }
1953
1954 #[tokio::test]
1955 async fn test_find_best_route_timeout_returns_best_so_far() {
1956 let token_a = token(0x01, "A");
1960 let token_b = token(0x02, "B");
1961
1962 let (market, manager) = setup_market(vec![
1964 ("pool1", &token_a, &token_b, MockProtocolSim::new(1.0)),
1965 ("pool2", &token_a, &token_b, MockProtocolSim::new(2.0)),
1966 ("pool3", &token_a, &token_b, MockProtocolSim::new(3.0)),
1967 ("pool4", &token_a, &token_b, MockProtocolSim::new(4.0)),
1968 ("pool5", &token_a, &token_b, MockProtocolSim::new(5.0)),
1969 ]);
1970
1971 let algorithm = MostLiquidAlgorithm::with_config(
1973 AlgorithmConfig::new(1, 1, Duration::from_millis(0), None).unwrap(),
1974 )
1975 .unwrap();
1976 let order = order(&token_a, &token_b, 100, OrderSide::Sell);
1977
1978 let result = algorithm
1979 .find_best_route(manager.graph(), market, None, None, &order)
1980 .await;
1981
1982 match result {
1987 Ok(r) => {
1988 assert_eq!(r.route().swaps().len(), 1);
1990 }
1991 Err(AlgorithmError::Timeout { .. }) => {
1992 }
1994 Err(e) => panic!("Unexpected error: {:?}", e),
1995 }
1996 }
1997
1998 #[rstest::rstest]
2001 #[case::default_config(1, 3, 50)]
2002 #[case::single_hop_only(1, 1, 100)]
2003 #[case::multi_hop_min(2, 5, 200)]
2004 #[case::zero_timeout(1, 3, 0)]
2005 #[case::large_values(10, 100, 10000)]
2006 fn test_algorithm_config_getters(
2007 #[case] min_hops: usize,
2008 #[case] max_hops: usize,
2009 #[case] timeout_ms: u64,
2010 ) {
2011 use crate::algorithm::Algorithm;
2012
2013 let algorithm = MostLiquidAlgorithm::with_config(
2014 AlgorithmConfig::new(min_hops, max_hops, Duration::from_millis(timeout_ms), None)
2015 .unwrap(),
2016 )
2017 .unwrap();
2018
2019 assert_eq!(algorithm.max_hops, max_hops);
2020 assert_eq!(algorithm.timeout, Duration::from_millis(timeout_ms));
2021 assert_eq!(algorithm.name(), "most_liquid");
2022 }
2023
2024 #[test]
2025 fn test_algorithm_default_config() {
2026 use crate::algorithm::Algorithm;
2027
2028 let algorithm = MostLiquidAlgorithm::new();
2029
2030 assert_eq!(algorithm.max_hops, 3);
2031 assert_eq!(algorithm.timeout, Duration::from_millis(500));
2032 assert_eq!(algorithm.name(), "most_liquid");
2033 }
2034
2035 #[tokio::test]
2038 async fn test_find_best_route_respects_max_routes_cap() {
2039 let token_a = token(0x01, "A");
2052 let token_b = token(0x02, "B");
2053
2054 let (market, manager) = setup_market(vec![
2055 ("pool1", &token_a, &token_b, MockProtocolSim::new(4.0).with_liquidity(1_000_000)),
2056 ("pool2", &token_a, &token_b, MockProtocolSim::new(3.0).with_liquidity(2_000_000)),
2057 ("pool3", &token_a, &token_b, MockProtocolSim::new(2.0).with_liquidity(3_000_000)),
2058 ("pool4", &token_a, &token_b, MockProtocolSim::new(1.0).with_liquidity(4_000_000)),
2059 ]);
2060
2061 let algorithm = MostLiquidAlgorithm::with_config(
2063 AlgorithmConfig::new(1, 1, Duration::from_millis(100), Some(2)).unwrap(),
2064 )
2065 .unwrap();
2066 let order = order(&token_a, &token_b, 1000, OrderSide::Sell);
2067 let result = algorithm
2068 .find_best_route(manager.graph(), market, None, None, &order)
2069 .await
2070 .unwrap();
2071
2072 assert_eq!(result.route().swaps().len(), 1);
2076 assert_eq!(result.route().swaps()[0].component_id(), "pool3");
2077 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(2000u64));
2078 }
2079
2080 #[tokio::test]
2081 async fn test_find_best_route_no_cap_when_max_routes_is_none() {
2082 let token_a = token(0x01, "A");
2084 let token_b = token(0x02, "B");
2085
2086 let (market, manager) = setup_market(vec![
2087 ("pool1", &token_a, &token_b, MockProtocolSim::new(4.0).with_liquidity(1_000_000)),
2088 ("pool2", &token_a, &token_b, MockProtocolSim::new(3.0).with_liquidity(2_000_000)),
2089 ("pool3", &token_a, &token_b, MockProtocolSim::new(2.0).with_liquidity(3_000_000)),
2090 ("pool4", &token_a, &token_b, MockProtocolSim::new(1.0).with_liquidity(4_000_000)),
2091 ]);
2092
2093 let algorithm = MostLiquidAlgorithm::with_config(
2094 AlgorithmConfig::new(1, 1, Duration::from_millis(100), None).unwrap(),
2095 )
2096 .unwrap();
2097 let order = order(&token_a, &token_b, 1000, OrderSide::Sell);
2098 let result = algorithm
2099 .find_best_route(manager.graph(), market, None, None, &order)
2100 .await
2101 .unwrap();
2102
2103 assert_eq!(result.route().swaps().len(), 1);
2105 assert_eq!(result.route().swaps()[0].component_id(), "pool1");
2106 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(4000u64));
2107 }
2108
2109 #[test]
2110 fn test_algorithm_config_rejects_zero_max_routes() {
2111 let result = AlgorithmConfig::new(1, 3, Duration::from_millis(100), Some(0));
2112 assert!(matches!(
2113 result,
2114 Err(AlgorithmError::InvalidConfiguration { reason }) if reason.contains("max_routes must be at least 1")
2115 ));
2116 }
2117
2118 #[test]
2119 fn test_algorithm_config_rejects_zero_min_hops() {
2120 let result = AlgorithmConfig::new(0, 3, Duration::from_millis(100), None);
2121 assert!(matches!(
2122 result,
2123 Err(AlgorithmError::InvalidConfiguration { reason }) if reason.contains("min_hops must be at least 1")
2124 ));
2125 }
2126
2127 #[test]
2128 fn test_algorithm_config_rejects_min_greater_than_max() {
2129 let result = AlgorithmConfig::new(5, 3, Duration::from_millis(100), None);
2130 assert!(matches!(
2131 result,
2132 Err(AlgorithmError::InvalidConfiguration { reason }) if reason.contains("cannot exceed")
2133 ));
2134 }
2135}