1use std::time::{Duration, Instant};
13
14use metrics::{counter, histogram};
15use num_bigint::{BigInt, BigUint};
16use num_traits::ToPrimitive;
17use petgraph::stable_graph::NodeIndex;
18use rustc_hash::{FxHashMap, FxHashSet};
19use smallvec::SmallVec;
20use tracing::{debug, instrument, trace};
21use tycho_simulation::{
22 tycho_common::simulation::protocol_sim::{Price, ProtocolSim},
23 tycho_core::models::{token::Token, Address},
24};
25
26use super::{Algorithm, AlgorithmConfig, NoPathReason};
27use crate::{
28 algorithm::{
29 path_scoring::{
30 rank_by_heuristic, simulate_token_path, FailedLegIx, HopResult, LegPools, PairWinners,
31 PoolQuote,
32 },
33 paths,
34 request::{SolveParts, SolveRequest},
35 sim_guard::GuardedProtocolSim,
36 swap_cache::{PoolDirection, Refusal, SwapCache, SwapResult},
37 },
38 derived::{computation::ComputationRequirements, types::TokenGasPrices},
39 feed::market_data::{MarketData, MarketState, StateLabel},
40 graph::{
41 GraphQueryFilter, RouteSearch, TokenPath, TopologyGraph, TopologyGraphManager, INLINE_EDGES,
42 },
43 types::{ComponentId, Route, RouteExclusions, RouteResult, Swap},
44 AlgorithmError,
45};
46
47const SELECTION: &str = "selection";
50
51#[derive(Debug, thiserror::Error)]
56enum MostLiquidError {
57 #[error("no pool between {from:?} and {to:?} could trade the amount")]
59 HopNotTradable { from: NodeIndex, to: NodeIndex },
60 #[error("token {0:?} not in the market subset")]
62 TokenMissing(NodeIndex),
63}
64
65type LegTokens<'a> = (&'a Token, &'a Token, Option<&'a Price>);
70
71#[derive(Clone, Copy)]
76struct SolveContext<'a> {
77 graph: &'a TopologyGraph<DepthAndPrice>,
78 market: &'a MarketState,
79 exclusions: &'a RouteExclusions,
81 token_prices: Option<&'a TokenGasPrices>,
82 amount_in: &'a BigUint,
83 gas_price: &'a BigUint,
85 start: Instant,
87}
88
89struct SolvedRoute {
91 hops: SmallVec<[HopResult; INLINE_EDGES]>,
92 net_amount_out: BigInt,
96}
97
98fn price_part(
103 value: &BigUint,
104 part: &'static str,
105 component_id: &ComponentId,
106 token_in: &Token,
107) -> Option<f64> {
108 match value.to_f64() {
109 Some(v) if v > 0.0 => Some(v),
110 Some(_) => {
111 trace!(
112 component_id = %component_id,
113 token_in = %token_in.address,
114 part,
115 "token price part is zero, skipping edge"
116 );
117 None
118 }
119 None => {
120 trace!(
121 component_id = %component_id,
122 token_in = %token_in.address,
123 part,
124 "token price part overflows f64, skipping edge"
125 );
126 None
127 }
128 }
129}
130
131#[derive(Default)]
136struct SolveReport {
137 paths_candidates: usize,
139 paths_to_simulate: usize,
141 paths_simulated: usize,
143 scoring_failures: usize,
145 simulation_failures: usize,
147 validation_failures: usize,
149}
150
151impl SolveReport {
152 fn coverage_pct(&self) -> f64 {
154 (self.paths_simulated as f64 / self.paths_to_simulate as f64) * 100.0
155 }
156
157 fn record(
159 &self,
160 best: Option<&RouteResult>,
161 market: &MarketState,
162 amount_in: &BigUint,
163 solve_time_ms: u64,
164 components_considered: usize,
165 ) {
166 counter!("algorithm.scoring_failures").increment(self.scoring_failures as u64);
167 counter!("algorithm.simulation_failures").increment(self.simulation_failures as u64);
168 counter!("algorithm.validation_failures").increment(self.validation_failures as u64);
169 histogram!("algorithm.simulation_coverage_pct").record(self.coverage_pct());
170
171 let block_number = market
172 .last_updated()
173 .map(|b| b.number());
174 let tokens_considered = market.token_registry_ref().len();
175 let Some(result) = best else {
176 debug!(
177 solve_time_ms,
178 block_number,
179 paths_candidates = self.paths_candidates,
180 paths_to_simulate = self.paths_to_simulate,
181 paths_simulated = self.paths_simulated,
182 simulation_failures = self.simulation_failures,
183 validation_failures = self.validation_failures,
184 simulation_coverage_pct = self.coverage_pct(),
185 components_considered,
186 tokens_considered,
187 "no viable route"
188 );
189 return;
190 };
191
192 let path_desc = result
193 .route()
194 .path_description(market.token_registry_ref());
195 let protocols = result
196 .route()
197 .swaps()
198 .iter()
199 .map(|s| s.protocol())
200 .collect::<Vec<_>>();
201 let price = amount_in
202 .to_f64()
203 .filter(|&v| v > 0.0)
204 .and_then(|amt_in| {
205 result
206 .net_amount_out()
207 .to_f64()
208 .map(|amt_out| amt_out / amt_in)
209 })
210 .unwrap_or(f64::NAN);
211
212 debug!(
213 solve_time_ms,
214 block_number,
215 paths_candidates = self.paths_candidates,
216 paths_to_simulate = self.paths_to_simulate,
217 paths_simulated = self.paths_simulated,
218 simulation_failures = self.simulation_failures,
219 validation_failures = self.validation_failures,
220 simulation_coverage_pct = self.coverage_pct(),
221 components_considered,
222 tokens_considered,
223 path = %path_desc,
224 amount_in = %amount_in,
225 net_amount_out = %result.net_amount_out(),
226 price_out_per_in = price,
227 hop_count = result.route().swaps().len(),
228 protocols = ?protocols,
229 "route found"
230 );
231 }
232}
233
234fn swap_on_route(
241 route: &Route,
242 token_prices: Option<&TokenGasPrices>,
243 gas_price: &BigUint,
244) -> BigInt {
245 let Some(last) = route.swaps().last() else {
246 return BigInt::ZERO;
247 };
248 let amount_out = BigInt::from(last.amount_out().clone());
249
250 let Some(price) = token_prices.and_then(|prices| prices.get(last.token_out())) else {
251 return amount_out;
252 };
253 let mut gas = BigUint::ZERO;
254 for swap in route.swaps() {
255 gas += swap.gas_estimate();
256 }
257
258 amount_out - BigInt::from(gas * gas_price * &price.numerator / &price.denominator)
259}
260
261pub struct MostLiquidAlgorithm {
263 query: GraphQueryFilter,
266 timeout: Duration,
267 max_routes: Option<usize>,
268 cache_pair_swaps: bool,
269}
270
271#[derive(Debug, Clone, Default)]
277pub struct DepthAndPrice {
278 pub spot_price: f64,
280 pub depth: f64,
282}
283
284impl DepthAndPrice {
285 #[cfg(test)]
287 pub fn new(spot_price: f64, depth: f64) -> Self {
288 Self { spot_price, depth }
289 }
290
291 #[cfg(any(test, feature = "test-utils"))]
293 pub fn from_protocol_sim<S: ProtocolSim + ?Sized>(
294 sim: &S,
295 token_in: &Token,
296 token_out: &Token,
297 ) -> Result<Self, AlgorithmError> {
298 Ok(Self {
299 spot_price: sim
300 .spot_price(token_in, token_out)
301 .map_err(|e| {
302 AlgorithmError::Other(format!("missing spot price for DepthAndPrice: {:?}", e))
303 })?,
304 depth: sim
305 .get_limits(token_in.address.clone(), token_out.address.clone())
306 .map_err(|e| {
307 AlgorithmError::Other(format!("missing depth for DepthAndPrice: {:?}", e))
308 })?
309 .0
310 .to_f64()
311 .ok_or_else(|| {
312 AlgorithmError::Other("depth conversion to f64 failed".to_string())
313 })?,
314 })
315 }
316}
317
318impl crate::graph::EdgeWeightFromSimAndDerived for DepthAndPrice {
319 fn from_sim_and_derived(
320 _sim: &dyn ProtocolSim,
321 component_id: &ComponentId,
322 token_in: &Token,
323 token_out: &Token,
324 derived: &crate::derived::DerivedData,
325 ) -> Option<Self> {
326 let key = (component_id.clone(), token_in.address.clone(), token_out.address.clone());
327
328 let spot_price = match derived
330 .spot_prices()
331 .and_then(|p| p.get(&key).copied())
332 {
333 Some(p) => p,
334 None => {
335 trace!(component_id = %component_id, "spot price not found, skipping edge");
336 return None;
337 }
338 };
339
340 let raw_depth = match derived
342 .component_depths()
343 .and_then(|d| d.get(&key))
344 {
345 Some(d) => d.to_f64().unwrap_or(0.0),
346 None => {
347 trace!(component_id = %component_id, "component depth not found, skipping edge");
348 return None;
349 }
350 };
351
352 let depth = match derived
357 .token_prices()
358 .and_then(|p| p.get(&token_in.address))
359 {
360 Some(price) => {
361 let num = price_part(&price.numerator, "numerator", component_id, token_in)?;
362 let den = price_part(&price.denominator, "denominator", component_id, token_in)?;
363 raw_depth * den / num
364 }
365 None => {
366 trace!(
367 component_id = %component_id,
368 token_in = %token_in.address,
369 "token price not found, skipping edge"
370 );
371 return None;
372 }
373 };
374
375 Some(Self { spot_price, depth })
376 }
377}
378
379impl MostLiquidAlgorithm {
380 pub fn new() -> Self {
382 Self {
383 query: GraphQueryFilter { min_hops: 1, max_hops: 3, connector_tokens: None },
384 timeout: Duration::from_millis(500),
385 max_routes: None,
386 cache_pair_swaps: true,
387 }
388 }
389
390 pub fn with_config(config: AlgorithmConfig) -> Result<Self, AlgorithmError> {
392 if config.min_hops() == 0 || config.min_hops() > config.max_hops() {
393 return Err(AlgorithmError::InvalidConfiguration {
394 reason: format!(
395 "invalid hop configuration: min_hops={} max_hops={}",
396 config.min_hops(),
397 config.max_hops()
398 ),
399 });
400 }
401
402 Ok(Self {
403 query: GraphQueryFilter {
404 min_hops: config.min_hops(),
405 max_hops: config.max_hops(),
406 connector_tokens: config.connector_tokens().cloned(),
407 },
408 timeout: config.timeout(),
409 max_routes: config.max_routes(),
410 cache_pair_swaps: true,
411 })
412 }
413
414 fn build_route(
422 ctx: &SolveContext<'_>,
423 token_path: &[NodeIndex],
424 solved: &SolvedRoute,
425 ) -> Result<Route, AlgorithmError> {
426 let SolveContext { graph, market, amount_in, .. } = *ctx;
427 let mut current_amount = amount_in.clone();
428 let mut swaps = Vec::with_capacity(solved.hops.len());
429 let mut tokens: FxHashMap<Address, Token> = FxHashMap::default();
430 let mut state_overrides: FxHashMap<ComponentId, Box<dyn ProtocolSim>> =
431 FxHashMap::default();
432
433 for (pair, hop) in token_path.windows(2).zip(&solved.hops) {
434 let address_in = &graph[pair[0]];
435 let address_out = &graph[pair[1]];
436 let token_in = paths::get_token(market, address_in)?;
437 let token_out = paths::get_token(market, address_out)?;
438
439 let component_id = graph
440 .pools_between(pair[0], pair[1])
441 .get(hop.pool_ix)
442 .map(|edge| &edge.component_id)
443 .ok_or_else(|| AlgorithmError::DataNotFound {
444 kind: "pool",
445 id: Some(format!("{address_in:?} -> {address_out:?}")),
446 })?;
447 let component = market
448 .get_component(component_id)
449 .ok_or_else(|| AlgorithmError::DataNotFound {
450 kind: "component",
451 id: Some(component_id.clone()),
452 })?;
453 let state = state_overrides
454 .get(component_id)
455 .map(Box::as_ref)
456 .or_else(|| market.get_simulation_state(component_id))
457 .ok_or_else(|| AlgorithmError::DataNotFound {
458 kind: "simulation state",
459 id: Some(component_id.clone()),
460 })?;
461 let result = state
462 .get_amount_out_guarded(current_amount.clone(), token_in, token_out)
463 .map_err(|e| AlgorithmError::Other(format!("simulation error: {e:?}")))?;
464
465 swaps.push(Swap::new(
466 component_id.clone(),
467 component.protocol_system.clone(),
468 token_in.address.clone(),
469 token_out.address.clone(),
470 current_amount.clone(),
471 result.amount.clone(),
472 result.gas,
473 component.clone(),
474 state.clone_box(),
475 ));
476 tokens
477 .entry(token_in.address.clone())
478 .or_insert_with(|| token_in.clone());
479 tokens
480 .entry(token_out.address.clone())
481 .or_insert_with(|| token_out.clone());
482
483 state_overrides.insert(component_id.clone(), result.new_state);
484 current_amount = result.amount;
485 }
486
487 Ok(Route::new(swaps, tokens)?)
488 }
489
490 async fn snapshot_market_state(
491 graph: &TopologyGraph<DepthAndPrice>,
492 market: MarketData,
493 label: Option<StateLabel>,
494 scored_paths: &[(TokenPath, f64)],
495 exclusions: &RouteExclusions,
496 ) -> Result<MarketState, AlgorithmError> {
497 let mut pairs: FxHashSet<(NodeIndex, NodeIndex)> = FxHashSet::default();
498 for (token_path, _) in scored_paths {
499 for pair in token_path.windows(2) {
500 pairs.insert((pair[0], pair[1]));
501 }
502 }
503 let mut component_ids: FxHashSet<&ComponentId> = FxHashSet::default();
504 for &(from, to) in &pairs {
505 for pool in graph.pools_between(from, to) {
506 if exclusions.excludes_pool(&pool.component_id) {
507 continue;
508 }
509 component_ids.insert(&pool.component_id);
510 }
511 }
512
513 let market = paths::read_market(&market, label).await?;
514 let market_subset = market.extract_subset_with_overlay(&component_ids);
515 drop(market);
516 Ok(market_subset)
517 }
518
519 fn solve_for_best_path(
520 &self,
521 scored_paths: &[(TokenPath, f64)],
522 report: &mut SolveReport,
523 ctx: &SolveContext,
524 ) -> Result<RouteResult, AlgorithmError> {
525 let mut best_route: Option<(&TokenPath, SolvedRoute)> = None;
526 let mut winners = PairWinners::new(self.cache_pair_swaps);
527 let mut swaps = SwapCache::new();
528 let timeout_ms = self.timeout.as_millis() as u64;
529
530 for (token_path, _) in scored_paths {
531 let elapsed_ms = ctx.start.elapsed().as_millis() as u64;
533 if elapsed_ms > timeout_ms {
534 break;
535 }
536
537 let solved = match Self::solve_token_path(ctx, token_path, &mut winners, &mut swaps) {
538 Ok(solved) => solved,
539 Err(e) => {
540 trace!(error = %e, "could not solve path");
541 report.simulation_failures += 1;
542 continue;
543 }
544 };
545
546 if best_route
548 .as_ref()
549 .is_none_or(|(_, previous): &(&TokenPath, SolvedRoute)| {
550 solved.net_amount_out > previous.net_amount_out
551 })
552 {
553 best_route = Some((token_path, solved));
554 }
555
556 report.paths_simulated += 1;
557 }
558
559 let best = match best_route {
562 Some((token_path, solved)) => {
563 let route = Self::build_route(ctx, token_path, &solved)?;
564 if let Err(e) = route.validate() {
565 trace!(error = %e, "best route failed validation");
566 report.validation_failures += 1;
567 None
568 } else {
569 let amount_out = swap_on_route(&route, ctx.token_prices, ctx.gas_price);
570 Some(RouteResult::new(route, amount_out, ctx.gas_price.clone()))
571 }
572 }
573 None => None,
574 };
575
576 let solve_time_ms = ctx.start.elapsed().as_millis() as u64;
577 report.record(
578 best.as_ref(),
579 ctx.market,
580 ctx.amount_in,
581 solve_time_ms,
582 ctx.market.component_count(),
583 );
584
585 match best {
586 Some(best_route) => Ok(best_route),
587 None => {
588 if solve_time_ms > timeout_ms {
589 Err(AlgorithmError::Timeout { elapsed_ms: solve_time_ms })
590 } else {
591 Err(AlgorithmError::InsufficientLiquidity)
592 }
593 }
594 }
595 }
596
597 fn solve_token_path<'g>(
621 ctx: &SolveContext<'g>,
622 token_path: &[NodeIndex],
623 winners: &mut PairWinners,
624 swaps: &mut SwapCache<'g>,
625 ) -> Result<SolvedRoute, MostLiquidError> {
626 let (graph, market, gas_price) = (ctx.graph, ctx.market, ctx.gas_price);
627
628 let mut legs: SmallVec<[LegPools<'g, DepthAndPrice, LegTokens<'g>>; INLINE_EDGES]> =
632 SmallVec::new();
633 for pair in token_path.windows(2) {
634 legs.push(LegPools {
635 pair: (pair[0], pair[1]),
636 pools: graph.pools_between(pair[0], pair[1]),
637 data: Self::get_pair_data(ctx, pair)?,
638 });
639 }
640
641 let scored =
642 simulate_token_path(&legs, ctx.amount_in, winners, |leg, amount, component_id| {
643 if ctx
644 .exclusions
645 .excludes_pool(component_id)
646 {
647 return None;
648 }
649 let (token_in, token_out, token_out_gas_price) = leg.data;
650 let paid = swaps.swap(
651 PoolDirection {
652 component_id,
653 address_in: &token_in.address,
654 address_out: &token_out.address,
655 },
656 amount,
657 SELECTION,
658 || {
659 let Some(state) = market.get_simulation_state(component_id) else {
660 return Err(Refusal::Failed);
661 };
662 state
663 .get_amount_out_guarded(amount.clone(), token_in, token_out)
664 .map(|result| SwapResult { amount_out: result.amount, gas: result.gas })
665 .map_err(|error| Refusal::of(&error))
666 },
667 true,
668 )?;
669 let net = match token_out_gas_price {
670 Some(price) => {
671 let cost = &paid.gas * gas_price * &price.numerator / &price.denominator;
672 BigInt::from(paid.amount_out.clone()) - BigInt::from(cost)
673 }
674 None => BigInt::from(paid.amount_out.clone()),
675 };
676 Some(PoolQuote { paid, net })
677 })
678 .map_err(|FailedLegIx(leg_ix)| MostLiquidError::HopNotTradable {
679 from: legs[leg_ix].pair.0,
680 to: legs[leg_ix].pair.1,
681 })?;
682
683 let net_amount_out = match legs.last().and_then(|leg| leg.data.2) {
686 Some(price) => {
687 let cost = scored.gas * ctx.gas_price * &price.numerator / &price.denominator;
688 BigInt::from(scored.amount_out) - BigInt::from(cost)
689 }
690 None => BigInt::from(scored.amount_out),
691 };
692
693 Ok(SolvedRoute { hops: scored.hops, net_amount_out })
694 }
695
696 fn get_pair_data<'a>(
697 ctx: &SolveContext<'a>,
698 pair: &[NodeIndex],
699 ) -> Result<LegTokens<'a>, MostLiquidError> {
700 let token_in = ctx
701 .market
702 .get_token(&ctx.graph[pair[0]])
703 .ok_or(MostLiquidError::TokenMissing(pair[0]))?;
704 let token_out = ctx
705 .market
706 .get_token(&ctx.graph[pair[1]])
707 .ok_or(MostLiquidError::TokenMissing(pair[1]))?;
708 let token_out_gas_price = ctx
709 .token_prices
710 .and_then(|prices| prices.get(&token_out.address));
711 Ok((token_in, token_out, token_out_gas_price))
712 }
713}
714
715impl Default for MostLiquidAlgorithm {
716 fn default() -> Self {
717 Self::new()
718 }
719}
720
721impl Algorithm for MostLiquidAlgorithm {
722 type GraphType = TopologyGraph<DepthAndPrice>;
723 type GraphManager = TopologyGraphManager<DepthAndPrice>;
724
725 fn name(&self) -> &str {
726 "most_liquid"
727 }
728
729 #[instrument(level = "debug", skip_all, fields(order_id = %request.order().id()))]
731 async fn find_best_route(
732 &self,
733 request: SolveRequest<'_, Self::GraphType>,
734 ) -> Result<RouteResult, AlgorithmError> {
735 let SolveParts { graph, order, market, label, derived, exclusions } = request.into_parts();
736 let start = Instant::now();
737
738 if !order.is_sell() {
740 return Err(AlgorithmError::ExactOutNotSupported);
741 }
742
743 let token_prices = match derived.as_ref() {
746 Some(derived) => derived
747 .read()
748 .await
749 .token_prices_shared(),
750 None => None,
751 };
752
753 let amount_in = order.amount().clone();
754
755 let search = RouteSearch { bounds: &self.query, exclusions: &exclusions };
758 let all_paths =
759 paths::find_token_paths(graph, order.token_in(), order.token_out(), search)?;
760 let n_paths = all_paths.len();
761 let no_path = |reason| AlgorithmError::NoPath {
762 from: order.token_in().clone(),
763 to: order.token_out().clone(),
764 reason,
765 };
766 if all_paths.is_empty() {
767 return Err(no_path(NoPathReason::NoGraphPath));
768 }
769
770 let mut scored_paths = rank_by_heuristic(graph, all_paths, &exclusions);
773 if scored_paths.is_empty() {
774 return Err(no_path(NoPathReason::NoScorablePaths));
775 }
776
777 let mut report = SolveReport {
778 paths_candidates: n_paths,
779 scoring_failures: n_paths - scored_paths.len(),
780 ..SolveReport::default()
781 };
782
783 if let Some(max_routes) = self.max_routes {
784 scored_paths.truncate(max_routes);
785 }
786 report.paths_to_simulate = scored_paths.len();
787
788 let market =
790 Self::snapshot_market_state(graph, market, label, &scored_paths, &exclusions).await?;
791 let gas_price = paths::fetch_gas_price(&market)?;
792
793 let ctx = SolveContext {
795 graph,
796 market: &market,
797 token_prices: token_prices.as_deref(),
798 amount_in: &amount_in,
799 gas_price: &gas_price,
800 start,
801 exclusions: &exclusions,
802 };
803
804 self.solve_for_best_path(&scored_paths, &mut report, &ctx)
805 }
806
807 fn computation_requirements(&self) -> ComputationRequirements {
808 ComputationRequirements::none()
816 .allow_stale("token_prices")
817 .expect("Conflicting Computation Requirements")
818 }
819
820 fn timeout(&self) -> Duration {
821 self.timeout
822 }
823}
824
825#[cfg(test)]
826mod tests {
827 use tycho_simulation::{
828 tycho_core::simulation::protocol_sim::Price,
829 tycho_ethereum::gas::{BlockGasPrice, GasPrice},
830 };
831
832 use super::*;
833 use crate::{
834 algorithm::test_utils::{
835 addr, component, order, setup_market_weighted, token, MockProtocolSim, ONE_ETH,
836 },
837 derived::{
838 computation::{FailedItem, FailedItemError},
839 types::TokenGasPrices,
840 DerivedData, SharedDerivedDataRef,
841 },
842 graph::GraphManager,
843 types::OrderSide,
844 };
845
846 fn wrap_market(market: MarketState) -> MarketData {
847 MarketData::new(std::sync::Arc::new(tokio::sync::RwLock::new(market)))
848 }
849
850 fn setup_derived_with_token_prices(token_addresses: &[Address]) -> SharedDerivedDataRef {
855 let mut token_prices: TokenGasPrices = FxHashMap::default();
856 for addr in token_addresses {
857 token_prices.insert(
859 addr.clone(),
860 Price { numerator: BigUint::from(1u64), denominator: BigUint::from(1u64) },
861 );
862 }
863
864 let mut derived_data = DerivedData::new();
865 derived_data.set_token_prices(token_prices, vec![], 1, true);
866 std::sync::Arc::new(tokio::sync::RwLock::new(derived_data))
867 }
868
869 fn make_mock_sim() -> MockProtocolSim {
870 MockProtocolSim::new(2.0)
871 }
872
873 fn pair_key(comp: &str, b_in: u8, b_out: u8) -> (String, Address, Address) {
874 (comp.to_string(), addr(b_in), addr(b_out))
875 }
876
877 fn pair_key_str(comp: &str, b_in: u8, b_out: u8) -> String {
878 format!("{comp}/{}/{}", addr(b_in), addr(b_out))
879 }
880
881 fn make_token_prices(addresses: &[Address]) -> TokenGasPrices {
882 let mut prices = TokenGasPrices::default();
883 for addr in addresses {
884 prices.insert(
886 addr.clone(),
887 Price { numerator: BigUint::from(1u64), denominator: BigUint::from(1u64) },
888 );
889 }
890 prices
891 }
892
893 #[test]
894 fn test_from_sim_and_derived_failed_spot_price_returns_none() {
895 let key = pair_key("component1", 0x01, 0x02);
896 let key_str = pair_key_str("component1", 0x01, 0x02);
897 let tok_in = token(0x01, "A");
898 let tok_out = token(0x02, "B");
899
900 let mut derived = DerivedData::new();
901 derived.set_spot_prices(
903 Default::default(),
904 vec![FailedItem {
905 key: key_str,
906 error: FailedItemError::SimulationFailed("sim error".into()),
907 }],
908 10,
909 true,
910 );
911 derived.set_component_depths(Default::default(), vec![], 10, true);
912 derived.set_token_prices(
913 make_token_prices(&[tok_in.address.clone(), tok_out.address.clone()]),
914 vec![],
915 10,
916 true,
917 );
918
919 let sim = make_mock_sim();
920 let result =
921 <DepthAndPrice as crate::graph::EdgeWeightFromSimAndDerived>::from_sim_and_derived(
922 &sim, &key.0, &tok_in, &tok_out, &derived,
923 );
924
925 assert!(result.is_none());
926 }
927
928 #[test]
929 fn test_from_sim_and_derived_failed_component_depth_returns_none() {
930 let key = pair_key("component1", 0x01, 0x02);
931 let key_str = pair_key_str("component1", 0x01, 0x02);
932 let tok_in = token(0x01, "A");
933 let tok_out = token(0x02, "B");
934
935 let mut derived = DerivedData::new();
936 let mut prices = crate::derived::types::SpotPrices::default();
938 prices.insert(key.clone(), 1.5);
939 derived.set_spot_prices(prices, vec![], 10, true);
940 derived.set_component_depths(
942 Default::default(),
943 vec![FailedItem {
944 key: key_str,
945 error: FailedItemError::SimulationFailed("depth error".into()),
946 }],
947 10,
948 true,
949 );
950 derived.set_token_prices(
951 make_token_prices(&[tok_in.address.clone(), tok_out.address.clone()]),
952 vec![],
953 10,
954 true,
955 );
956
957 let sim = make_mock_sim();
958 let result =
959 <DepthAndPrice as crate::graph::EdgeWeightFromSimAndDerived>::from_sim_and_derived(
960 &sim, &key.0, &tok_in, &tok_out, &derived,
961 );
962
963 assert!(result.is_none());
964 }
965
966 #[test]
967 fn test_from_sim_and_derived_both_failed_returns_none() {
968 let key = pair_key("component1", 0x01, 0x02);
969 let key_str = pair_key_str("component1", 0x01, 0x02);
970 let tok_in = token(0x01, "A");
971 let tok_out = token(0x02, "B");
972
973 let mut derived = DerivedData::new();
974 derived.set_spot_prices(
975 Default::default(),
976 vec![FailedItem {
977 key: key_str.clone(),
978 error: FailedItemError::SimulationFailed("spot error".into()),
979 }],
980 10,
981 true,
982 );
983 derived.set_component_depths(
984 Default::default(),
985 vec![FailedItem {
986 key: key_str,
987 error: FailedItemError::SimulationFailed("depth error".into()),
988 }],
989 10,
990 true,
991 );
992 derived.set_token_prices(
993 make_token_prices(&[tok_in.address.clone(), tok_out.address.clone()]),
994 vec![],
995 10,
996 true,
997 );
998
999 let sim = make_mock_sim();
1000 let result =
1001 <DepthAndPrice as crate::graph::EdgeWeightFromSimAndDerived>::from_sim_and_derived(
1002 &sim, &key.0, &tok_in, &tok_out, &derived,
1003 );
1004
1005 assert!(result.is_none());
1006 }
1007
1008 #[test]
1009 fn test_from_sim_and_derived_missing_token_price_returns_none() {
1010 let key = pair_key("component1", 0x01, 0x02);
1011 let tok_in = token(0x01, "A");
1012 let tok_out = token(0x02, "B");
1013
1014 let mut derived = DerivedData::new();
1015 let mut prices = crate::derived::types::SpotPrices::default();
1017 prices.insert(key.clone(), 1.5);
1018 derived.set_spot_prices(prices, vec![], 10, true);
1019
1020 let mut depths = crate::derived::types::ComponentDepths::default();
1021 depths.insert(key.clone(), BigUint::from(1000u64));
1022 derived.set_component_depths(depths, vec![], 10, true);
1023
1024 let sim = make_mock_sim();
1027 let result =
1028 <DepthAndPrice as crate::graph::EdgeWeightFromSimAndDerived>::from_sim_and_derived(
1029 &sim, &key.0, &tok_in, &tok_out, &derived,
1030 );
1031
1032 assert!(
1033 result.is_none(),
1034 "should return None when token price is missing for depth normalization"
1035 );
1036 }
1037
1038 #[test]
1039 fn test_from_sim_and_derived_normalizes_depth_to_eth() {
1040 let key = pair_key("component1", 0x01, 0x02);
1041 let tok_in = token(0x01, "A");
1042 let tok_out = token(0x02, "B");
1043
1044 let mut derived = DerivedData::new();
1045
1046 let mut spot = crate::derived::types::SpotPrices::default();
1048 spot.insert(key.clone(), 2.0);
1049 derived.set_spot_prices(spot, vec![], 10, true);
1050
1051 let mut depths = crate::derived::types::ComponentDepths::default();
1053 depths.insert(key.clone(), BigUint::from(2_000_000u64));
1054 derived.set_component_depths(depths, vec![], 10, true);
1055
1056 let mut token_prices = TokenGasPrices::default();
1059 token_prices.insert(
1060 tok_in.address.clone(),
1061 Price { numerator: BigUint::from(2000u64), denominator: BigUint::from(1u64) },
1062 );
1063 derived.set_token_prices(token_prices, vec![], 10, true);
1064
1065 let sim = make_mock_sim();
1066 let result =
1067 <DepthAndPrice as crate::graph::EdgeWeightFromSimAndDerived>::from_sim_and_derived(
1068 &sim, &key.0, &tok_in, &tok_out, &derived,
1069 );
1070
1071 let data = result.expect("should return Some when all data present");
1072 assert!((data.spot_price - 2.0).abs() < f64::EPSILON, "spot price should be 2.0");
1073 assert!(
1075 (data.depth - 1000.0).abs() < f64::EPSILON,
1076 "depth should be 1000.0 ETH, got {}",
1077 data.depth
1078 );
1079 }
1080
1081 #[test]
1082 fn test_from_sim_and_derived_normalizes_depth_fractional_price() {
1083 let key = pair_key("component1", 0x01, 0x02);
1084 let tok_in = token(0x01, "A");
1085 let tok_out = token(0x02, "B");
1086
1087 let mut derived = DerivedData::new();
1088
1089 let mut spot = crate::derived::types::SpotPrices::default();
1090 spot.insert(key.clone(), 0.5);
1091 derived.set_spot_prices(spot, vec![], 10, true);
1092
1093 let mut depths = crate::derived::types::ComponentDepths::default();
1095 depths.insert(key.clone(), BigUint::from(500u64));
1096 derived.set_component_depths(depths, vec![], 10, true);
1097
1098 let mut token_prices = TokenGasPrices::default();
1101 token_prices.insert(
1102 tok_in.address.clone(),
1103 Price { numerator: BigUint::from(3u64), denominator: BigUint::from(2u64) },
1104 );
1105 derived.set_token_prices(token_prices, vec![], 10, true);
1106
1107 let sim = make_mock_sim();
1108 let result =
1109 <DepthAndPrice as crate::graph::EdgeWeightFromSimAndDerived>::from_sim_and_derived(
1110 &sim, &key.0, &tok_in, &tok_out, &derived,
1111 );
1112
1113 let data = result.expect("should return Some when all data present");
1114 let expected_depth = 500.0 * 2.0 / 3.0;
1115 assert!(
1116 (data.depth - expected_depth).abs() < 1e-10,
1117 "depth should be {expected_depth}, got {}",
1118 data.depth
1119 );
1120 }
1121
1122 #[tokio::test]
1125 async fn test_find_best_route_with_excluded_endpoints() {
1126 let a = token(0x01, "A");
1127 let b = token(0x02, "B");
1128 let c = token(0x03, "C");
1129 let (market, manager) = setup_market_weighted(vec![
1130 ("ab", &a, &b, MockProtocolSim::new(2.0)),
1131 ("bc", &b, &c, MockProtocolSim::new(2.0)),
1132 ]);
1133 let order = order(&a, &c, 1000, OrderSide::Sell);
1134 let result = MostLiquidAlgorithm::with_config(
1135 AlgorithmConfig::new(1, 2, Duration::from_secs(1), None).unwrap(),
1136 )
1137 .unwrap()
1138 .find_best_route(SolveRequest::new(manager.graph(), market, &order).with_exclusions(
1139 RouteExclusions::default().with_tokens([a.address.clone(), c.address.clone()]),
1140 ))
1141 .await
1142 .unwrap();
1143 assert_eq!(result.route().swaps().len(), 2);
1144 assert_eq!(result.route().swaps()[0].component_id(), "ab");
1145 assert_eq!(result.route().swaps()[1].component_id(), "bc");
1146 }
1147
1148 #[tokio::test]
1150 async fn test_find_best_route_with_excluded_pool() {
1151 let token_a = token(0x01, "A");
1152 let token_b = token(0x02, "B");
1153
1154 let (market, manager) = setup_market_weighted(vec![
1155 ("best", &token_a, &token_b, MockProtocolSim::new(3.0)),
1156 ("second", &token_a, &token_b, MockProtocolSim::new(2.0)),
1157 ]);
1158
1159 let algorithm = MostLiquidAlgorithm::with_config(
1160 AlgorithmConfig::new(1, 1, Duration::from_millis(100), None).unwrap(),
1161 )
1162 .unwrap();
1163 let order = order(&token_a, &token_b, 1000, OrderSide::Sell);
1164
1165 let result = algorithm
1166 .find_best_route(
1167 SolveRequest::new(manager.graph(), market, &order)
1168 .with_exclusions(RouteExclusions::default().with_pools(["best".to_string()])),
1169 )
1170 .await
1171 .unwrap();
1172
1173 assert_eq!(result.route().swaps()[0].component_id(), "second");
1174 }
1175
1176 #[tokio::test]
1177 async fn test_find_best_route_single_path() {
1178 let token_a = token(0x01, "A");
1179 let token_b = token(0x02, "B");
1180
1181 let (market, manager) = setup_market_weighted(vec![(
1182 "component1",
1183 &token_a,
1184 &token_b,
1185 MockProtocolSim::new(2.0),
1186 )]);
1187
1188 let algorithm = MostLiquidAlgorithm::with_config(
1189 AlgorithmConfig::new(1, 1, Duration::from_millis(100), None).unwrap(),
1190 )
1191 .unwrap();
1192 let order = order(&token_a, &token_b, ONE_ETH, OrderSide::Sell);
1193 let result = algorithm
1194 .find_best_route(SolveRequest::new(manager.graph(), market, &order))
1195 .await
1196 .unwrap();
1197
1198 assert_eq!(result.route().swaps().len(), 1);
1199 assert_eq!(*result.route().swaps()[0].amount_in(), BigUint::from(ONE_ETH));
1200 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(ONE_ETH * 2));
1201 }
1202
1203 #[tokio::test]
1204 async fn test_find_best_route_ranks_by_net_amount_out() {
1205 let token_a = token(0x01, "A");
1216 let token_b = token(0x02, "B");
1217
1218 let (market, manager) = setup_market_weighted(vec![
1219 ("best", &token_a, &token_b, MockProtocolSim::new(3.0).with_gas(10)),
1220 ("low_out", &token_a, &token_b, MockProtocolSim::new(2.0).with_gas(5)),
1221 ("high_gas", &token_a, &token_b, MockProtocolSim::new(4.0).with_gas(30)),
1222 ]);
1223
1224 let algorithm = MostLiquidAlgorithm::with_config(
1225 AlgorithmConfig::new(1, 1, Duration::from_millis(100), None).unwrap(),
1226 )
1227 .unwrap();
1228 let order = order(&token_a, &token_b, 1000, OrderSide::Sell);
1229
1230 let derived = setup_derived_with_token_prices(std::slice::from_ref(&token_b.address));
1232
1233 let result = algorithm
1234 .find_best_route(
1235 SolveRequest::new(manager.graph(), market, &order).with_derived(derived),
1236 )
1237 .await
1238 .unwrap();
1239
1240 assert_eq!(result.route().swaps().len(), 1);
1242 assert_eq!(result.route().swaps()[0].component_id(), "best");
1243 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(3000u64));
1244 assert_eq!(result.net_amount_out(), &BigInt::from(2000)); }
1246
1247 #[tokio::test]
1248 async fn test_find_best_route_no_path_returns_error() {
1249 let token_a = token(0x01, "A");
1250 let token_b = token(0x02, "B");
1251 let token_c = token(0x03, "C"); let (market, manager) = setup_market_weighted(vec![(
1254 "component1",
1255 &token_a,
1256 &token_b,
1257 MockProtocolSim::new(2.0),
1258 )]);
1259
1260 let algorithm = MostLiquidAlgorithm::new();
1261 let order = order(&token_a, &token_c, ONE_ETH, OrderSide::Sell);
1262
1263 let result = algorithm
1264 .find_best_route(SolveRequest::new(manager.graph(), market, &order))
1265 .await;
1266 assert!(matches!(result, Err(AlgorithmError::NoPath { .. })));
1267 }
1268
1269 #[tokio::test]
1270 async fn test_find_best_route_multi_hop() {
1271 let token_a = token(0x01, "A");
1272 let token_b = token(0x02, "B");
1273 let token_c = token(0x03, "C");
1274
1275 let (market, manager) = setup_market_weighted(vec![
1276 ("component1", &token_a, &token_b, MockProtocolSim::new(2.0)),
1277 ("component2", &token_b, &token_c, MockProtocolSim::new(3.0)),
1278 ]);
1279
1280 let algorithm = MostLiquidAlgorithm::with_config(
1281 AlgorithmConfig::new(1, 2, Duration::from_millis(100), None).unwrap(),
1282 )
1283 .unwrap();
1284 let order = order(&token_a, &token_c, ONE_ETH, OrderSide::Sell);
1285
1286 let result = algorithm
1287 .find_best_route(SolveRequest::new(manager.graph(), market, &order))
1288 .await
1289 .unwrap();
1290
1291 assert_eq!(result.route().swaps().len(), 2);
1293 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(ONE_ETH * 2));
1294 assert_eq!(result.route().swaps()[0].component_id(), "component1".to_string());
1295 assert_eq!(*result.route().swaps()[1].amount_out(), BigUint::from(ONE_ETH * 2 * 3));
1296 assert_eq!(result.route().swaps()[1].component_id(), "component2".to_string());
1297 }
1298
1299 fn setup_market_multi_token(
1305 components: Vec<(&str, Vec<Token>, MockProtocolSim)>,
1306 tokens: &[Token],
1307 ) -> (MarketData, TopologyGraphManager<DepthAndPrice>) {
1308 let mut market = MarketState::new();
1309 market.update_gas_price(BlockGasPrice {
1310 block_number: 1,
1311 block_hash: Default::default(),
1312 block_timestamp: 0,
1313 pricing: GasPrice::Legacy { gas_price: BigUint::from(1u64) },
1314 });
1315 market.upsert_tokens(tokens.to_vec());
1316 let protocol_components: Vec<_> = components
1317 .iter()
1318 .map(|(id, tokens, _)| component(id, tokens))
1319 .collect();
1320 market.upsert_components(protocol_components);
1321 let states: Vec<_> = components
1322 .into_iter()
1323 .map(|(id, _, sim)| (id.to_string(), Box::new(sim) as Box<dyn ProtocolSim>))
1324 .collect();
1325 market.update_states(states);
1326
1327 let mut manager = TopologyGraphManager::default();
1328 manager.initialize_graph(&market.component_topology());
1329 (wrap_market(market), manager)
1330 }
1331
1332 #[tokio::test]
1338 async fn test_find_best_route_never_takes_one_pool_twice() {
1339 let token_a = token(0x01, "A");
1340 let token_b = token(0x02, "B");
1341 let token_c = token(0x03, "C");
1342
1343 let (market, manager) = setup_market_multi_token(
1344 vec![
1345 (
1346 "multi",
1347 vec![token_a.clone(), token_b.clone(), token_c.clone()],
1348 MockProtocolSim::new(3.0),
1349 ),
1350 ("bc", vec![token_b.clone(), token_c.clone()], MockProtocolSim::new(2.0)),
1351 ],
1352 &[token_a.clone(), token_b.clone(), token_c.clone()],
1353 );
1354
1355 let algorithm = MostLiquidAlgorithm::with_config(
1357 AlgorithmConfig::new(2, 2, Duration::from_millis(100), None).unwrap(),
1358 )
1359 .unwrap();
1360 let order = order(&token_a, &token_c, 1000, OrderSide::Sell);
1361
1362 let result = algorithm
1363 .find_best_route(SolveRequest::new(manager.graph(), market, &order))
1364 .await
1365 .unwrap();
1366
1367 assert_eq!(result.route().swaps().len(), 2);
1368 assert_eq!(result.route().swaps()[0].component_id(), "multi");
1369 assert_eq!(result.route().swaps()[1].component_id(), "bc");
1370 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(3000u64));
1372 assert_eq!(*result.route().swaps()[1].amount_out(), BigUint::from(6000u64));
1373 }
1374
1375 #[tokio::test]
1379 async fn test_find_best_route_drops_sequence_with_no_pool_left() {
1380 let token_a = token(0x01, "A");
1381 let token_b = token(0x02, "B");
1382 let token_c = token(0x03, "C");
1383
1384 let (market, manager) = setup_market_multi_token(
1385 vec![(
1386 "multi",
1387 vec![token_a.clone(), token_b.clone(), token_c.clone()],
1388 MockProtocolSim::new(3.0),
1389 )],
1390 &[token_a.clone(), token_b.clone(), token_c.clone()],
1391 );
1392
1393 let algorithm = MostLiquidAlgorithm::with_config(
1394 AlgorithmConfig::new(2, 2, Duration::from_millis(100), None).unwrap(),
1395 )
1396 .unwrap();
1397 let order = order(&token_a, &token_c, 1000, OrderSide::Sell);
1398
1399 let result = algorithm
1400 .find_best_route(SolveRequest::new(manager.graph(), market, &order))
1401 .await;
1402
1403 assert!(matches!(result, Err(AlgorithmError::InsufficientLiquidity)));
1404 }
1405
1406 #[tokio::test]
1413 async fn test_find_best_route_uses_pools_without_edge_weights() {
1414 let token_a = token(0x01, "A");
1416 let token_b = token(0x02, "B");
1417
1418 let mut market = MarketState::new();
1420 let component1_state = MockProtocolSim::new(2.0);
1421 let component2_state = MockProtocolSim::new(3.0); let component1_comp = component("component1", &[token_a.clone(), token_b.clone()]);
1424 let component2_comp = component("component2", &[token_a.clone(), token_b.clone()]);
1425
1426 market.update_gas_price(BlockGasPrice {
1428 block_number: 1,
1429 block_hash: Default::default(),
1430 block_timestamp: 0,
1431 pricing: GasPrice::Legacy { gas_price: BigUint::from(1u64) },
1432 });
1433
1434 market.upsert_components(vec![component1_comp, component2_comp]);
1436
1437 market.update_states(vec![
1439 ("component1".to_string(), Box::new(component1_state.clone()) as Box<dyn ProtocolSim>),
1440 ("component2".to_string(), Box::new(component2_state) as Box<dyn ProtocolSim>),
1441 ]);
1442
1443 market.upsert_tokens(vec![token_a.clone(), token_b.clone()]);
1445
1446 let mut manager = TopologyGraphManager::default();
1448 manager.initialize_graph(&market.component_topology());
1449
1450 let weight =
1452 DepthAndPrice::from_protocol_sim(&component1_state, &token_a, &token_b).unwrap();
1453 manager
1454 .set_pool_weight(
1455 &"component1".to_string(),
1456 &token_a.address,
1457 &token_b.address,
1458 weight,
1459 false,
1460 )
1461 .unwrap();
1462
1463 let algorithm = MostLiquidAlgorithm::with_config(
1465 AlgorithmConfig::new(1, 1, Duration::from_millis(100), None).unwrap(),
1466 )
1467 .unwrap();
1468 let order = order(&token_a, &token_b, ONE_ETH, OrderSide::Sell);
1469 let market = wrap_market(market);
1470 let result = algorithm
1471 .find_best_route(SolveRequest::new(manager.graph(), market, &order))
1472 .await
1473 .unwrap();
1474
1475 assert_eq!(result.route().swaps().len(), 1);
1478 assert_eq!(result.route().swaps()[0].component_id(), "component2");
1479 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(ONE_ETH * 3));
1480 }
1481
1482 #[tokio::test]
1487 async fn test_find_best_route_without_any_derived_data() {
1488 let token_a = token(0x01, "A");
1490 let token_b = token(0x02, "B");
1491
1492 let mut market = MarketState::new();
1493 let component_state = MockProtocolSim::new(2.0);
1494 let comp = component("component1", &[token_a.clone(), token_b.clone()]);
1495
1496 market.update_gas_price(BlockGasPrice {
1498 block_number: 1,
1499 block_hash: Default::default(),
1500 block_timestamp: 0,
1501 pricing: GasPrice::Eip1559 {
1502 base_fee_per_gas: BigUint::from(1u64),
1503 max_priority_fee_per_gas: BigUint::from(0u64),
1504 },
1505 });
1506
1507 market.upsert_components(vec![comp]);
1508 market.update_states(vec![(
1509 "component1".to_string(),
1510 Box::new(component_state) as Box<dyn ProtocolSim>,
1511 )]);
1512 market.upsert_tokens(vec![token_a.clone(), token_b.clone()]);
1513
1514 let mut manager = TopologyGraphManager::default();
1516 manager.initialize_graph(&market.component_topology());
1517
1518 let algorithm = MostLiquidAlgorithm::new();
1519 let order = order(&token_a, &token_b, ONE_ETH, OrderSide::Sell);
1520 let market = wrap_market(market);
1521
1522 let result = algorithm
1523 .find_best_route(SolveRequest::new(manager.graph(), market, &order))
1524 .await
1525 .expect("an unranked route is still a route");
1526
1527 assert_eq!(result.route().swaps().len(), 1);
1528 assert_eq!(result.route().swaps()[0].component_id(), "component1");
1529 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(ONE_ETH * 2));
1530 }
1531
1532 #[tokio::test]
1533 async fn test_find_best_route_gas_exceeds_output_returns_negative_net() {
1534 let token_a = token(0x01, "A");
1535 let token_b = token(0x02, "B");
1536
1537 let (market, manager) = setup_market_weighted(vec![(
1538 "component1",
1539 &token_a,
1540 &token_b,
1541 MockProtocolSim::new(2.0),
1542 )]);
1543 let mut market_write = market.try_write().unwrap();
1544
1545 market_write.update_gas_price(BlockGasPrice {
1548 block_number: 1,
1549 block_hash: Default::default(),
1550 block_timestamp: 0,
1551 pricing: GasPrice::Eip1559 {
1552 base_fee_per_gas: BigUint::from(1_000_000u64),
1553 max_priority_fee_per_gas: BigUint::from(1_000_000u64),
1554 },
1555 });
1556 drop(market_write); let algorithm = MostLiquidAlgorithm::new();
1559 let order = order(&token_a, &token_b, 1, OrderSide::Sell); let derived = setup_derived_with_token_prices(std::slice::from_ref(&token_b.address));
1563
1564 let result = algorithm
1566 .find_best_route(
1567 SolveRequest::new(manager.graph(), market, &order).with_derived(derived),
1568 )
1569 .await
1570 .expect("should return route even with negative net_amount_out");
1571
1572 assert_eq!(result.route().swaps().len(), 1);
1574 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(2u64)); let expected_net = BigInt::from(2) - BigInt::from(100_000_000_000u64);
1578 assert_eq!(result.net_amount_out(), &expected_net);
1579 }
1580
1581 #[tokio::test]
1582 async fn test_find_best_route_insufficient_liquidity() {
1583 let token_a = token(0x01, "A");
1585 let token_b = token(0x02, "B");
1586
1587 let (market, manager) = setup_market_weighted(vec![(
1588 "component1",
1589 &token_a,
1590 &token_b,
1591 MockProtocolSim::new(2.0).with_liquidity(1000),
1592 )]);
1593
1594 let algorithm = MostLiquidAlgorithm::new();
1595 let order = order(&token_a, &token_b, ONE_ETH, OrderSide::Sell); let result = algorithm
1598 .find_best_route(SolveRequest::new(manager.graph(), market, &order))
1599 .await;
1600 assert!(matches!(result, Err(AlgorithmError::InsufficientLiquidity)));
1601 }
1602
1603 #[tokio::test]
1604 async fn test_find_best_route_missing_gas_price_returns_error() {
1605 let token_a = token(0x01, "A");
1607 let token_b = token(0x02, "B");
1608
1609 let mut market = MarketState::new();
1610 let component_state = MockProtocolSim::new(2.0);
1611 let comp = component("component1", &[token_a.clone(), token_b.clone()]);
1612
1613 market.upsert_components(vec![comp]);
1615 market.update_states(vec![(
1616 "component1".to_string(),
1617 Box::new(component_state.clone()) as Box<dyn ProtocolSim>,
1618 )]);
1619 market.upsert_tokens(vec![token_a.clone(), token_b.clone()]);
1620
1621 let mut manager = TopologyGraphManager::default();
1623 manager.initialize_graph(&market.component_topology());
1624 let weight =
1625 DepthAndPrice::from_protocol_sim(&component_state, &token_a, &token_b).unwrap();
1626 manager
1627 .set_pool_weight(
1628 &"component1".to_string(),
1629 &token_a.address,
1630 &token_b.address,
1631 weight,
1632 false,
1633 )
1634 .unwrap();
1635
1636 let algorithm = MostLiquidAlgorithm::new();
1637 let order = order(&token_a, &token_b, ONE_ETH, OrderSide::Sell);
1638 let market = wrap_market(market);
1639
1640 let result = algorithm
1641 .find_best_route(SolveRequest::new(manager.graph(), market, &order))
1642 .await;
1643
1644 assert!(matches!(result, Err(AlgorithmError::DataNotFound { kind: "gas price", .. })));
1646 }
1647
1648 #[tokio::test]
1650 async fn test_find_best_route_circular_arbitrage() {
1651 let token_a = token(0x01, "A");
1652 let token_b = token(0x02, "B");
1653
1654 let (market, manager) = setup_market_weighted(vec![
1657 ("component1", &token_a, &token_b, MockProtocolSim::new(2.0)),
1658 ("component2", &token_a, &token_b, MockProtocolSim::new(3.0)),
1659 ]);
1660
1661 let algorithm = MostLiquidAlgorithm::with_config(
1663 AlgorithmConfig::new(2, 2, Duration::from_millis(100), None).unwrap(),
1664 )
1665 .unwrap();
1666
1667 let order = order(&token_a, &token_a, 100, OrderSide::Sell);
1669
1670 let result = algorithm
1671 .find_best_route(SolveRequest::new(manager.graph(), market, &order))
1672 .await
1673 .unwrap();
1674
1675 assert_eq!(result.route().swaps().len(), 2, "Should have 2 swaps for circular route");
1677
1678 assert_eq!(*result.route().swaps()[0].token_in(), token_a.address);
1680 assert_eq!(*result.route().swaps()[0].token_out(), token_b.address);
1681 assert_eq!(result.route().swaps()[0].component_id(), "component2");
1682 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(300u64));
1683
1684 assert_eq!(*result.route().swaps()[1].token_in(), token_b.address);
1686 assert_eq!(*result.route().swaps()[1].token_out(), token_a.address);
1687 assert_eq!(result.route().swaps()[1].component_id(), "component1");
1688 assert_eq!(*result.route().swaps()[1].amount_out(), BigUint::from(150u64));
1689
1690 assert_eq!(result.route().swaps()[0].token_in(), result.route().swaps()[1].token_out());
1692 }
1693
1694 #[tokio::test]
1697 async fn test_find_best_route_circular_needs_two_pools() {
1698 let token_a = token(0x01, "A");
1699 let token_b = token(0x02, "B");
1700
1701 let (market, manager) = setup_market_weighted(vec![(
1702 "component1",
1703 &token_a,
1704 &token_b,
1705 MockProtocolSim::new(2.0),
1706 )]);
1707
1708 let algorithm = MostLiquidAlgorithm::with_config(
1709 AlgorithmConfig::new(2, 2, Duration::from_millis(100), None).unwrap(),
1710 )
1711 .unwrap();
1712 let order = order(&token_a, &token_a, 100, OrderSide::Sell);
1713
1714 let result = algorithm
1715 .find_best_route(SolveRequest::new(manager.graph(), market, &order))
1716 .await;
1717
1718 assert!(matches!(result, Err(AlgorithmError::InsufficientLiquidity)));
1719 }
1720
1721 #[tokio::test]
1722 async fn test_find_best_route_respects_min_hops() {
1723 let token_a = token(0x01, "A");
1726 let token_b = token(0x02, "B");
1727 let token_c = token(0x03, "C");
1728
1729 let (market, manager) = setup_market_weighted(vec![
1730 ("component_ab", &token_a, &token_b, MockProtocolSim::new(10.0)), ("component_ac", &token_a, &token_c, MockProtocolSim::new(2.0)), ("component_cb", &token_c, &token_b, MockProtocolSim::new(3.0)), ]);
1736
1737 let algorithm = MostLiquidAlgorithm::with_config(
1739 AlgorithmConfig::new(2, 3, Duration::from_millis(100), None).unwrap(),
1740 )
1741 .unwrap();
1742 let order = order(&token_a, &token_b, 100, OrderSide::Sell);
1743
1744 let derived = setup_derived_with_token_prices(std::slice::from_ref(&token_b.address));
1747
1748 let result = algorithm
1749 .find_best_route(
1750 SolveRequest::new(manager.graph(), market, &order).with_derived(derived),
1751 )
1752 .await
1753 .unwrap();
1754
1755 assert_eq!(result.route().swaps().len(), 2, "Should use 2-hop path due to min_hops=2");
1757 assert_eq!(result.route().swaps()[0].component_id(), "component_ac");
1758 assert_eq!(result.route().swaps()[1].component_id(), "component_cb");
1759 }
1760
1761 #[tokio::test]
1762 async fn test_find_best_route_respects_max_hops() {
1763 let token_a = token(0x01, "A");
1766 let token_b = token(0x02, "B");
1767 let token_c = token(0x03, "C");
1768
1769 let (market, manager) = setup_market_weighted(vec![
1770 ("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
1771 ("component_bc", &token_b, &token_c, MockProtocolSim::new(3.0)),
1772 ]);
1773
1774 let algorithm = MostLiquidAlgorithm::with_config(
1776 AlgorithmConfig::new(1, 1, Duration::from_millis(100), None).unwrap(),
1777 )
1778 .unwrap();
1779 let order = order(&token_a, &token_c, 100, OrderSide::Sell);
1780
1781 let result = algorithm
1782 .find_best_route(SolveRequest::new(manager.graph(), market, &order))
1783 .await;
1784 assert!(
1785 matches!(result, Err(AlgorithmError::NoPath { reason: NoPathReason::NoGraphPath, .. })),
1786 "Should fail when max_hops is insufficient"
1787 );
1788 }
1789
1790 #[tokio::test]
1791 async fn test_find_best_route_timeout_returns_best_so_far() {
1792 let token_a = token(0x01, "A");
1796 let token_b = token(0x02, "B");
1797
1798 let (market, manager) = setup_market_weighted(vec![
1800 ("component1", &token_a, &token_b, MockProtocolSim::new(1.0)),
1801 ("component2", &token_a, &token_b, MockProtocolSim::new(2.0)),
1802 ("component3", &token_a, &token_b, MockProtocolSim::new(3.0)),
1803 ("component4", &token_a, &token_b, MockProtocolSim::new(4.0)),
1804 ("component5", &token_a, &token_b, MockProtocolSim::new(5.0)),
1805 ]);
1806
1807 let algorithm = MostLiquidAlgorithm::with_config(
1809 AlgorithmConfig::new(1, 1, Duration::from_millis(0), None).unwrap(),
1810 )
1811 .unwrap();
1812 let order = order(&token_a, &token_b, 100, OrderSide::Sell);
1813
1814 let result = algorithm
1815 .find_best_route(SolveRequest::new(manager.graph(), market, &order))
1816 .await;
1817
1818 match result {
1823 Ok(r) => {
1824 assert_eq!(r.route().swaps().len(), 1);
1826 }
1827 Err(AlgorithmError::Timeout { .. }) => {
1828 }
1830 Err(e) => panic!("Unexpected error: {:?}", e),
1831 }
1832 }
1833
1834 #[rstest::rstest]
1837 #[case::default_config(1, 3, 50)]
1838 #[case::single_hop_only(1, 1, 100)]
1839 #[case::multi_hop_min(2, 5, 200)]
1840 #[case::zero_timeout(1, 3, 0)]
1841 #[case::large_values(10, 100, 10000)]
1842 fn test_algorithm_config_getters(
1843 #[case] min_hops: usize,
1844 #[case] max_hops: usize,
1845 #[case] timeout_ms: u64,
1846 ) {
1847 use crate::algorithm::Algorithm;
1848
1849 let algorithm = MostLiquidAlgorithm::with_config(
1850 AlgorithmConfig::new(min_hops, max_hops, Duration::from_millis(timeout_ms), None)
1851 .unwrap(),
1852 )
1853 .unwrap();
1854
1855 assert_eq!(algorithm.query.max_hops, max_hops);
1856 assert_eq!(algorithm.timeout, Duration::from_millis(timeout_ms));
1857 assert_eq!(algorithm.name(), "most_liquid");
1858 }
1859
1860 #[test]
1861 fn test_algorithm_default_config() {
1862 use crate::algorithm::Algorithm;
1863
1864 let algorithm = MostLiquidAlgorithm::new();
1865
1866 assert_eq!(algorithm.query.max_hops, 3);
1867 assert_eq!(algorithm.timeout, Duration::from_millis(500));
1868 assert_eq!(algorithm.name(), "most_liquid");
1869 }
1870
1871 #[tokio::test]
1880 async fn test_each_leg_takes_its_best_paying_pool() {
1881 let token_a = token(0x01, "A");
1882 let token_b = token(0x02, "B");
1883 let token_c = token(0x03, "C");
1884
1885 let (market, manager) = setup_market_weighted(vec![
1887 ("ab1", &token_a, &token_b, MockProtocolSim::new(1.0).with_liquidity(9_000_000)),
1888 ("ab2", &token_a, &token_b, MockProtocolSim::new(3.0).with_liquidity(1_000_000)),
1889 ("bc1", &token_b, &token_c, MockProtocolSim::new(1.0).with_liquidity(9_000_000)),
1890 ("bc2", &token_b, &token_c, MockProtocolSim::new(5.0).with_liquidity(1_000_000)),
1891 ]);
1892
1893 let algorithm = MostLiquidAlgorithm::with_config(
1894 AlgorithmConfig::new(2, 2, Duration::from_millis(100), None).unwrap(),
1895 )
1896 .unwrap();
1897 let order = order(&token_a, &token_c, 1000, OrderSide::Sell);
1898 let result = algorithm
1899 .find_best_route(SolveRequest::new(manager.graph(), market, &order))
1900 .await
1901 .unwrap();
1902
1903 let chosen: Vec<&str> = result
1904 .route()
1905 .swaps()
1906 .iter()
1907 .map(|swap| swap.component_id())
1908 .collect();
1909 assert_eq!(chosen, vec!["ab2", "bc2"], "each hop must take its best-paying pool");
1910
1911 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(3000u64));
1914 assert_eq!(*result.route().swaps()[1].amount_in(), BigUint::from(3000u64));
1915 assert_eq!(*result.route().swaps()[1].amount_out(), BigUint::from(15000u64));
1916 }
1917
1918 #[tokio::test]
1924 async fn test_max_routes_caps_token_sequences_not_pools_on_a_pair() {
1925 let token_a = token(0x01, "A");
1934 let token_b = token(0x02, "B");
1935
1936 let (market, manager) = setup_market_weighted(vec![
1937 ("component1", &token_a, &token_b, MockProtocolSim::new(4.0).with_liquidity(1_000_000)),
1938 ("component2", &token_a, &token_b, MockProtocolSim::new(3.0).with_liquidity(2_000_000)),
1939 ("component3", &token_a, &token_b, MockProtocolSim::new(2.0).with_liquidity(3_000_000)),
1940 ("component4", &token_a, &token_b, MockProtocolSim::new(1.0).with_liquidity(4_000_000)),
1941 ]);
1942
1943 let algorithm = MostLiquidAlgorithm::with_config(
1944 AlgorithmConfig::new(1, 1, Duration::from_millis(100), Some(2)).unwrap(),
1945 )
1946 .unwrap();
1947 let order = order(&token_a, &token_b, 1000, OrderSide::Sell);
1948 let result = algorithm
1949 .find_best_route(SolveRequest::new(manager.graph(), market, &order))
1950 .await
1951 .unwrap();
1952
1953 assert_eq!(result.route().swaps().len(), 1);
1956 assert_eq!(result.route().swaps()[0].component_id(), "component1");
1957 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(4000u64));
1958 }
1959
1960 #[tokio::test]
1961 async fn test_find_best_route_no_cap_when_max_routes_is_none() {
1962 let token_a = token(0x01, "A");
1964 let token_b = token(0x02, "B");
1965
1966 let (market, manager) = setup_market_weighted(vec![
1967 ("component1", &token_a, &token_b, MockProtocolSim::new(4.0).with_liquidity(1_000_000)),
1968 ("component2", &token_a, &token_b, MockProtocolSim::new(3.0).with_liquidity(2_000_000)),
1969 ("component3", &token_a, &token_b, MockProtocolSim::new(2.0).with_liquidity(3_000_000)),
1970 ("component4", &token_a, &token_b, MockProtocolSim::new(1.0).with_liquidity(4_000_000)),
1971 ]);
1972
1973 let algorithm = MostLiquidAlgorithm::with_config(
1974 AlgorithmConfig::new(1, 1, Duration::from_millis(100), None).unwrap(),
1975 )
1976 .unwrap();
1977 let order = order(&token_a, &token_b, 1000, OrderSide::Sell);
1978 let result = algorithm
1979 .find_best_route(SolveRequest::new(manager.graph(), market, &order))
1980 .await
1981 .unwrap();
1982
1983 assert_eq!(result.route().swaps().len(), 1);
1985 assert_eq!(result.route().swaps()[0].component_id(), "component1");
1986 assert_eq!(*result.route().swaps()[0].amount_out(), BigUint::from(4000u64));
1987 }
1988
1989 #[test]
1992 fn test_algorithm_config_rejects_zero_max_routes() {
1993 let result = AlgorithmConfig::new(1, 3, Duration::from_millis(100), Some(0));
1994 assert!(matches!(
1995 result,
1996 Err(AlgorithmError::InvalidConfiguration { reason }) if reason.contains("max_routes must be at least 1")
1997 ));
1998 }
1999
2000 #[test]
2001 fn test_algorithm_config_rejects_zero_min_hops() {
2002 let result = AlgorithmConfig::new(0, 3, Duration::from_millis(100), None);
2003 assert!(matches!(
2004 result,
2005 Err(AlgorithmError::InvalidConfiguration { reason }) if reason.contains("min_hops must be at least 1")
2006 ));
2007 }
2008
2009 #[test]
2010 fn test_algorithm_config_rejects_min_greater_than_max() {
2011 let result = AlgorithmConfig::new(5, 3, Duration::from_millis(100), None);
2012 assert!(matches!(
2013 result,
2014 Err(AlgorithmError::InvalidConfiguration { reason }) if reason.contains("cannot exceed")
2015 ));
2016 }
2017}