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 let mut validation_failures = 0usize;
592
593 let mut best: Option<RouteResult> = None;
595 let timeout_ms = self.timeout.as_millis() as u64;
596
597 for (edge_path, _) in scored_paths {
598 let elapsed_ms = start.elapsed().as_millis() as u64;
600 if elapsed_ms > timeout_ms {
601 break;
602 }
603
604 let result = match Self::simulate_path(
605 &edge_path,
606 &market,
607 token_prices.as_ref(),
608 amount_in.clone(),
609 ) {
610 Ok(r) => r,
611 Err(e) => {
612 trace!(error = %e, "simulation failed for path");
613 simulation_failures += 1;
614 continue;
615 }
616 };
617
618 if let Err(e) = result.route().validate() {
620 trace!(error = %e, "skipping invalid route");
621 validation_failures += 1;
622 continue;
623 }
624
625 if best
627 .as_ref()
628 .map(|best| result.net_amount_out() > best.net_amount_out())
629 .unwrap_or(true)
630 {
631 best = Some(result);
632 }
633
634 paths_simulated += 1;
635 }
636
637 let solve_time_ms = start.elapsed().as_millis() as u64;
639 let block_number = market
640 .last_updated()
641 .map(|b| b.number());
642 let coverage_pct = if paths_to_simulate == 0 {
644 100.0
645 } else {
646 (paths_simulated as f64 / paths_to_simulate as f64) * 100.0
647 };
648
649 counter!("algorithm.scoring_failures").increment(scoring_failures as u64);
651 counter!("algorithm.simulation_failures").increment(simulation_failures as u64);
652 counter!("algorithm.validation_failures").increment(validation_failures as u64);
653 histogram!("algorithm.simulation_coverage_pct").record(coverage_pct);
654
655 match &best {
656 Some(result) => {
657 let tokens = market.token_registry_ref();
658 let path_desc = result.route().path_description(tokens);
659 let protocols = result
660 .route()
661 .swaps()
662 .iter()
663 .map(|s| s.protocol())
664 .collect::<Vec<_>>();
665
666 let price = amount_in
667 .to_f64()
668 .filter(|&v| v > 0.0)
669 .and_then(|amt_in| {
670 result
671 .net_amount_out()
672 .to_f64()
673 .map(|amt_out| amt_out / amt_in)
674 })
675 .unwrap_or(f64::NAN);
676
677 debug!(
678 solve_time_ms,
679 block_number,
680 paths_candidates,
681 paths_to_simulate,
682 paths_simulated,
683 simulation_failures,
684 validation_failures,
685 simulation_coverage_pct = coverage_pct,
686 components_considered = component_ids.len(),
687 tokens_considered = market.token_registry_ref().len(),
688 path = %path_desc,
689 amount_in = %amount_in,
690 net_amount_out = %result.net_amount_out(),
691 price_out_per_in = price,
692 hop_count = result.route().swaps().len(),
693 protocols = ?protocols,
694 "route found"
695 );
696 }
697 None => {
698 debug!(
699 solve_time_ms,
700 block_number,
701 paths_candidates,
702 paths_to_simulate,
703 paths_simulated,
704 simulation_failures,
705 validation_failures,
706 simulation_coverage_pct = coverage_pct,
707 components_considered = component_ids.len(),
708 tokens_considered = market.token_registry_ref().len(),
709 "no viable route"
710 );
711 }
712 }
713
714 best.ok_or({
715 if solve_time_ms > timeout_ms {
716 AlgorithmError::Timeout { elapsed_ms: solve_time_ms }
717 } else {
718 AlgorithmError::InsufficientLiquidity
719 }
720 })
721 }
722
723 fn computation_requirements(&self) -> ComputationRequirements {
724 ComputationRequirements::none()
732 .allow_stale("token_prices")
733 .expect("Conflicting Computation Requirements")
734 }
735
736 fn timeout(&self) -> Duration {
737 self.timeout
738 }
739}
740
741#[cfg(test)]
742mod tests {
743 use std::collections::HashSet;
744
745 use rstest::rstest;
746 use tycho_simulation::{
747 tycho_core::simulation::protocol_sim::Price,
748 tycho_ethereum::gas::{BlockGasPrice, GasPrice},
749 };
750
751 use super::*;
752 use crate::{
753 algorithm::test_utils::{
754 addr, component,
755 fixtures::{addrs, diamond_graph, linear_graph, parallel_graph},
756 market_read, order, setup_market_weighted, token, MockProtocolSim, ONE_ETH,
757 },
758 derived::{
759 computation::{FailedItem, FailedItemError},
760 types::TokenGasPrices,
761 DerivedData,
762 },
763 graph::GraphManager,
764 types::OrderSide,
765 };
766
767 fn wrap_market(market: MarketState) -> MarketData {
768 MarketData::new(std::sync::Arc::new(tokio::sync::RwLock::new(market)))
769 }
770
771 fn setup_derived_with_token_prices(token_addresses: &[Address]) -> SharedDerivedDataRef {
776 let mut token_prices: TokenGasPrices = HashMap::new();
777 for addr in token_addresses {
778 token_prices.insert(
780 addr.clone(),
781 Price { numerator: BigUint::from(1u64), denominator: BigUint::from(1u64) },
782 );
783 }
784
785 let mut derived_data = DerivedData::new();
786 derived_data.set_token_prices(token_prices, vec![], 1, true);
787 std::sync::Arc::new(tokio::sync::RwLock::new(derived_data))
788 }
789 #[test]
792 fn test_try_score_path_calculates_correctly() {
793 let (a, b, c, _) = addrs();
794 let mut m = linear_graph();
795
796 m.set_edge_weight(&"ab".to_string(), &a, &b, DepthAndPrice::new(2.0, 1000.0), false)
798 .unwrap();
799 m.set_edge_weight(&"bc".to_string(), &b, &c, DepthAndPrice::new(0.5, 500.0), false)
800 .unwrap();
801
802 let graph = m.graph();
804 let paths = MostLiquidAlgorithm::find_paths(graph, &a, &c, 2, 2, None).unwrap();
805 assert_eq!(paths.len(), 1);
806 let path = &paths[0];
807
808 let expected = 2.0 * 0.5 * 500.0;
810 let score = MostLiquidAlgorithm::try_score_path(path).unwrap();
811 assert_eq!(score, expected, "expected {expected}, got {score}");
812 }
813
814 #[test]
815 fn test_try_score_path_empty_returns_none() {
816 let path: Path<DepthAndPrice> = Path::new();
817 assert_eq!(MostLiquidAlgorithm::try_score_path(&path), None);
818 }
819
820 #[test]
821 fn test_try_score_path_missing_weight_returns_none() {
822 let (a, b, _, _) = addrs();
823 let m = linear_graph();
824 let graph = m.graph();
825 let paths = MostLiquidAlgorithm::find_paths(graph, &a, &b, 1, 1, None).unwrap();
826 assert_eq!(paths.len(), 1);
827 assert!(MostLiquidAlgorithm::try_score_path(&paths[0]).is_none());
828 }
829
830 #[test]
831 fn test_try_score_path_circular_route() {
832 let (a, b, _, _) = addrs();
834 let mut m = linear_graph();
835
836 m.set_edge_weight(&"ab".to_string(), &a, &b, DepthAndPrice::new(2.0, 1000.0), false)
840 .unwrap();
841 m.set_edge_weight(&"ab".to_string(), &b, &a, DepthAndPrice::new(0.6, 800.0), false)
842 .unwrap();
843
844 let graph = m.graph();
845 let paths = MostLiquidAlgorithm::find_paths(graph, &a, &a, 2, 2, None).unwrap();
847
848 assert_eq!(paths.len(), 1);
850
851 let score = MostLiquidAlgorithm::try_score_path(&paths[0]).unwrap();
856 let expected = 2.0 * 0.6 * 800.0;
857 assert_eq!(score, expected, "expected {expected}, got {score}");
858 }
859
860 fn make_mock_sim() -> MockProtocolSim {
861 MockProtocolSim::new(2.0)
862 }
863
864 fn pair_key(comp: &str, b_in: u8, b_out: u8) -> (String, Address, Address) {
865 (comp.to_string(), addr(b_in), addr(b_out))
866 }
867
868 fn pair_key_str(comp: &str, b_in: u8, b_out: u8) -> String {
869 format!("{comp}/{}/{}", addr(b_in), addr(b_out))
870 }
871
872 fn make_token_prices(addresses: &[Address]) -> TokenGasPrices {
873 let mut prices = TokenGasPrices::new();
874 for addr in addresses {
875 prices.insert(
877 addr.clone(),
878 Price { numerator: BigUint::from(1u64), denominator: BigUint::from(1u64) },
879 );
880 }
881 prices
882 }
883
884 #[test]
885 fn test_from_sim_and_derived_failed_spot_price_returns_none() {
886 let key = pair_key("pool1", 0x01, 0x02);
887 let key_str = pair_key_str("pool1", 0x01, 0x02);
888 let tok_in = token(0x01, "A");
889 let tok_out = token(0x02, "B");
890
891 let mut derived = DerivedData::new();
892 derived.set_spot_prices(
894 Default::default(),
895 vec![FailedItem {
896 key: key_str,
897 error: FailedItemError::SimulationFailed("sim error".into()),
898 }],
899 10,
900 true,
901 );
902 derived.set_pool_depths(Default::default(), vec![], 10, true);
903 derived.set_token_prices(
904 make_token_prices(&[tok_in.address.clone(), tok_out.address.clone()]),
905 vec![],
906 10,
907 true,
908 );
909
910 let sim = make_mock_sim();
911 let result =
912 <DepthAndPrice as crate::graph::EdgeWeightFromSimAndDerived>::from_sim_and_derived(
913 &sim, &key.0, &tok_in, &tok_out, &derived,
914 );
915
916 assert!(result.is_none());
917 }
918
919 #[test]
920 fn test_from_sim_and_derived_failed_pool_depth_returns_none() {
921 let key = pair_key("pool1", 0x01, 0x02);
922 let key_str = pair_key_str("pool1", 0x01, 0x02);
923 let tok_in = token(0x01, "A");
924 let tok_out = token(0x02, "B");
925
926 let mut derived = DerivedData::new();
927 let mut prices = crate::derived::types::SpotPrices::default();
929 prices.insert(key.clone(), 1.5);
930 derived.set_spot_prices(prices, vec![], 10, true);
931 derived.set_pool_depths(
933 Default::default(),
934 vec![FailedItem {
935 key: key_str,
936 error: FailedItemError::SimulationFailed("depth error".into()),
937 }],
938 10,
939 true,
940 );
941 derived.set_token_prices(
942 make_token_prices(&[tok_in.address.clone(), tok_out.address.clone()]),
943 vec![],
944 10,
945 true,
946 );
947
948 let sim = make_mock_sim();
949 let result =
950 <DepthAndPrice as crate::graph::EdgeWeightFromSimAndDerived>::from_sim_and_derived(
951 &sim, &key.0, &tok_in, &tok_out, &derived,
952 );
953
954 assert!(result.is_none());
955 }
956
957 #[test]
958 fn test_from_sim_and_derived_both_failed_returns_none() {
959 let key = pair_key("pool1", 0x01, 0x02);
960 let key_str = pair_key_str("pool1", 0x01, 0x02);
961 let tok_in = token(0x01, "A");
962 let tok_out = token(0x02, "B");
963
964 let mut derived = DerivedData::new();
965 derived.set_spot_prices(
966 Default::default(),
967 vec![FailedItem {
968 key: key_str.clone(),
969 error: FailedItemError::SimulationFailed("spot error".into()),
970 }],
971 10,
972 true,
973 );
974 derived.set_pool_depths(
975 Default::default(),
976 vec![FailedItem {
977 key: key_str,
978 error: FailedItemError::SimulationFailed("depth error".into()),
979 }],
980 10,
981 true,
982 );
983 derived.set_token_prices(
984 make_token_prices(&[tok_in.address.clone(), tok_out.address.clone()]),
985 vec![],
986 10,
987 true,
988 );
989
990 let sim = make_mock_sim();
991 let result =
992 <DepthAndPrice as crate::graph::EdgeWeightFromSimAndDerived>::from_sim_and_derived(
993 &sim, &key.0, &tok_in, &tok_out, &derived,
994 );
995
996 assert!(result.is_none());
997 }
998
999 #[test]
1000 fn test_from_sim_and_derived_missing_token_price_returns_none() {
1001 let key = pair_key("pool1", 0x01, 0x02);
1002 let tok_in = token(0x01, "A");
1003 let tok_out = token(0x02, "B");
1004
1005 let mut derived = DerivedData::new();
1006 let mut prices = crate::derived::types::SpotPrices::default();
1008 prices.insert(key.clone(), 1.5);
1009 derived.set_spot_prices(prices, vec![], 10, true);
1010
1011 let mut depths = crate::derived::types::PoolDepths::default();
1012 depths.insert(key.clone(), BigUint::from(1000u64));
1013 derived.set_pool_depths(depths, vec![], 10, true);
1014
1015 let sim = make_mock_sim();
1018 let result =
1019 <DepthAndPrice as crate::graph::EdgeWeightFromSimAndDerived>::from_sim_and_derived(
1020 &sim, &key.0, &tok_in, &tok_out, &derived,
1021 );
1022
1023 assert!(
1024 result.is_none(),
1025 "should return None when token price is missing for depth normalization"
1026 );
1027 }
1028
1029 #[test]
1030 fn test_from_sim_and_derived_normalizes_depth_to_eth() {
1031 let key = pair_key("pool1", 0x01, 0x02);
1032 let tok_in = token(0x01, "A");
1033 let tok_out = token(0x02, "B");
1034
1035 let mut derived = DerivedData::new();
1036
1037 let mut spot = crate::derived::types::SpotPrices::default();
1039 spot.insert(key.clone(), 2.0);
1040 derived.set_spot_prices(spot, vec![], 10, true);
1041
1042 let mut depths = crate::derived::types::PoolDepths::default();
1044 depths.insert(key.clone(), BigUint::from(2_000_000u64));
1045 derived.set_pool_depths(depths, vec![], 10, true);
1046
1047 let mut token_prices = TokenGasPrices::new();
1050 token_prices.insert(
1051 tok_in.address.clone(),
1052 Price { numerator: BigUint::from(2000u64), denominator: BigUint::from(1u64) },
1053 );
1054 derived.set_token_prices(token_prices, vec![], 10, true);
1055
1056 let sim = make_mock_sim();
1057 let result =
1058 <DepthAndPrice as crate::graph::EdgeWeightFromSimAndDerived>::from_sim_and_derived(
1059 &sim, &key.0, &tok_in, &tok_out, &derived,
1060 );
1061
1062 let data = result.expect("should return Some when all data present");
1063 assert!((data.spot_price - 2.0).abs() < f64::EPSILON, "spot price should be 2.0");
1064 assert!(
1066 (data.depth - 1000.0).abs() < f64::EPSILON,
1067 "depth should be 1000.0 ETH, got {}",
1068 data.depth
1069 );
1070 }
1071
1072 #[test]
1073 fn test_from_sim_and_derived_normalizes_depth_fractional_price() {
1074 let key = pair_key("pool1", 0x01, 0x02);
1075 let tok_in = token(0x01, "A");
1076 let tok_out = token(0x02, "B");
1077
1078 let mut derived = DerivedData::new();
1079
1080 let mut spot = crate::derived::types::SpotPrices::default();
1081 spot.insert(key.clone(), 0.5);
1082 derived.set_spot_prices(spot, vec![], 10, true);
1083
1084 let mut depths = crate::derived::types::PoolDepths::default();
1086 depths.insert(key.clone(), BigUint::from(500u64));
1087 derived.set_pool_depths(depths, vec![], 10, true);
1088
1089 let mut token_prices = TokenGasPrices::new();
1092 token_prices.insert(
1093 tok_in.address.clone(),
1094 Price { numerator: BigUint::from(3u64), denominator: BigUint::from(2u64) },
1095 );
1096 derived.set_token_prices(token_prices, vec![], 10, true);
1097
1098 let sim = make_mock_sim();
1099 let result =
1100 <DepthAndPrice as crate::graph::EdgeWeightFromSimAndDerived>::from_sim_and_derived(
1101 &sim, &key.0, &tok_in, &tok_out, &derived,
1102 );
1103
1104 let data = result.expect("should return Some when all data present");
1105 let expected_depth = 500.0 * 2.0 / 3.0;
1106 assert!(
1107 (data.depth - expected_depth).abs() < 1e-10,
1108 "depth should be {expected_depth}, got {}",
1109 data.depth
1110 );
1111 }
1112
1113 fn all_ids(paths: Vec<Path<'_, DepthAndPrice>>) -> HashSet<Vec<&str>> {
1116 paths
1117 .iter()
1118 .map(|p| {
1119 p.iter()
1120 .map(|(_, e, _)| e.component_id.as_str())
1121 .collect()
1122 })
1123 .collect()
1124 }
1125
1126 #[test]
1127 fn test_find_paths_linear_forward_and_reverse() {
1128 let (a, b, c, d) = addrs();
1129 let m = linear_graph();
1130 let g = m.graph();
1131
1132 let p = MostLiquidAlgorithm::find_paths(g, &a, &b, 1, 1, None).unwrap();
1134 assert_eq!(all_ids(p), HashSet::from([vec!["ab"]]));
1135
1136 let p = MostLiquidAlgorithm::find_paths(g, &a, &c, 1, 2, None).unwrap();
1137 assert_eq!(all_ids(p), HashSet::from([vec!["ab", "bc"]]));
1138
1139 let p = MostLiquidAlgorithm::find_paths(g, &a, &d, 1, 3, None).unwrap();
1140 assert_eq!(all_ids(p), HashSet::from([vec!["ab", "bc", "cd"]]));
1141
1142 let p = MostLiquidAlgorithm::find_paths(g, &d, &a, 1, 3, None).unwrap();
1144 assert_eq!(all_ids(p), HashSet::from([vec!["cd", "bc", "ab"]]));
1145 }
1146
1147 #[test]
1148 fn test_find_paths_respects_hop_bounds() {
1149 let (a, _, c, d) = addrs();
1150 let m = linear_graph();
1151 let g = m.graph();
1152
1153 assert!(MostLiquidAlgorithm::find_paths(g, &a, &d, 1, 2, None)
1155 .unwrap()
1156 .is_empty());
1157
1158 assert!(MostLiquidAlgorithm::find_paths(g, &a, &c, 3, 3, None)
1160 .unwrap()
1161 .is_empty());
1162 }
1163
1164 #[test]
1165 fn test_find_paths_parallel_pools() {
1166 let (a, b, c, _) = addrs();
1167 let m = parallel_graph();
1168 let g = m.graph();
1169
1170 let p = MostLiquidAlgorithm::find_paths(g, &a, &b, 1, 1, None).unwrap();
1172 assert_eq!(all_ids(p), HashSet::from([vec!["ab1"], vec!["ab2"], vec!["ab3"]]));
1173
1174 let p = MostLiquidAlgorithm::find_paths(g, &a, &c, 1, 2, None).unwrap();
1176 assert_eq!(
1177 all_ids(p),
1178 HashSet::from([
1179 vec!["ab1", "bc1"],
1180 vec!["ab1", "bc2"],
1181 vec!["ab2", "bc1"],
1182 vec!["ab2", "bc2"],
1183 vec!["ab3", "bc1"],
1184 vec!["ab3", "bc2"],
1185 ])
1186 );
1187 }
1188
1189 #[test]
1190 fn test_find_paths_diamond_multiple_routes() {
1191 let (a, _, _, d) = addrs();
1192 let m = diamond_graph();
1193 let g = m.graph();
1194
1195 let p = MostLiquidAlgorithm::find_paths(g, &a, &d, 1, 2, None).unwrap();
1197 assert_eq!(all_ids(p), HashSet::from([vec!["ab", "bd"], vec!["ac", "cd"]]));
1198 }
1199
1200 #[test]
1201 fn test_find_paths_no_intermediate_cycles() {
1202 let (a, b, _, _) = addrs();
1203 let m = linear_graph();
1204 let g = m.graph();
1205
1206 let p = MostLiquidAlgorithm::find_paths(g, &a, &b, 1, 3, None).unwrap();
1211 assert_eq!(all_ids(p), HashSet::from([vec!["ab"]]));
1212 }
1213
1214 #[test]
1215 fn test_find_paths_cyclic_same_source_dest() {
1216 let (a, _, _, _) = addrs();
1217 let m = parallel_graph();
1219 let g = m.graph();
1220
1221 let p = MostLiquidAlgorithm::find_paths(g, &a, &a, 2, 2, None).unwrap();
1224 assert_eq!(
1225 all_ids(p),
1226 HashSet::from([
1227 vec!["ab1", "ab1"],
1228 vec!["ab1", "ab2"],
1229 vec!["ab1", "ab3"],
1230 vec!["ab2", "ab1"],
1231 vec!["ab2", "ab2"],
1232 vec!["ab2", "ab3"],
1233 vec!["ab3", "ab1"],
1234 vec!["ab3", "ab2"],
1235 vec!["ab3", "ab3"],
1236 ])
1237 );
1238 }
1239
1240 #[rstest]
1241 #[case::source_not_in_graph(false, true)]
1242 #[case::dest_not_in_graph(true, false)]
1243 fn test_find_paths_token_not_in_graph(#[case] from_exists: bool, #[case] to_exists: bool) {
1244 let (a, b, _, _) = addrs();
1246 let non_existent = addr(0x99);
1247 let m = linear_graph();
1248 let g = m.graph();
1249
1250 let from = if from_exists { a } else { non_existent.clone() };
1251 let to = if to_exists { b } else { non_existent };
1252
1253 let result = MostLiquidAlgorithm::find_paths(g, &from, &to, 1, 3, None);
1254
1255 assert!(matches!(result, Err(AlgorithmError::NoPath { .. })));
1256 }
1257
1258 #[rstest]
1259 #[case::min_greater_than_max(3, 1)]
1260 #[case::min_hops_zero(0, 1)]
1261 fn test_find_paths_invalid_configuration(#[case] min_hops: usize, #[case] max_hops: usize) {
1262 let (a, b, _, _) = addrs();
1263 let m = linear_graph();
1264 let g = m.graph();
1265
1266 assert!(matches!(
1267 MostLiquidAlgorithm::find_paths(g, &a, &b, min_hops, max_hops, None)
1268 .err()
1269 .unwrap(),
1270 AlgorithmError::InvalidConfiguration { reason: _ }
1271 ));
1272 }
1273
1274 #[test]
1275 fn test_find_paths_bfs_ordering() {
1276 let (a, b, c, d) = addrs();
1281 let e = addr(0x0E);
1282 let mut m = PetgraphStableDiGraphManager::<DepthAndPrice>::new();
1283 let mut t = HashMap::new();
1284 t.insert("ae".into(), vec![a.clone(), e.clone()]);
1285 t.insert("ab".into(), vec![a.clone(), b.clone()]);
1286 t.insert("be".into(), vec![b, e.clone()]);
1287 t.insert("ac".into(), vec![a.clone(), c.clone()]);
1288 t.insert("cd".into(), vec![c, d.clone()]);
1289 t.insert("de".into(), vec![d, e.clone()]);
1290 m.initialize_graph(&t);
1291 let g = m.graph();
1292
1293 let p = MostLiquidAlgorithm::find_paths(g, &a, &e, 1, 3, None).unwrap();
1294
1295 assert_eq!(p.len(), 3, "Expected 3 paths total");
1297 assert_eq!(p[0].len(), 1, "First path should be 1-hop");
1298 assert_eq!(p[1].len(), 2, "Second path should be 2-hop");
1299 assert_eq!(p[2].len(), 3, "Third path should be 3-hop");
1300 }
1301
1302 #[test]
1310 fn test_simulate_path_single_hop() {
1311 let token_a = token(0x01, "A");
1312 let token_b = token(0x02, "B");
1313
1314 let (market, manager) =
1315 setup_market_weighted(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1316
1317 let paths = MostLiquidAlgorithm::find_paths(
1318 manager.graph(),
1319 &token_a.address,
1320 &token_b.address,
1321 1,
1322 1,
1323 None,
1324 )
1325 .unwrap();
1326 let path = paths.into_iter().next().unwrap();
1327
1328 let result = MostLiquidAlgorithm::simulate_path(
1329 &path,
1330 market_read(&market).base_market_state(),
1331 None,
1332 BigUint::from(100u64),
1333 )
1334 .unwrap();
1335
1336 assert_eq!(result.route().swaps().len(), 1);
1337 assert_eq!(*result.route().swaps()[0].amount_in(), BigUint::from(100u64));
1338 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(200u64)); assert_eq!(result.route().swaps()[0].component_id(), "pool1");
1340 }
1341
1342 #[test]
1343 fn test_simulate_path_multi_hop_chains_amounts() {
1344 let token_a = token(0x01, "A");
1345 let token_b = token(0x02, "B");
1346 let token_c = token(0x03, "C");
1347
1348 let (market, manager) = setup_market_weighted(vec![
1349 ("pool1", &token_a, &token_b, MockProtocolSim::new(2.0)),
1350 ("pool2", &token_b, &token_c, MockProtocolSim::new(3.0)),
1351 ]);
1352
1353 let paths = MostLiquidAlgorithm::find_paths(
1354 manager.graph(),
1355 &token_a.address,
1356 &token_c.address,
1357 2,
1358 2,
1359 None,
1360 )
1361 .unwrap();
1362 let path = paths.into_iter().next().unwrap();
1363
1364 let result = MostLiquidAlgorithm::simulate_path(
1365 &path,
1366 market_read(&market).base_market_state(),
1367 None,
1368 BigUint::from(10u64),
1369 )
1370 .unwrap();
1371
1372 assert_eq!(result.route().swaps().len(), 2);
1373 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(20u64));
1375 assert_eq!(*result.route().swaps()[1].amount_in(), BigUint::from(20u64));
1377 assert_eq!(*result.route().swaps()[1].amount_out(), BigUint::from(60u64));
1378 }
1379
1380 #[test]
1381 fn test_simulate_path_same_pool_twice_uses_updated_state() {
1382 let token_a = token(0x01, "A");
1385 let token_b = token(0x02, "B");
1386
1387 let (market, manager) =
1388 setup_market_weighted(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1389
1390 let paths = MostLiquidAlgorithm::find_paths(
1393 manager.graph(),
1394 &token_a.address,
1395 &token_a.address,
1396 2,
1397 2,
1398 None,
1399 )
1400 .unwrap();
1401
1402 assert_eq!(paths.len(), 1);
1404 let path = paths[0].clone();
1405
1406 let result = MostLiquidAlgorithm::simulate_path(
1407 &path,
1408 market_read(&market).base_market_state(),
1409 None,
1410 BigUint::from(10u64),
1411 )
1412 .unwrap();
1413
1414 assert_eq!(result.route().swaps().len(), 2);
1415 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(20u64));
1417 assert_eq!(*result.route().swaps()[1].amount_out(), BigUint::from(6u64));
1419 }
1420
1421 #[test]
1422 fn test_simulate_path_missing_token_returns_data_not_found() {
1423 let token_a = token(0x01, "A");
1424 let token_b = token(0x02, "B");
1425 let token_c = token(0x03, "C");
1426
1427 let (market, _) =
1428 setup_market_weighted(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1429 let market = market_read(&market);
1430
1431 let mut topology = market.component_topology();
1433 topology
1434 .insert("pool2".to_string(), vec![token_b.address.clone(), token_c.address.clone()]);
1435 let mut manager = PetgraphStableDiGraphManager::default();
1436 manager.initialize_graph(&topology);
1437
1438 let graph = manager.graph();
1439 let paths =
1440 MostLiquidAlgorithm::find_paths(graph, &token_a.address, &token_c.address, 2, 2, None)
1441 .unwrap();
1442 let path = paths.into_iter().next().unwrap();
1443
1444 let result = MostLiquidAlgorithm::simulate_path(
1445 &path,
1446 market.base_market_state(),
1447 None,
1448 BigUint::from(100u64),
1449 );
1450 assert!(matches!(result, Err(AlgorithmError::DataNotFound { kind: "token", .. })));
1451 }
1452
1453 #[test]
1454 fn test_simulate_path_missing_component_returns_data_not_found() {
1455 let token_a = token(0x01, "A");
1456 let token_b = token(0x02, "B");
1457 let (market, manager) =
1458 setup_market_weighted(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1459
1460 let mut market_write = market.try_write().unwrap();
1462 market_write.remove_components([&"pool1".to_string()]);
1463 drop(market_write);
1464
1465 let graph = manager.graph();
1466 let paths =
1467 MostLiquidAlgorithm::find_paths(graph, &token_a.address, &token_b.address, 1, 1, None)
1468 .unwrap();
1469 let path = paths.into_iter().next().unwrap();
1470
1471 let result = MostLiquidAlgorithm::simulate_path(
1472 &path,
1473 market_read(&market).base_market_state(),
1474 None,
1475 BigUint::from(100u64),
1476 );
1477 assert!(matches!(result, Err(AlgorithmError::DataNotFound { kind: "component", .. })));
1478 }
1479
1480 #[test]
1483 fn test_connector_tokens_blocks_disallowed_intermediate() {
1484 let (a, b, c, d) = addrs();
1486 let m = diamond_graph();
1487 let g = m.graph();
1488 let allowed: HashSet<Address> = [c.clone()].into();
1489 let paths = MostLiquidAlgorithm::find_paths(g, &a, &d, 1, 2, Some(&allowed)).unwrap();
1490 let intermediates: HashSet<&Address> = paths
1491 .iter()
1492 .flat_map(|p| p.iter().map(|(node, _, _)| node))
1493 .filter(|addr| *addr != &a && *addr != &d)
1494 .collect();
1495 assert!(!intermediates.contains(&b), "B should be blocked");
1497 assert!(intermediates.contains(&c), "C should be allowed");
1498 }
1499
1500 #[test]
1501 fn test_connector_tokens_allows_endpoints_even_if_not_listed() {
1502 let (a, b, _, _) = addrs();
1504 let m = linear_graph();
1505 let g = m.graph();
1506 let allowed: HashSet<Address> = HashSet::new(); let paths = MostLiquidAlgorithm::find_paths(g, &a, &b, 1, 1, Some(&allowed)).unwrap();
1509 assert!(!paths.is_empty(), "1-hop direct route should survive empty allowlist");
1510 }
1511
1512 #[test]
1513 fn test_connector_tokens_none_is_unrestricted() {
1514 let (a, b, c, d) = addrs();
1516 let m = diamond_graph();
1517 let g = m.graph();
1518 let paths = MostLiquidAlgorithm::find_paths(g, &a, &d, 1, 2, None).unwrap();
1519 let intermediates: HashSet<&Address> = paths
1520 .iter()
1521 .flat_map(|p| p.iter().map(|(node, _, _)| node))
1522 .filter(|addr| *addr != &a && *addr != &d)
1523 .collect();
1524 assert!(intermediates.contains(&b), "B should appear with no restriction");
1525 assert!(intermediates.contains(&c), "C should appear with no restriction");
1526 }
1527
1528 #[tokio::test]
1531 async fn test_find_best_route_single_path() {
1532 let token_a = token(0x01, "A");
1533 let token_b = token(0x02, "B");
1534
1535 let (market, manager) =
1536 setup_market_weighted(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1537
1538 let algorithm = MostLiquidAlgorithm::with_config(
1539 AlgorithmConfig::new(1, 1, Duration::from_millis(100), None).unwrap(),
1540 )
1541 .unwrap();
1542 let order = order(&token_a, &token_b, ONE_ETH, OrderSide::Sell);
1543 let result = algorithm
1544 .find_best_route(manager.graph(), market, None, None, &order)
1545 .await
1546 .unwrap();
1547
1548 assert_eq!(result.route().swaps().len(), 1);
1549 assert_eq!(*result.route().swaps()[0].amount_in(), BigUint::from(ONE_ETH));
1550 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(ONE_ETH * 2));
1551 }
1552
1553 #[tokio::test]
1554 async fn test_find_best_route_ranks_by_net_amount_out() {
1555 let token_a = token(0x01, "A");
1566 let token_b = token(0x02, "B");
1567
1568 let (market, manager) = setup_market_weighted(vec![
1569 ("best", &token_a, &token_b, MockProtocolSim::new(3.0).with_gas(10)),
1570 ("low_out", &token_a, &token_b, MockProtocolSim::new(2.0).with_gas(5)),
1571 ("high_gas", &token_a, &token_b, MockProtocolSim::new(4.0).with_gas(30)),
1572 ]);
1573
1574 let algorithm = MostLiquidAlgorithm::with_config(
1575 AlgorithmConfig::new(1, 1, Duration::from_millis(100), None).unwrap(),
1576 )
1577 .unwrap();
1578 let order = order(&token_a, &token_b, 1000, OrderSide::Sell);
1579
1580 let derived = setup_derived_with_token_prices(std::slice::from_ref(&token_b.address));
1582
1583 let result = algorithm
1584 .find_best_route(manager.graph(), market, None, Some(derived), &order)
1585 .await
1586 .unwrap();
1587
1588 assert_eq!(result.route().swaps().len(), 1);
1590 assert_eq!(result.route().swaps()[0].component_id(), "best");
1591 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(3000u64));
1592 assert_eq!(result.net_amount_out(), &BigInt::from(2000)); }
1594
1595 #[tokio::test]
1596 async fn test_find_best_route_no_path_returns_error() {
1597 let token_a = token(0x01, "A");
1598 let token_b = token(0x02, "B");
1599 let token_c = token(0x03, "C"); let (market, manager) =
1602 setup_market_weighted(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1603
1604 let algorithm = MostLiquidAlgorithm::new();
1605 let order = order(&token_a, &token_c, ONE_ETH, OrderSide::Sell);
1606
1607 let result = algorithm
1608 .find_best_route(manager.graph(), market, None, None, &order)
1609 .await;
1610 assert!(matches!(result, Err(AlgorithmError::NoPath { .. })));
1611 }
1612
1613 #[tokio::test]
1614 async fn test_find_best_route_multi_hop() {
1615 let token_a = token(0x01, "A");
1616 let token_b = token(0x02, "B");
1617 let token_c = token(0x03, "C");
1618
1619 let (market, manager) = setup_market_weighted(vec![
1620 ("pool1", &token_a, &token_b, MockProtocolSim::new(2.0)),
1621 ("pool2", &token_b, &token_c, MockProtocolSim::new(3.0)),
1622 ]);
1623
1624 let algorithm = MostLiquidAlgorithm::with_config(
1625 AlgorithmConfig::new(1, 2, Duration::from_millis(100), None).unwrap(),
1626 )
1627 .unwrap();
1628 let order = order(&token_a, &token_c, ONE_ETH, OrderSide::Sell);
1629
1630 let result = algorithm
1631 .find_best_route(manager.graph(), market, None, None, &order)
1632 .await
1633 .unwrap();
1634
1635 assert_eq!(result.route().swaps().len(), 2);
1637 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(ONE_ETH * 2));
1638 assert_eq!(result.route().swaps()[0].component_id(), "pool1".to_string());
1639 assert_eq!(*result.route().swaps()[1].amount_out(), BigUint::from(ONE_ETH * 2 * 3));
1640 assert_eq!(result.route().swaps()[1].component_id(), "pool2".to_string());
1641 }
1642
1643 #[tokio::test]
1644 async fn test_find_best_route_skips_paths_without_edge_weights() {
1645 let token_a = token(0x01, "A");
1647 let token_b = token(0x02, "B");
1648
1649 let mut market = MarketState::new();
1651 let pool1_state = MockProtocolSim::new(2.0);
1652 let pool2_state = MockProtocolSim::new(3.0); let pool1_comp = component("pool1", &[token_a.clone(), token_b.clone()]);
1655 let pool2_comp = component("pool2", &[token_a.clone(), token_b.clone()]);
1656
1657 market.update_gas_price(BlockGasPrice {
1659 block_number: 1,
1660 block_hash: Default::default(),
1661 block_timestamp: 0,
1662 pricing: GasPrice::Legacy { gas_price: BigUint::from(1u64) },
1663 });
1664
1665 market.upsert_components(vec![pool1_comp, pool2_comp]);
1667
1668 market.update_states(vec![
1670 ("pool1".to_string(), Box::new(pool1_state.clone()) as Box<dyn ProtocolSim>),
1671 ("pool2".to_string(), Box::new(pool2_state) as Box<dyn ProtocolSim>),
1672 ]);
1673
1674 market.upsert_tokens(vec![token_a.clone(), token_b.clone()]);
1676
1677 let mut manager = PetgraphStableDiGraphManager::default();
1679 manager.initialize_graph(&market.component_topology());
1680
1681 let weight = DepthAndPrice::from_protocol_sim(&pool1_state, &token_a, &token_b).unwrap();
1683 manager
1684 .set_edge_weight(
1685 &"pool1".to_string(),
1686 &token_a.address,
1687 &token_b.address,
1688 weight,
1689 false,
1690 )
1691 .unwrap();
1692
1693 let algorithm = MostLiquidAlgorithm::with_config(
1695 AlgorithmConfig::new(1, 1, Duration::from_millis(100), None).unwrap(),
1696 )
1697 .unwrap();
1698 let order = order(&token_a, &token_b, ONE_ETH, OrderSide::Sell);
1699 let market = wrap_market(market);
1700 let result = algorithm
1701 .find_best_route(manager.graph(), market, None, None, &order)
1702 .await
1703 .unwrap();
1704
1705 assert_eq!(result.route().swaps().len(), 1);
1707 assert_eq!(result.route().swaps()[0].component_id(), "pool1");
1708 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(ONE_ETH * 2));
1709 }
1710
1711 #[tokio::test]
1712 async fn test_find_best_route_no_scorable_paths() {
1713 let token_a = token(0x01, "A");
1715 let token_b = token(0x02, "B");
1716
1717 let mut market = MarketState::new();
1718 let pool_state = MockProtocolSim::new(2.0);
1719 let pool_comp = component("pool1", &[token_a.clone(), token_b.clone()]);
1720
1721 market.update_gas_price(BlockGasPrice {
1723 block_number: 1,
1724 block_hash: Default::default(),
1725 block_timestamp: 0,
1726 pricing: GasPrice::Eip1559 {
1727 base_fee_per_gas: BigUint::from(1u64),
1728 max_priority_fee_per_gas: BigUint::from(0u64),
1729 },
1730 });
1731
1732 market.upsert_components(vec![pool_comp]);
1733 market.update_states(vec![(
1734 "pool1".to_string(),
1735 Box::new(pool_state) as Box<dyn ProtocolSim>,
1736 )]);
1737 market.upsert_tokens(vec![token_a.clone(), token_b.clone()]);
1738
1739 let mut manager = PetgraphStableDiGraphManager::default();
1741 manager.initialize_graph(&market.component_topology());
1742
1743 let algorithm = MostLiquidAlgorithm::new();
1744 let order = order(&token_a, &token_b, ONE_ETH, OrderSide::Sell);
1745 let market = wrap_market(market);
1746
1747 let result = algorithm
1748 .find_best_route(manager.graph(), market, None, None, &order)
1749 .await;
1750 assert!(matches!(
1751 result,
1752 Err(AlgorithmError::NoPath { reason: NoPathReason::NoScorablePaths, .. })
1753 ));
1754 }
1755
1756 #[tokio::test]
1757 async fn test_find_best_route_gas_exceeds_output_returns_negative_net() {
1758 let token_a = token(0x01, "A");
1759 let token_b = token(0x02, "B");
1760
1761 let (market, manager) =
1762 setup_market_weighted(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1763 let mut market_write = market.try_write().unwrap();
1764
1765 market_write.update_gas_price(BlockGasPrice {
1768 block_number: 1,
1769 block_hash: Default::default(),
1770 block_timestamp: 0,
1771 pricing: GasPrice::Eip1559 {
1772 base_fee_per_gas: BigUint::from(1_000_000u64),
1773 max_priority_fee_per_gas: BigUint::from(1_000_000u64),
1774 },
1775 });
1776 drop(market_write); let algorithm = MostLiquidAlgorithm::new();
1779 let order = order(&token_a, &token_b, 1, OrderSide::Sell); let derived = setup_derived_with_token_prices(std::slice::from_ref(&token_b.address));
1783
1784 let result = algorithm
1786 .find_best_route(manager.graph(), market, None, Some(derived), &order)
1787 .await
1788 .expect("should return route even with negative net_amount_out");
1789
1790 assert_eq!(result.route().swaps().len(), 1);
1792 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(2u64)); let expected_net = BigInt::from(2) - BigInt::from(100_000_000_000u64);
1796 assert_eq!(result.net_amount_out(), &expected_net);
1797 }
1798
1799 #[tokio::test]
1800 async fn test_find_best_route_insufficient_liquidity() {
1801 let token_a = token(0x01, "A");
1803 let token_b = token(0x02, "B");
1804
1805 let (market, manager) = setup_market_weighted(vec![(
1806 "pool1",
1807 &token_a,
1808 &token_b,
1809 MockProtocolSim::new(2.0).with_liquidity(1000),
1810 )]);
1811
1812 let algorithm = MostLiquidAlgorithm::new();
1813 let order = order(&token_a, &token_b, ONE_ETH, OrderSide::Sell); let result = algorithm
1816 .find_best_route(manager.graph(), market, None, None, &order)
1817 .await;
1818 assert!(matches!(result, Err(AlgorithmError::InsufficientLiquidity)));
1819 }
1820
1821 #[tokio::test]
1822 async fn test_find_best_route_missing_gas_price_returns_error() {
1823 let token_a = token(0x01, "A");
1825 let token_b = token(0x02, "B");
1826
1827 let mut market = MarketState::new();
1828 let pool_state = MockProtocolSim::new(2.0);
1829 let pool_comp = component("pool1", &[token_a.clone(), token_b.clone()]);
1830
1831 market.upsert_components(vec![pool_comp]);
1833 market.update_states(vec![(
1834 "pool1".to_string(),
1835 Box::new(pool_state.clone()) as Box<dyn ProtocolSim>,
1836 )]);
1837 market.upsert_tokens(vec![token_a.clone(), token_b.clone()]);
1838
1839 let mut manager = PetgraphStableDiGraphManager::default();
1841 manager.initialize_graph(&market.component_topology());
1842 let weight = DepthAndPrice::from_protocol_sim(&pool_state, &token_a, &token_b).unwrap();
1843 manager
1844 .set_edge_weight(
1845 &"pool1".to_string(),
1846 &token_a.address,
1847 &token_b.address,
1848 weight,
1849 false,
1850 )
1851 .unwrap();
1852
1853 let algorithm = MostLiquidAlgorithm::new();
1854 let order = order(&token_a, &token_b, ONE_ETH, OrderSide::Sell);
1855 let market = wrap_market(market);
1856
1857 let result = algorithm
1858 .find_best_route(manager.graph(), market, None, None, &order)
1859 .await;
1860
1861 assert!(matches!(result, Err(AlgorithmError::DataNotFound { kind: "gas price", .. })));
1863 }
1864
1865 #[tokio::test]
1866 async fn test_find_best_route_circular_arbitrage() {
1867 let token_a = token(0x01, "A");
1868 let token_b = token(0x02, "B");
1869
1870 let (market, manager) =
1873 setup_market_weighted(vec![("pool1", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1874
1875 let algorithm = MostLiquidAlgorithm::with_config(
1877 AlgorithmConfig::new(2, 2, Duration::from_millis(100), None).unwrap(),
1878 )
1879 .unwrap();
1880
1881 let order = order(&token_a, &token_a, 100, OrderSide::Sell);
1883
1884 let result = algorithm
1885 .find_best_route(manager.graph(), market, None, None, &order)
1886 .await
1887 .unwrap();
1888
1889 assert_eq!(result.route().swaps().len(), 2, "Should have 2 swaps for circular route");
1891
1892 assert_eq!(*result.route().swaps()[0].token_in(), token_a.address);
1894 assert_eq!(*result.route().swaps()[0].token_out(), token_b.address);
1895 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(200u64));
1896
1897 assert_eq!(*result.route().swaps()[1].token_in(), token_b.address);
1899 assert_eq!(*result.route().swaps()[1].token_out(), token_a.address);
1900 assert_eq!(*result.route().swaps()[1].amount_out(), BigUint::from(66u64));
1901
1902 assert_eq!(result.route().swaps()[0].token_in(), result.route().swaps()[1].token_out());
1904 }
1905
1906 #[tokio::test]
1907 async fn test_find_best_route_respects_min_hops() {
1908 let token_a = token(0x01, "A");
1911 let token_b = token(0x02, "B");
1912 let token_c = token(0x03, "C");
1913
1914 let (market, manager) = setup_market_weighted(vec![
1915 ("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)), ]);
1920
1921 let algorithm = MostLiquidAlgorithm::with_config(
1923 AlgorithmConfig::new(2, 3, Duration::from_millis(100), None).unwrap(),
1924 )
1925 .unwrap();
1926 let order = order(&token_a, &token_b, 100, OrderSide::Sell);
1927
1928 let derived = setup_derived_with_token_prices(std::slice::from_ref(&token_b.address));
1931
1932 let result = algorithm
1933 .find_best_route(manager.graph(), market, None, Some(derived), &order)
1934 .await
1935 .unwrap();
1936
1937 assert_eq!(result.route().swaps().len(), 2, "Should use 2-hop path due to min_hops=2");
1939 assert_eq!(result.route().swaps()[0].component_id(), "pool_ac");
1940 assert_eq!(result.route().swaps()[1].component_id(), "pool_cb");
1941 }
1942
1943 #[tokio::test]
1944 async fn test_find_best_route_respects_max_hops() {
1945 let token_a = token(0x01, "A");
1948 let token_b = token(0x02, "B");
1949 let token_c = token(0x03, "C");
1950
1951 let (market, manager) = setup_market_weighted(vec![
1952 ("pool_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
1953 ("pool_bc", &token_b, &token_c, MockProtocolSim::new(3.0)),
1954 ]);
1955
1956 let algorithm = MostLiquidAlgorithm::with_config(
1958 AlgorithmConfig::new(1, 1, Duration::from_millis(100), None).unwrap(),
1959 )
1960 .unwrap();
1961 let order = order(&token_a, &token_c, 100, OrderSide::Sell);
1962
1963 let result = algorithm
1964 .find_best_route(manager.graph(), market, None, None, &order)
1965 .await;
1966 assert!(
1967 matches!(result, Err(AlgorithmError::NoPath { .. })),
1968 "Should return NoPath when max_hops is insufficient"
1969 );
1970 }
1971
1972 #[tokio::test]
1973 async fn test_find_best_route_timeout_returns_best_so_far() {
1974 let token_a = token(0x01, "A");
1978 let token_b = token(0x02, "B");
1979
1980 let (market, manager) = setup_market_weighted(vec![
1982 ("pool1", &token_a, &token_b, MockProtocolSim::new(1.0)),
1983 ("pool2", &token_a, &token_b, MockProtocolSim::new(2.0)),
1984 ("pool3", &token_a, &token_b, MockProtocolSim::new(3.0)),
1985 ("pool4", &token_a, &token_b, MockProtocolSim::new(4.0)),
1986 ("pool5", &token_a, &token_b, MockProtocolSim::new(5.0)),
1987 ]);
1988
1989 let algorithm = MostLiquidAlgorithm::with_config(
1991 AlgorithmConfig::new(1, 1, Duration::from_millis(0), None).unwrap(),
1992 )
1993 .unwrap();
1994 let order = order(&token_a, &token_b, 100, OrderSide::Sell);
1995
1996 let result = algorithm
1997 .find_best_route(manager.graph(), market, None, None, &order)
1998 .await;
1999
2000 match result {
2005 Ok(r) => {
2006 assert_eq!(r.route().swaps().len(), 1);
2008 }
2009 Err(AlgorithmError::Timeout { .. }) => {
2010 }
2012 Err(e) => panic!("Unexpected error: {:?}", e),
2013 }
2014 }
2015
2016 #[rstest::rstest]
2019 #[case::default_config(1, 3, 50)]
2020 #[case::single_hop_only(1, 1, 100)]
2021 #[case::multi_hop_min(2, 5, 200)]
2022 #[case::zero_timeout(1, 3, 0)]
2023 #[case::large_values(10, 100, 10000)]
2024 fn test_algorithm_config_getters(
2025 #[case] min_hops: usize,
2026 #[case] max_hops: usize,
2027 #[case] timeout_ms: u64,
2028 ) {
2029 use crate::algorithm::Algorithm;
2030
2031 let algorithm = MostLiquidAlgorithm::with_config(
2032 AlgorithmConfig::new(min_hops, max_hops, Duration::from_millis(timeout_ms), None)
2033 .unwrap(),
2034 )
2035 .unwrap();
2036
2037 assert_eq!(algorithm.max_hops, max_hops);
2038 assert_eq!(algorithm.timeout, Duration::from_millis(timeout_ms));
2039 assert_eq!(algorithm.name(), "most_liquid");
2040 }
2041
2042 #[test]
2043 fn test_algorithm_default_config() {
2044 use crate::algorithm::Algorithm;
2045
2046 let algorithm = MostLiquidAlgorithm::new();
2047
2048 assert_eq!(algorithm.max_hops, 3);
2049 assert_eq!(algorithm.timeout, Duration::from_millis(500));
2050 assert_eq!(algorithm.name(), "most_liquid");
2051 }
2052
2053 #[tokio::test]
2056 async fn test_find_best_route_respects_max_routes_cap() {
2057 let token_a = token(0x01, "A");
2070 let token_b = token(0x02, "B");
2071
2072 let (market, manager) = setup_market_weighted(vec![
2073 ("pool1", &token_a, &token_b, MockProtocolSim::new(4.0).with_liquidity(1_000_000)),
2074 ("pool2", &token_a, &token_b, MockProtocolSim::new(3.0).with_liquidity(2_000_000)),
2075 ("pool3", &token_a, &token_b, MockProtocolSim::new(2.0).with_liquidity(3_000_000)),
2076 ("pool4", &token_a, &token_b, MockProtocolSim::new(1.0).with_liquidity(4_000_000)),
2077 ]);
2078
2079 let algorithm = MostLiquidAlgorithm::with_config(
2081 AlgorithmConfig::new(1, 1, Duration::from_millis(100), Some(2)).unwrap(),
2082 )
2083 .unwrap();
2084 let order = order(&token_a, &token_b, 1000, OrderSide::Sell);
2085 let result = algorithm
2086 .find_best_route(manager.graph(), market, None, None, &order)
2087 .await
2088 .unwrap();
2089
2090 assert_eq!(result.route().swaps().len(), 1);
2094 assert_eq!(result.route().swaps()[0].component_id(), "pool3");
2095 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(2000u64));
2096 }
2097
2098 #[tokio::test]
2099 async fn test_find_best_route_no_cap_when_max_routes_is_none() {
2100 let token_a = token(0x01, "A");
2102 let token_b = token(0x02, "B");
2103
2104 let (market, manager) = setup_market_weighted(vec![
2105 ("pool1", &token_a, &token_b, MockProtocolSim::new(4.0).with_liquidity(1_000_000)),
2106 ("pool2", &token_a, &token_b, MockProtocolSim::new(3.0).with_liquidity(2_000_000)),
2107 ("pool3", &token_a, &token_b, MockProtocolSim::new(2.0).with_liquidity(3_000_000)),
2108 ("pool4", &token_a, &token_b, MockProtocolSim::new(1.0).with_liquidity(4_000_000)),
2109 ]);
2110
2111 let algorithm = MostLiquidAlgorithm::with_config(
2112 AlgorithmConfig::new(1, 1, Duration::from_millis(100), None).unwrap(),
2113 )
2114 .unwrap();
2115 let order = order(&token_a, &token_b, 1000, OrderSide::Sell);
2116 let result = algorithm
2117 .find_best_route(manager.graph(), market, None, None, &order)
2118 .await
2119 .unwrap();
2120
2121 assert_eq!(result.route().swaps().len(), 1);
2123 assert_eq!(result.route().swaps()[0].component_id(), "pool1");
2124 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(4000u64));
2125 }
2126
2127 #[test]
2128 fn test_algorithm_config_rejects_zero_max_routes() {
2129 let result = AlgorithmConfig::new(1, 3, Duration::from_millis(100), Some(0));
2130 assert!(matches!(
2131 result,
2132 Err(AlgorithmError::InvalidConfiguration { reason }) if reason.contains("max_routes must be at least 1")
2133 ));
2134 }
2135
2136 #[test]
2137 fn test_algorithm_config_rejects_zero_min_hops() {
2138 let result = AlgorithmConfig::new(0, 3, Duration::from_millis(100), None);
2139 assert!(matches!(
2140 result,
2141 Err(AlgorithmError::InvalidConfiguration { reason }) if reason.contains("min_hops must be at least 1")
2142 ));
2143 }
2144
2145 #[test]
2146 fn test_algorithm_config_rejects_min_greater_than_max() {
2147 let result = AlgorithmConfig::new(5, 3, Duration::from_millis(100), None);
2148 assert!(matches!(
2149 result,
2150 Err(AlgorithmError::InvalidConfiguration { reason }) if reason.contains("cannot exceed")
2151 ));
2152 }
2153}