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};
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::{paths, sim_guard::GuardedProtocolSim},
47 derived::{
48 computation::ComputationRequirements,
49 types::{SpotPrices, TokenGasPrices},
50 SharedDerivedDataRef,
51 },
52 feed::market_data::{MarketData, MarketState, StateLabel},
53 graph::{petgraph::StableDiGraph, PetgraphStableDiGraphManager},
54 types::{ComponentId, Order, Route, RouteResult, Swap},
55};
56
57type Subgraph<'a> = (
63 FxHashMap<NodeIndex, Vec<(NodeIndex, ComponentId)>>,
64 FxHashSet<NodeIndex>,
65 FxHashSet<&'a ComponentId>,
66);
67
68pub(crate) struct BellmanFordContext {
75 pub(crate) token_in_node: NodeIndex,
76 pub(crate) token_out_node: NodeIndex,
77 pub(crate) adj: FxHashMap<NodeIndex, Vec<(NodeIndex, ComponentId)>>,
78 pub(crate) token_map: FxHashMap<NodeIndex, Arc<Token>>,
79 pub(crate) market_data: MarketState,
80 pub(crate) gas_price_wei: Option<BigUint>,
81 pub(crate) token_prices: Option<TokenGasPrices>,
82 pub(crate) spot_prices: Option<SpotPrices>,
83 pub(crate) node_address: FxHashMap<NodeIndex, Address>,
84 pub(crate) max_idx: usize,
85 pub(crate) scoring: RouteScoringMode,
86}
87
88pub(crate) enum RouteScoringMode {
90 GrossOutput,
92 NetOutput,
94}
95
96#[derive(Default)]
98pub(crate) struct FindRouteOptions {
99 pub(crate) overrides: MarketOverrides,
102}
103
104struct SPFAResult {
106 amount: Vec<BigUint>,
108 predecessor: Vec<Option<(NodeIndex, ComponentId)>>,
110 edge_gas: Vec<BigUint>,
112 spot_product: Vec<f64>,
114 input_below_hop_gas: bool,
117}
118
119pub struct BellmanFordAlgorithm {
125 max_hops: usize,
126 timeout: Duration,
127 gas_aware: bool,
128 connector_tokens: Option<FxHashSet<Address>>,
129}
130
131impl Default for BellmanFordAlgorithm {
132 fn default() -> Self {
133 Self::with_config(AlgorithmConfig::default())
134 }
135}
136
137impl BellmanFordAlgorithm {
138 pub(crate) fn with_config(config: AlgorithmConfig) -> Self {
139 Self {
140 max_hops: config.max_hops(),
141 timeout: config.timeout(),
142 gas_aware: config.gas_aware(),
143 connector_tokens: config.connector_tokens().cloned(),
144 }
145 }
146
147 pub(crate) async fn build_context(
154 &self,
155 graph: &StableDiGraph<()>,
156 market: MarketData,
157 label: Option<StateLabel>,
158 derived: Option<SharedDerivedDataRef>,
159 order: &Order,
160 ) -> Result<BellmanFordContext, AlgorithmError> {
161 if !order.is_sell() {
162 return Err(AlgorithmError::ExactOutNotSupported);
163 }
164
165 let (token_prices, spot_prices) = if let Some(ref d) = derived {
166 let guard = d.read().await;
167 (guard.token_prices().cloned(), guard.spot_prices().cloned())
168 } else {
169 (None, None)
170 };
171
172 let token_in_node = graph
173 .node_indices()
174 .find(|&n| &graph[n] == order.token_in())
175 .ok_or(AlgorithmError::NoPath {
176 from: order.token_in().clone(),
177 to: order.token_out().clone(),
178 reason: NoPathReason::SourceTokenNotInGraph,
179 })?;
180 let token_out_node = graph
181 .node_indices()
182 .find(|&n| &graph[n] == order.token_out())
183 .ok_or(AlgorithmError::NoPath {
184 from: order.token_in().clone(),
185 to: order.token_out().clone(),
186 reason: NoPathReason::DestinationTokenNotInGraph,
187 })?;
188
189 if token_in_node == token_out_node {
190 return Err(AlgorithmError::NoPath {
191 from: order.token_in().clone(),
192 to: order.token_out().clone(),
193 reason: NoPathReason::NoGraphPath,
194 });
195 }
196
197 let (adj, token_nodes, component_ids) =
200 Self::get_subgraph(graph, token_in_node, token_out_node, self.max_hops).ok_or_else(
201 || AlgorithmError::NoPath {
202 from: order.token_in().clone(),
203 to: order.token_out().clone(),
204 reason: NoPathReason::NoGraphPath,
205 },
206 )?;
207
208 let market_view = paths::read_market(&market, label).await?;
209 let token_map: FxHashMap<NodeIndex, Arc<Token>> = token_nodes
210 .iter()
211 .filter_map(|&node| {
212 market_view
213 .get_token_shared(&graph[node])
214 .map(|token| (node, Arc::clone(token)))
215 })
216 .collect();
217 let market_data = market_view.extract_subset_with_overlay(&component_ids);
218 let gas_price_wei = market_data
219 .gas_price()
220 .map(|gp| gp.effective_gas_price().clone());
221 drop(market_view);
222
223 let node_address: FxHashMap<NodeIndex, Address> = token_map
224 .iter()
225 .map(|(&node, token)| (node, token.address.clone()))
226 .collect();
227
228 let max_idx = graph
229 .node_indices()
230 .map(|n| n.index())
231 .max()
232 .unwrap_or(0) +
233 1;
234
235 let scoring = if self.gas_aware {
236 RouteScoringMode::NetOutput
237 } else {
238 RouteScoringMode::GrossOutput
239 };
240
241 debug!(
242 edges = adj
243 .values()
244 .map(Vec::len)
245 .sum::<usize>(),
246 tokens = token_map.len(),
247 "subgraph extracted"
248 );
249
250 Ok(BellmanFordContext {
251 token_in_node,
252 token_out_node,
253 adj,
254 token_map,
255 market_data,
256 gas_price_wei,
257 token_prices,
258 spot_prices,
259 node_address,
260 max_idx,
261 scoring,
262 })
263 }
264
265 pub(crate) fn find_single_route(
272 &self,
273 ctx: &BellmanFordContext,
274 order: &Order,
275 opts: FindRouteOptions,
276 ) -> Result<RouteResult, AlgorithmError> {
277 let start = Instant::now();
278
279 let spfa = self.run_spfa(ctx, order, &opts.overrides, start);
280
281 let out_idx = ctx.token_out_node.index();
282 if spfa.amount[out_idx].is_zero() {
283 let reason = if spfa.input_below_hop_gas {
287 NoPathReason::AmountTooSmall
288 } else {
289 NoPathReason::NoGraphPath
290 };
291 return Err(AlgorithmError::NoPath {
292 from: order.token_in().clone(),
293 to: order.token_out().clone(),
294 reason,
295 });
296 }
297
298 let path_edges =
302 Self::reconstruct_path(ctx.token_out_node, ctx.token_in_node, &spfa.predecessor)?;
303
304 let route =
305 Self::build_route(ctx, &path_edges, &spfa.amount, &spfa.edge_gas, &opts.overrides)?;
306
307 let final_amount_out = spfa.amount[out_idx].clone();
308 let gas_price = ctx
309 .gas_price_wei
310 .clone()
311 .unwrap_or_default();
312
313 let net_amount_out = Self::compute_net_amount_out(
314 &final_amount_out,
315 &route,
316 &gas_price,
317 ctx.token_prices.as_ref(),
318 &spfa.spot_product,
319 &ctx.node_address,
320 ctx.token_in_node,
321 )?;
322
323 let result = RouteResult::new(route, net_amount_out, gas_price);
324
325 let solve_time_ms = start.elapsed().as_millis() as u64;
326 debug!(
327 solve_time_ms,
328 hops = result.route().swaps().len(),
329 amount_in = %order.amount(),
330 amount_out = %final_amount_out,
331 net_amount_out = %result.net_amount_out(),
332 "bellman_ford route found"
333 );
334
335 Ok(result)
336 }
337
338 fn run_spfa(
344 &self,
345 ctx: &BellmanFordContext,
346 order: &Order,
347 overrides: &MarketOverrides,
348 start: Instant,
349 ) -> SPFAResult {
350 let mut amount: Vec<BigUint> = vec![BigUint::ZERO; ctx.max_idx];
354 let mut predecessor: Vec<Option<(NodeIndex, ComponentId)>> = vec![None; ctx.max_idx];
355 let mut edge_gas: Vec<BigUint> = vec![BigUint::ZERO; ctx.max_idx];
356 let mut cumul_gas: Vec<BigUint> = vec![BigUint::ZERO; ctx.max_idx];
357
358 amount[ctx.token_in_node.index()] = order.amount().clone();
359
360 let mut spot_product: Vec<f64> = vec![0.0; ctx.max_idx];
363 spot_product[ctx.token_in_node.index()] = 1.0;
364
365 let mut input_below_hop_gas = false;
366
367 let gas_aware = matches!(ctx.scoring, RouteScoringMode::NetOutput) &&
368 ctx.gas_price_wei.is_some() &&
369 ctx.token_prices.is_some();
370 if !gas_aware && matches!(ctx.scoring, RouteScoringMode::NetOutput) {
371 debug!("gas-aware comparison disabled (missing gas_price or token_prices)");
372 } else if matches!(ctx.scoring, RouteScoringMode::GrossOutput) {
373 debug!("gas-aware comparison disabled by config");
374 }
375
376 let mut active_nodes: Vec<NodeIndex> = vec![ctx.token_in_node];
377
378 for round in 0..self.max_hops {
379 if start.elapsed() >= self.timeout {
380 debug!(round, "timeout during relaxation");
381 break;
382 }
383 if active_nodes.is_empty() {
384 debug!(round, "no active nodes, stopping early");
385 break;
386 }
387
388 let mut next_active: FxHashSet<NodeIndex> = FxHashSet::default();
389
390 for &u in &active_nodes {
391 let u_idx = u.index();
392 if amount[u_idx].is_zero() {
393 continue;
394 }
395
396 let Some(token_u) = ctx.token_map.get(&u) else { continue };
397 let Some(edges) = ctx.adj.get(&u) else { continue };
398
399 for (v, component_id) in edges {
400 let v_idx = v.index();
401
402 if Self::path_has_conflict(u, *v, component_id, &predecessor) {
404 continue;
405 }
406
407 if !self.connector_allows(ctx, order, *v) {
410 continue;
411 }
412
413 let Some(token_v) = ctx.token_map.get(v) else { continue };
414
415 let sim: &dyn tycho_simulation::tycho_common::simulation::protocol_sim::ProtocolSim =
417 if let Some(s) = overrides.get(component_id) {
418 s
419 } else if let Some(s) = ctx.market_data.get_simulation_state(component_id) {
420 s
421 } else {
422 continue;
423 };
424
425 let result =
426 match sim.get_amount_out_guarded(amount[u_idx].clone(), token_u, token_v) {
427 Ok(r) => r,
428 Err(e) => {
429 trace!(
430 component_id,
431 error = %e,
432 "simulation failed, skipping edge"
433 );
434 continue;
435 }
436 };
437
438 let candidate_cumul_gas = &cumul_gas[u_idx] + &result.gas;
439
440 let candidate_spot = Self::compute_edge_spot_product(
443 spot_product[u_idx],
444 component_id,
445 ctx.node_address.get(&u),
446 ctx.node_address.get(v),
447 ctx.spot_prices.as_ref(),
448 );
449
450 let is_better = if gas_aware {
452 let v_price = Self::resolve_token_price(
453 ctx.node_address.get(v),
454 ctx.token_prices.as_ref(),
455 candidate_spot,
456 ctx.node_address.get(&ctx.token_in_node),
457 );
458 let net_candidate = Self::gas_adjusted_amount(
459 &result.amount,
460 &candidate_cumul_gas,
461 ctx.gas_price_wei.as_ref().unwrap(),
462 v_price.as_ref(),
463 );
464 if !input_below_hop_gas && net_candidate <= BigInt::ZERO {
469 let u_price = Self::resolve_token_price(
470 ctx.node_address.get(&u),
471 ctx.token_prices.as_ref(),
472 spot_product[u_idx],
473 ctx.node_address.get(&ctx.token_in_node),
474 );
475 if Self::gas_adjusted_amount(
476 &amount[u_idx],
477 &result.gas,
478 ctx.gas_price_wei.as_ref().unwrap(),
479 u_price.as_ref(),
480 ) <= BigInt::ZERO
481 {
482 input_below_hop_gas = true;
483 }
484 }
485 let net_existing = Self::gas_adjusted_amount(
486 &amount[v_idx],
487 &cumul_gas[v_idx],
488 ctx.gas_price_wei.as_ref().unwrap(),
489 v_price.as_ref(),
490 );
491 net_candidate > net_existing
492 } else {
493 if result.amount.is_zero() {
494 input_below_hop_gas = true;
495 }
496 result.amount > amount[v_idx]
497 };
498
499 if is_better {
500 spot_product[v_idx] = candidate_spot;
501 amount[v_idx] = result.amount;
502 predecessor[v_idx] = Some((u, component_id.clone()));
503 edge_gas[v_idx] = result.gas;
504 cumul_gas[v_idx] = candidate_cumul_gas;
505 next_active.insert(*v);
506 }
507 }
508 }
509
510 active_nodes = next_active.into_iter().collect();
511 active_nodes.sort_unstable();
516 }
517
518 SPFAResult { amount, predecessor, edge_gas, spot_product, input_below_hop_gas }
519 }
520
521 fn connector_allows(&self, ctx: &BellmanFordContext, order: &Order, v: NodeIndex) -> bool {
524 let (Some(tokens), Some(v_addr)) = (&self.connector_tokens, ctx.node_address.get(&v))
525 else {
526 return true;
527 };
528 v_addr == order.token_in() || v_addr == order.token_out() || tokens.contains(v_addr)
529 }
530
531 fn build_route(
533 ctx: &BellmanFordContext,
534 path_edges: &[(NodeIndex, NodeIndex, ComponentId)],
535 amount: &[BigUint],
536 edge_gas: &[BigUint],
537 overrides: &MarketOverrides,
538 ) -> Result<Route, AlgorithmError> {
539 let mut swaps = Vec::with_capacity(path_edges.len());
540 let mut tokens: FxHashMap<Address, Token> = FxHashMap::default();
541
542 for (from_node, to_node, component_id) in path_edges {
543 let token_in = ctx
544 .token_map
545 .get(from_node)
546 .ok_or_else(|| AlgorithmError::DataNotFound {
547 kind: "token",
548 id: Some(format!("{:?}", from_node)),
549 })?;
550 let token_out = ctx
551 .token_map
552 .get(to_node)
553 .ok_or_else(|| AlgorithmError::DataNotFound {
554 kind: "token",
555 id: Some(format!("{:?}", to_node)),
556 })?;
557 let component = ctx
558 .market_data
559 .get_component(component_id)
560 .ok_or_else(|| AlgorithmError::DataNotFound {
561 kind: "component",
562 id: Some(component_id.clone()),
563 })?;
564 let sim_state = overrides
567 .get(component_id)
568 .or_else(|| {
569 ctx.market_data
570 .get_simulation_state(component_id)
571 })
572 .ok_or_else(|| AlgorithmError::DataNotFound {
573 kind: "simulation state",
574 id: Some(component_id.clone()),
575 })?;
576
577 swaps.push(Swap::new(
578 component_id.clone(),
579 component.protocol_system.clone(),
580 token_in.address.clone(),
581 token_out.address.clone(),
582 amount[from_node.index()].clone(),
583 amount[to_node.index()].clone(),
584 edge_gas[to_node.index()].clone(),
585 component.clone(),
586 sim_state.clone_box(),
587 ));
588 tokens
589 .entry(token_in.address.clone())
590 .or_insert_with(|| Token::clone(token_in));
591 tokens
592 .entry(token_out.address.clone())
593 .or_insert_with(|| Token::clone(token_out));
594 }
595
596 Ok(Route::new(swaps, tokens)?)
597 }
598
599 fn gas_adjusted_amount(
604 gross: &BigUint,
605 cumul_gas: &BigUint,
606 gas_price_wei: &BigUint,
607 token_price: Option<&Price>,
608 ) -> BigInt {
609 match token_price {
610 Some(price) if !price.denominator.is_zero() => {
611 let gas_cost = cumul_gas * gas_price_wei * &price.numerator / &price.denominator;
612 BigInt::from(gross.clone()) - BigInt::from(gas_cost)
613 }
614 _ => BigInt::from(gross.clone()),
615 }
616 }
617
618 fn compute_edge_spot_product(
623 parent_spot: f64,
624 component_id: &ComponentId,
625 u_addr: Option<&Address>,
626 v_addr: Option<&Address>,
627 spot_prices: Option<&SpotPrices>,
628 ) -> f64 {
629 if parent_spot == 0.0 {
630 return 0.0;
631 }
632 let (Some(u), Some(v), Some(prices)) = (u_addr, v_addr, spot_prices) else {
633 return 0.0;
634 };
635 let key = (component_id.clone(), u.clone(), v.clone());
636 match prices.get(&key) {
637 Some(&spot) if spot > 0.0 => parent_spot * spot,
638 _ => 0.0,
639 }
640 }
641
642 fn resolve_token_price(
649 v_addr: Option<&Address>,
650 token_prices: Option<&TokenGasPrices>,
651 spot_product: f64,
652 token_in_addr: Option<&Address>,
653 ) -> Option<Price> {
654 let prices = token_prices?;
655 let addr = v_addr?;
656
657 if let Some(price) = prices.get(addr) {
659 return Some(price.clone());
660 }
661
662 if spot_product > 0.0 {
664 if let Some(in_price) = token_in_addr.and_then(|a| prices.get(a)) {
665 let in_rate_f64 = in_price.numerator.to_f64()? / in_price.denominator.to_f64()?;
666 let estimated_rate = in_rate_f64 * spot_product;
667 let denom = BigUint::from(10u64).pow(18);
668 let numer_f64 = estimated_rate * 1e18;
669 if numer_f64.is_finite() && numer_f64 > 0.0 {
670 return Some(Price {
671 numerator: BigUint::from(numer_f64 as u128),
672 denominator: denom,
673 });
674 }
675 }
676 }
677
678 None
679 }
680
681 pub(crate) fn path_has_conflict(
684 from: NodeIndex,
685 target_node: NodeIndex,
686 target_component: &ComponentId,
687 predecessor: &[Option<(NodeIndex, ComponentId)>],
688 ) -> bool {
689 let mut current = from;
690 loop {
691 if current == target_node {
692 return true;
693 }
694 match &predecessor[current.index()] {
695 Some((prev, cid)) => {
696 if cid == target_component {
697 return true;
698 }
699 current = *prev;
700 }
701 None => return false,
702 }
703 }
704 }
705
706 pub(crate) fn reconstruct_path(
709 token_out: NodeIndex,
710 token_in: NodeIndex,
711 predecessor: &[Option<(NodeIndex, ComponentId)>],
712 ) -> Result<Vec<(NodeIndex, NodeIndex, ComponentId)>, AlgorithmError> {
713 let mut path = Vec::new();
714 let mut current = token_out;
715 let mut visited = FxHashSet::default();
716
717 while current != token_in {
718 if !visited.insert(current) {
719 return Err(AlgorithmError::Other("cycle in predecessor chain".to_string()));
720 }
721
722 let idx = current.index();
723 match &predecessor
724 .get(idx)
725 .and_then(|p| p.as_ref())
726 {
727 Some((prev_node, component_id)) => {
728 path.push((*prev_node, current, component_id.clone()));
729 current = *prev_node;
730 }
731 None => {
732 return Err(AlgorithmError::Other(format!(
733 "broken predecessor chain at node {idx}"
734 )));
735 }
736 }
737 }
738
739 path.reverse();
740 Ok(path)
741 }
742
743 pub(crate) fn get_subgraph<'a>(
758 graph: &'a StableDiGraph<()>,
759 token_in: NodeIndex,
760 token_out: NodeIndex,
761 max_hops: usize,
762 ) -> Option<Subgraph<'a>> {
763 let hops_to_token_out = Self::get_hops_to_reach(graph, token_out, max_hops);
767
768 let mut adj: FxHashMap<NodeIndex, Vec<(NodeIndex, ComponentId)>> = FxHashMap::default();
769 let mut token_nodes: FxHashSet<NodeIndex> = FxHashSet::default();
770 let mut component_ids: FxHashSet<&ComponentId> = FxHashSet::default();
771 let mut visited_nodes = FxHashSet::default();
772 let mut queued_nodes = VecDeque::new();
773
774 visited_nodes.insert(token_in);
775 token_nodes.insert(token_in);
776 queued_nodes.push_back((token_in, 0usize));
777
778 while let Some((node, depth_walked)) = queued_nodes.pop_front() {
779 if depth_walked >= max_hops {
780 continue;
781 }
782 for edge in graph.edges(node) {
783 let next_token = edge.target();
784
785 let Some(&hops_left) = hops_to_token_out.get(&next_token) else {
787 continue;
789 };
790
791 if depth_walked + 1 + hops_left > max_hops {
792 continue;
794 }
795
796 let component_id = &edge.weight().component_id;
797 adj.entry(node)
798 .or_default()
799 .push((next_token, component_id.clone()));
800 component_ids.insert(component_id);
801 token_nodes.insert(next_token);
802
803 if visited_nodes.insert(next_token) {
804 queued_nodes.push_back((next_token, depth_walked + 1));
805 }
806 }
807 }
808
809 if adj.is_empty() {
810 return None;
811 }
812
813 Some((adj, token_nodes, component_ids))
814 }
815
816 fn get_hops_to_reach(
818 graph: &StableDiGraph<()>,
819 from: NodeIndex,
820 max_hops: usize,
821 ) -> FxHashMap<NodeIndex, usize> {
822 let mut hops_to_reach: FxHashMap<NodeIndex, usize> = FxHashMap::default();
823 hops_to_reach.insert(from, 0);
824
825 let mut frontier = vec![from];
826 for depth in 1..=max_hops {
827 let mut next = Vec::new();
828 for node in frontier {
829 for neighbor in graph.neighbors(node) {
830 if hops_to_reach.contains_key(&neighbor) {
831 continue;
832 }
833 hops_to_reach.insert(neighbor, depth);
834 next.push(neighbor);
835 }
836 }
837 frontier = next;
838 }
839
840 hops_to_reach
841 }
842
843 #[allow(clippy::too_many_arguments)]
849 fn compute_net_amount_out(
850 amount_out: &BigUint,
851 route: &Route,
852 gas_price: &BigUint,
853 token_prices: Option<&TokenGasPrices>,
854 spot_product: &[f64],
855 node_address: &FxHashMap<NodeIndex, Address>,
856 token_in_node: NodeIndex,
857 ) -> Result<BigInt, AlgorithmError> {
858 let last_swap = route.swaps().last().ok_or_else(|| {
859 AlgorithmError::Other("compute_net_amount_out called with empty route".to_string())
860 })?;
861
862 let total_gas = route.total_gas();
863
864 if gas_price.is_zero() {
865 warn!("missing gas price, returning gross amount_out");
866 return Ok(BigInt::from(amount_out.clone()));
867 }
868
869 let gas_cost_wei = &total_gas * gas_price;
870
871 let out_addr = last_swap.token_out();
873 let out_node_spot = node_address
874 .iter()
875 .find(|(_, addr)| *addr == out_addr)
876 .and_then(|(node, _)| spot_product.get(node.index()).copied())
877 .unwrap_or(0.0);
878
879 let output_price = Self::resolve_token_price(
880 Some(out_addr),
881 token_prices,
882 out_node_spot,
883 node_address.get(&token_in_node),
884 );
885
886 Ok(match output_price {
887 Some(price) if !price.denominator.is_zero() => {
888 let gas_cost = &gas_cost_wei * &price.numerator / &price.denominator;
889 BigInt::from(amount_out.clone()) - BigInt::from(gas_cost)
890 }
891 _ => {
892 warn!("no gas price for output token, returning gross amount_out");
893 BigInt::from(amount_out.clone())
894 }
895 })
896 }
897}
898
899impl Algorithm for BellmanFordAlgorithm {
900 type GraphType = StableDiGraph<()>;
901 type GraphManager = PetgraphStableDiGraphManager<()>;
902
903 fn name(&self) -> &str {
904 "bellman_ford"
905 }
906
907 #[instrument(level = "debug", skip_all, fields(order_id = %order.id()))]
908 async fn find_best_route(
909 &self,
910 graph: &Self::GraphType,
911 market: MarketData,
912 label: Option<StateLabel>,
913 derived: Option<SharedDerivedDataRef>,
914 order: &Order,
915 ) -> Result<RouteResult, AlgorithmError> {
916 let ctx = self
917 .build_context(graph, market, label, derived, order)
918 .await?;
919 self.find_single_route(&ctx, order, FindRouteOptions::default())
920 }
921
922 fn computation_requirements(&self) -> ComputationRequirements {
923 ComputationRequirements::none()
927 .allow_stale("token_prices")
928 .expect("token_prices requirement conflicts (bug)")
929 .allow_stale("spot_prices")
930 .expect("spot_prices requirement conflicts (bug)")
931 }
932
933 fn timeout(&self) -> Duration {
934 self.timeout
935 }
936}
937
938#[cfg(test)]
939mod tests {
940 use std::sync::Arc;
941
942 use num_bigint::BigInt;
943 use tokio::sync::RwLock;
944 use tycho_simulation::{
945 tycho_common::{models::Address, simulation::protocol_sim::ProtocolSim},
946 tycho_ethereum::gas::{BlockGasPrice, GasPrice},
947 };
948
949 use super::*;
950 use crate::{
951 algorithm::test_utils::{component, order, token, MockProtocolSim},
952 derived::{types::TokenGasPrices, DerivedData},
953 feed::market_data::{MarketData, MarketState},
954 graph::GraphManager,
955 types::quote::OrderSide,
956 };
957
958 fn setup_market_bf(
962 components: Vec<(&str, &Token, &Token, MockProtocolSim)>,
963 ) -> (MarketData, PetgraphStableDiGraphManager<()>) {
964 let mut market = MarketState::new();
965
966 market.update_gas_price(BlockGasPrice {
967 block_number: 1,
968 block_hash: Default::default(),
969 block_timestamp: 0,
970 pricing: GasPrice::Legacy { gas_price: BigUint::from(100u64) },
971 });
972 market.update_last_updated(crate::types::BlockInfo::new(1, "0x00".into(), 0));
973
974 for (component_id, token_in, token_out, state) in components {
975 let tokens = vec![token_in.clone(), token_out.clone()];
976 let comp = component(component_id, &tokens);
977 market.upsert_components(std::iter::once(comp));
978 market.update_states([(
979 component_id.to_string(),
980 Box::new(state) as Box<dyn ProtocolSim>,
981 )]);
982 market.upsert_tokens(tokens);
983 }
984
985 let mut graph_manager = PetgraphStableDiGraphManager::default();
986 graph_manager.initialize_graph(&market.component_topology());
987
988 (MarketData::new(Arc::new(RwLock::new(market))), graph_manager)
989 }
990
991 fn setup_derived_with_token_prices(
992 token_addresses: &[Address],
993 ) -> crate::derived::SharedDerivedDataRef {
994 use tycho_simulation::tycho_core::simulation::protocol_sim::Price;
995
996 let mut token_prices: TokenGasPrices = FxHashMap::default();
997 for address in token_addresses {
998 token_prices.insert(
999 address.clone(),
1000 Price { numerator: BigUint::from(1u64), denominator: BigUint::from(1u64) },
1001 );
1002 }
1003
1004 let mut derived_data = DerivedData::new();
1005 derived_data.set_token_prices(token_prices, vec![], 1, true);
1006 Arc::new(RwLock::new(derived_data))
1007 }
1008
1009 fn bf_algorithm(max_hops: usize, timeout_ms: u64) -> BellmanFordAlgorithm {
1010 BellmanFordAlgorithm::with_config(
1011 AlgorithmConfig::new(1, max_hops, Duration::from_millis(timeout_ms), None).unwrap(),
1012 )
1013 }
1014
1015 #[test]
1027 fn test_get_subgraph_keeps_full_length_routes_and_drops_dead_ends() {
1028 let token_a = token(0x01, "A");
1029 let token_b = token(0x02, "B");
1030 let token_c = token(0x03, "C");
1031 let token_d = token(0x04, "D");
1032
1033 let (_, manager) = setup_market_bf(vec![
1034 ("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
1035 ("component_bc", &token_b, &token_c, MockProtocolSim::new(3.0)),
1036 ("component_ad", &token_a, &token_d, MockProtocolSim::new(5.0)),
1037 ]);
1038 let graph = manager.graph();
1039 let node = |address: &Address| {
1040 graph
1041 .node_indices()
1042 .find(|&n| &graph[n] == address)
1043 .expect("token in graph")
1044 };
1045 let (adj, _, component_ids) = BellmanFordAlgorithm::get_subgraph(
1046 graph,
1047 node(&token_a.address),
1048 node(&token_c.address),
1049 2,
1050 )
1051 .unwrap();
1052
1053 let kept = |id: &str| {
1054 component_ids
1055 .iter()
1056 .any(|component_id| *component_id == id)
1057 };
1058
1059 assert!(kept("component_ab"), "the route's first hop must survive");
1061 assert!(kept("component_bc"), "the route's second hop must survive");
1062 assert!(!kept("component_ad"), "a dead end must not be kept");
1065
1066 let from_b = adj
1069 .get(&node(&token_b.address))
1070 .map(Vec::as_slice)
1071 .unwrap_or_default();
1072 assert!(
1073 from_b
1074 .iter()
1075 .all(|(target, _)| *target != node(&token_a.address)),
1076 "B -> A cannot finish the route and must not be kept"
1077 );
1078 }
1079
1080 #[test]
1092 fn test_get_subgraph_drops_detours_that_cannot_finish_in_budget() {
1093 let token_a = token(0x01, "A");
1094 let token_b = token(0x02, "B");
1095 let token_c = token(0x03, "C");
1096 let token_d = token(0x04, "D");
1097 let token_e = token(0x05, "E");
1098
1099 let (_, manager) = setup_market_bf(vec![
1100 ("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
1101 ("component_bc", &token_b, &token_c, MockProtocolSim::new(3.0)),
1102 ("component_ad", &token_a, &token_d, MockProtocolSim::new(5.0)),
1103 ("component_de", &token_d, &token_e, MockProtocolSim::new(5.0)),
1104 ("component_ec", &token_e, &token_c, MockProtocolSim::new(5.0)),
1105 ]);
1106 let graph = manager.graph();
1107 let node = |address: &Address| {
1108 graph
1109 .node_indices()
1110 .find(|&n| &graph[n] == address)
1111 .expect("token in graph")
1112 };
1113 let (_, token_nodes, component_ids) = BellmanFordAlgorithm::get_subgraph(
1114 graph,
1115 node(&token_a.address),
1116 node(&token_c.address),
1117 2,
1118 )
1119 .unwrap();
1120
1121 let kept = |id: &str| {
1122 component_ids
1123 .iter()
1124 .any(|component_id| *component_id == id)
1125 };
1126
1127 assert!(kept("component_ab"), "the two-hop route's first leg must survive");
1128 assert!(kept("component_bc"), "the two-hop route's second leg must survive");
1129
1130 assert!(!kept("component_ad"), "the step into a detour must not be kept");
1132 assert!(!kept("component_de"), "nor anything further along it");
1133 assert!(!kept("component_ec"), "nor its last leg into the destination");
1134 assert!(
1135 !token_nodes.contains(&node(&token_d.address)),
1136 "a token no legal route reaches must not be kept"
1137 );
1138 }
1139
1140 #[tokio::test]
1141 async fn test_linear_path_found() {
1142 let token_a = token(0x01, "A");
1143 let token_b = token(0x02, "B");
1144 let token_c = token(0x03, "C");
1145 let token_d = token(0x04, "D");
1146
1147 let (market, manager) = setup_market_bf(vec![
1148 ("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
1149 ("component_bc", &token_b, &token_c, MockProtocolSim::new(3.0)),
1150 ("component_cd", &token_c, &token_d, MockProtocolSim::new(4.0)),
1151 ]);
1152
1153 let algo = bf_algorithm(4, 1000);
1154 let ord = order(&token_a, &token_d, 100, OrderSide::Sell);
1155
1156 let result = algo
1157 .find_best_route(manager.graph(), market, None, None, &ord)
1158 .await
1159 .unwrap();
1160
1161 assert_eq!(result.route().swaps().len(), 3);
1162 assert_eq!(result.route().swaps()[0].amount_out(), &BigUint::from(200u64));
1164 assert_eq!(result.route().swaps()[1].amount_out(), &BigUint::from(600u64));
1165 assert_eq!(result.route().swaps()[2].amount_out(), &BigUint::from(2400u64));
1166 }
1167
1168 #[tokio::test]
1169 async fn test_picks_better_of_two_paths() {
1170 let token_a = token(0x01, "A");
1172 let token_b = token(0x02, "B");
1173 let token_c = token(0x03, "C");
1174 let token_d = token(0x04, "D");
1175
1176 let (market, manager) = setup_market_bf(vec![
1177 ("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
1178 ("component_bd", &token_b, &token_d, MockProtocolSim::new(3.0)),
1179 ("component_ac", &token_a, &token_c, MockProtocolSim::new(4.0)),
1180 ("component_cd", &token_c, &token_d, MockProtocolSim::new(1.0)),
1181 ]);
1182
1183 let algo = bf_algorithm(3, 1000);
1184 let ord = order(&token_a, &token_d, 100, OrderSide::Sell);
1185
1186 let result = algo
1187 .find_best_route(manager.graph(), market, None, None, &ord)
1188 .await
1189 .unwrap();
1190
1191 assert_eq!(result.route().swaps().len(), 2);
1193 assert_eq!(result.route().swaps()[0].component_id(), "component_ab");
1194 assert_eq!(result.route().swaps()[1].component_id(), "component_bd");
1195 assert_eq!(result.route().swaps()[1].amount_out(), &BigUint::from(600u64));
1196 }
1197
1198 #[tokio::test]
1199 async fn test_parallel_components() {
1200 let token_a = token(0x01, "A");
1202 let token_b = token(0x02, "B");
1203
1204 let (market, manager) = setup_market_bf(vec![
1205 ("component1", &token_a, &token_b, MockProtocolSim::new(2.0)),
1206 ("component2", &token_a, &token_b, MockProtocolSim::new(5.0)),
1207 ]);
1208
1209 let algo = bf_algorithm(2, 1000);
1210 let ord = order(&token_a, &token_b, 100, OrderSide::Sell);
1211
1212 let result = algo
1213 .find_best_route(manager.graph(), market, None, None, &ord)
1214 .await
1215 .unwrap();
1216
1217 assert_eq!(result.route().swaps().len(), 1);
1218 assert_eq!(result.route().swaps()[0].component_id(), "component2");
1219 assert_eq!(result.route().swaps()[0].amount_out(), &BigUint::from(500u64));
1220 }
1221
1222 #[tokio::test]
1223 async fn test_no_path_returns_error() {
1224 let token_a = token(0x01, "A");
1225 let token_b = token(0x02, "B");
1226 let token_c = token(0x03, "C");
1227
1228 let (market, manager) =
1230 setup_market_bf(vec![("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1231
1232 {
1234 let mut m = market.write().await;
1235 m.upsert_tokens(vec![token_c.clone()]);
1236 }
1237
1238 let algo = bf_algorithm(3, 1000);
1239 let ord = order(&token_a, &token_c, 100, OrderSide::Sell);
1240
1241 let result = algo
1242 .find_best_route(manager.graph(), market, None, None, &ord)
1243 .await;
1244 assert!(matches!(result, Err(AlgorithmError::NoPath { .. })));
1245 }
1246
1247 #[tokio::test]
1248 async fn test_source_not_in_graph() {
1249 let token_a = token(0x01, "A");
1250 let token_b = token(0x02, "B");
1251 let token_x = token(0x99, "X");
1252
1253 let (market, manager) =
1254 setup_market_bf(vec![("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1255
1256 let algo = bf_algorithm(3, 1000);
1257 let ord = order(&token_x, &token_b, 100, OrderSide::Sell);
1258
1259 let result = algo
1260 .find_best_route(manager.graph(), market, None, None, &ord)
1261 .await;
1262 assert!(matches!(
1263 result,
1264 Err(AlgorithmError::NoPath { reason: NoPathReason::SourceTokenNotInGraph, .. })
1265 ));
1266 }
1267
1268 #[tokio::test]
1269 async fn test_amount_too_small_when_reachable_but_zero_output() {
1270 let token_a = token(0x01, "A");
1271 let token_b = token(0x02, "B");
1272 let (market, manager) =
1274 setup_market_bf(vec![("component_ab", &token_a, &token_b, MockProtocolSim::new(0.5))]);
1275 let algo = bf_algorithm(3, 1000);
1276 let ord = order(&token_a, &token_b, 1, OrderSide::Sell);
1277
1278 let result = algo
1279 .find_best_route(manager.graph(), market, None, None, &ord)
1280 .await;
1281 assert!(matches!(
1282 result,
1283 Err(AlgorithmError::NoPath { reason: NoPathReason::AmountTooSmall, .. })
1284 ));
1285 }
1286
1287 #[tokio::test]
1288 async fn test_amount_too_small_when_dust_occurs_mid_route() {
1289 let token_a = token(0x01, "A");
1292 let token_b = token(0x02, "B");
1293 let token_c = token(0x03, "C");
1294 let (market, manager) = setup_market_bf(vec![
1295 ("component_ab", &token_a, &token_b, MockProtocolSim::new(0.5)),
1296 ("component_bc", &token_b, &token_c, MockProtocolSim::new(2.0)),
1297 ]);
1298 let algo = bf_algorithm(3, 1000);
1299 let ord = order(&token_a, &token_c, 1, OrderSide::Sell);
1300
1301 let result = algo
1302 .find_best_route(manager.graph(), market, None, None, &ord)
1303 .await;
1304 assert!(matches!(
1305 result,
1306 Err(AlgorithmError::NoPath { reason: NoPathReason::AmountTooSmall, .. })
1307 ));
1308 }
1309
1310 #[tokio::test]
1311 async fn test_no_graph_path_when_amount_too_large_for_liquidity() {
1312 let token_a = token(0x01, "A");
1315 let token_b = token(0x02, "B");
1316 let (market, manager) = setup_market_bf(vec![(
1317 "component_ab",
1318 &token_a,
1319 &token_b,
1320 MockProtocolSim::new(2.0).with_liquidity(500),
1321 )]);
1322 let algo = bf_algorithm(2, 1000);
1323 let ord = order(&token_a, &token_b, 1000, OrderSide::Sell);
1324
1325 let result = algo
1326 .find_best_route(manager.graph(), market, None, None, &ord)
1327 .await;
1328 assert!(matches!(
1329 result,
1330 Err(AlgorithmError::NoPath { reason: NoPathReason::NoGraphPath, .. })
1331 ));
1332 }
1333
1334 #[tokio::test]
1335 async fn test_no_graph_path_when_unreachable_within_hops() {
1336 let token_a = token(0x01, "A");
1338 let token_b = token(0x02, "B");
1339 let token_c = token(0x03, "C");
1340 let (market, manager) = setup_market_bf(vec![
1341 ("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
1342 ("component_bc", &token_b, &token_c, MockProtocolSim::new(2.0)),
1343 ]);
1344 let algo = bf_algorithm(1, 1000);
1345 let ord = order(&token_a, &token_c, 100, OrderSide::Sell);
1346
1347 let result = algo
1348 .find_best_route(manager.graph(), market, None, None, &ord)
1349 .await;
1350 assert!(matches!(
1351 result,
1352 Err(AlgorithmError::NoPath { reason: NoPathReason::NoGraphPath, .. })
1353 ));
1354 }
1355
1356 #[tokio::test]
1357 async fn test_no_graph_path_when_connector_tokens_exclude_intermediate() {
1358 let token_a = token(0x01, "A");
1361 let token_b = token(0x02, "B");
1362 let token_c = token(0x03, "C");
1363
1364 let (market, manager) = setup_market_bf(vec![
1365 ("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
1366 ("component_bc", &token_b, &token_c, MockProtocolSim::new(2.0)),
1367 ]);
1368
1369 let algo = BellmanFordAlgorithm::with_config(
1370 AlgorithmConfig::new(1, 3, Duration::from_millis(1000), None)
1371 .unwrap()
1372 .with_connector_tokens(FxHashSet::default()),
1373 );
1374 let ord = order(&token_a, &token_c, 100, OrderSide::Sell);
1375
1376 let result = algo
1377 .find_best_route(manager.graph(), market, None, None, &ord)
1378 .await;
1379 assert!(matches!(
1380 result,
1381 Err(AlgorithmError::NoPath { reason: NoPathReason::NoGraphPath, .. })
1382 ));
1383 }
1384
1385 #[tokio::test]
1386 async fn test_destination_not_in_graph() {
1387 let token_a = token(0x01, "A");
1388 let token_b = token(0x02, "B");
1389 let token_x = token(0x99, "X");
1390
1391 let (market, manager) =
1392 setup_market_bf(vec![("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1393
1394 let algo = bf_algorithm(3, 1000);
1395 let ord = order(&token_a, &token_x, 100, OrderSide::Sell);
1396
1397 let result = algo
1398 .find_best_route(manager.graph(), market, None, None, &ord)
1399 .await;
1400 assert!(matches!(
1401 result,
1402 Err(AlgorithmError::NoPath { reason: NoPathReason::DestinationTokenNotInGraph, .. })
1403 ));
1404 }
1405
1406 #[tokio::test]
1407 async fn test_respects_max_hops() {
1408 let token_a = token(0x01, "A");
1410 let token_b = token(0x02, "B");
1411 let token_c = token(0x03, "C");
1412 let token_d = token(0x04, "D");
1413
1414 let (market, manager) = setup_market_bf(vec![
1415 ("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
1416 ("component_bc", &token_b, &token_c, MockProtocolSim::new(3.0)),
1417 ("component_cd", &token_c, &token_d, MockProtocolSim::new(4.0)),
1418 ]);
1419
1420 let algo = bf_algorithm(2, 1000);
1421 let ord = order(&token_a, &token_d, 100, OrderSide::Sell);
1422
1423 let result = algo
1424 .find_best_route(manager.graph(), market, None, None, &ord)
1425 .await;
1426 assert!(
1427 matches!(result, Err(AlgorithmError::NoPath { .. })),
1428 "Should not find 3-hop path with max_hops=2"
1429 );
1430 }
1431
1432 #[tokio::test]
1433 async fn test_source_token_revisit_blocked() {
1434 let token_a = token(0x01, "A");
1437 let token_b = token(0x02, "B");
1438 let token_c = token(0x03, "C");
1439
1440 let (market, manager) = setup_market_bf(vec![
1441 ("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
1442 ("component_bc", &token_b, &token_c, MockProtocolSim::new(3.0)),
1443 ]);
1444
1445 let algo = bf_algorithm(4, 1000);
1446 let ord = order(&token_a, &token_c, 100, OrderSide::Sell);
1447
1448 let result = algo
1449 .find_best_route(manager.graph(), market, None, None, &ord)
1450 .await
1451 .unwrap();
1452
1453 assert_eq!(result.route().swaps().len(), 2);
1455 assert_eq!(result.route().swaps()[0].component_id(), "component_ab");
1456 assert_eq!(result.route().swaps()[1].component_id(), "component_bc");
1457 assert_eq!(result.route().swaps()[1].amount_out(), &BigUint::from(600u64));
1458 }
1459
1460 #[tokio::test]
1461 async fn test_hub_token_revisit_blocked() {
1462 let token_a = token(0x01, "A");
1465 let token_c = token(0x02, "C");
1466 let token_b = token(0x03, "B");
1467 let token_d = token(0x04, "D");
1468
1469 let (market, manager) = setup_market_bf(vec![
1470 ("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
1471 ("component_bc", &token_b, &token_c, MockProtocolSim::new(3.0)),
1472 ("component_cb", &token_c, &token_b, MockProtocolSim::new(100.0)),
1473 ("component_bd", &token_b, &token_d, MockProtocolSim::new(2.0)),
1474 ]);
1475
1476 let algo = bf_algorithm(4, 1000);
1477 let ord = order(&token_a, &token_d, 100, OrderSide::Sell);
1478
1479 let result = algo
1480 .find_best_route(manager.graph(), market, None, None, &ord)
1481 .await
1482 .unwrap();
1483
1484 assert_eq!(result.route().swaps().len(), 2, "should use direct 2-hop path");
1487 assert_eq!(result.route().swaps()[0].component_id(), "component_ab");
1488 assert_eq!(result.route().swaps()[1].component_id(), "component_bd");
1489 assert_eq!(result.route().swaps()[1].amount_out(), &BigUint::from(400u64));
1490 }
1491
1492 #[tokio::test]
1493 async fn test_route_amounts_are_sequential() {
1494 let token_a = token(0x01, "A");
1496 let token_b = token(0x02, "B");
1497 let token_c = token(0x03, "C");
1498
1499 let (market, manager) = setup_market_bf(vec![
1500 ("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
1501 ("component_bc", &token_b, &token_c, MockProtocolSim::new(3.0)),
1502 ]);
1503
1504 let algo = bf_algorithm(3, 1000);
1505 let ord = order(&token_a, &token_c, 100, OrderSide::Sell);
1506
1507 let result = algo
1508 .find_best_route(manager.graph(), market, None, None, &ord)
1509 .await
1510 .unwrap();
1511
1512 assert_eq!(result.route().swaps().len(), 2);
1513 assert_eq!(result.route().swaps()[1].amount_in(), result.route().swaps()[0].amount_out());
1515 }
1516
1517 #[tokio::test]
1518 async fn test_gas_deduction() {
1519 let token_a = token(0x01, "A");
1520 let token_b = token(0x02, "B");
1521
1522 let (market, manager) = setup_market_bf(vec![(
1523 "component1",
1524 &token_a,
1525 &token_b,
1526 MockProtocolSim::new(2.0).with_gas(10),
1527 )]);
1528
1529 let algo = bf_algorithm(2, 1000);
1530 let ord = order(&token_a, &token_b, 1000, OrderSide::Sell);
1531
1532 let derived = setup_derived_with_token_prices(std::slice::from_ref(&token_b.address));
1533
1534 let result = algo
1535 .find_best_route(manager.graph(), market, None, Some(derived), &ord)
1536 .await
1537 .unwrap();
1538
1539 assert_eq!(result.route().swaps()[0].amount_out(), &BigUint::from(2000u64));
1543 assert_eq!(result.net_amount_out(), &BigInt::from(1000));
1544 }
1545
1546 #[tokio::test]
1547 async fn test_timeout_respected() {
1548 let token_a = token(0x01, "A");
1549 let token_b = token(0x02, "B");
1550 let token_c = token(0x03, "C");
1551
1552 let (market, manager) = setup_market_bf(vec![
1553 ("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
1554 ("component_bc", &token_b, &token_c, MockProtocolSim::new(3.0)),
1555 ]);
1556
1557 let algo = bf_algorithm(3, 0);
1559 let ord = order(&token_a, &token_c, 100, OrderSide::Sell);
1560
1561 let result = algo
1562 .find_best_route(manager.graph(), market, None, None, &ord)
1563 .await;
1564
1565 match result {
1570 Ok(r) => {
1571 assert!(!r.route().swaps().is_empty());
1572 }
1573 Err(AlgorithmError::Timeout { .. }) | Err(AlgorithmError::NoPath { .. }) => {
1574 }
1576 Err(e) => panic!("Unexpected error: {:?}", e),
1577 }
1578 }
1579
1580 #[tokio::test]
1583 async fn test_with_fees() {
1584 let token_a = token(0x01, "A");
1585 let token_b = token(0x02, "B");
1586
1587 let (market, manager) = setup_market_bf(vec![(
1589 "component1",
1590 &token_a,
1591 &token_b,
1592 MockProtocolSim::new(2.0).with_fee(0.1),
1593 )]);
1594
1595 let algo = bf_algorithm(2, 1000);
1596 let ord = order(&token_a, &token_b, 1000, OrderSide::Sell);
1597
1598 let result = algo
1599 .find_best_route(manager.graph(), market, None, None, &ord)
1600 .await
1601 .unwrap();
1602
1603 assert_eq!(result.route().swaps()[0].amount_out(), &BigUint::from(1800u64));
1605 }
1606
1607 #[tokio::test]
1608 async fn test_large_trade_slippage() {
1609 let token_a = token(0x01, "A");
1610 let token_b = token(0x02, "B");
1611
1612 let (market, manager) = setup_market_bf(vec![(
1614 "component1",
1615 &token_a,
1616 &token_b,
1617 MockProtocolSim::new(2.0).with_liquidity(500),
1618 )]);
1619
1620 let algo = bf_algorithm(2, 1000);
1621 let ord = order(&token_a, &token_b, 1000, OrderSide::Sell);
1622
1623 let result = algo
1625 .find_best_route(manager.graph(), market, None, None, &ord)
1626 .await;
1627 assert!(
1628 matches!(result, Err(AlgorithmError::NoPath { .. })),
1629 "Should fail when trade exceeds component liquidity"
1630 );
1631 }
1632
1633 #[tokio::test]
1634 async fn test_disconnected_tokens_return_no_path() {
1635 let token_a = token(0x01, "A");
1637 let token_b = token(0x02, "B");
1638 let token_d = token(0x04, "D");
1639 let token_e = token(0x05, "E");
1640
1641 let (market, manager) = setup_market_bf(vec![
1642 ("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
1643 ("component_de", &token_d, &token_e, MockProtocolSim::new(4.0)),
1644 ]);
1645
1646 let algo = bf_algorithm(3, 1000);
1647 let ord = order(&token_a, &token_e, 100, OrderSide::Sell);
1648
1649 let result = algo
1650 .find_best_route(manager.graph(), market, None, None, &ord)
1651 .await;
1652 assert!(
1653 matches!(result, Err(AlgorithmError::NoPath { .. })),
1654 "should not find path to disconnected component"
1655 );
1656 }
1657
1658 #[tokio::test]
1659 async fn test_spfa_skips_failed_simulations() {
1660 let token_a = token(0x01, "A");
1662 let token_b = token(0x02, "B");
1663 let token_c = token(0x03, "C");
1664
1665 let (market, manager) = setup_market_bf(vec![
1666 ("component_ab_bad", &token_a, &token_b, MockProtocolSim::new(2.0).with_liquidity(0)),
1668 ("component_ac", &token_a, &token_c, MockProtocolSim::new(2.0)),
1670 ("component_cb", &token_c, &token_b, MockProtocolSim::new(3.0)),
1671 ]);
1672
1673 let algo = bf_algorithm(3, 1000);
1674 let ord = order(&token_a, &token_b, 100, OrderSide::Sell);
1675
1676 let result = algo
1677 .find_best_route(manager.graph(), market, None, None, &ord)
1678 .await;
1679
1680 match result {
1684 Ok(r) => {
1685 assert!(!r.route().swaps().is_empty());
1687 }
1688 Err(AlgorithmError::NoPath { .. }) => {
1689 }
1692 Err(e) => panic!("Unexpected error: {:?}", e),
1693 }
1694 }
1695
1696 #[tokio::test]
1697 async fn test_resimulation_produces_correct_amounts() {
1698 let token_a = token(0x01, "A");
1700 let token_b = token(0x02, "B");
1701 let token_c = token(0x03, "C");
1702
1703 let (market, manager) = setup_market_bf(vec![
1704 ("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
1705 ("component_bc", &token_b, &token_c, MockProtocolSim::new(3.0)),
1706 ]);
1707
1708 let algo = bf_algorithm(3, 1000);
1709 let ord = order(&token_a, &token_c, 100, OrderSide::Sell);
1710
1711 let result = algo
1712 .find_best_route(manager.graph(), market, None, None, &ord)
1713 .await
1714 .unwrap();
1715
1716 assert_eq!(result.route().swaps()[0].amount_in(), &BigUint::from(100u64));
1719 assert_eq!(result.route().swaps()[0].amount_out(), &BigUint::from(200u64));
1720 assert_eq!(result.route().swaps()[1].amount_in(), &BigUint::from(200u64));
1721 assert_eq!(result.route().swaps()[1].amount_out(), &BigUint::from(600u64));
1722 }
1723
1724 #[test]
1727 fn algorithm_name() {
1728 let algo = bf_algorithm(4, 200);
1729 assert_eq!(algo.name(), "bellman_ford");
1730 }
1731
1732 #[test]
1733 fn algorithm_timeout() {
1734 let algo = bf_algorithm(4, 200);
1735 assert_eq!(algo.timeout(), Duration::from_millis(200));
1736 }
1737
1738 #[tokio::test]
1741 async fn test_gas_aware_relaxation_picks_cheaper_path() {
1742 let token_a = token(0x01, "A");
1757 let token_b = token(0x02, "B");
1758 let token_c = token(0x03, "C");
1759 let token_d = token(0x04, "D");
1760
1761 let high_gas: u64 = 100_000_000;
1762 let low_gas: u64 = 100;
1763
1764 let (market, manager) = setup_market_bf(vec![
1765 ("component_ab", &token_a, &token_b, MockProtocolSim::new(3.0).with_gas(high_gas)),
1766 ("component_bd", &token_b, &token_d, MockProtocolSim::new(2.0).with_gas(high_gas)),
1767 ("component_ac", &token_a, &token_c, MockProtocolSim::new(2.0).with_gas(low_gas)),
1768 ("component_cd", &token_c, &token_d, MockProtocolSim::new(2.0).with_gas(low_gas)),
1769 ]);
1770
1771 let algo = bf_algorithm(3, 1000);
1772 let ord = order(&token_a, &token_d, 1_000_000_000, OrderSide::Sell);
1773
1774 let derived = setup_derived_with_token_prices(&[
1776 token_a.address.clone(),
1777 token_b.address.clone(),
1778 token_c.address.clone(),
1779 token_d.address.clone(),
1780 ]);
1781
1782 let result = algo
1783 .find_best_route(manager.graph(), market, None, Some(derived), &ord)
1784 .await
1785 .unwrap();
1786
1787 assert_eq!(result.route().swaps().len(), 2);
1789 assert_eq!(result.route().swaps()[0].component_id(), "component_ac");
1790 assert_eq!(result.route().swaps()[1].component_id(), "component_cd");
1791 }
1792
1793 #[tokio::test]
1794 async fn test_gas_aware_falls_back_to_gross_without_derived() {
1795 let token_a = token(0x01, "A");
1798 let token_b = token(0x02, "B");
1799 let token_c = token(0x03, "C");
1800 let token_d = token(0x04, "D");
1801
1802 let high_gas: u64 = 100_000_000;
1803 let low_gas: u64 = 100;
1804
1805 let (market, manager) = setup_market_bf(vec![
1806 ("component_ab", &token_a, &token_b, MockProtocolSim::new(3.0).with_gas(high_gas)),
1807 ("component_bd", &token_b, &token_d, MockProtocolSim::new(2.0).with_gas(high_gas)),
1808 ("component_ac", &token_a, &token_c, MockProtocolSim::new(2.0).with_gas(low_gas)),
1809 ("component_cd", &token_c, &token_d, MockProtocolSim::new(2.0).with_gas(low_gas)),
1810 ]);
1811
1812 let algo = bf_algorithm(3, 1000);
1813 let ord = order(&token_a, &token_d, 1_000_000_000, OrderSide::Sell);
1814
1815 let result = algo
1817 .find_best_route(manager.graph(), market, None, None, &ord)
1818 .await
1819 .unwrap();
1820
1821 assert_eq!(result.route().swaps().len(), 2);
1823 assert_eq!(result.route().swaps()[0].component_id(), "component_ab");
1824 assert_eq!(result.route().swaps()[1].component_id(), "component_bd");
1825 }
1826
1827 #[tokio::test]
1828 async fn test_amount_too_small_when_net_uneconomic_after_gas() {
1829 let token_a = token(0x01, "A");
1832 let token_b = token(0x02, "B");
1833
1834 let (market, manager) = setup_market_bf(vec![(
1835 "component_ab",
1836 &token_a,
1837 &token_b,
1838 MockProtocolSim::new(2.0).with_gas(10),
1839 )]);
1840
1841 let algo = bf_algorithm(2, 1000);
1842 let ord = order(&token_a, &token_b, 1, OrderSide::Sell);
1843 let derived =
1844 setup_derived_with_token_prices(&[token_a.address.clone(), token_b.address.clone()]);
1845
1846 let result = algo
1847 .find_best_route(manager.graph(), market, None, Some(derived), &ord)
1848 .await;
1849 assert!(matches!(
1850 result,
1851 Err(AlgorithmError::NoPath { reason: NoPathReason::AmountTooSmall, .. })
1852 ));
1853 }
1854
1855 #[tokio::test]
1856 async fn test_no_graph_path_when_output_uneconomic_but_input_economic() {
1857 let token_a = token(0x01, "A");
1861 let token_b = token(0x02, "B");
1862
1863 let (market, manager) = setup_market_bf(vec![(
1864 "component_ab",
1865 &token_a,
1866 &token_b,
1867 MockProtocolSim::new(0.05).with_gas(10),
1868 )]);
1869
1870 let algo = bf_algorithm(1, 1000);
1871 let ord = order(&token_a, &token_b, 10_000, OrderSide::Sell);
1872 let derived =
1873 setup_derived_with_token_prices(&[token_a.address.clone(), token_b.address.clone()]);
1874
1875 let result = algo
1876 .find_best_route(manager.graph(), market, None, Some(derived), &ord)
1877 .await;
1878 assert!(matches!(
1879 result,
1880 Err(AlgorithmError::NoPath { reason: NoPathReason::NoGraphPath, .. })
1881 ));
1882 }
1883
1884 fn bf_algorithm_with_connectors(
1888 max_hops: usize,
1889 timeout_ms: u64,
1890 connector_tokens: FxHashSet<Address>,
1891 ) -> BellmanFordAlgorithm {
1892 BellmanFordAlgorithm::with_config(
1893 AlgorithmConfig::new(1, max_hops, Duration::from_millis(timeout_ms), None)
1894 .unwrap()
1895 .with_connector_tokens(connector_tokens),
1896 )
1897 }
1898
1899 #[tokio::test]
1900 async fn test_connector_tokens_blocks_disallowed_intermediate() {
1901 let token_a = token(0x01, "A");
1908 let token_b = token(0x02, "B");
1909 let token_c = token(0x03, "C");
1910 let token_d = token(0x04, "D");
1911
1912 let (market, manager) = setup_market_bf(vec![
1913 ("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
1914 ("component_bd", &token_b, &token_d, MockProtocolSim::new(2.0)),
1915 ("component_ac", &token_a, &token_c, MockProtocolSim::new(3.0)),
1916 ("component_cd", &token_c, &token_d, MockProtocolSim::new(3.0)),
1917 ]);
1918
1919 let connectors: FxHashSet<Address> = FxHashSet::from_iter([token_c.address.clone()]);
1920 let algo = bf_algorithm_with_connectors(3, 1000, connectors);
1921 let ord = order(&token_a, &token_d, 100, OrderSide::Sell);
1922
1923 let result = algo
1924 .find_best_route(manager.graph(), market, None, None, &ord)
1925 .await
1926 .unwrap();
1927
1928 assert_eq!(result.route().swaps().len(), 2);
1930 assert_eq!(result.route().swaps()[0].component_id(), "component_ac");
1931 assert_eq!(result.route().swaps()[1].component_id(), "component_cd");
1932 }
1933
1934 #[tokio::test]
1935 async fn test_connector_tokens_allows_endpoints_even_if_not_listed() {
1936 let token_a = token(0x01, "A");
1938 let token_b = token(0x02, "B");
1939
1940 let (market, manager) =
1941 setup_market_bf(vec![("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1942
1943 let algo = bf_algorithm_with_connectors(1, 1000, FxHashSet::default());
1945 let ord = order(&token_a, &token_b, 100, OrderSide::Sell);
1946
1947 let result = algo
1948 .find_best_route(manager.graph(), market, None, None, &ord)
1949 .await
1950 .unwrap();
1951
1952 assert_eq!(result.route().swaps().len(), 1);
1953 assert_eq!(result.route().swaps()[0].amount_out(), &BigUint::from(200u64));
1954 }
1955
1956 #[tokio::test]
1957 async fn test_connector_tokens_none_is_unrestricted() {
1958 let token_a = token(0x01, "A");
1960 let token_b = token(0x02, "B");
1961 let token_c = token(0x03, "C");
1962 let token_d = token(0x04, "D");
1963
1964 let (market, manager) = setup_market_bf(vec![
1965 ("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
1966 ("component_bd", &token_b, &token_d, MockProtocolSim::new(3.0)),
1967 ("component_ac", &token_a, &token_c, MockProtocolSim::new(1.0)),
1968 ("component_cd", &token_c, &token_d, MockProtocolSim::new(1.0)),
1969 ]);
1970
1971 let algo = bf_algorithm(3, 1000);
1972 let ord = order(&token_a, &token_d, 100, OrderSide::Sell);
1973
1974 let result = algo
1975 .find_best_route(manager.graph(), market, None, None, &ord)
1976 .await
1977 .unwrap();
1978
1979 assert_eq!(result.route().swaps()[0].component_id(), "component_ab");
1981 assert_eq!(result.route().swaps()[1].component_id(), "component_bd");
1982 assert_eq!(result.route().swaps()[1].amount_out(), &BigUint::from(600u64));
1983 }
1984
1985 #[test]
1986 fn test_path_has_conflict_detects_node_and_component() {
1987 let mut pred: Vec<Option<(NodeIndex, ComponentId)>> = vec![None; 4];
1989 pred[1] = Some((NodeIndex::new(0), "component_a".into()));
1990 pred[2] = Some((NodeIndex::new(1), "component_b".into()));
1991
1992 assert!(BellmanFordAlgorithm::path_has_conflict(
1994 NodeIndex::new(2),
1995 NodeIndex::new(0),
1996 &"any".into(),
1997 &pred
1998 ));
1999 assert!(!BellmanFordAlgorithm::path_has_conflict(
2000 NodeIndex::new(2),
2001 NodeIndex::new(3),
2002 &"any".into(),
2003 &pred
2004 ));
2005 assert!(BellmanFordAlgorithm::path_has_conflict(
2007 NodeIndex::new(2),
2008 NodeIndex::new(2),
2009 &"any".into(),
2010 &pred
2011 ));
2012
2013 assert!(BellmanFordAlgorithm::path_has_conflict(
2015 NodeIndex::new(2),
2016 NodeIndex::new(3),
2017 &"component_a".into(),
2018 &pred
2019 ));
2020 assert!(BellmanFordAlgorithm::path_has_conflict(
2021 NodeIndex::new(2),
2022 NodeIndex::new(3),
2023 &"component_b".into(),
2024 &pred
2025 ));
2026 assert!(!BellmanFordAlgorithm::path_has_conflict(
2027 NodeIndex::new(2),
2028 NodeIndex::new(3),
2029 &"component_c".into(),
2030 &pred
2031 ));
2032 }
2033
2034 #[tokio::test]
2035 async fn test_find_single_route_with_state_overrides() {
2036 let token_a = token(0x01, "A");
2037 let token_b = token(0x02, "B");
2038
2039 let (market, manager) =
2040 setup_market_bf(vec![("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0))]);
2041
2042 let algo = bf_algorithm(2, 1000);
2043 let ord = order(&token_a, &token_b, 1000, OrderSide::Sell);
2044
2045 let ctx = algo
2046 .build_context(manager.graph(), market, None, None, &ord)
2047 .await
2048 .unwrap();
2049
2050 let normal = algo
2052 .find_single_route(&ctx, &ord, FindRouteOptions::default())
2053 .unwrap();
2054 assert_eq!(normal.route().swaps()[0].amount_out(), &BigUint::from(2000u64));
2055
2056 let opts = FindRouteOptions {
2058 overrides: MarketOverrides::empty()
2059 .with_override("component_ab".to_string(), Box::new(MockProtocolSim::new(1.0))),
2060 };
2061 let overridden = algo
2062 .find_single_route(&ctx, &ord, opts)
2063 .unwrap();
2064 assert_eq!(overridden.route().swaps()[0].amount_out(), &BigUint::from(1000u64));
2065
2066 assert!(
2067 overridden.route().swaps()[0].amount_out() < normal.route().swaps()[0].amount_out()
2068 );
2069 }
2070
2071 #[tokio::test]
2072 async fn test_single_find_route_options_default() {
2073 use super::super::split_primitives::MarketOverrides;
2074
2075 let token_a = token(0x01, "A");
2076 let token_b = token(0x02, "B");
2077
2078 let (market, manager) =
2079 setup_market_bf(vec![("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0))]);
2080
2081 let algo = bf_algorithm(2, 1000);
2082 let ord = order(&token_a, &token_b, 1000, OrderSide::Sell);
2083
2084 let ctx = algo
2085 .build_context(manager.graph(), market, None, None, &ord)
2086 .await
2087 .unwrap();
2088
2089 let with_default = algo
2090 .find_single_route(&ctx, &ord, FindRouteOptions::default())
2091 .unwrap();
2092 let with_empty = algo
2093 .find_single_route(&ctx, &ord, FindRouteOptions { overrides: MarketOverrides::empty() })
2094 .unwrap();
2095
2096 assert_eq!(
2097 with_default.route().swaps()[0].amount_out(),
2098 with_empty.route().swaps()[0].amount_out()
2099 );
2100 assert_eq!(with_default.route().swaps()[0].amount_out(), &BigUint::from(2000u64));
2101 }
2102}