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 let mut tokens: HashMap<Address, Token> = HashMap::new();
362
363 for (address_in, edge_data, address_out) in path.iter() {
364 let token_in = market
366 .get_token(address_in)
367 .ok_or_else(|| AlgorithmError::DataNotFound {
368 kind: "token",
369 id: Some(format!("{:?}", address_in)),
370 })?;
371 let token_out = market
372 .get_token(address_out)
373 .ok_or_else(|| AlgorithmError::DataNotFound {
374 kind: "token",
375 id: Some(format!("{:?}", address_out)),
376 })?;
377
378 let component_id = &edge_data.component_id;
379 let component = market
380 .get_component(component_id)
381 .ok_or_else(|| AlgorithmError::DataNotFound {
382 kind: "component",
383 id: Some(component_id.clone()),
384 })?;
385 let component_state = market
386 .get_simulation_state(component_id)
387 .ok_or_else(|| AlgorithmError::DataNotFound {
388 kind: "simulation state",
389 id: Some(component_id.clone()),
390 })?;
391
392 let state = state_overrides
393 .get(component_id)
394 .map(Box::as_ref)
395 .unwrap_or(component_state);
396
397 let result = state
399 .get_amount_out(current_amount.clone(), token_in, token_out)
400 .map_err(|e| AlgorithmError::Other(format!("simulation error: {:?}", e)))?;
401
402 swaps.push(Swap::new(
404 component_id.clone(),
405 component.protocol_system.clone(),
406 token_in.address.clone(),
407 token_out.address.clone(),
408 current_amount.clone(),
409 result.amount.clone(),
410 result.gas,
411 component.clone(),
412 state.clone_box(),
413 ));
414 tokens
415 .entry(token_in.address.clone())
416 .or_insert_with(|| token_in.clone());
417 tokens
418 .entry(token_out.address.clone())
419 .or_insert_with(|| token_out.clone());
420
421 state_overrides.insert(component_id, result.new_state);
422 current_amount = result.amount;
423 }
424
425 let route = Route::new(swaps, tokens);
427 let output_amount = route
428 .swaps()
429 .last()
430 .map(|s| s.amount_out().clone())
431 .unwrap_or_else(|| BigUint::ZERO);
432
433 let gas_price = market
434 .gas_price()
435 .ok_or(AlgorithmError::DataNotFound { kind: "gas price", id: None })?
436 .effective_gas_price()
437 .clone();
438
439 let net_amount_out = if let Some(last_swap) = route.swaps().last() {
440 let total_gas = route.total_gas();
441 let gas_cost_wei = &total_gas * &gas_price;
442
443 let gas_cost_in_output_token: Option<BigUint> = token_prices
445 .and_then(|prices| prices.get(last_swap.token_out()))
446 .map(|price| {
447 &gas_cost_wei * &price.numerator / &price.denominator
450 });
451
452 match gas_cost_in_output_token {
453 Some(gas_cost) => BigInt::from(output_amount) - BigInt::from(gas_cost),
454 None => {
455 BigInt::from(output_amount)
458 }
459 }
460 } else {
461 BigInt::from(output_amount)
462 };
463
464 Ok(RouteResult::new(route, net_amount_out, gas_price))
465 }
466}
467
468impl Default for MostLiquidAlgorithm {
469 fn default() -> Self {
470 Self::new()
471 }
472}
473
474impl Algorithm for MostLiquidAlgorithm {
475 type GraphType = StableDiGraph<DepthAndPrice>;
476 type GraphManager = PetgraphStableDiGraphManager<DepthAndPrice>;
477
478 fn name(&self) -> &str {
479 "most_liquid"
480 }
481
482 #[instrument(level = "debug", skip_all, fields(order_id = %order.id()))]
484 async fn find_best_route(
485 &self,
486 graph: &Self::GraphType,
487 market: MarketData,
488 label: Option<StateLabel>,
489 derived: Option<SharedDerivedDataRef>,
490 order: &Order,
491 ) -> Result<RouteResult, AlgorithmError> {
492 let start = Instant::now();
493
494 if !order.is_sell() {
496 return Err(AlgorithmError::ExactOutNotSupported);
497 }
498
499 let token_prices = if let Some(ref derived) = derived {
501 derived
502 .read()
503 .await
504 .token_prices()
505 .cloned()
506 } else {
507 None
508 };
509
510 let amount_in = order.amount().clone();
511
512 let all_paths = Self::find_paths(
514 graph,
515 order.token_in(),
516 order.token_out(),
517 self.min_hops,
518 self.max_hops,
519 self.connector_tokens.as_ref(),
520 )?;
521
522 let paths_candidates = all_paths.len();
523 if paths_candidates == 0 {
524 return Err(AlgorithmError::NoPath {
525 from: order.token_in().clone(),
526 to: order.token_out().clone(),
527 reason: NoPathReason::NoGraphPath,
528 });
529 }
530
531 let mut scored_paths: Vec<(Path<DepthAndPrice>, f64)> = all_paths
534 .into_iter()
535 .filter_map(|path| {
536 let score = Self::try_score_path(&path)?;
537 Some((path, score))
538 })
539 .collect();
540
541 scored_paths.sort_by(|(_, a_score), (_, b_score)| {
542 b_score
544 .partial_cmp(a_score)
545 .unwrap_or(std::cmp::Ordering::Equal)
546 });
547
548 if let Some(max_routes) = self.max_routes {
549 scored_paths.truncate(max_routes);
550 }
551
552 let paths_to_simulate = scored_paths.len();
553 let scoring_failures = paths_candidates - paths_to_simulate;
554 if paths_to_simulate == 0 {
555 return Err(AlgorithmError::NoPath {
556 from: order.token_in().clone(),
557 to: order.token_out().clone(),
558 reason: NoPathReason::NoScorablePaths,
559 });
560 }
561
562 let component_ids: HashSet<ComponentId> = scored_paths
564 .iter()
565 .flat_map(|(path, _)| {
566 path.edge_iter()
567 .iter()
568 .map(|e| e.component_id.clone())
569 })
570 .collect();
571
572 let market = {
574 let market = match label.as_ref() {
575 Some(l) => market
576 .read_labeled(l)
577 .await
578 .map_err(|e| AlgorithmError::Other(e.to_string()))?,
579 None => market.read().await,
580 };
581 if market.gas_price().is_none() {
582 return Err(AlgorithmError::DataNotFound { kind: "gas price", id: None });
583 }
584 let market_subset = market.extract_subset_with_overlay(&component_ids);
585 drop(market);
586 market_subset
587 };
588
589 let mut paths_simulated = 0usize;
590 let mut simulation_failures = 0usize;
591
592 let mut best: Option<RouteResult> = None;
594 let timeout_ms = self.timeout.as_millis() as u64;
595
596 for (edge_path, _) in scored_paths {
597 let elapsed_ms = start.elapsed().as_millis() as u64;
599 if elapsed_ms > timeout_ms {
600 break;
601 }
602
603 let result = match Self::simulate_path(
604 &edge_path,
605 &market,
606 token_prices.as_ref(),
607 amount_in.clone(),
608 ) {
609 Ok(r) => r,
610 Err(e) => {
611 trace!(error = %e, "simulation failed for path");
612 simulation_failures += 1;
613 continue;
614 }
615 };
616
617 if best
619 .as_ref()
620 .map(|best| result.net_amount_out() > best.net_amount_out())
621 .unwrap_or(true)
622 {
623 best = Some(result);
624 }
625
626 paths_simulated += 1;
627 }
628
629 let solve_time_ms = start.elapsed().as_millis() as u64;
631 let block_number = market
632 .last_updated()
633 .map(|b| b.number());
634 let coverage_pct = if paths_to_simulate == 0 {
636 100.0
637 } else {
638 (paths_simulated as f64 / paths_to_simulate as f64) * 100.0
639 };
640
641 counter!("algorithm.scoring_failures").increment(scoring_failures as u64);
643 counter!("algorithm.simulation_failures").increment(simulation_failures as u64);
644 histogram!("algorithm.simulation_coverage_pct").record(coverage_pct);
645
646 match &best {
647 Some(result) => {
648 let tokens = market.token_registry_ref();
649 let path_desc = result.route().path_description(tokens);
650 let protocols = result
651 .route()
652 .swaps()
653 .iter()
654 .map(|s| s.protocol())
655 .collect::<Vec<_>>();
656
657 let price = amount_in
658 .to_f64()
659 .filter(|&v| v > 0.0)
660 .and_then(|amt_in| {
661 result
662 .net_amount_out()
663 .to_f64()
664 .map(|amt_out| amt_out / amt_in)
665 })
666 .unwrap_or(f64::NAN);
667
668 debug!(
669 solve_time_ms,
670 block_number,
671 paths_candidates,
672 paths_to_simulate,
673 paths_simulated,
674 simulation_failures,
675 simulation_coverage_pct = coverage_pct,
676 components_considered = component_ids.len(),
677 tokens_considered = market.token_registry_ref().len(),
678 path = %path_desc,
679 amount_in = %amount_in,
680 net_amount_out = %result.net_amount_out(),
681 price_out_per_in = price,
682 hop_count = result.route().swaps().len(),
683 protocols = ?protocols,
684 "route found"
685 );
686 }
687 None => {
688 debug!(
689 solve_time_ms,
690 block_number,
691 paths_candidates,
692 paths_to_simulate,
693 paths_simulated,
694 simulation_failures,
695 simulation_coverage_pct = coverage_pct,
696 components_considered = component_ids.len(),
697 tokens_considered = market.token_registry_ref().len(),
698 "no viable route"
699 );
700 }
701 }
702
703 best.ok_or({
704 if solve_time_ms > timeout_ms {
705 AlgorithmError::Timeout { elapsed_ms: solve_time_ms }
706 } else {
707 AlgorithmError::InsufficientLiquidity
708 }
709 })
710 }
711
712 fn computation_requirements(&self) -> ComputationRequirements {
713 ComputationRequirements::none()
721 .allow_stale("token_prices")
722 .expect("Conflicting Computation Requirements")
723 }
724
725 fn timeout(&self) -> Duration {
726 self.timeout
727 }
728}
729
730#[cfg(test)]
731mod tests {
732 use std::collections::HashSet;
733
734 use rstest::rstest;
735 use tycho_simulation::{
736 tycho_core::simulation::protocol_sim::Price,
737 tycho_ethereum::gas::{BlockGasPrice, GasPrice},
738 };
739
740 use super::*;
741 use crate::{
742 algorithm::test_utils::{
743 addr, component,
744 fixtures::{addrs, diamond_graph, linear_graph, parallel_graph},
745 market_read, order, setup_market_weighted, token, MockProtocolSim, ONE_ETH,
746 },
747 derived::{
748 computation::{FailedItem, FailedItemError},
749 types::TokenGasPrices,
750 DerivedData,
751 },
752 graph::GraphManager,
753 types::OrderSide,
754 };
755
756 fn wrap_market(market: MarketState) -> MarketData {
757 MarketData::new(std::sync::Arc::new(tokio::sync::RwLock::new(market)))
758 }
759
760 fn setup_derived_with_token_prices(token_addresses: &[Address]) -> SharedDerivedDataRef {
765 let mut token_prices: TokenGasPrices = HashMap::new();
766 for addr in token_addresses {
767 token_prices.insert(
769 addr.clone(),
770 Price { numerator: BigUint::from(1u64), denominator: BigUint::from(1u64) },
771 );
772 }
773
774 let mut derived_data = DerivedData::new();
775 derived_data.set_token_prices(token_prices, vec![], 1, true);
776 std::sync::Arc::new(tokio::sync::RwLock::new(derived_data))
777 }
778 #[test]
781 fn test_try_score_path_calculates_correctly() {
782 let (a, b, c, _) = addrs();
783 let mut m = linear_graph();
784
785 m.set_edge_weight(&"ab".to_string(), &a, &b, DepthAndPrice::new(2.0, 1000.0), false)
787 .unwrap();
788 m.set_edge_weight(&"bc".to_string(), &b, &c, DepthAndPrice::new(0.5, 500.0), false)
789 .unwrap();
790
791 let graph = m.graph();
793 let paths = MostLiquidAlgorithm::find_paths(graph, &a, &c, 2, 2, None).unwrap();
794 assert_eq!(paths.len(), 1);
795 let path = &paths[0];
796
797 let expected = 2.0 * 0.5 * 500.0;
799 let score = MostLiquidAlgorithm::try_score_path(path).unwrap();
800 assert_eq!(score, expected, "expected {expected}, got {score}");
801 }
802
803 #[test]
804 fn test_try_score_path_empty_returns_none() {
805 let path: Path<DepthAndPrice> = Path::new();
806 assert_eq!(MostLiquidAlgorithm::try_score_path(&path), None);
807 }
808
809 #[test]
810 fn test_try_score_path_missing_weight_returns_none() {
811 let (a, b, _, _) = addrs();
812 let m = linear_graph();
813 let graph = m.graph();
814 let paths = MostLiquidAlgorithm::find_paths(graph, &a, &b, 1, 1, None).unwrap();
815 assert_eq!(paths.len(), 1);
816 assert!(MostLiquidAlgorithm::try_score_path(&paths[0]).is_none());
817 }
818
819 #[test]
820 fn test_try_score_path_circular_route() {
821 let (a, b, _, _) = addrs();
823 let mut m = linear_graph();
824
825 m.set_edge_weight(&"ab".to_string(), &a, &b, DepthAndPrice::new(2.0, 1000.0), false)
829 .unwrap();
830 m.set_edge_weight(&"ab".to_string(), &b, &a, DepthAndPrice::new(0.6, 800.0), false)
831 .unwrap();
832
833 let graph = m.graph();
834 let paths = MostLiquidAlgorithm::find_paths(graph, &a, &a, 2, 2, None).unwrap();
836
837 assert_eq!(paths.len(), 1);
839
840 let score = MostLiquidAlgorithm::try_score_path(&paths[0]).unwrap();
845 let expected = 2.0 * 0.6 * 800.0;
846 assert_eq!(score, expected, "expected {expected}, got {score}");
847 }
848
849 fn make_mock_sim() -> MockProtocolSim {
850 MockProtocolSim::new(2.0)
851 }
852
853 fn pair_key(comp: &str, b_in: u8, b_out: u8) -> (String, Address, Address) {
854 (comp.to_string(), addr(b_in), addr(b_out))
855 }
856
857 fn pair_key_str(comp: &str, b_in: u8, b_out: u8) -> String {
858 format!("{comp}/{}/{}", addr(b_in), addr(b_out))
859 }
860
861 fn make_token_prices(addresses: &[Address]) -> TokenGasPrices {
862 let mut prices = TokenGasPrices::new();
863 for addr in addresses {
864 prices.insert(
866 addr.clone(),
867 Price { numerator: BigUint::from(1u64), denominator: BigUint::from(1u64) },
868 );
869 }
870 prices
871 }
872
873 #[test]
874 fn test_from_sim_and_derived_failed_spot_price_returns_none() {
875 let key = pair_key("pool1", 0x01, 0x02);
876 let key_str = pair_key_str("pool1", 0x01, 0x02);
877 let tok_in = token(0x01, "A");
878 let tok_out = token(0x02, "B");
879
880 let mut derived = DerivedData::new();
881 derived.set_spot_prices(
883 Default::default(),
884 vec![FailedItem {
885 key: key_str,
886 error: FailedItemError::SimulationFailed("sim error".into()),
887 }],
888 10,
889 true,
890 );
891 derived.set_pool_depths(Default::default(), vec![], 10, true);
892 derived.set_token_prices(
893 make_token_prices(&[tok_in.address.clone(), tok_out.address.clone()]),
894 vec![],
895 10,
896 true,
897 );
898
899 let sim = make_mock_sim();
900 let result =
901 <DepthAndPrice as crate::graph::EdgeWeightFromSimAndDerived>::from_sim_and_derived(
902 &sim, &key.0, &tok_in, &tok_out, &derived,
903 );
904
905 assert!(result.is_none());
906 }
907
908 #[test]
909 fn test_from_sim_and_derived_failed_pool_depth_returns_none() {
910 let key = pair_key("pool1", 0x01, 0x02);
911 let key_str = pair_key_str("pool1", 0x01, 0x02);
912 let tok_in = token(0x01, "A");
913 let tok_out = token(0x02, "B");
914
915 let mut derived = DerivedData::new();
916 let mut prices = crate::derived::types::SpotPrices::default();
918 prices.insert(key.clone(), 1.5);
919 derived.set_spot_prices(prices, vec![], 10, true);
920 derived.set_pool_depths(
922 Default::default(),
923 vec![FailedItem {
924 key: key_str,
925 error: FailedItemError::SimulationFailed("depth error".into()),
926 }],
927 10,
928 true,
929 );
930 derived.set_token_prices(
931 make_token_prices(&[tok_in.address.clone(), tok_out.address.clone()]),
932 vec![],
933 10,
934 true,
935 );
936
937 let sim = make_mock_sim();
938 let result =
939 <DepthAndPrice as crate::graph::EdgeWeightFromSimAndDerived>::from_sim_and_derived(
940 &sim, &key.0, &tok_in, &tok_out, &derived,
941 );
942
943 assert!(result.is_none());
944 }
945
946 #[test]
947 fn test_from_sim_and_derived_both_failed_returns_none() {
948 let key = pair_key("pool1", 0x01, 0x02);
949 let key_str = pair_key_str("pool1", 0x01, 0x02);
950 let tok_in = token(0x01, "A");
951 let tok_out = token(0x02, "B");
952
953 let mut derived = DerivedData::new();
954 derived.set_spot_prices(
955 Default::default(),
956 vec![FailedItem {
957 key: key_str.clone(),
958 error: FailedItemError::SimulationFailed("spot error".into()),
959 }],
960 10,
961 true,
962 );
963 derived.set_pool_depths(
964 Default::default(),
965 vec![FailedItem {
966 key: key_str,
967 error: FailedItemError::SimulationFailed("depth error".into()),
968 }],
969 10,
970 true,
971 );
972 derived.set_token_prices(
973 make_token_prices(&[tok_in.address.clone(), tok_out.address.clone()]),
974 vec![],
975 10,
976 true,
977 );
978
979 let sim = make_mock_sim();
980 let result =
981 <DepthAndPrice as crate::graph::EdgeWeightFromSimAndDerived>::from_sim_and_derived(
982 &sim, &key.0, &tok_in, &tok_out, &derived,
983 );
984
985 assert!(result.is_none());
986 }
987
988 #[test]
989 fn test_from_sim_and_derived_missing_token_price_returns_none() {
990 let key = pair_key("pool1", 0x01, 0x02);
991 let tok_in = token(0x01, "A");
992 let tok_out = token(0x02, "B");
993
994 let mut derived = DerivedData::new();
995 let mut prices = crate::derived::types::SpotPrices::default();
997 prices.insert(key.clone(), 1.5);
998 derived.set_spot_prices(prices, vec![], 10, true);
999
1000 let mut depths = crate::derived::types::PoolDepths::default();
1001 depths.insert(key.clone(), BigUint::from(1000u64));
1002 derived.set_pool_depths(depths, vec![], 10, true);
1003
1004 let sim = make_mock_sim();
1007 let result =
1008 <DepthAndPrice as crate::graph::EdgeWeightFromSimAndDerived>::from_sim_and_derived(
1009 &sim, &key.0, &tok_in, &tok_out, &derived,
1010 );
1011
1012 assert!(
1013 result.is_none(),
1014 "should return None when token price is missing for depth normalization"
1015 );
1016 }
1017
1018 #[test]
1019 fn test_from_sim_and_derived_normalizes_depth_to_eth() {
1020 let key = pair_key("pool1", 0x01, 0x02);
1021 let tok_in = token(0x01, "A");
1022 let tok_out = token(0x02, "B");
1023
1024 let mut derived = DerivedData::new();
1025
1026 let mut spot = crate::derived::types::SpotPrices::default();
1028 spot.insert(key.clone(), 2.0);
1029 derived.set_spot_prices(spot, vec![], 10, true);
1030
1031 let mut depths = crate::derived::types::PoolDepths::default();
1033 depths.insert(key.clone(), BigUint::from(2_000_000u64));
1034 derived.set_pool_depths(depths, vec![], 10, true);
1035
1036 let mut token_prices = TokenGasPrices::new();
1039 token_prices.insert(
1040 tok_in.address.clone(),
1041 Price { numerator: BigUint::from(2000u64), denominator: BigUint::from(1u64) },
1042 );
1043 derived.set_token_prices(token_prices, vec![], 10, true);
1044
1045 let sim = make_mock_sim();
1046 let result =
1047 <DepthAndPrice as crate::graph::EdgeWeightFromSimAndDerived>::from_sim_and_derived(
1048 &sim, &key.0, &tok_in, &tok_out, &derived,
1049 );
1050
1051 let data = result.expect("should return Some when all data present");
1052 assert!((data.spot_price - 2.0).abs() < f64::EPSILON, "spot price should be 2.0");
1053 assert!(
1055 (data.depth - 1000.0).abs() < f64::EPSILON,
1056 "depth should be 1000.0 ETH, got {}",
1057 data.depth
1058 );
1059 }
1060
1061 #[test]
1062 fn test_from_sim_and_derived_normalizes_depth_fractional_price() {
1063 let key = pair_key("pool1", 0x01, 0x02);
1064 let tok_in = token(0x01, "A");
1065 let tok_out = token(0x02, "B");
1066
1067 let mut derived = DerivedData::new();
1068
1069 let mut spot = crate::derived::types::SpotPrices::default();
1070 spot.insert(key.clone(), 0.5);
1071 derived.set_spot_prices(spot, vec![], 10, true);
1072
1073 let mut depths = crate::derived::types::PoolDepths::default();
1075 depths.insert(key.clone(), BigUint::from(500u64));
1076 derived.set_pool_depths(depths, vec![], 10, true);
1077
1078 let mut token_prices = TokenGasPrices::new();
1081 token_prices.insert(
1082 tok_in.address.clone(),
1083 Price { numerator: BigUint::from(3u64), denominator: BigUint::from(2u64) },
1084 );
1085 derived.set_token_prices(token_prices, vec![], 10, true);
1086
1087 let sim = make_mock_sim();
1088 let result =
1089 <DepthAndPrice as crate::graph::EdgeWeightFromSimAndDerived>::from_sim_and_derived(
1090 &sim, &key.0, &tok_in, &tok_out, &derived,
1091 );
1092
1093 let data = result.expect("should return Some when all data present");
1094 let expected_depth = 500.0 * 2.0 / 3.0;
1095 assert!(
1096 (data.depth - expected_depth).abs() < 1e-10,
1097 "depth should be {expected_depth}, got {}",
1098 data.depth
1099 );
1100 }
1101
1102 fn all_ids(paths: Vec<Path<'_, DepthAndPrice>>) -> HashSet<Vec<&str>> {
1105 paths
1106 .iter()
1107 .map(|p| {
1108 p.iter()
1109 .map(|(_, e, _)| e.component_id.as_str())
1110 .collect()
1111 })
1112 .collect()
1113 }
1114
1115 #[test]
1116 fn test_find_paths_linear_forward_and_reverse() {
1117 let (a, b, c, d) = addrs();
1118 let m = linear_graph();
1119 let g = m.graph();
1120
1121 let p = MostLiquidAlgorithm::find_paths(g, &a, &b, 1, 1, None).unwrap();
1123 assert_eq!(all_ids(p), HashSet::from([vec!["ab"]]));
1124
1125 let p = MostLiquidAlgorithm::find_paths(g, &a, &c, 1, 2, None).unwrap();
1126 assert_eq!(all_ids(p), HashSet::from([vec!["ab", "bc"]]));
1127
1128 let p = MostLiquidAlgorithm::find_paths(g, &a, &d, 1, 3, None).unwrap();
1129 assert_eq!(all_ids(p), HashSet::from([vec!["ab", "bc", "cd"]]));
1130
1131 let p = MostLiquidAlgorithm::find_paths(g, &d, &a, 1, 3, None).unwrap();
1133 assert_eq!(all_ids(p), HashSet::from([vec!["cd", "bc", "ab"]]));
1134 }
1135
1136 #[test]
1137 fn test_find_paths_respects_hop_bounds() {
1138 let (a, _, c, d) = addrs();
1139 let m = linear_graph();
1140 let g = m.graph();
1141
1142 assert!(MostLiquidAlgorithm::find_paths(g, &a, &d, 1, 2, None)
1144 .unwrap()
1145 .is_empty());
1146
1147 assert!(MostLiquidAlgorithm::find_paths(g, &a, &c, 3, 3, None)
1149 .unwrap()
1150 .is_empty());
1151 }
1152
1153 #[test]
1154 fn test_find_paths_parallel_pools() {
1155 let (a, b, c, _) = addrs();
1156 let m = parallel_graph();
1157 let g = m.graph();
1158
1159 let p = MostLiquidAlgorithm::find_paths(g, &a, &b, 1, 1, None).unwrap();
1161 assert_eq!(all_ids(p), HashSet::from([vec!["ab1"], vec!["ab2"], vec!["ab3"]]));
1162
1163 let p = MostLiquidAlgorithm::find_paths(g, &a, &c, 1, 2, None).unwrap();
1165 assert_eq!(
1166 all_ids(p),
1167 HashSet::from([
1168 vec!["ab1", "bc1"],
1169 vec!["ab1", "bc2"],
1170 vec!["ab2", "bc1"],
1171 vec!["ab2", "bc2"],
1172 vec!["ab3", "bc1"],
1173 vec!["ab3", "bc2"],
1174 ])
1175 );
1176 }
1177
1178 #[test]
1179 fn test_find_paths_diamond_multiple_routes() {
1180 let (a, _, _, d) = addrs();
1181 let m = diamond_graph();
1182 let g = m.graph();
1183
1184 let p = MostLiquidAlgorithm::find_paths(g, &a, &d, 1, 2, None).unwrap();
1186 assert_eq!(all_ids(p), HashSet::from([vec!["ab", "bd"], vec!["ac", "cd"]]));
1187 }
1188
1189 #[test]
1190 fn test_find_paths_no_intermediate_cycles() {
1191 let (a, b, _, _) = addrs();
1192 let m = linear_graph();
1193 let g = m.graph();
1194
1195 let p = MostLiquidAlgorithm::find_paths(g, &a, &b, 1, 3, None).unwrap();
1200 assert_eq!(all_ids(p), HashSet::from([vec!["ab"]]));
1201 }
1202
1203 #[test]
1204 fn test_find_paths_cyclic_same_source_dest() {
1205 let (a, _, _, _) = addrs();
1206 let m = parallel_graph();
1208 let g = m.graph();
1209
1210 let p = MostLiquidAlgorithm::find_paths(g, &a, &a, 2, 2, None).unwrap();
1213 assert_eq!(
1214 all_ids(p),
1215 HashSet::from([
1216 vec!["ab1", "ab1"],
1217 vec!["ab1", "ab2"],
1218 vec!["ab1", "ab3"],
1219 vec!["ab2", "ab1"],
1220 vec!["ab2", "ab2"],
1221 vec!["ab2", "ab3"],
1222 vec!["ab3", "ab1"],
1223 vec!["ab3", "ab2"],
1224 vec!["ab3", "ab3"],
1225 ])
1226 );
1227 }
1228
1229 #[rstest]
1230 #[case::source_not_in_graph(false, true)]
1231 #[case::dest_not_in_graph(true, false)]
1232 fn test_find_paths_token_not_in_graph(#[case] from_exists: bool, #[case] to_exists: bool) {
1233 let (a, b, _, _) = addrs();
1235 let non_existent = addr(0x99);
1236 let m = linear_graph();
1237 let g = m.graph();
1238
1239 let from = if from_exists { a } else { non_existent.clone() };
1240 let to = if to_exists { b } else { non_existent };
1241
1242 let result = MostLiquidAlgorithm::find_paths(g, &from, &to, 1, 3, None);
1243
1244 assert!(matches!(result, Err(AlgorithmError::NoPath { .. })));
1245 }
1246
1247 #[rstest]
1248 #[case::min_greater_than_max(3, 1)]
1249 #[case::min_hops_zero(0, 1)]
1250 fn test_find_paths_invalid_configuration(#[case] min_hops: usize, #[case] max_hops: usize) {
1251 let (a, b, _, _) = addrs();
1252 let m = linear_graph();
1253 let g = m.graph();
1254
1255 assert!(matches!(
1256 MostLiquidAlgorithm::find_paths(g, &a, &b, min_hops, max_hops, None)
1257 .err()
1258 .unwrap(),
1259 AlgorithmError::InvalidConfiguration { reason: _ }
1260 ));
1261 }
1262
1263 #[test]
1264 fn test_find_paths_bfs_ordering() {
1265 let (a, b, c, d) = addrs();
1270 let e = addr(0x0E);
1271 let mut m = PetgraphStableDiGraphManager::<DepthAndPrice>::new();
1272 let mut t = HashMap::new();
1273 t.insert("ae".into(), vec![a.clone(), e.clone()]);
1274 t.insert("ab".into(), vec![a.clone(), b.clone()]);
1275 t.insert("be".into(), vec![b, e.clone()]);
1276 t.insert("ac".into(), vec![a.clone(), c.clone()]);
1277 t.insert("cd".into(), vec![c, d.clone()]);
1278 t.insert("de".into(), vec![d, e.clone()]);
1279 m.initialize_graph(&t);
1280 let g = m.graph();
1281
1282 let p = MostLiquidAlgorithm::find_paths(g, &a, &e, 1, 3, None).unwrap();
1283
1284 assert_eq!(p.len(), 3, "Expected 3 paths total");
1286 assert_eq!(p[0].len(), 1, "First path should be 1-hop");
1287 assert_eq!(p[1].len(), 2, "Second path should be 2-hop");
1288 assert_eq!(p[2].len(), 3, "Third path should be 3-hop");
1289 }
1290
1291 #[test]
1299 fn test_simulate_path_single_hop() {
1300 let token_a = token(0x01, "A");
1301 let token_b = token(0x02, "B");
1302
1303 let (market, manager) =
1304 setup_market_weighted(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1305
1306 let paths = MostLiquidAlgorithm::find_paths(
1307 manager.graph(),
1308 &token_a.address,
1309 &token_b.address,
1310 1,
1311 1,
1312 None,
1313 )
1314 .unwrap();
1315 let path = paths.into_iter().next().unwrap();
1316
1317 let result = MostLiquidAlgorithm::simulate_path(
1318 &path,
1319 market_read(&market).base_market_state(),
1320 None,
1321 BigUint::from(100u64),
1322 )
1323 .unwrap();
1324
1325 assert_eq!(result.route().swaps().len(), 1);
1326 assert_eq!(*result.route().swaps()[0].amount_in(), BigUint::from(100u64));
1327 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(200u64)); assert_eq!(result.route().swaps()[0].component_id(), "pool1");
1329 }
1330
1331 #[test]
1332 fn test_simulate_path_multi_hop_chains_amounts() {
1333 let token_a = token(0x01, "A");
1334 let token_b = token(0x02, "B");
1335 let token_c = token(0x03, "C");
1336
1337 let (market, manager) = setup_market_weighted(vec![
1338 ("pool1", &token_a, &token_b, MockProtocolSim::new(2.0)),
1339 ("pool2", &token_b, &token_c, MockProtocolSim::new(3.0)),
1340 ]);
1341
1342 let paths = MostLiquidAlgorithm::find_paths(
1343 manager.graph(),
1344 &token_a.address,
1345 &token_c.address,
1346 2,
1347 2,
1348 None,
1349 )
1350 .unwrap();
1351 let path = paths.into_iter().next().unwrap();
1352
1353 let result = MostLiquidAlgorithm::simulate_path(
1354 &path,
1355 market_read(&market).base_market_state(),
1356 None,
1357 BigUint::from(10u64),
1358 )
1359 .unwrap();
1360
1361 assert_eq!(result.route().swaps().len(), 2);
1362 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(20u64));
1364 assert_eq!(*result.route().swaps()[1].amount_in(), BigUint::from(20u64));
1366 assert_eq!(*result.route().swaps()[1].amount_out(), BigUint::from(60u64));
1367 }
1368
1369 #[test]
1370 fn test_simulate_path_same_pool_twice_uses_updated_state() {
1371 let token_a = token(0x01, "A");
1374 let token_b = token(0x02, "B");
1375
1376 let (market, manager) =
1377 setup_market_weighted(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1378
1379 let paths = MostLiquidAlgorithm::find_paths(
1382 manager.graph(),
1383 &token_a.address,
1384 &token_a.address,
1385 2,
1386 2,
1387 None,
1388 )
1389 .unwrap();
1390
1391 assert_eq!(paths.len(), 1);
1393 let path = paths[0].clone();
1394
1395 let result = MostLiquidAlgorithm::simulate_path(
1396 &path,
1397 market_read(&market).base_market_state(),
1398 None,
1399 BigUint::from(10u64),
1400 )
1401 .unwrap();
1402
1403 assert_eq!(result.route().swaps().len(), 2);
1404 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(20u64));
1406 assert_eq!(*result.route().swaps()[1].amount_out(), BigUint::from(6u64));
1408 }
1409
1410 #[test]
1411 fn test_simulate_path_missing_token_returns_data_not_found() {
1412 let token_a = token(0x01, "A");
1413 let token_b = token(0x02, "B");
1414 let token_c = token(0x03, "C");
1415
1416 let (market, _) =
1417 setup_market_weighted(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1418 let market = market_read(&market);
1419
1420 let mut topology = market.component_topology();
1422 topology
1423 .insert("pool2".to_string(), vec![token_b.address.clone(), token_c.address.clone()]);
1424 let mut manager = PetgraphStableDiGraphManager::default();
1425 manager.initialize_graph(&topology);
1426
1427 let graph = manager.graph();
1428 let paths =
1429 MostLiquidAlgorithm::find_paths(graph, &token_a.address, &token_c.address, 2, 2, None)
1430 .unwrap();
1431 let path = paths.into_iter().next().unwrap();
1432
1433 let result = MostLiquidAlgorithm::simulate_path(
1434 &path,
1435 market.base_market_state(),
1436 None,
1437 BigUint::from(100u64),
1438 );
1439 assert!(matches!(result, Err(AlgorithmError::DataNotFound { kind: "token", .. })));
1440 }
1441
1442 #[test]
1443 fn test_simulate_path_missing_component_returns_data_not_found() {
1444 let token_a = token(0x01, "A");
1445 let token_b = token(0x02, "B");
1446 let (market, manager) =
1447 setup_market_weighted(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1448
1449 let mut market_write = market.try_write().unwrap();
1451 market_write.remove_components([&"pool1".to_string()]);
1452 drop(market_write);
1453
1454 let graph = manager.graph();
1455 let paths =
1456 MostLiquidAlgorithm::find_paths(graph, &token_a.address, &token_b.address, 1, 1, None)
1457 .unwrap();
1458 let path = paths.into_iter().next().unwrap();
1459
1460 let result = MostLiquidAlgorithm::simulate_path(
1461 &path,
1462 market_read(&market).base_market_state(),
1463 None,
1464 BigUint::from(100u64),
1465 );
1466 assert!(matches!(result, Err(AlgorithmError::DataNotFound { kind: "component", .. })));
1467 }
1468
1469 #[test]
1472 fn test_connector_tokens_blocks_disallowed_intermediate() {
1473 let (a, b, c, d) = addrs();
1475 let m = diamond_graph();
1476 let g = m.graph();
1477 let allowed: HashSet<Address> = [c.clone()].into();
1478 let paths = MostLiquidAlgorithm::find_paths(g, &a, &d, 1, 2, Some(&allowed)).unwrap();
1479 let intermediates: HashSet<&Address> = paths
1480 .iter()
1481 .flat_map(|p| p.iter().map(|(node, _, _)| node))
1482 .filter(|addr| *addr != &a && *addr != &d)
1483 .collect();
1484 assert!(!intermediates.contains(&b), "B should be blocked");
1486 assert!(intermediates.contains(&c), "C should be allowed");
1487 }
1488
1489 #[test]
1490 fn test_connector_tokens_allows_endpoints_even_if_not_listed() {
1491 let (a, b, _, _) = addrs();
1493 let m = linear_graph();
1494 let g = m.graph();
1495 let allowed: HashSet<Address> = HashSet::new(); let paths = MostLiquidAlgorithm::find_paths(g, &a, &b, 1, 1, Some(&allowed)).unwrap();
1498 assert!(!paths.is_empty(), "1-hop direct route should survive empty allowlist");
1499 }
1500
1501 #[test]
1502 fn test_connector_tokens_none_is_unrestricted() {
1503 let (a, b, c, d) = addrs();
1505 let m = diamond_graph();
1506 let g = m.graph();
1507 let paths = MostLiquidAlgorithm::find_paths(g, &a, &d, 1, 2, None).unwrap();
1508 let intermediates: HashSet<&Address> = paths
1509 .iter()
1510 .flat_map(|p| p.iter().map(|(node, _, _)| node))
1511 .filter(|addr| *addr != &a && *addr != &d)
1512 .collect();
1513 assert!(intermediates.contains(&b), "B should appear with no restriction");
1514 assert!(intermediates.contains(&c), "C should appear with no restriction");
1515 }
1516
1517 #[tokio::test]
1520 async fn test_find_best_route_single_path() {
1521 let token_a = token(0x01, "A");
1522 let token_b = token(0x02, "B");
1523
1524 let (market, manager) =
1525 setup_market_weighted(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1526
1527 let algorithm = MostLiquidAlgorithm::with_config(
1528 AlgorithmConfig::new(1, 1, Duration::from_millis(100), None).unwrap(),
1529 )
1530 .unwrap();
1531 let order = order(&token_a, &token_b, ONE_ETH, OrderSide::Sell);
1532 let result = algorithm
1533 .find_best_route(manager.graph(), market, None, None, &order)
1534 .await
1535 .unwrap();
1536
1537 assert_eq!(result.route().swaps().len(), 1);
1538 assert_eq!(*result.route().swaps()[0].amount_in(), BigUint::from(ONE_ETH));
1539 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(ONE_ETH * 2));
1540 }
1541
1542 #[tokio::test]
1543 async fn test_find_best_route_ranks_by_net_amount_out() {
1544 let token_a = token(0x01, "A");
1555 let token_b = token(0x02, "B");
1556
1557 let (market, manager) = setup_market_weighted(vec![
1558 ("best", &token_a, &token_b, MockProtocolSim::new(3.0).with_gas(10)),
1559 ("low_out", &token_a, &token_b, MockProtocolSim::new(2.0).with_gas(5)),
1560 ("high_gas", &token_a, &token_b, MockProtocolSim::new(4.0).with_gas(30)),
1561 ]);
1562
1563 let algorithm = MostLiquidAlgorithm::with_config(
1564 AlgorithmConfig::new(1, 1, Duration::from_millis(100), None).unwrap(),
1565 )
1566 .unwrap();
1567 let order = order(&token_a, &token_b, 1000, OrderSide::Sell);
1568
1569 let derived = setup_derived_with_token_prices(std::slice::from_ref(&token_b.address));
1571
1572 let result = algorithm
1573 .find_best_route(manager.graph(), market, None, Some(derived), &order)
1574 .await
1575 .unwrap();
1576
1577 assert_eq!(result.route().swaps().len(), 1);
1579 assert_eq!(result.route().swaps()[0].component_id(), "best");
1580 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(3000u64));
1581 assert_eq!(result.net_amount_out(), &BigInt::from(2000)); }
1583
1584 #[tokio::test]
1585 async fn test_find_best_route_no_path_returns_error() {
1586 let token_a = token(0x01, "A");
1587 let token_b = token(0x02, "B");
1588 let token_c = token(0x03, "C"); let (market, manager) =
1591 setup_market_weighted(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1592
1593 let algorithm = MostLiquidAlgorithm::new();
1594 let order = order(&token_a, &token_c, ONE_ETH, OrderSide::Sell);
1595
1596 let result = algorithm
1597 .find_best_route(manager.graph(), market, None, None, &order)
1598 .await;
1599 assert!(matches!(result, Err(AlgorithmError::NoPath { .. })));
1600 }
1601
1602 #[tokio::test]
1603 async fn test_find_best_route_multi_hop() {
1604 let token_a = token(0x01, "A");
1605 let token_b = token(0x02, "B");
1606 let token_c = token(0x03, "C");
1607
1608 let (market, manager) = setup_market_weighted(vec![
1609 ("pool1", &token_a, &token_b, MockProtocolSim::new(2.0)),
1610 ("pool2", &token_b, &token_c, MockProtocolSim::new(3.0)),
1611 ]);
1612
1613 let algorithm = MostLiquidAlgorithm::with_config(
1614 AlgorithmConfig::new(1, 2, Duration::from_millis(100), None).unwrap(),
1615 )
1616 .unwrap();
1617 let order = order(&token_a, &token_c, ONE_ETH, OrderSide::Sell);
1618
1619 let result = algorithm
1620 .find_best_route(manager.graph(), market, None, None, &order)
1621 .await
1622 .unwrap();
1623
1624 assert_eq!(result.route().swaps().len(), 2);
1626 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(ONE_ETH * 2));
1627 assert_eq!(result.route().swaps()[0].component_id(), "pool1".to_string());
1628 assert_eq!(*result.route().swaps()[1].amount_out(), BigUint::from(ONE_ETH * 2 * 3));
1629 assert_eq!(result.route().swaps()[1].component_id(), "pool2".to_string());
1630 }
1631
1632 #[tokio::test]
1633 async fn test_find_best_route_skips_paths_without_edge_weights() {
1634 let token_a = token(0x01, "A");
1636 let token_b = token(0x02, "B");
1637
1638 let mut market = MarketState::new();
1640 let pool1_state = MockProtocolSim::new(2.0);
1641 let pool2_state = MockProtocolSim::new(3.0); let pool1_comp = component("pool1", &[token_a.clone(), token_b.clone()]);
1644 let pool2_comp = component("pool2", &[token_a.clone(), token_b.clone()]);
1645
1646 market.update_gas_price(BlockGasPrice {
1648 block_number: 1,
1649 block_hash: Default::default(),
1650 block_timestamp: 0,
1651 pricing: GasPrice::Legacy { gas_price: BigUint::from(1u64) },
1652 });
1653
1654 market.upsert_components(vec![pool1_comp, pool2_comp]);
1656
1657 market.update_states(vec![
1659 ("pool1".to_string(), Box::new(pool1_state.clone()) as Box<dyn ProtocolSim>),
1660 ("pool2".to_string(), Box::new(pool2_state) as Box<dyn ProtocolSim>),
1661 ]);
1662
1663 market.upsert_tokens(vec![token_a.clone(), token_b.clone()]);
1665
1666 let mut manager = PetgraphStableDiGraphManager::default();
1668 manager.initialize_graph(&market.component_topology());
1669
1670 let weight = DepthAndPrice::from_protocol_sim(&pool1_state, &token_a, &token_b).unwrap();
1672 manager
1673 .set_edge_weight(
1674 &"pool1".to_string(),
1675 &token_a.address,
1676 &token_b.address,
1677 weight,
1678 false,
1679 )
1680 .unwrap();
1681
1682 let algorithm = MostLiquidAlgorithm::with_config(
1684 AlgorithmConfig::new(1, 1, Duration::from_millis(100), None).unwrap(),
1685 )
1686 .unwrap();
1687 let order = order(&token_a, &token_b, ONE_ETH, OrderSide::Sell);
1688 let market = wrap_market(market);
1689 let result = algorithm
1690 .find_best_route(manager.graph(), market, None, None, &order)
1691 .await
1692 .unwrap();
1693
1694 assert_eq!(result.route().swaps().len(), 1);
1696 assert_eq!(result.route().swaps()[0].component_id(), "pool1");
1697 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(ONE_ETH * 2));
1698 }
1699
1700 #[tokio::test]
1701 async fn test_find_best_route_no_scorable_paths() {
1702 let token_a = token(0x01, "A");
1704 let token_b = token(0x02, "B");
1705
1706 let mut market = MarketState::new();
1707 let pool_state = MockProtocolSim::new(2.0);
1708 let pool_comp = component("pool1", &[token_a.clone(), token_b.clone()]);
1709
1710 market.update_gas_price(BlockGasPrice {
1712 block_number: 1,
1713 block_hash: Default::default(),
1714 block_timestamp: 0,
1715 pricing: GasPrice::Eip1559 {
1716 base_fee_per_gas: BigUint::from(1u64),
1717 max_priority_fee_per_gas: BigUint::from(0u64),
1718 },
1719 });
1720
1721 market.upsert_components(vec![pool_comp]);
1722 market.update_states(vec![(
1723 "pool1".to_string(),
1724 Box::new(pool_state) as Box<dyn ProtocolSim>,
1725 )]);
1726 market.upsert_tokens(vec![token_a.clone(), token_b.clone()]);
1727
1728 let mut manager = PetgraphStableDiGraphManager::default();
1730 manager.initialize_graph(&market.component_topology());
1731
1732 let algorithm = MostLiquidAlgorithm::new();
1733 let order = order(&token_a, &token_b, ONE_ETH, OrderSide::Sell);
1734 let market = wrap_market(market);
1735
1736 let result = algorithm
1737 .find_best_route(manager.graph(), market, None, None, &order)
1738 .await;
1739 assert!(matches!(
1740 result,
1741 Err(AlgorithmError::NoPath { reason: NoPathReason::NoScorablePaths, .. })
1742 ));
1743 }
1744
1745 #[tokio::test]
1746 async fn test_find_best_route_gas_exceeds_output_returns_negative_net() {
1747 let token_a = token(0x01, "A");
1748 let token_b = token(0x02, "B");
1749
1750 let (market, manager) =
1751 setup_market_weighted(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1752 let mut market_write = market.try_write().unwrap();
1753
1754 market_write.update_gas_price(BlockGasPrice {
1757 block_number: 1,
1758 block_hash: Default::default(),
1759 block_timestamp: 0,
1760 pricing: GasPrice::Eip1559 {
1761 base_fee_per_gas: BigUint::from(1_000_000u64),
1762 max_priority_fee_per_gas: BigUint::from(1_000_000u64),
1763 },
1764 });
1765 drop(market_write); let algorithm = MostLiquidAlgorithm::new();
1768 let order = order(&token_a, &token_b, 1, OrderSide::Sell); let derived = setup_derived_with_token_prices(std::slice::from_ref(&token_b.address));
1772
1773 let result = algorithm
1775 .find_best_route(manager.graph(), market, None, Some(derived), &order)
1776 .await
1777 .expect("should return route even with negative net_amount_out");
1778
1779 assert_eq!(result.route().swaps().len(), 1);
1781 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(2u64)); let expected_net = BigInt::from(2) - BigInt::from(100_000_000_000u64);
1785 assert_eq!(result.net_amount_out(), &expected_net);
1786 }
1787
1788 #[tokio::test]
1789 async fn test_find_best_route_insufficient_liquidity() {
1790 let token_a = token(0x01, "A");
1792 let token_b = token(0x02, "B");
1793
1794 let (market, manager) = setup_market_weighted(vec![(
1795 "pool1",
1796 &token_a,
1797 &token_b,
1798 MockProtocolSim::new(2.0).with_liquidity(1000),
1799 )]);
1800
1801 let algorithm = MostLiquidAlgorithm::new();
1802 let order = order(&token_a, &token_b, ONE_ETH, OrderSide::Sell); let result = algorithm
1805 .find_best_route(manager.graph(), market, None, None, &order)
1806 .await;
1807 assert!(matches!(result, Err(AlgorithmError::InsufficientLiquidity)));
1808 }
1809
1810 #[tokio::test]
1811 async fn test_find_best_route_missing_gas_price_returns_error() {
1812 let token_a = token(0x01, "A");
1814 let token_b = token(0x02, "B");
1815
1816 let mut market = MarketState::new();
1817 let pool_state = MockProtocolSim::new(2.0);
1818 let pool_comp = component("pool1", &[token_a.clone(), token_b.clone()]);
1819
1820 market.upsert_components(vec![pool_comp]);
1822 market.update_states(vec![(
1823 "pool1".to_string(),
1824 Box::new(pool_state.clone()) as Box<dyn ProtocolSim>,
1825 )]);
1826 market.upsert_tokens(vec![token_a.clone(), token_b.clone()]);
1827
1828 let mut manager = PetgraphStableDiGraphManager::default();
1830 manager.initialize_graph(&market.component_topology());
1831 let weight = DepthAndPrice::from_protocol_sim(&pool_state, &token_a, &token_b).unwrap();
1832 manager
1833 .set_edge_weight(
1834 &"pool1".to_string(),
1835 &token_a.address,
1836 &token_b.address,
1837 weight,
1838 false,
1839 )
1840 .unwrap();
1841
1842 let algorithm = MostLiquidAlgorithm::new();
1843 let order = order(&token_a, &token_b, ONE_ETH, OrderSide::Sell);
1844 let market = wrap_market(market);
1845
1846 let result = algorithm
1847 .find_best_route(manager.graph(), market, None, None, &order)
1848 .await;
1849
1850 assert!(matches!(result, Err(AlgorithmError::DataNotFound { kind: "gas price", .. })));
1852 }
1853
1854 #[tokio::test]
1855 async fn test_find_best_route_circular_arbitrage() {
1856 let token_a = token(0x01, "A");
1857 let token_b = token(0x02, "B");
1858
1859 let (market, manager) =
1862 setup_market_weighted(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1863
1864 let algorithm = MostLiquidAlgorithm::with_config(
1866 AlgorithmConfig::new(2, 2, Duration::from_millis(100), None).unwrap(),
1867 )
1868 .unwrap();
1869
1870 let order = order(&token_a, &token_a, 100, OrderSide::Sell);
1872
1873 let result = algorithm
1874 .find_best_route(manager.graph(), market, None, None, &order)
1875 .await
1876 .unwrap();
1877
1878 assert_eq!(result.route().swaps().len(), 2, "Should have 2 swaps for circular route");
1880
1881 assert_eq!(*result.route().swaps()[0].token_in(), token_a.address);
1883 assert_eq!(*result.route().swaps()[0].token_out(), token_b.address);
1884 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(200u64));
1885
1886 assert_eq!(*result.route().swaps()[1].token_in(), token_b.address);
1888 assert_eq!(*result.route().swaps()[1].token_out(), token_a.address);
1889 assert_eq!(*result.route().swaps()[1].amount_out(), BigUint::from(66u64));
1890
1891 assert_eq!(result.route().swaps()[0].token_in(), result.route().swaps()[1].token_out());
1893 }
1894
1895 #[tokio::test]
1896 async fn test_find_best_route_respects_min_hops() {
1897 let token_a = token(0x01, "A");
1900 let token_b = token(0x02, "B");
1901 let token_c = token(0x03, "C");
1902
1903 let (market, manager) = setup_market_weighted(vec![
1904 ("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)), ]);
1909
1910 let algorithm = MostLiquidAlgorithm::with_config(
1912 AlgorithmConfig::new(2, 3, Duration::from_millis(100), None).unwrap(),
1913 )
1914 .unwrap();
1915 let order = order(&token_a, &token_b, 100, OrderSide::Sell);
1916
1917 let derived = setup_derived_with_token_prices(std::slice::from_ref(&token_b.address));
1920
1921 let result = algorithm
1922 .find_best_route(manager.graph(), market, None, Some(derived), &order)
1923 .await
1924 .unwrap();
1925
1926 assert_eq!(result.route().swaps().len(), 2, "Should use 2-hop path due to min_hops=2");
1928 assert_eq!(result.route().swaps()[0].component_id(), "pool_ac");
1929 assert_eq!(result.route().swaps()[1].component_id(), "pool_cb");
1930 }
1931
1932 #[tokio::test]
1933 async fn test_find_best_route_respects_max_hops() {
1934 let token_a = token(0x01, "A");
1937 let token_b = token(0x02, "B");
1938 let token_c = token(0x03, "C");
1939
1940 let (market, manager) = setup_market_weighted(vec![
1941 ("pool_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
1942 ("pool_bc", &token_b, &token_c, MockProtocolSim::new(3.0)),
1943 ]);
1944
1945 let algorithm = MostLiquidAlgorithm::with_config(
1947 AlgorithmConfig::new(1, 1, Duration::from_millis(100), None).unwrap(),
1948 )
1949 .unwrap();
1950 let order = order(&token_a, &token_c, 100, OrderSide::Sell);
1951
1952 let result = algorithm
1953 .find_best_route(manager.graph(), market, None, None, &order)
1954 .await;
1955 assert!(
1956 matches!(result, Err(AlgorithmError::NoPath { .. })),
1957 "Should return NoPath when max_hops is insufficient"
1958 );
1959 }
1960
1961 #[tokio::test]
1962 async fn test_find_best_route_timeout_returns_best_so_far() {
1963 let token_a = token(0x01, "A");
1967 let token_b = token(0x02, "B");
1968
1969 let (market, manager) = setup_market_weighted(vec![
1971 ("pool1", &token_a, &token_b, MockProtocolSim::new(1.0)),
1972 ("pool2", &token_a, &token_b, MockProtocolSim::new(2.0)),
1973 ("pool3", &token_a, &token_b, MockProtocolSim::new(3.0)),
1974 ("pool4", &token_a, &token_b, MockProtocolSim::new(4.0)),
1975 ("pool5", &token_a, &token_b, MockProtocolSim::new(5.0)),
1976 ]);
1977
1978 let algorithm = MostLiquidAlgorithm::with_config(
1980 AlgorithmConfig::new(1, 1, Duration::from_millis(0), None).unwrap(),
1981 )
1982 .unwrap();
1983 let order = order(&token_a, &token_b, 100, OrderSide::Sell);
1984
1985 let result = algorithm
1986 .find_best_route(manager.graph(), market, None, None, &order)
1987 .await;
1988
1989 match result {
1994 Ok(r) => {
1995 assert_eq!(r.route().swaps().len(), 1);
1997 }
1998 Err(AlgorithmError::Timeout { .. }) => {
1999 }
2001 Err(e) => panic!("Unexpected error: {:?}", e),
2002 }
2003 }
2004
2005 #[rstest::rstest]
2008 #[case::default_config(1, 3, 50)]
2009 #[case::single_hop_only(1, 1, 100)]
2010 #[case::multi_hop_min(2, 5, 200)]
2011 #[case::zero_timeout(1, 3, 0)]
2012 #[case::large_values(10, 100, 10000)]
2013 fn test_algorithm_config_getters(
2014 #[case] min_hops: usize,
2015 #[case] max_hops: usize,
2016 #[case] timeout_ms: u64,
2017 ) {
2018 use crate::algorithm::Algorithm;
2019
2020 let algorithm = MostLiquidAlgorithm::with_config(
2021 AlgorithmConfig::new(min_hops, max_hops, Duration::from_millis(timeout_ms), None)
2022 .unwrap(),
2023 )
2024 .unwrap();
2025
2026 assert_eq!(algorithm.max_hops, max_hops);
2027 assert_eq!(algorithm.timeout, Duration::from_millis(timeout_ms));
2028 assert_eq!(algorithm.name(), "most_liquid");
2029 }
2030
2031 #[test]
2032 fn test_algorithm_default_config() {
2033 use crate::algorithm::Algorithm;
2034
2035 let algorithm = MostLiquidAlgorithm::new();
2036
2037 assert_eq!(algorithm.max_hops, 3);
2038 assert_eq!(algorithm.timeout, Duration::from_millis(500));
2039 assert_eq!(algorithm.name(), "most_liquid");
2040 }
2041
2042 #[tokio::test]
2045 async fn test_find_best_route_respects_max_routes_cap() {
2046 let token_a = token(0x01, "A");
2059 let token_b = token(0x02, "B");
2060
2061 let (market, manager) = setup_market_weighted(vec![
2062 ("pool1", &token_a, &token_b, MockProtocolSim::new(4.0).with_liquidity(1_000_000)),
2063 ("pool2", &token_a, &token_b, MockProtocolSim::new(3.0).with_liquidity(2_000_000)),
2064 ("pool3", &token_a, &token_b, MockProtocolSim::new(2.0).with_liquidity(3_000_000)),
2065 ("pool4", &token_a, &token_b, MockProtocolSim::new(1.0).with_liquidity(4_000_000)),
2066 ]);
2067
2068 let algorithm = MostLiquidAlgorithm::with_config(
2070 AlgorithmConfig::new(1, 1, Duration::from_millis(100), Some(2)).unwrap(),
2071 )
2072 .unwrap();
2073 let order = order(&token_a, &token_b, 1000, OrderSide::Sell);
2074 let result = algorithm
2075 .find_best_route(manager.graph(), market, None, None, &order)
2076 .await
2077 .unwrap();
2078
2079 assert_eq!(result.route().swaps().len(), 1);
2083 assert_eq!(result.route().swaps()[0].component_id(), "pool3");
2084 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(2000u64));
2085 }
2086
2087 #[tokio::test]
2088 async fn test_find_best_route_no_cap_when_max_routes_is_none() {
2089 let token_a = token(0x01, "A");
2091 let token_b = token(0x02, "B");
2092
2093 let (market, manager) = setup_market_weighted(vec![
2094 ("pool1", &token_a, &token_b, MockProtocolSim::new(4.0).with_liquidity(1_000_000)),
2095 ("pool2", &token_a, &token_b, MockProtocolSim::new(3.0).with_liquidity(2_000_000)),
2096 ("pool3", &token_a, &token_b, MockProtocolSim::new(2.0).with_liquidity(3_000_000)),
2097 ("pool4", &token_a, &token_b, MockProtocolSim::new(1.0).with_liquidity(4_000_000)),
2098 ]);
2099
2100 let algorithm = MostLiquidAlgorithm::with_config(
2101 AlgorithmConfig::new(1, 1, Duration::from_millis(100), None).unwrap(),
2102 )
2103 .unwrap();
2104 let order = order(&token_a, &token_b, 1000, OrderSide::Sell);
2105 let result = algorithm
2106 .find_best_route(manager.graph(), market, None, None, &order)
2107 .await
2108 .unwrap();
2109
2110 assert_eq!(result.route().swaps().len(), 1);
2112 assert_eq!(result.route().swaps()[0].component_id(), "pool1");
2113 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(4000u64));
2114 }
2115
2116 #[test]
2117 fn test_algorithm_config_rejects_zero_max_routes() {
2118 let result = AlgorithmConfig::new(1, 3, Duration::from_millis(100), Some(0));
2119 assert!(matches!(
2120 result,
2121 Err(AlgorithmError::InvalidConfiguration { reason }) if reason.contains("max_routes must be at least 1")
2122 ));
2123 }
2124
2125 #[test]
2126 fn test_algorithm_config_rejects_zero_min_hops() {
2127 let result = AlgorithmConfig::new(0, 3, Duration::from_millis(100), None);
2128 assert!(matches!(
2129 result,
2130 Err(AlgorithmError::InvalidConfiguration { reason }) if reason.contains("min_hops must be at least 1")
2131 ));
2132 }
2133
2134 #[test]
2135 fn test_algorithm_config_rejects_min_greater_than_max() {
2136 let result = AlgorithmConfig::new(5, 3, Duration::from_millis(100), None);
2137 assert!(matches!(
2138 result,
2139 Err(AlgorithmError::InvalidConfiguration { reason }) if reason.contains("cannot exceed")
2140 ));
2141 }
2142}