1use std::{
27 collections::VecDeque,
28 sync::Arc,
29 time::{Duration, Instant},
30};
31
32use num_bigint::{BigInt, BigUint};
33use num_traits::{ToPrimitive, Zero};
34use petgraph::{graph::NodeIndex, prelude::EdgeRef, stable_graph::EdgeReference};
35use rustc_hash::{FxHashMap, FxHashSet};
36use tracing::{debug, instrument, trace, warn};
37use tycho_simulation::{
38 tycho_common::models::Address,
39 tycho_core::{models::token::Token, simulation::protocol_sim::Price},
40};
41
42use super::{
43 split_primitives::MarketOverrides, Algorithm, AlgorithmConfig, AlgorithmError, NoPathReason,
44};
45use crate::{
46 algorithm::{
47 paths,
48 request::{SolveParts, SolveRequest},
49 sim_guard::GuardedProtocolSim,
50 },
51 derived::{
52 computation::ComputationRequirements,
53 types::{SpotPrices, TokenGasPrices},
54 },
55 feed::market_data::{MarketData, MarketState},
56 graph::{petgraph::StableDiGraph, EdgeData, PetgraphStableDiGraphManager},
57 types::{ComponentId, Order, Route, RouteExclusions, RouteResult, Swap},
58};
59
60struct Subgraph<'a> {
62 adjacency: FxHashMap<NodeIndex, Vec<(NodeIndex, ComponentId)>>,
63 token_nodes: FxHashSet<NodeIndex>,
64 component_ids: FxHashSet<&'a ComponentId>,
68}
69
70pub(crate) struct BellmanFordContext {
76 pub(crate) token_in_node: NodeIndex,
77 pub(crate) token_out_node: Option<NodeIndex>,
80 pub(crate) adj: FxHashMap<NodeIndex, Vec<(NodeIndex, ComponentId)>>,
81 pub(crate) token_map: FxHashMap<NodeIndex, Arc<Token>>,
82 pub(crate) market_data: MarketState,
83 pub(crate) gas_price_wei: Option<BigUint>,
84 pub(crate) token_prices: Option<TokenGasPrices>,
85 pub(crate) spot_prices: Option<SpotPrices>,
86 pub(crate) node_address: FxHashMap<NodeIndex, Address>,
87 pub(crate) max_idx: usize,
88 pub(crate) scoring: RouteScoringMode,
89}
90
91impl BellmanFordContext {
92 pub(crate) fn reroot_toward<'a>(
101 &mut self,
102 graph: &'a StableDiGraph<()>,
103 token_in_node: NodeIndex,
104 token_out_node: NodeIndex,
105 hops_to_token_out: &FxHashMap<NodeIndex, usize>,
106 max_hops: usize,
107 ) -> Option<FxHashSet<&'a ComponentId>> {
108 let subgraph = BellmanFordAlgorithm::get_subgraph_with_hop_map(
109 graph,
110 (token_in_node, Some(token_out_node)),
111 Some(hops_to_token_out),
112 max_hops,
113 &RouteExclusions::default(),
114 )?;
115 self.adj = subgraph.adjacency;
116 self.token_in_node = token_in_node;
117 self.token_out_node = Some(token_out_node);
118 Some(subgraph.component_ids)
119 }
120}
121
122pub(crate) struct ReachOutcome {
124 pub(crate) reached: FxHashMap<Address, ReachedToken>,
127 pub(crate) timed_out: bool,
130}
131
132pub(crate) struct ReachedToken {
135 pub(crate) amount_out: BigUint,
138 pub(crate) components: Vec<ComponentId>,
140}
141
142pub(crate) enum RouteScoringMode {
144 GrossOutput,
146 NetOutput,
148}
149
150#[derive(Default)]
152pub(crate) struct FindRouteOptions {
153 pub(crate) overrides: MarketOverrides,
156}
157
158struct SPFAResult {
160 amount: Vec<BigUint>,
162 predecessor: Vec<Option<(NodeIndex, ComponentId)>>,
164 edge_gas: Vec<BigUint>,
166 spot_product: Vec<f64>,
168 input_below_hop_gas: bool,
171 timed_out: bool,
173}
174
175pub struct BellmanFordAlgorithm {
181 max_hops: usize,
182 timeout: Duration,
183 gas_aware: bool,
184 connector_tokens: Option<FxHashSet<Address>>,
185}
186
187impl Default for BellmanFordAlgorithm {
188 fn default() -> Self {
189 Self::with_config(AlgorithmConfig::default())
190 }
191}
192
193impl BellmanFordAlgorithm {
194 pub(crate) fn with_config(config: AlgorithmConfig) -> Self {
195 Self {
196 max_hops: config.max_hops(),
197 timeout: config.timeout(),
198 gas_aware: config.gas_aware(),
199 connector_tokens: config.connector_tokens().cloned(),
200 }
201 }
202
203 pub(crate) fn max_hops(&self) -> usize {
206 self.max_hops
207 }
208
209 pub(crate) async fn build_context(
216 &self,
217 request: SolveRequest<'_, StableDiGraph<()>>,
218 ) -> Result<BellmanFordContext, AlgorithmError> {
219 let SolveParts { graph, order, market, label, derived, exclusions } = request.into_parts();
220 if !order.is_sell() {
221 return Err(AlgorithmError::ExactOutNotSupported);
222 }
223
224 let (token_prices, spot_prices) = if let Some(ref d) = derived {
225 let guard = d.read().await;
226 (guard.token_prices().cloned(), guard.spot_prices().cloned())
227 } else {
228 (None, None)
229 };
230
231 let token_in_node = graph
232 .node_indices()
233 .find(|&n| &graph[n] == order.token_in())
234 .ok_or(AlgorithmError::NoPath {
235 from: order.token_in().clone(),
236 to: order.token_out().clone(),
237 reason: NoPathReason::SourceTokenNotInGraph,
238 })?;
239 let token_out_node = graph
240 .node_indices()
241 .find(|&n| &graph[n] == order.token_out())
242 .ok_or(AlgorithmError::NoPath {
243 from: order.token_in().clone(),
244 to: order.token_out().clone(),
245 reason: NoPathReason::DestinationTokenNotInGraph,
246 })?;
247
248 if token_in_node == token_out_node {
249 return Err(AlgorithmError::NoPath {
250 from: order.token_in().clone(),
251 to: order.token_out().clone(),
252 reason: NoPathReason::NoGraphPath,
253 });
254 }
255
256 let subgraph =
257 Self::get_subgraph(graph, token_in_node, token_out_node, self.max_hops, &exclusions)
258 .ok_or_else(|| AlgorithmError::NoPath {
259 from: order.token_in().clone(),
260 to: order.token_out().clone(),
261 reason: NoPathReason::NoGraphPath,
262 })?;
263 let market_view = paths::read_market(&market, label).await?;
266 let market_data = market_view.extract_subset_with_overlay(&subgraph.component_ids);
267 drop(market_view);
268 let mut ctx = self.context_from_snapshot(
269 graph,
270 market_data,
271 subgraph,
272 token_in_node,
273 Some(token_out_node),
274 );
275 ctx.token_prices = token_prices;
276 ctx.spot_prices = spot_prices;
277 Ok(ctx)
278 }
279
280 pub(crate) async fn build_context_from_source_token(
298 &self,
299 graph: &StableDiGraph<()>,
300 market: MarketData,
301 token_in: &Address,
302 walk_hops: usize,
303 prune_toward: Option<&FxHashSet<Address>>,
304 ) -> Option<BellmanFordContext> {
305 let mut token_in_node = None;
306 let mut target_nodes = Vec::new();
307 for node in graph.node_indices() {
308 let address = &graph[node];
309 if address == token_in {
310 token_in_node = Some(node);
311 }
312 if prune_toward.is_some_and(|targets| targets.contains(address)) {
313 target_nodes.push(node);
314 }
315 }
316 let token_in_node = token_in_node?;
317 let exclusions = RouteExclusions::default();
320 let hops_to_targets = prune_toward.map(|_| {
321 Self::get_hops_to_reach_any(
322 graph,
323 target_nodes,
324 (token_in_node, token_in_node),
325 walk_hops,
326 &exclusions,
327 )
328 });
329 let subgraph = Self::get_subgraph_with_hop_map(
330 graph,
331 (token_in_node, None),
332 hops_to_targets.as_ref(),
333 walk_hops,
334 &exclusions,
335 )?;
336 let market_data = market
337 .extract_subset_batched(&subgraph.component_ids)
338 .await;
339 Some(self.context_from_snapshot(graph, market_data, subgraph, token_in_node, None))
340 }
341
342 fn context_from_snapshot(
353 &self,
354 graph: &StableDiGraph<()>,
355 market_data: MarketState,
356 subgraph: Subgraph<'_>,
357 token_in_node: NodeIndex,
358 token_out_node: Option<NodeIndex>,
359 ) -> BellmanFordContext {
360 let Subgraph { adjacency: adj, token_nodes, component_ids: _ } = subgraph;
361
362 let token_map: FxHashMap<NodeIndex, Arc<Token>> = token_nodes
363 .iter()
364 .filter_map(|&node| {
365 market_data
366 .get_token_shared(&graph[node])
367 .map(|token| (node, Arc::clone(token)))
368 })
369 .collect();
370 let gas_price_wei = market_data
371 .gas_price()
372 .map(|gp| gp.effective_gas_price().clone());
373
374 let node_address: FxHashMap<NodeIndex, Address> = token_map
375 .iter()
376 .map(|(&node, token)| (node, token.address.clone()))
377 .collect();
378
379 let max_idx = graph
380 .node_indices()
381 .map(|n| n.index())
382 .max()
383 .unwrap_or(0) +
384 1;
385
386 let scoring = if self.gas_aware {
387 RouteScoringMode::NetOutput
388 } else {
389 RouteScoringMode::GrossOutput
390 };
391
392 debug!(
393 edges = adj
394 .values()
395 .map(Vec::len)
396 .sum::<usize>(),
397 tokens = token_map.len(),
398 "subgraph extracted"
399 );
400
401 BellmanFordContext {
402 token_in_node,
403 token_out_node,
404 adj,
405 token_map,
406 market_data,
407 gas_price_wei,
408 token_prices: None,
409 spot_prices: None,
410 node_address,
411 max_idx,
412 scoring,
413 }
414 }
415
416 pub(crate) fn reach_from_source_token(
428 &self,
429 ctx: &BellmanFordContext,
430 amount_in: &BigUint,
431 ) -> ReachOutcome {
432 let spfa = self.run_spfa(ctx, amount_in, &MarketOverrides::default(), Instant::now());
433
434 let mut reached = FxHashMap::default();
435 let mut dropped = 0usize;
436 for (idx, amount) in spfa.amount.iter().enumerate() {
437 if amount.is_zero() || idx == ctx.token_in_node.index() {
438 continue;
439 }
440 let node = NodeIndex::new(idx);
441 let Some(address) = ctx.node_address.get(&node) else {
442 trace!(node = idx, "destination dropped: no token metadata for node");
443 dropped += 1;
444 continue;
445 };
446 let path_edges = match Self::reconstruct_path(
447 node,
448 ctx.token_in_node,
449 &spfa.predecessor,
450 ) {
451 Ok(path_edges) => path_edges,
452 Err(error) => {
453 trace!(node = idx, token = %address, %error, "destination dropped: path reconstruction failed");
454 dropped += 1;
455 continue;
456 }
457 };
458 let components = path_edges
459 .into_iter()
460 .map(|(_, _, component_id)| component_id)
461 .collect();
462 reached
463 .insert(address.clone(), ReachedToken { amount_out: amount.clone(), components });
464 }
465
466 debug!(
467 reached = reached.len(),
468 dropped,
469 timed_out = spfa.timed_out,
470 "found a route to every reachable destination from one relaxation"
471 );
472 ReachOutcome { reached, timed_out: spfa.timed_out }
473 }
474
475 pub(crate) fn find_single_route(
482 &self,
483 ctx: &BellmanFordContext,
484 order: &Order,
485 opts: FindRouteOptions,
486 ) -> Result<RouteResult, AlgorithmError> {
487 let start = Instant::now();
488
489 let Some(token_out_node) = ctx.token_out_node else {
490 return Err(AlgorithmError::Other(
491 "find_single_route needs a context built with a destination".to_string(),
492 ));
493 };
494 debug_assert!(
497 ctx.node_address.get(&ctx.token_in_node) == Some(order.token_in()) &&
498 ctx.node_address.get(&token_out_node) == Some(order.token_out()),
499 "context endpoints do not match the order's token pair"
500 );
501
502 let spfa = self.run_spfa(ctx, order.amount(), &opts.overrides, start);
503
504 let out_idx = token_out_node.index();
505 if spfa.amount[out_idx].is_zero() {
506 let reason = if spfa.input_below_hop_gas {
510 NoPathReason::AmountTooSmall
511 } else {
512 NoPathReason::NoGraphPath
513 };
514 return Err(AlgorithmError::NoPath {
515 from: order.token_in().clone(),
516 to: order.token_out().clone(),
517 reason,
518 });
519 }
520
521 let path_edges =
525 Self::reconstruct_path(token_out_node, ctx.token_in_node, &spfa.predecessor)?;
526
527 let route =
528 Self::build_route(ctx, &path_edges, &spfa.amount, &spfa.edge_gas, &opts.overrides)?;
529
530 let final_amount_out = spfa.amount[out_idx].clone();
531 let gas_price = ctx
532 .gas_price_wei
533 .clone()
534 .unwrap_or_default();
535
536 let net_amount_out = match ctx.scoring {
537 RouteScoringMode::GrossOutput => BigInt::from(final_amount_out.clone()),
540 RouteScoringMode::NetOutput => Self::compute_net_amount_out(
541 &final_amount_out,
542 &route,
543 &gas_price,
544 ctx.token_prices.as_ref(),
545 &spfa.spot_product,
546 &ctx.node_address,
547 ctx.token_in_node,
548 )?,
549 };
550
551 let result = RouteResult::new(route, net_amount_out, gas_price);
552
553 let solve_time_ms = start.elapsed().as_millis() as u64;
554 debug!(
555 solve_time_ms,
556 hops = result.route().swaps().len(),
557 amount_in = %order.amount(),
558 amount_out = %final_amount_out,
559 net_amount_out = %result.net_amount_out(),
560 "bellman_ford route found"
561 );
562
563 Ok(result)
564 }
565
566 fn run_spfa(
572 &self,
573 ctx: &BellmanFordContext,
574 amount_in: &BigUint,
575 overrides: &MarketOverrides,
576 start: Instant,
577 ) -> SPFAResult {
578 let mut amount: Vec<BigUint> = vec![BigUint::ZERO; ctx.max_idx];
582 let mut predecessor: Vec<Option<(NodeIndex, ComponentId)>> = vec![None; ctx.max_idx];
583 let mut edge_gas: Vec<BigUint> = vec![BigUint::ZERO; ctx.max_idx];
584 let mut cumul_gas: Vec<BigUint> = vec![BigUint::ZERO; ctx.max_idx];
585
586 amount[ctx.token_in_node.index()] = amount_in.clone();
587
588 let mut spot_product: Vec<f64> = vec![0.0; ctx.max_idx];
591 spot_product[ctx.token_in_node.index()] = 1.0;
592
593 let mut input_below_hop_gas = false;
594
595 let gas_aware = matches!(ctx.scoring, RouteScoringMode::NetOutput) &&
596 ctx.gas_price_wei.is_some() &&
597 ctx.token_prices.is_some();
598 if !gas_aware && matches!(ctx.scoring, RouteScoringMode::NetOutput) {
599 debug!("gas-aware comparison disabled (missing gas_price or token_prices)");
600 } else if matches!(ctx.scoring, RouteScoringMode::GrossOutput) {
601 debug!("gas-aware comparison disabled by config");
602 }
603
604 let mut active_nodes: Vec<NodeIndex> = vec![ctx.token_in_node];
605 let mut timed_out = false;
606
607 for round in 0..self.max_hops {
608 if start.elapsed() >= self.timeout {
609 debug!(round, "timeout during relaxation");
610 timed_out = true;
611 break;
612 }
613 if active_nodes.is_empty() {
614 debug!(round, "no active nodes, stopping early");
615 break;
616 }
617
618 let mut next_active: FxHashSet<NodeIndex> = FxHashSet::default();
619
620 for &u in &active_nodes {
621 let u_idx = u.index();
622 if amount[u_idx].is_zero() {
623 continue;
624 }
625
626 let Some(token_u) = ctx.token_map.get(&u) else { continue };
627 let Some(edges) = ctx.adj.get(&u) else { continue };
628
629 for (v, component_id) in edges {
630 let v_idx = v.index();
631
632 if Self::path_has_conflict(u, *v, component_id, &predecessor) {
634 continue;
635 }
636
637 if !self.connector_allows(ctx, *v) {
640 continue;
641 }
642
643 let Some(token_v) = ctx.token_map.get(v) else { continue };
644
645 let sim: &dyn tycho_simulation::tycho_common::simulation::protocol_sim::ProtocolSim =
647 if let Some(s) = overrides.get(component_id) {
648 s
649 } else if let Some(s) = ctx.market_data.get_simulation_state(component_id) {
650 s
651 } else {
652 continue;
653 };
654
655 let result =
656 match sim.get_amount_out_guarded(amount[u_idx].clone(), token_u, token_v) {
657 Ok(r) => r,
658 Err(e) => {
659 trace!(
660 component_id,
661 error = %e,
662 "simulation failed, skipping edge"
663 );
664 continue;
665 }
666 };
667
668 let candidate_cumul_gas = &cumul_gas[u_idx] + &result.gas;
669
670 let candidate_spot = Self::compute_edge_spot_product(
673 spot_product[u_idx],
674 component_id,
675 ctx.node_address.get(&u),
676 ctx.node_address.get(v),
677 ctx.spot_prices.as_ref(),
678 );
679
680 let is_better = if gas_aware {
682 let v_price = Self::resolve_token_price(
683 ctx.node_address.get(v),
684 ctx.token_prices.as_ref(),
685 candidate_spot,
686 ctx.node_address.get(&ctx.token_in_node),
687 );
688 let net_candidate = Self::gas_adjusted_amount(
689 &result.amount,
690 &candidate_cumul_gas,
691 ctx.gas_price_wei.as_ref().unwrap(),
692 v_price.as_ref(),
693 );
694 if !input_below_hop_gas && net_candidate <= BigInt::ZERO {
699 let u_price = Self::resolve_token_price(
700 ctx.node_address.get(&u),
701 ctx.token_prices.as_ref(),
702 spot_product[u_idx],
703 ctx.node_address.get(&ctx.token_in_node),
704 );
705 if Self::gas_adjusted_amount(
706 &amount[u_idx],
707 &result.gas,
708 ctx.gas_price_wei.as_ref().unwrap(),
709 u_price.as_ref(),
710 ) <= BigInt::ZERO
711 {
712 input_below_hop_gas = true;
713 }
714 }
715 let net_existing = Self::gas_adjusted_amount(
716 &amount[v_idx],
717 &cumul_gas[v_idx],
718 ctx.gas_price_wei.as_ref().unwrap(),
719 v_price.as_ref(),
720 );
721 net_candidate > net_existing
722 } else {
723 if result.amount.is_zero() {
724 input_below_hop_gas = true;
725 }
726 result.amount > amount[v_idx]
727 };
728
729 if is_better {
730 spot_product[v_idx] = candidate_spot;
731 amount[v_idx] = result.amount;
732 predecessor[v_idx] = Some((u, component_id.clone()));
733 edge_gas[v_idx] = result.gas;
734 cumul_gas[v_idx] = candidate_cumul_gas;
735 next_active.insert(*v);
736 }
737 }
738 }
739
740 active_nodes = next_active.into_iter().collect();
741 active_nodes.sort_unstable();
746 }
747
748 SPFAResult { amount, predecessor, edge_gas, spot_product, input_below_hop_gas, timed_out }
749 }
750
751 fn connector_allows(&self, ctx: &BellmanFordContext, v: NodeIndex) -> bool {
754 let (Some(tokens), Some(v_addr)) = (&self.connector_tokens, ctx.node_address.get(&v))
755 else {
756 return true;
757 };
758 v == ctx.token_in_node || ctx.token_out_node == Some(v) || tokens.contains(v_addr)
759 }
760
761 fn build_route(
763 ctx: &BellmanFordContext,
764 path_edges: &[(NodeIndex, NodeIndex, ComponentId)],
765 amount: &[BigUint],
766 edge_gas: &[BigUint],
767 overrides: &MarketOverrides,
768 ) -> Result<Route, AlgorithmError> {
769 let mut swaps = Vec::with_capacity(path_edges.len());
770 let mut tokens: FxHashMap<Address, Token> = FxHashMap::default();
771
772 for (from_node, to_node, component_id) in path_edges {
773 let token_in = ctx
774 .token_map
775 .get(from_node)
776 .ok_or_else(|| AlgorithmError::DataNotFound {
777 kind: "token",
778 id: Some(format!("{:?}", from_node)),
779 })?;
780 let token_out = ctx
781 .token_map
782 .get(to_node)
783 .ok_or_else(|| AlgorithmError::DataNotFound {
784 kind: "token",
785 id: Some(format!("{:?}", to_node)),
786 })?;
787 let component = ctx
788 .market_data
789 .get_component(component_id)
790 .ok_or_else(|| AlgorithmError::DataNotFound {
791 kind: "component",
792 id: Some(component_id.clone()),
793 })?;
794 let sim_state = overrides
797 .get(component_id)
798 .or_else(|| {
799 ctx.market_data
800 .get_simulation_state(component_id)
801 })
802 .ok_or_else(|| AlgorithmError::DataNotFound {
803 kind: "simulation state",
804 id: Some(component_id.clone()),
805 })?;
806
807 swaps.push(Swap::new(
808 component_id.clone(),
809 component.protocol_system.clone(),
810 token_in.address.clone(),
811 token_out.address.clone(),
812 amount[from_node.index()].clone(),
813 amount[to_node.index()].clone(),
814 edge_gas[to_node.index()].clone(),
815 component.clone(),
816 sim_state.clone_box(),
817 ));
818 tokens
819 .entry(token_in.address.clone())
820 .or_insert_with(|| Token::clone(token_in));
821 tokens
822 .entry(token_out.address.clone())
823 .or_insert_with(|| Token::clone(token_out));
824 }
825
826 Ok(Route::new(swaps, tokens)?)
827 }
828
829 fn gas_adjusted_amount(
834 gross: &BigUint,
835 cumul_gas: &BigUint,
836 gas_price_wei: &BigUint,
837 token_price: Option<&Price>,
838 ) -> BigInt {
839 match token_price {
840 Some(price) if !price.denominator.is_zero() => {
841 let gas_cost = cumul_gas * gas_price_wei * &price.numerator / &price.denominator;
842 BigInt::from(gross.clone()) - BigInt::from(gas_cost)
843 }
844 _ => BigInt::from(gross.clone()),
845 }
846 }
847
848 fn compute_edge_spot_product(
853 parent_spot: f64,
854 component_id: &ComponentId,
855 u_addr: Option<&Address>,
856 v_addr: Option<&Address>,
857 spot_prices: Option<&SpotPrices>,
858 ) -> f64 {
859 if parent_spot == 0.0 {
860 return 0.0;
861 }
862 let (Some(u), Some(v), Some(prices)) = (u_addr, v_addr, spot_prices) else {
863 return 0.0;
864 };
865 let key = (component_id.clone(), u.clone(), v.clone());
866 match prices.get(&key) {
867 Some(&spot) if spot > 0.0 => parent_spot * spot,
868 _ => 0.0,
869 }
870 }
871
872 fn resolve_token_price(
879 v_addr: Option<&Address>,
880 token_prices: Option<&TokenGasPrices>,
881 spot_product: f64,
882 token_in_addr: Option<&Address>,
883 ) -> Option<Price> {
884 let prices = token_prices?;
885 let addr = v_addr?;
886
887 if let Some(price) = prices.get(addr) {
889 return Some(price.clone());
890 }
891
892 if spot_product > 0.0 {
894 if let Some(in_price) = token_in_addr.and_then(|a| prices.get(a)) {
895 let in_rate_f64 = in_price.numerator.to_f64()? / in_price.denominator.to_f64()?;
896 let estimated_rate = in_rate_f64 * spot_product;
897 let denom = BigUint::from(10u64).pow(18);
898 let numer_f64 = estimated_rate * 1e18;
899 if numer_f64.is_finite() && numer_f64 > 0.0 {
900 return Some(Price {
901 numerator: BigUint::from(numer_f64 as u128),
902 denominator: denom,
903 });
904 }
905 }
906 }
907
908 None
909 }
910
911 pub(crate) fn path_has_conflict(
914 from: NodeIndex,
915 target_node: NodeIndex,
916 target_component: &ComponentId,
917 predecessor: &[Option<(NodeIndex, ComponentId)>],
918 ) -> bool {
919 let mut current = from;
920 loop {
921 if current == target_node {
922 return true;
923 }
924 match &predecessor[current.index()] {
925 Some((prev, cid)) => {
926 if cid == target_component {
927 return true;
928 }
929 current = *prev;
930 }
931 None => return false,
932 }
933 }
934 }
935
936 pub(crate) fn reconstruct_path(
939 token_out: NodeIndex,
940 token_in: NodeIndex,
941 predecessor: &[Option<(NodeIndex, ComponentId)>],
942 ) -> Result<Vec<(NodeIndex, NodeIndex, ComponentId)>, AlgorithmError> {
943 let mut path = Vec::new();
944 let mut current = token_out;
945 let mut visited = FxHashSet::default();
946
947 while current != token_in {
948 if !visited.insert(current) {
949 return Err(AlgorithmError::Other("cycle in predecessor chain".to_string()));
950 }
951
952 let idx = current.index();
953 match &predecessor
954 .get(idx)
955 .and_then(|p| p.as_ref())
956 {
957 Some((prev_node, component_id)) => {
958 path.push((*prev_node, current, component_id.clone()));
959 current = *prev_node;
960 }
961 None => {
962 return Err(AlgorithmError::Other(format!(
963 "broken predecessor chain at node {idx}"
964 )));
965 }
966 }
967 }
968
969 path.reverse();
970 Ok(path)
971 }
972
973 fn get_subgraph<'a>(
989 graph: &'a StableDiGraph<()>,
990 token_in: NodeIndex,
991 token_out: NodeIndex,
992 max_hops: usize,
993 exclusions: &RouteExclusions,
994 ) -> Option<Subgraph<'a>> {
995 let hops_to_token_out =
999 Self::get_hops_to_reach(graph, token_in, token_out, max_hops, exclusions);
1000 Self::get_subgraph_with_hop_map(
1001 graph,
1002 (token_in, Some(token_out)),
1003 Some(&hops_to_token_out),
1004 max_hops,
1005 exclusions,
1006 )
1007 }
1008
1009 fn get_subgraph_with_hop_map<'a>(
1015 graph: &'a StableDiGraph<()>,
1016 endpoints: (NodeIndex, Option<NodeIndex>),
1017 hops_to_token_out: Option<&FxHashMap<NodeIndex, usize>>,
1018 max_hops: usize,
1019 exclusions: &RouteExclusions,
1020 ) -> Option<Subgraph<'a>> {
1021 let (token_in, token_out) = endpoints;
1022 let mut adj: FxHashMap<NodeIndex, Vec<(NodeIndex, ComponentId)>> = FxHashMap::default();
1023 let mut token_nodes: FxHashSet<NodeIndex> = FxHashSet::default();
1024 let mut component_ids: FxHashSet<&ComponentId> = FxHashSet::default();
1025 let mut visited_nodes = FxHashSet::default();
1026 let mut queued_nodes = VecDeque::new();
1027
1028 visited_nodes.insert(token_in);
1029 token_nodes.insert(token_in);
1030 queued_nodes.push_back((token_in, 0usize));
1031
1032 while let Some((node, depth_walked)) = queued_nodes.pop_front() {
1033 if depth_walked >= max_hops {
1034 continue;
1035 }
1036 for edge in graph.edges(node) {
1037 let next_token = edge.target();
1038
1039 if !Self::can_cross(
1042 graph,
1043 edge,
1044 (token_in, token_out.unwrap_or(token_in)),
1045 exclusions,
1046 ) {
1047 continue;
1048 }
1049
1050 if let Some(hops_to_token_out) = &hops_to_token_out {
1051 let Some(&hops_left) = hops_to_token_out.get(&next_token) else {
1054 continue;
1056 };
1057
1058 if depth_walked + 1 + hops_left > max_hops {
1059 continue;
1061 }
1062 }
1063
1064 let component_id = &edge.weight().component_id;
1065 adj.entry(node)
1066 .or_default()
1067 .push((next_token, component_id.clone()));
1068 component_ids.insert(component_id);
1069 token_nodes.insert(next_token);
1070
1071 if visited_nodes.insert(next_token) {
1072 queued_nodes.push_back((next_token, depth_walked + 1));
1073 }
1074 }
1075 }
1076
1077 if adj.is_empty() {
1078 return None;
1079 }
1080
1081 Some(Subgraph { adjacency: adj, token_nodes, component_ids })
1082 }
1083
1084 fn can_cross(
1086 graph: &StableDiGraph<()>,
1087 edge: EdgeReference<'_, EdgeData<()>>,
1088 endpoints: (NodeIndex, NodeIndex),
1089 exclusions: &RouteExclusions,
1090 ) -> bool {
1091 !exclusions.excludes_pool(&edge.weight().component_id) &&
1092 exclusions
1093 .allows_token(&graph[edge.target()], (&graph[endpoints.0], &graph[endpoints.1]))
1094 }
1095
1096 pub(crate) fn get_hops_to_reach(
1100 graph: &StableDiGraph<()>,
1101 token_in: NodeIndex,
1102 token_out: NodeIndex,
1103 max_hops: usize,
1104 exclusions: &RouteExclusions,
1105 ) -> FxHashMap<NodeIndex, usize> {
1106 Self::get_hops_to_reach_any(graph, [token_out], (token_in, token_out), max_hops, exclusions)
1107 }
1108
1109 fn get_hops_to_reach_any(
1114 graph: &StableDiGraph<()>,
1115 sources: impl IntoIterator<Item = NodeIndex>,
1116 endpoints: (NodeIndex, NodeIndex),
1117 max_hops: usize,
1118 exclusions: &RouteExclusions,
1119 ) -> FxHashMap<NodeIndex, usize> {
1120 let mut hops_to_reach: FxHashMap<NodeIndex, usize> = FxHashMap::default();
1121 let mut frontier = Vec::new();
1122 for source in sources {
1123 hops_to_reach.insert(source, 0);
1124 frontier.push(source);
1125 }
1126 for depth in 1..=max_hops {
1127 let mut next = Vec::new();
1128 for node in frontier {
1129 for edge in graph.edges(node) {
1130 let neighbor = edge.target();
1131 if hops_to_reach.contains_key(&neighbor) ||
1132 !Self::can_cross(graph, edge, endpoints, exclusions)
1133 {
1134 continue;
1135 }
1136 hops_to_reach.insert(neighbor, depth);
1137 next.push(neighbor);
1138 }
1139 }
1140 frontier = next;
1141 }
1142
1143 hops_to_reach
1144 }
1145
1146 #[allow(clippy::too_many_arguments)]
1152 fn compute_net_amount_out(
1153 amount_out: &BigUint,
1154 route: &Route,
1155 gas_price: &BigUint,
1156 token_prices: Option<&TokenGasPrices>,
1157 spot_product: &[f64],
1158 node_address: &FxHashMap<NodeIndex, Address>,
1159 token_in_node: NodeIndex,
1160 ) -> Result<BigInt, AlgorithmError> {
1161 let last_swap = route.swaps().last().ok_or_else(|| {
1162 AlgorithmError::Other("compute_net_amount_out called with empty route".to_string())
1163 })?;
1164
1165 let total_gas = route.total_gas();
1166
1167 if gas_price.is_zero() {
1168 warn!("missing gas price, returning gross amount_out");
1169 return Ok(BigInt::from(amount_out.clone()));
1170 }
1171
1172 let gas_cost_wei = &total_gas * gas_price;
1173
1174 let out_addr = last_swap.token_out();
1176 let out_node_spot = node_address
1177 .iter()
1178 .find(|(_, addr)| *addr == out_addr)
1179 .and_then(|(node, _)| spot_product.get(node.index()).copied())
1180 .unwrap_or(0.0);
1181
1182 let output_price = Self::resolve_token_price(
1183 Some(out_addr),
1184 token_prices,
1185 out_node_spot,
1186 node_address.get(&token_in_node),
1187 );
1188
1189 Ok(match output_price {
1190 Some(price) if !price.denominator.is_zero() => {
1191 let gas_cost = &gas_cost_wei * &price.numerator / &price.denominator;
1192 BigInt::from(amount_out.clone()) - BigInt::from(gas_cost)
1193 }
1194 _ => {
1195 debug!("no gas price for output token, returning gross amount_out");
1196 BigInt::from(amount_out.clone())
1197 }
1198 })
1199 }
1200}
1201
1202impl Algorithm for BellmanFordAlgorithm {
1203 type GraphType = StableDiGraph<()>;
1204 type GraphManager = PetgraphStableDiGraphManager<()>;
1205
1206 fn name(&self) -> &str {
1207 "bellman_ford"
1208 }
1209
1210 #[instrument(level = "debug", skip_all, fields(order_id = %request.order().id()))]
1211 async fn find_best_route(
1212 &self,
1213 request: SolveRequest<'_, Self::GraphType>,
1214 ) -> Result<RouteResult, AlgorithmError> {
1215 let order = request.order();
1216 let ctx = self.build_context(request).await?;
1217 self.find_single_route(&ctx, order, FindRouteOptions::default())
1218 }
1219
1220 fn computation_requirements(&self) -> ComputationRequirements {
1221 ComputationRequirements::none()
1225 .allow_stale("token_prices")
1226 .expect("token_prices requirement conflicts (bug)")
1227 .allow_stale("spot_prices")
1228 .expect("spot_prices requirement conflicts (bug)")
1229 }
1230
1231 fn timeout(&self) -> Duration {
1232 self.timeout
1233 }
1234}
1235
1236#[cfg(test)]
1237mod tests {
1238 use std::sync::Arc;
1239
1240 use num_bigint::BigInt;
1241 use tokio::sync::RwLock;
1242 use tycho_simulation::{
1243 tycho_common::{models::Address, simulation::protocol_sim::ProtocolSim},
1244 tycho_ethereum::gas::{BlockGasPrice, GasPrice},
1245 };
1246
1247 use super::*;
1248 use crate::{
1249 algorithm::test_utils::{component, order, token, MockProtocolSim},
1250 derived::{types::TokenGasPrices, DerivedData},
1251 feed::market_data::{MarketData, MarketState},
1252 graph::GraphManager,
1253 types::quote::OrderSide,
1254 };
1255
1256 fn setup_market_bf(
1260 components: Vec<(&str, &Token, &Token, MockProtocolSim)>,
1261 ) -> (MarketData, PetgraphStableDiGraphManager<()>) {
1262 let mut market = MarketState::new();
1263
1264 market.update_gas_price(BlockGasPrice {
1265 block_number: 1,
1266 block_hash: Default::default(),
1267 block_timestamp: 0,
1268 pricing: GasPrice::Legacy { gas_price: BigUint::from(100u64) },
1269 });
1270 market.update_last_updated(crate::types::BlockInfo::new(1, "0x00".into(), 0));
1271
1272 for (component_id, token_in, token_out, state) in components {
1273 let tokens = vec![token_in.clone(), token_out.clone()];
1274 let comp = component(component_id, &tokens);
1275 market.upsert_components(std::iter::once(comp));
1276 market.update_states([(
1277 component_id.to_string(),
1278 Box::new(state) as Box<dyn ProtocolSim>,
1279 )]);
1280 market.upsert_tokens(tokens);
1281 }
1282
1283 let mut graph_manager = PetgraphStableDiGraphManager::default();
1284 graph_manager.initialize_graph(&market.component_topology());
1285
1286 (MarketData::new(Arc::new(RwLock::new(market))), graph_manager)
1287 }
1288
1289 fn setup_derived_with_token_prices(
1290 token_addresses: &[Address],
1291 ) -> crate::derived::SharedDerivedDataRef {
1292 use tycho_simulation::tycho_core::simulation::protocol_sim::Price;
1293
1294 let mut token_prices: TokenGasPrices = FxHashMap::default();
1295 for address in token_addresses {
1296 token_prices.insert(
1297 address.clone(),
1298 Price { numerator: BigUint::from(1u64), denominator: BigUint::from(1u64) },
1299 );
1300 }
1301
1302 let mut derived_data = DerivedData::new();
1303 derived_data.set_token_prices(token_prices, vec![], 1, true);
1304 Arc::new(RwLock::new(derived_data))
1305 }
1306
1307 fn bf_algorithm(max_hops: usize, timeout_ms: u64) -> BellmanFordAlgorithm {
1308 BellmanFordAlgorithm::with_config(
1309 AlgorithmConfig::new(1, max_hops, Duration::from_millis(timeout_ms), None).unwrap(),
1310 )
1311 }
1312
1313 #[tokio::test]
1316 async fn test_find_best_route_with_excluded_endpoints() {
1317 let a = token(0x01, "A");
1318 let b = token(0x02, "B");
1319 let c = token(0x03, "C");
1320 let (market, manager) = setup_market_bf(vec![
1321 ("ab", &a, &b, MockProtocolSim::new(2.0)),
1322 ("bc", &b, &c, MockProtocolSim::new(2.0)),
1323 ]);
1324 let order = order(&a, &c, 1000, OrderSide::Sell);
1325 let result = bf_algorithm(2, 1000)
1326 .find_best_route(SolveRequest::new(manager.graph(), market, &order).with_exclusions(
1327 RouteExclusions::default().with_tokens([a.address.clone(), c.address.clone()]),
1328 ))
1329 .await
1330 .unwrap();
1331 assert_eq!(result.route().swaps().len(), 2);
1332 assert_eq!(result.route().swaps()[0].component_id(), "ab");
1333 assert_eq!(result.route().swaps()[1].component_id(), "bc");
1334 }
1335
1336 #[tokio::test]
1338 async fn test_find_best_route_with_excluded_pool() {
1339 let token_a = token(0x01, "A");
1340 let token_b = token(0x02, "B");
1341
1342 let (market, manager) = setup_market_bf(vec![
1343 ("best", &token_a, &token_b, MockProtocolSim::new(3.0)),
1344 ("second", &token_a, &token_b, MockProtocolSim::new(2.0)),
1345 ]);
1346 let ord = order(&token_a, &token_b, 1000, OrderSide::Sell);
1347
1348 let result = bf_algorithm(2, 1000)
1349 .find_best_route(
1350 SolveRequest::new(manager.graph(), market, &ord)
1351 .with_exclusions(RouteExclusions::default().with_pools(["best".to_string()])),
1352 )
1353 .await
1354 .unwrap();
1355
1356 assert_eq!(result.route().swaps()[0].component_id(), "second");
1357 }
1358
1359 #[test]
1369 fn test_get_subgraph_keeps_full_length_routes_and_drops_dead_ends() {
1370 let token_a = token(0x01, "A");
1371 let token_b = token(0x02, "B");
1372 let token_c = token(0x03, "C");
1373 let token_d = token(0x04, "D");
1374
1375 let (_, manager) = setup_market_bf(vec![
1376 ("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
1377 ("component_bc", &token_b, &token_c, MockProtocolSim::new(3.0)),
1378 ("component_ad", &token_a, &token_d, MockProtocolSim::new(5.0)),
1379 ]);
1380 let graph = manager.graph();
1381 let node = |address: &Address| {
1382 graph
1383 .node_indices()
1384 .find(|&n| &graph[n] == address)
1385 .expect("token in graph")
1386 };
1387 let Subgraph { adjacency: adj, component_ids, .. } = BellmanFordAlgorithm::get_subgraph(
1388 graph,
1389 node(&token_a.address),
1390 node(&token_c.address),
1391 2,
1392 &RouteExclusions::default(),
1393 )
1394 .unwrap();
1395
1396 let kept = |id: &str| {
1397 component_ids
1398 .iter()
1399 .any(|component_id| *component_id == id)
1400 };
1401
1402 assert!(kept("component_ab"), "the route's first hop must survive");
1404 assert!(kept("component_bc"), "the route's second hop must survive");
1405 assert!(!kept("component_ad"), "a dead end must not be kept");
1408
1409 let from_b = adj
1412 .get(&node(&token_b.address))
1413 .map(Vec::as_slice)
1414 .unwrap_or_default();
1415 assert!(
1416 from_b
1417 .iter()
1418 .all(|(target, _)| *target != node(&token_a.address)),
1419 "B -> A cannot finish the route and must not be kept"
1420 );
1421 }
1422
1423 #[test]
1435 fn test_get_subgraph_drops_detours_that_cannot_finish_in_budget() {
1436 let token_a = token(0x01, "A");
1437 let token_b = token(0x02, "B");
1438 let token_c = token(0x03, "C");
1439 let token_d = token(0x04, "D");
1440 let token_e = token(0x05, "E");
1441
1442 let (_, manager) = setup_market_bf(vec![
1443 ("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
1444 ("component_bc", &token_b, &token_c, MockProtocolSim::new(3.0)),
1445 ("component_ad", &token_a, &token_d, MockProtocolSim::new(5.0)),
1446 ("component_de", &token_d, &token_e, MockProtocolSim::new(5.0)),
1447 ("component_ec", &token_e, &token_c, MockProtocolSim::new(5.0)),
1448 ]);
1449 let graph = manager.graph();
1450 let node = |address: &Address| {
1451 graph
1452 .node_indices()
1453 .find(|&n| &graph[n] == address)
1454 .expect("token in graph")
1455 };
1456 let Subgraph { token_nodes, component_ids, .. } = BellmanFordAlgorithm::get_subgraph(
1457 graph,
1458 node(&token_a.address),
1459 node(&token_c.address),
1460 2,
1461 &RouteExclusions::default(),
1462 )
1463 .unwrap();
1464
1465 let kept = |id: &str| {
1466 component_ids
1467 .iter()
1468 .any(|component_id| *component_id == id)
1469 };
1470
1471 assert!(kept("component_ab"), "the two-hop route's first leg must survive");
1472 assert!(kept("component_bc"), "the two-hop route's second leg must survive");
1473
1474 assert!(!kept("component_ad"), "the step into a detour must not be kept");
1476 assert!(!kept("component_de"), "nor anything further along it");
1477 assert!(!kept("component_ec"), "nor its last leg into the destination");
1478 assert!(
1479 !token_nodes.contains(&node(&token_d.address)),
1480 "a token no legal route reaches must not be kept"
1481 );
1482 }
1483
1484 #[test]
1490 fn test_subgraph_without_destination() {
1491 let token_g = token(0x01, "G");
1492 let token_b = token(0x02, "B");
1493 let token_c = token(0x03, "C");
1494 let token_d = token(0x04, "D");
1495
1496 let (_, manager) = setup_market_bf(vec![
1497 ("component_gb", &token_g, &token_b, MockProtocolSim::new(2.0)),
1498 ("component_bc", &token_b, &token_c, MockProtocolSim::new(2.0)),
1499 ("component_cd", &token_c, &token_d, MockProtocolSim::new(2.0)),
1500 ]);
1501 let graph = manager.graph();
1502 let node = |address: &Address| {
1503 graph
1504 .node_indices()
1505 .find(|&n| &graph[n] == address)
1506 .expect("token in graph")
1507 };
1508 let Subgraph { token_nodes, component_ids, .. } =
1509 BellmanFordAlgorithm::get_subgraph_with_hop_map(
1510 graph,
1511 (node(&token_g.address), None),
1512 None,
1513 2,
1514 &RouteExclusions::default(),
1515 )
1516 .unwrap();
1517
1518 let kept = |id: &str| {
1519 component_ids
1520 .iter()
1521 .any(|component_id| *component_id == id)
1522 };
1523
1524 assert!(kept("component_gb"), "the first hop is within budget");
1525 assert!(kept("component_bc"), "the second hop spends the budget exactly");
1526 assert!(!kept("component_cd"), "an edge past the hop budget must not be kept");
1527 assert!(
1528 !token_nodes.contains(&node(&token_d.address)),
1529 "a token past the hop budget must not be kept"
1530 );
1531 }
1532
1533 #[tokio::test]
1534 async fn test_linear_path_found() {
1535 let token_a = token(0x01, "A");
1536 let token_b = token(0x02, "B");
1537 let token_c = token(0x03, "C");
1538 let token_d = token(0x04, "D");
1539
1540 let (market, manager) = setup_market_bf(vec![
1541 ("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
1542 ("component_bc", &token_b, &token_c, MockProtocolSim::new(3.0)),
1543 ("component_cd", &token_c, &token_d, MockProtocolSim::new(4.0)),
1544 ]);
1545
1546 let algo = bf_algorithm(4, 1000);
1547 let ord = order(&token_a, &token_d, 100, OrderSide::Sell);
1548
1549 let result = algo
1550 .find_best_route(SolveRequest::new(manager.graph(), market, &ord))
1551 .await
1552 .unwrap();
1553
1554 assert_eq!(result.route().swaps().len(), 3);
1555 assert_eq!(result.route().swaps()[0].amount_out(), &BigUint::from(200u64));
1557 assert_eq!(result.route().swaps()[1].amount_out(), &BigUint::from(600u64));
1558 assert_eq!(result.route().swaps()[2].amount_out(), &BigUint::from(2400u64));
1559 }
1560
1561 #[tokio::test]
1562 async fn test_picks_better_of_two_paths() {
1563 let token_a = token(0x01, "A");
1565 let token_b = token(0x02, "B");
1566 let token_c = token(0x03, "C");
1567 let token_d = token(0x04, "D");
1568
1569 let (market, manager) = setup_market_bf(vec![
1570 ("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
1571 ("component_bd", &token_b, &token_d, MockProtocolSim::new(3.0)),
1572 ("component_ac", &token_a, &token_c, MockProtocolSim::new(4.0)),
1573 ("component_cd", &token_c, &token_d, MockProtocolSim::new(1.0)),
1574 ]);
1575
1576 let algo = bf_algorithm(3, 1000);
1577 let ord = order(&token_a, &token_d, 100, OrderSide::Sell);
1578
1579 let result = algo
1580 .find_best_route(SolveRequest::new(manager.graph(), market, &ord))
1581 .await
1582 .unwrap();
1583
1584 assert_eq!(result.route().swaps().len(), 2);
1586 assert_eq!(result.route().swaps()[0].component_id(), "component_ab");
1587 assert_eq!(result.route().swaps()[1].component_id(), "component_bd");
1588 assert_eq!(result.route().swaps()[1].amount_out(), &BigUint::from(600u64));
1589 }
1590
1591 #[tokio::test]
1592 async fn test_parallel_components() {
1593 let token_a = token(0x01, "A");
1595 let token_b = token(0x02, "B");
1596
1597 let (market, manager) = setup_market_bf(vec![
1598 ("component1", &token_a, &token_b, MockProtocolSim::new(2.0)),
1599 ("component2", &token_a, &token_b, MockProtocolSim::new(5.0)),
1600 ]);
1601
1602 let algo = bf_algorithm(2, 1000);
1603 let ord = order(&token_a, &token_b, 100, OrderSide::Sell);
1604
1605 let result = algo
1606 .find_best_route(SolveRequest::new(manager.graph(), market, &ord))
1607 .await
1608 .unwrap();
1609
1610 assert_eq!(result.route().swaps().len(), 1);
1611 assert_eq!(result.route().swaps()[0].component_id(), "component2");
1612 assert_eq!(result.route().swaps()[0].amount_out(), &BigUint::from(500u64));
1613 }
1614
1615 #[tokio::test]
1616 async fn test_no_path_returns_error() {
1617 let token_a = token(0x01, "A");
1618 let token_b = token(0x02, "B");
1619 let token_c = token(0x03, "C");
1620
1621 let (market, manager) =
1623 setup_market_bf(vec![("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1624
1625 {
1627 let mut m = market.write().await;
1628 m.upsert_tokens(vec![token_c.clone()]);
1629 }
1630
1631 let algo = bf_algorithm(3, 1000);
1632 let ord = order(&token_a, &token_c, 100, OrderSide::Sell);
1633
1634 let result = algo
1635 .find_best_route(SolveRequest::new(manager.graph(), market, &ord))
1636 .await;
1637 assert!(matches!(result, Err(AlgorithmError::NoPath { .. })));
1638 }
1639
1640 #[tokio::test]
1641 async fn test_source_not_in_graph() {
1642 let token_a = token(0x01, "A");
1643 let token_b = token(0x02, "B");
1644 let token_x = token(0x99, "X");
1645
1646 let (market, manager) =
1647 setup_market_bf(vec![("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1648
1649 let algo = bf_algorithm(3, 1000);
1650 let ord = order(&token_x, &token_b, 100, OrderSide::Sell);
1651
1652 let result = algo
1653 .find_best_route(SolveRequest::new(manager.graph(), market, &ord))
1654 .await;
1655 assert!(matches!(
1656 result,
1657 Err(AlgorithmError::NoPath { reason: NoPathReason::SourceTokenNotInGraph, .. })
1658 ));
1659 }
1660
1661 #[tokio::test]
1662 async fn test_amount_too_small_when_reachable_but_zero_output() {
1663 let token_a = token(0x01, "A");
1664 let token_b = token(0x02, "B");
1665 let (market, manager) =
1667 setup_market_bf(vec![("component_ab", &token_a, &token_b, MockProtocolSim::new(0.5))]);
1668 let algo = bf_algorithm(3, 1000);
1669 let ord = order(&token_a, &token_b, 1, OrderSide::Sell);
1670
1671 let result = algo
1672 .find_best_route(SolveRequest::new(manager.graph(), market, &ord))
1673 .await;
1674 assert!(matches!(
1675 result,
1676 Err(AlgorithmError::NoPath { reason: NoPathReason::AmountTooSmall, .. })
1677 ));
1678 }
1679
1680 #[tokio::test]
1681 async fn test_amount_too_small_when_dust_occurs_mid_route() {
1682 let token_a = token(0x01, "A");
1685 let token_b = token(0x02, "B");
1686 let token_c = token(0x03, "C");
1687 let (market, manager) = setup_market_bf(vec![
1688 ("component_ab", &token_a, &token_b, MockProtocolSim::new(0.5)),
1689 ("component_bc", &token_b, &token_c, MockProtocolSim::new(2.0)),
1690 ]);
1691 let algo = bf_algorithm(3, 1000);
1692 let ord = order(&token_a, &token_c, 1, OrderSide::Sell);
1693
1694 let result = algo
1695 .find_best_route(SolveRequest::new(manager.graph(), market, &ord))
1696 .await;
1697 assert!(matches!(
1698 result,
1699 Err(AlgorithmError::NoPath { reason: NoPathReason::AmountTooSmall, .. })
1700 ));
1701 }
1702
1703 #[tokio::test]
1704 async fn test_no_graph_path_when_amount_too_large_for_liquidity() {
1705 let token_a = token(0x01, "A");
1708 let token_b = token(0x02, "B");
1709 let (market, manager) = setup_market_bf(vec![(
1710 "component_ab",
1711 &token_a,
1712 &token_b,
1713 MockProtocolSim::new(2.0).with_liquidity(500),
1714 )]);
1715 let algo = bf_algorithm(2, 1000);
1716 let ord = order(&token_a, &token_b, 1000, OrderSide::Sell);
1717
1718 let result = algo
1719 .find_best_route(SolveRequest::new(manager.graph(), market, &ord))
1720 .await;
1721 assert!(matches!(
1722 result,
1723 Err(AlgorithmError::NoPath { reason: NoPathReason::NoGraphPath, .. })
1724 ));
1725 }
1726
1727 #[tokio::test]
1728 async fn test_no_graph_path_when_unreachable_within_hops() {
1729 let token_a = token(0x01, "A");
1731 let token_b = token(0x02, "B");
1732 let token_c = token(0x03, "C");
1733 let (market, manager) = setup_market_bf(vec![
1734 ("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
1735 ("component_bc", &token_b, &token_c, MockProtocolSim::new(2.0)),
1736 ]);
1737 let algo = bf_algorithm(1, 1000);
1738 let ord = order(&token_a, &token_c, 100, OrderSide::Sell);
1739
1740 let result = algo
1741 .find_best_route(SolveRequest::new(manager.graph(), market, &ord))
1742 .await;
1743 assert!(matches!(
1744 result,
1745 Err(AlgorithmError::NoPath { reason: NoPathReason::NoGraphPath, .. })
1746 ));
1747 }
1748
1749 #[tokio::test]
1750 async fn test_reach_from_source_token_covers_branches_off_any_pair() {
1751 let token_g = token(0x01, "G");
1754 let token_a = token(0x02, "A");
1755 let token_b = token(0x03, "B");
1756 let token_c = token(0x04, "C");
1757
1758 let (market, manager) = setup_market_bf(vec![
1759 ("component_ga", &token_g, &token_a, MockProtocolSim::new(2.0)),
1760 ("component_gb", &token_g, &token_b, MockProtocolSim::new(2.0)),
1761 ("component_bc", &token_b, &token_c, MockProtocolSim::new(2.0)),
1762 ]);
1763
1764 let algo = bf_algorithm(3, 1000);
1765 let ctx = algo
1766 .build_context_from_source_token(manager.graph(), market, &token_g.address, 3, None)
1767 .await
1768 .expect("gas token has outgoing edges");
1769 let routes = algo.reach_from_source_token(&ctx, &BigUint::from(100u64));
1770
1771 let reached: FxHashSet<Address> = routes.reached.keys().cloned().collect();
1772 let expected: FxHashSet<Address> = [&token_a, &token_b, &token_c]
1773 .into_iter()
1774 .map(|t| t.address.clone())
1775 .collect();
1776 assert_eq!(reached, expected);
1777 }
1778
1779 #[tokio::test]
1780 async fn test_reach_from_source_token_zero_timeout() {
1781 let token_g = token(0x01, "G");
1784 let token_a = token(0x02, "A");
1785 let (market, manager) =
1786 setup_market_bf(vec![("component_ga", &token_g, &token_a, MockProtocolSim::new(2.0))]);
1787
1788 let algo = bf_algorithm(3, 0);
1789 let ctx = algo
1790 .build_context_from_source_token(manager.graph(), market, &token_g.address, 3, None)
1791 .await
1792 .expect("source has outgoing edges");
1793
1794 let routes = algo.reach_from_source_token(&ctx, &BigUint::from(100u64));
1795
1796 assert!(routes.timed_out, "a zero timeout must be reported as a cut-short relaxation");
1797 assert!(routes.reached.is_empty());
1798 }
1799
1800 #[tokio::test]
1801 async fn test_context_pruned_toward_filter_tokens() {
1802 let token_g = token(0x01, "G");
1805 let token_a = token(0x02, "A");
1806 let token_b = token(0x03, "B");
1807 let token_c = token(0x04, "C");
1808
1809 let (market, manager) = setup_market_bf(vec![
1810 ("component_ga", &token_g, &token_a, MockProtocolSim::new(2.0)),
1811 ("component_gb", &token_g, &token_b, MockProtocolSim::new(2.0)),
1812 ("component_bc", &token_b, &token_c, MockProtocolSim::new(2.0)),
1813 ]);
1814
1815 let algo = bf_algorithm(3, 1000);
1816 let filter: FxHashSet<Address> = [token_a.address.clone()]
1817 .into_iter()
1818 .collect();
1819 let ctx = algo
1820 .build_context_from_source_token(
1821 manager.graph(),
1822 market,
1823 &token_g.address,
1824 2,
1825 Some(&filter),
1826 )
1827 .await
1828 .expect("the filter token is reachable");
1829
1830 assert!(ctx
1831 .market_data
1832 .get_simulation_state("component_ga")
1833 .is_some());
1834 assert!(
1835 ctx.market_data
1836 .get_simulation_state("component_gb")
1837 .is_none(),
1838 "the B branch sits on no G-A candidate path"
1839 );
1840 assert!(ctx
1841 .market_data
1842 .get_simulation_state("component_bc")
1843 .is_none());
1844
1845 let routes = algo.reach_from_source_token(&ctx, &BigUint::from(100u64));
1846 let reached: FxHashSet<Address> = routes.reached.keys().cloned().collect();
1847 assert_eq!(reached, filter);
1848 }
1849
1850 #[tokio::test]
1851 async fn test_find_single_route_rejects_a_context_built_without_destination() {
1852 let token_a = token(0x01, "A");
1853 let token_b = token(0x02, "B");
1854 let (market, manager) =
1855 setup_market_bf(vec![("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1856
1857 let algo = bf_algorithm(3, 1000);
1858 let ctx = algo
1859 .build_context_from_source_token(manager.graph(), market, &token_a.address, 3, None)
1860 .await
1861 .expect("source has outgoing edges");
1862 let ord = order(&token_a, &token_b, 100, OrderSide::Sell);
1863
1864 let result = algo.find_single_route(&ctx, &ord, FindRouteOptions::default());
1865 assert!(
1866 matches!(result, Err(AlgorithmError::Other(_))),
1867 "expected an Other error, got {result:?}"
1868 );
1869 }
1870
1871 #[tokio::test]
1872 async fn test_find_single_route_after_reroot() {
1873 let token_g = token(0x01, "G");
1876 let token_b = token(0x02, "B");
1877 let token_c = token(0x03, "C");
1878 let (market, manager) = setup_market_bf(vec![
1879 ("component_gb", &token_g, &token_b, MockProtocolSim::new(2.0)),
1880 ("component_bc", &token_b, &token_c, MockProtocolSim::new(2.0)),
1881 ]);
1882 let graph = manager.graph();
1883
1884 let algo = bf_algorithm(3, 1000);
1885 let mut ctx = algo
1886 .build_context_from_source_token(graph, market, &token_g.address, 3, None)
1887 .await
1888 .expect("source has outgoing edges");
1889 let node_of = |address: &Address| {
1890 graph
1891 .node_indices()
1892 .find(|&n| &graph[n] == address)
1893 .expect("token is in the graph")
1894 };
1895
1896 let gas_node = ctx.token_in_node;
1897 let hops_to_gas = BellmanFordAlgorithm::get_hops_to_reach(
1898 graph,
1899 gas_node,
1900 gas_node,
1901 3,
1902 &RouteExclusions::default(),
1903 );
1904 ctx.reroot_toward(graph, node_of(&token_c.address), gas_node, &hops_to_gas, 3)
1905 .expect("a C-to-G path exists");
1906 let ord = order(&token_c, &token_g, 100, OrderSide::Sell);
1907 let result = algo
1908 .find_single_route(&ctx, &ord, FindRouteOptions::default())
1909 .expect("re-rooted context solves back to its source");
1910
1911 assert_eq!(
1913 result
1914 .route()
1915 .amount_out(&token_g.address),
1916 BigUint::from(25u64)
1917 );
1918 }
1919
1920 #[tokio::test]
1921 async fn test_no_graph_path_when_connector_tokens_exclude_intermediate() {
1922 let token_a = token(0x01, "A");
1925 let token_b = token(0x02, "B");
1926 let token_c = token(0x03, "C");
1927
1928 let (market, manager) = setup_market_bf(vec![
1929 ("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
1930 ("component_bc", &token_b, &token_c, MockProtocolSim::new(2.0)),
1931 ]);
1932
1933 let algo = BellmanFordAlgorithm::with_config(
1934 AlgorithmConfig::new(1, 3, Duration::from_millis(1000), None)
1935 .unwrap()
1936 .with_connector_tokens(FxHashSet::default()),
1937 );
1938 let ord = order(&token_a, &token_c, 100, OrderSide::Sell);
1939
1940 let result = algo
1941 .find_best_route(SolveRequest::new(manager.graph(), market, &ord))
1942 .await;
1943 assert!(matches!(
1944 result,
1945 Err(AlgorithmError::NoPath { reason: NoPathReason::NoGraphPath, .. })
1946 ));
1947 }
1948
1949 #[tokio::test]
1950 async fn test_destination_not_in_graph() {
1951 let token_a = token(0x01, "A");
1952 let token_b = token(0x02, "B");
1953 let token_x = token(0x99, "X");
1954
1955 let (market, manager) =
1956 setup_market_bf(vec![("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1957
1958 let algo = bf_algorithm(3, 1000);
1959 let ord = order(&token_a, &token_x, 100, OrderSide::Sell);
1960
1961 let result = algo
1962 .find_best_route(SolveRequest::new(manager.graph(), market, &ord))
1963 .await;
1964 assert!(matches!(
1965 result,
1966 Err(AlgorithmError::NoPath { reason: NoPathReason::DestinationTokenNotInGraph, .. })
1967 ));
1968 }
1969
1970 #[tokio::test]
1971 async fn test_respects_max_hops() {
1972 let token_a = token(0x01, "A");
1974 let token_b = token(0x02, "B");
1975 let token_c = token(0x03, "C");
1976 let token_d = token(0x04, "D");
1977
1978 let (market, manager) = setup_market_bf(vec![
1979 ("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
1980 ("component_bc", &token_b, &token_c, MockProtocolSim::new(3.0)),
1981 ("component_cd", &token_c, &token_d, MockProtocolSim::new(4.0)),
1982 ]);
1983
1984 let algo = bf_algorithm(2, 1000);
1985 let ord = order(&token_a, &token_d, 100, OrderSide::Sell);
1986
1987 let result = algo
1988 .find_best_route(SolveRequest::new(manager.graph(), market, &ord))
1989 .await;
1990 assert!(
1991 matches!(result, Err(AlgorithmError::NoPath { .. })),
1992 "Should not find 3-hop path with max_hops=2"
1993 );
1994 }
1995
1996 #[tokio::test]
1997 async fn test_source_token_revisit_blocked() {
1998 let token_a = token(0x01, "A");
2001 let token_b = token(0x02, "B");
2002 let token_c = token(0x03, "C");
2003
2004 let (market, manager) = setup_market_bf(vec![
2005 ("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
2006 ("component_bc", &token_b, &token_c, MockProtocolSim::new(3.0)),
2007 ]);
2008
2009 let algo = bf_algorithm(4, 1000);
2010 let ord = order(&token_a, &token_c, 100, OrderSide::Sell);
2011
2012 let result = algo
2013 .find_best_route(SolveRequest::new(manager.graph(), market, &ord))
2014 .await
2015 .unwrap();
2016
2017 assert_eq!(result.route().swaps().len(), 2);
2019 assert_eq!(result.route().swaps()[0].component_id(), "component_ab");
2020 assert_eq!(result.route().swaps()[1].component_id(), "component_bc");
2021 assert_eq!(result.route().swaps()[1].amount_out(), &BigUint::from(600u64));
2022 }
2023
2024 #[tokio::test]
2025 async fn test_hub_token_revisit_blocked() {
2026 let token_a = token(0x01, "A");
2029 let token_c = token(0x02, "C");
2030 let token_b = token(0x03, "B");
2031 let token_d = token(0x04, "D");
2032
2033 let (market, manager) = setup_market_bf(vec![
2034 ("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
2035 ("component_bc", &token_b, &token_c, MockProtocolSim::new(3.0)),
2036 ("component_cb", &token_c, &token_b, MockProtocolSim::new(100.0)),
2037 ("component_bd", &token_b, &token_d, MockProtocolSim::new(2.0)),
2038 ]);
2039
2040 let algo = bf_algorithm(4, 1000);
2041 let ord = order(&token_a, &token_d, 100, OrderSide::Sell);
2042
2043 let result = algo
2044 .find_best_route(SolveRequest::new(manager.graph(), market, &ord))
2045 .await
2046 .unwrap();
2047
2048 assert_eq!(result.route().swaps().len(), 2, "should use direct 2-hop path");
2051 assert_eq!(result.route().swaps()[0].component_id(), "component_ab");
2052 assert_eq!(result.route().swaps()[1].component_id(), "component_bd");
2053 assert_eq!(result.route().swaps()[1].amount_out(), &BigUint::from(400u64));
2054 }
2055
2056 #[tokio::test]
2057 async fn test_route_amounts_are_sequential() {
2058 let token_a = token(0x01, "A");
2060 let token_b = token(0x02, "B");
2061 let token_c = token(0x03, "C");
2062
2063 let (market, manager) = setup_market_bf(vec![
2064 ("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
2065 ("component_bc", &token_b, &token_c, MockProtocolSim::new(3.0)),
2066 ]);
2067
2068 let algo = bf_algorithm(3, 1000);
2069 let ord = order(&token_a, &token_c, 100, OrderSide::Sell);
2070
2071 let result = algo
2072 .find_best_route(SolveRequest::new(manager.graph(), market, &ord))
2073 .await
2074 .unwrap();
2075
2076 assert_eq!(result.route().swaps().len(), 2);
2077 assert_eq!(result.route().swaps()[1].amount_in(), result.route().swaps()[0].amount_out());
2079 }
2080
2081 #[tokio::test]
2082 async fn test_gas_deduction() {
2083 let token_a = token(0x01, "A");
2084 let token_b = token(0x02, "B");
2085
2086 let (market, manager) = setup_market_bf(vec![(
2087 "component1",
2088 &token_a,
2089 &token_b,
2090 MockProtocolSim::new(2.0).with_gas(10),
2091 )]);
2092
2093 let algo = bf_algorithm(2, 1000);
2094 let ord = order(&token_a, &token_b, 1000, OrderSide::Sell);
2095
2096 let derived = setup_derived_with_token_prices(std::slice::from_ref(&token_b.address));
2097
2098 let result = algo
2099 .find_best_route(SolveRequest::new(manager.graph(), market, &ord).with_derived(derived))
2100 .await
2101 .unwrap();
2102
2103 assert_eq!(result.route().swaps()[0].amount_out(), &BigUint::from(2000u64));
2107 assert_eq!(result.net_amount_out(), &BigInt::from(1000));
2108 }
2109
2110 #[tokio::test]
2111 async fn test_timeout_respected() {
2112 let token_a = token(0x01, "A");
2113 let token_b = token(0x02, "B");
2114 let token_c = token(0x03, "C");
2115
2116 let (market, manager) = setup_market_bf(vec![
2117 ("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
2118 ("component_bc", &token_b, &token_c, MockProtocolSim::new(3.0)),
2119 ]);
2120
2121 let algo = bf_algorithm(3, 0);
2123 let ord = order(&token_a, &token_c, 100, OrderSide::Sell);
2124
2125 let result = algo
2126 .find_best_route(SolveRequest::new(manager.graph(), market, &ord))
2127 .await;
2128
2129 match result {
2134 Ok(r) => {
2135 assert!(!r.route().swaps().is_empty());
2136 }
2137 Err(AlgorithmError::Timeout { .. }) | Err(AlgorithmError::NoPath { .. }) => {
2138 }
2140 Err(e) => panic!("Unexpected error: {:?}", e),
2141 }
2142 }
2143
2144 #[tokio::test]
2147 async fn test_with_fees() {
2148 let token_a = token(0x01, "A");
2149 let token_b = token(0x02, "B");
2150
2151 let (market, manager) = setup_market_bf(vec![(
2153 "component1",
2154 &token_a,
2155 &token_b,
2156 MockProtocolSim::new(2.0).with_fee(0.1),
2157 )]);
2158
2159 let algo = bf_algorithm(2, 1000);
2160 let ord = order(&token_a, &token_b, 1000, OrderSide::Sell);
2161
2162 let result = algo
2163 .find_best_route(SolveRequest::new(manager.graph(), market, &ord))
2164 .await
2165 .unwrap();
2166
2167 assert_eq!(result.route().swaps()[0].amount_out(), &BigUint::from(1800u64));
2169 }
2170
2171 #[tokio::test]
2172 async fn test_large_trade_slippage() {
2173 let token_a = token(0x01, "A");
2174 let token_b = token(0x02, "B");
2175
2176 let (market, manager) = setup_market_bf(vec![(
2178 "component1",
2179 &token_a,
2180 &token_b,
2181 MockProtocolSim::new(2.0).with_liquidity(500),
2182 )]);
2183
2184 let algo = bf_algorithm(2, 1000);
2185 let ord = order(&token_a, &token_b, 1000, OrderSide::Sell);
2186
2187 let result = algo
2189 .find_best_route(SolveRequest::new(manager.graph(), market, &ord))
2190 .await;
2191 assert!(
2192 matches!(result, Err(AlgorithmError::NoPath { .. })),
2193 "Should fail when trade exceeds component liquidity"
2194 );
2195 }
2196
2197 #[tokio::test]
2198 async fn test_disconnected_tokens_return_no_path() {
2199 let token_a = token(0x01, "A");
2201 let token_b = token(0x02, "B");
2202 let token_d = token(0x04, "D");
2203 let token_e = token(0x05, "E");
2204
2205 let (market, manager) = setup_market_bf(vec![
2206 ("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
2207 ("component_de", &token_d, &token_e, MockProtocolSim::new(4.0)),
2208 ]);
2209
2210 let algo = bf_algorithm(3, 1000);
2211 let ord = order(&token_a, &token_e, 100, OrderSide::Sell);
2212
2213 let result = algo
2214 .find_best_route(SolveRequest::new(manager.graph(), market, &ord))
2215 .await;
2216 assert!(
2217 matches!(result, Err(AlgorithmError::NoPath { .. })),
2218 "should not find path to disconnected component"
2219 );
2220 }
2221
2222 #[tokio::test]
2223 async fn test_spfa_skips_failed_simulations() {
2224 let token_a = token(0x01, "A");
2226 let token_b = token(0x02, "B");
2227 let token_c = token(0x03, "C");
2228
2229 let (market, manager) = setup_market_bf(vec![
2230 ("component_ab_bad", &token_a, &token_b, MockProtocolSim::new(2.0).with_liquidity(0)),
2232 ("component_ac", &token_a, &token_c, MockProtocolSim::new(2.0)),
2234 ("component_cb", &token_c, &token_b, MockProtocolSim::new(3.0)),
2235 ]);
2236
2237 let algo = bf_algorithm(3, 1000);
2238 let ord = order(&token_a, &token_b, 100, OrderSide::Sell);
2239
2240 let result = algo
2241 .find_best_route(SolveRequest::new(manager.graph(), market, &ord))
2242 .await;
2243
2244 match result {
2248 Ok(r) => {
2249 assert!(!r.route().swaps().is_empty());
2251 }
2252 Err(AlgorithmError::NoPath { .. }) => {
2253 }
2256 Err(e) => panic!("Unexpected error: {:?}", e),
2257 }
2258 }
2259
2260 #[tokio::test]
2261 async fn test_resimulation_produces_correct_amounts() {
2262 let token_a = token(0x01, "A");
2264 let token_b = token(0x02, "B");
2265 let token_c = token(0x03, "C");
2266
2267 let (market, manager) = setup_market_bf(vec![
2268 ("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
2269 ("component_bc", &token_b, &token_c, MockProtocolSim::new(3.0)),
2270 ]);
2271
2272 let algo = bf_algorithm(3, 1000);
2273 let ord = order(&token_a, &token_c, 100, OrderSide::Sell);
2274
2275 let result = algo
2276 .find_best_route(SolveRequest::new(manager.graph(), market, &ord))
2277 .await
2278 .unwrap();
2279
2280 assert_eq!(result.route().swaps()[0].amount_in(), &BigUint::from(100u64));
2283 assert_eq!(result.route().swaps()[0].amount_out(), &BigUint::from(200u64));
2284 assert_eq!(result.route().swaps()[1].amount_in(), &BigUint::from(200u64));
2285 assert_eq!(result.route().swaps()[1].amount_out(), &BigUint::from(600u64));
2286 }
2287
2288 #[test]
2291 fn algorithm_name() {
2292 let algo = bf_algorithm(4, 200);
2293 assert_eq!(algo.name(), "bellman_ford");
2294 }
2295
2296 #[test]
2297 fn algorithm_timeout() {
2298 let algo = bf_algorithm(4, 200);
2299 assert_eq!(algo.timeout(), Duration::from_millis(200));
2300 }
2301
2302 #[tokio::test]
2305 async fn test_gas_aware_relaxation_picks_cheaper_path() {
2306 let token_a = token(0x01, "A");
2321 let token_b = token(0x02, "B");
2322 let token_c = token(0x03, "C");
2323 let token_d = token(0x04, "D");
2324
2325 let high_gas: u64 = 100_000_000;
2326 let low_gas: u64 = 100;
2327
2328 let (market, manager) = setup_market_bf(vec![
2329 ("component_ab", &token_a, &token_b, MockProtocolSim::new(3.0).with_gas(high_gas)),
2330 ("component_bd", &token_b, &token_d, MockProtocolSim::new(2.0).with_gas(high_gas)),
2331 ("component_ac", &token_a, &token_c, MockProtocolSim::new(2.0).with_gas(low_gas)),
2332 ("component_cd", &token_c, &token_d, MockProtocolSim::new(2.0).with_gas(low_gas)),
2333 ]);
2334
2335 let algo = bf_algorithm(3, 1000);
2336 let ord = order(&token_a, &token_d, 1_000_000_000, OrderSide::Sell);
2337
2338 let derived = setup_derived_with_token_prices(&[
2340 token_a.address.clone(),
2341 token_b.address.clone(),
2342 token_c.address.clone(),
2343 token_d.address.clone(),
2344 ]);
2345
2346 let result = algo
2347 .find_best_route(SolveRequest::new(manager.graph(), market, &ord).with_derived(derived))
2348 .await
2349 .unwrap();
2350
2351 assert_eq!(result.route().swaps().len(), 2);
2353 assert_eq!(result.route().swaps()[0].component_id(), "component_ac");
2354 assert_eq!(result.route().swaps()[1].component_id(), "component_cd");
2355 }
2356
2357 #[tokio::test]
2358 async fn test_gas_aware_falls_back_to_gross_without_derived() {
2359 let token_a = token(0x01, "A");
2362 let token_b = token(0x02, "B");
2363 let token_c = token(0x03, "C");
2364 let token_d = token(0x04, "D");
2365
2366 let high_gas: u64 = 100_000_000;
2367 let low_gas: u64 = 100;
2368
2369 let (market, manager) = setup_market_bf(vec![
2370 ("component_ab", &token_a, &token_b, MockProtocolSim::new(3.0).with_gas(high_gas)),
2371 ("component_bd", &token_b, &token_d, MockProtocolSim::new(2.0).with_gas(high_gas)),
2372 ("component_ac", &token_a, &token_c, MockProtocolSim::new(2.0).with_gas(low_gas)),
2373 ("component_cd", &token_c, &token_d, MockProtocolSim::new(2.0).with_gas(low_gas)),
2374 ]);
2375
2376 let algo = bf_algorithm(3, 1000);
2377 let ord = order(&token_a, &token_d, 1_000_000_000, OrderSide::Sell);
2378
2379 let result = algo
2381 .find_best_route(SolveRequest::new(manager.graph(), market, &ord))
2382 .await
2383 .unwrap();
2384
2385 assert_eq!(result.route().swaps().len(), 2);
2387 assert_eq!(result.route().swaps()[0].component_id(), "component_ab");
2388 assert_eq!(result.route().swaps()[1].component_id(), "component_bd");
2389 }
2390
2391 #[tokio::test]
2392 async fn test_amount_too_small_when_net_uneconomic_after_gas() {
2393 let token_a = token(0x01, "A");
2396 let token_b = token(0x02, "B");
2397
2398 let (market, manager) = setup_market_bf(vec![(
2399 "component_ab",
2400 &token_a,
2401 &token_b,
2402 MockProtocolSim::new(2.0).with_gas(10),
2403 )]);
2404
2405 let algo = bf_algorithm(2, 1000);
2406 let ord = order(&token_a, &token_b, 1, OrderSide::Sell);
2407 let derived =
2408 setup_derived_with_token_prices(&[token_a.address.clone(), token_b.address.clone()]);
2409
2410 let result = algo
2411 .find_best_route(SolveRequest::new(manager.graph(), market, &ord).with_derived(derived))
2412 .await;
2413 assert!(matches!(
2414 result,
2415 Err(AlgorithmError::NoPath { reason: NoPathReason::AmountTooSmall, .. })
2416 ));
2417 }
2418
2419 #[tokio::test]
2420 async fn test_no_graph_path_when_output_uneconomic_but_input_economic() {
2421 let token_a = token(0x01, "A");
2425 let token_b = token(0x02, "B");
2426
2427 let (market, manager) = setup_market_bf(vec![(
2428 "component_ab",
2429 &token_a,
2430 &token_b,
2431 MockProtocolSim::new(0.05).with_gas(10),
2432 )]);
2433
2434 let algo = bf_algorithm(1, 1000);
2435 let ord = order(&token_a, &token_b, 10_000, OrderSide::Sell);
2436 let derived =
2437 setup_derived_with_token_prices(&[token_a.address.clone(), token_b.address.clone()]);
2438
2439 let result = algo
2440 .find_best_route(SolveRequest::new(manager.graph(), market, &ord).with_derived(derived))
2441 .await;
2442 assert!(matches!(
2443 result,
2444 Err(AlgorithmError::NoPath { reason: NoPathReason::NoGraphPath, .. })
2445 ));
2446 }
2447
2448 fn bf_algorithm_with_connectors(
2452 max_hops: usize,
2453 timeout_ms: u64,
2454 connector_tokens: FxHashSet<Address>,
2455 ) -> BellmanFordAlgorithm {
2456 BellmanFordAlgorithm::with_config(
2457 AlgorithmConfig::new(1, max_hops, Duration::from_millis(timeout_ms), None)
2458 .unwrap()
2459 .with_connector_tokens(connector_tokens),
2460 )
2461 }
2462
2463 #[tokio::test]
2464 async fn test_connector_tokens_blocks_disallowed_intermediate() {
2465 let token_a = token(0x01, "A");
2472 let token_b = token(0x02, "B");
2473 let token_c = token(0x03, "C");
2474 let token_d = token(0x04, "D");
2475
2476 let (market, manager) = setup_market_bf(vec![
2477 ("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
2478 ("component_bd", &token_b, &token_d, MockProtocolSim::new(2.0)),
2479 ("component_ac", &token_a, &token_c, MockProtocolSim::new(3.0)),
2480 ("component_cd", &token_c, &token_d, MockProtocolSim::new(3.0)),
2481 ]);
2482
2483 let connectors: FxHashSet<Address> = FxHashSet::from_iter([token_c.address.clone()]);
2484 let algo = bf_algorithm_with_connectors(3, 1000, connectors);
2485 let ord = order(&token_a, &token_d, 100, OrderSide::Sell);
2486
2487 let result = algo
2488 .find_best_route(SolveRequest::new(manager.graph(), market, &ord))
2489 .await
2490 .unwrap();
2491
2492 assert_eq!(result.route().swaps().len(), 2);
2494 assert_eq!(result.route().swaps()[0].component_id(), "component_ac");
2495 assert_eq!(result.route().swaps()[1].component_id(), "component_cd");
2496 }
2497
2498 #[tokio::test]
2499 async fn test_connector_tokens_allows_endpoints_even_if_not_listed() {
2500 let token_a = token(0x01, "A");
2502 let token_b = token(0x02, "B");
2503
2504 let (market, manager) =
2505 setup_market_bf(vec![("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0))]);
2506
2507 let algo = bf_algorithm_with_connectors(1, 1000, FxHashSet::default());
2509 let ord = order(&token_a, &token_b, 100, OrderSide::Sell);
2510
2511 let result = algo
2512 .find_best_route(SolveRequest::new(manager.graph(), market, &ord))
2513 .await
2514 .unwrap();
2515
2516 assert_eq!(result.route().swaps().len(), 1);
2517 assert_eq!(result.route().swaps()[0].amount_out(), &BigUint::from(200u64));
2518 }
2519
2520 #[tokio::test]
2521 async fn test_connector_tokens_none_is_unrestricted() {
2522 let token_a = token(0x01, "A");
2524 let token_b = token(0x02, "B");
2525 let token_c = token(0x03, "C");
2526 let token_d = token(0x04, "D");
2527
2528 let (market, manager) = setup_market_bf(vec![
2529 ("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
2530 ("component_bd", &token_b, &token_d, MockProtocolSim::new(3.0)),
2531 ("component_ac", &token_a, &token_c, MockProtocolSim::new(1.0)),
2532 ("component_cd", &token_c, &token_d, MockProtocolSim::new(1.0)),
2533 ]);
2534
2535 let algo = bf_algorithm(3, 1000);
2536 let ord = order(&token_a, &token_d, 100, OrderSide::Sell);
2537
2538 let result = algo
2539 .find_best_route(SolveRequest::new(manager.graph(), market, &ord))
2540 .await
2541 .unwrap();
2542
2543 assert_eq!(result.route().swaps()[0].component_id(), "component_ab");
2545 assert_eq!(result.route().swaps()[1].component_id(), "component_bd");
2546 assert_eq!(result.route().swaps()[1].amount_out(), &BigUint::from(600u64));
2547 }
2548
2549 #[test]
2550 fn test_path_has_conflict_detects_node_and_component() {
2551 let mut pred: Vec<Option<(NodeIndex, ComponentId)>> = vec![None; 4];
2553 pred[1] = Some((NodeIndex::new(0), "component_a".into()));
2554 pred[2] = Some((NodeIndex::new(1), "component_b".into()));
2555
2556 assert!(BellmanFordAlgorithm::path_has_conflict(
2558 NodeIndex::new(2),
2559 NodeIndex::new(0),
2560 &"any".into(),
2561 &pred
2562 ));
2563 assert!(!BellmanFordAlgorithm::path_has_conflict(
2564 NodeIndex::new(2),
2565 NodeIndex::new(3),
2566 &"any".into(),
2567 &pred
2568 ));
2569 assert!(BellmanFordAlgorithm::path_has_conflict(
2571 NodeIndex::new(2),
2572 NodeIndex::new(2),
2573 &"any".into(),
2574 &pred
2575 ));
2576
2577 assert!(BellmanFordAlgorithm::path_has_conflict(
2579 NodeIndex::new(2),
2580 NodeIndex::new(3),
2581 &"component_a".into(),
2582 &pred
2583 ));
2584 assert!(BellmanFordAlgorithm::path_has_conflict(
2585 NodeIndex::new(2),
2586 NodeIndex::new(3),
2587 &"component_b".into(),
2588 &pred
2589 ));
2590 assert!(!BellmanFordAlgorithm::path_has_conflict(
2591 NodeIndex::new(2),
2592 NodeIndex::new(3),
2593 &"component_c".into(),
2594 &pred
2595 ));
2596 }
2597
2598 #[tokio::test]
2599 async fn test_find_single_route_with_state_overrides() {
2600 let token_a = token(0x01, "A");
2601 let token_b = token(0x02, "B");
2602
2603 let (market, manager) =
2604 setup_market_bf(vec![("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0))]);
2605
2606 let algo = bf_algorithm(2, 1000);
2607 let ord = order(&token_a, &token_b, 1000, OrderSide::Sell);
2608
2609 let ctx = algo
2610 .build_context(SolveRequest::new(manager.graph(), market, &ord))
2611 .await
2612 .unwrap();
2613
2614 let normal = algo
2616 .find_single_route(&ctx, &ord, FindRouteOptions::default())
2617 .unwrap();
2618 assert_eq!(normal.route().swaps()[0].amount_out(), &BigUint::from(2000u64));
2619
2620 let opts = FindRouteOptions {
2622 overrides: MarketOverrides::empty()
2623 .with_override("component_ab".to_string(), Box::new(MockProtocolSim::new(1.0))),
2624 };
2625 let overridden = algo
2626 .find_single_route(&ctx, &ord, opts)
2627 .unwrap();
2628 assert_eq!(overridden.route().swaps()[0].amount_out(), &BigUint::from(1000u64));
2629
2630 assert!(
2631 overridden.route().swaps()[0].amount_out() < normal.route().swaps()[0].amount_out()
2632 );
2633 }
2634
2635 #[tokio::test]
2636 async fn test_single_find_route_options_default() {
2637 use super::super::split_primitives::MarketOverrides;
2638
2639 let token_a = token(0x01, "A");
2640 let token_b = token(0x02, "B");
2641
2642 let (market, manager) =
2643 setup_market_bf(vec![("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0))]);
2644
2645 let algo = bf_algorithm(2, 1000);
2646 let ord = order(&token_a, &token_b, 1000, OrderSide::Sell);
2647
2648 let ctx = algo
2649 .build_context(SolveRequest::new(manager.graph(), market, &ord))
2650 .await
2651 .unwrap();
2652
2653 let with_default = algo
2654 .find_single_route(&ctx, &ord, FindRouteOptions::default())
2655 .unwrap();
2656 let with_empty = algo
2657 .find_single_route(&ctx, &ord, FindRouteOptions { overrides: MarketOverrides::empty() })
2658 .unwrap();
2659
2660 assert_eq!(
2661 with_default.route().swaps()[0].amount_out(),
2662 with_empty.route().swaps()[0].amount_out()
2663 );
2664 assert_eq!(with_default.route().swaps()[0].amount_out(), &BigUint::from(2000u64));
2665 }
2666}