1use std::{
27 collections::{HashMap, HashSet, VecDeque},
28 time::{Duration, Instant},
29};
30
31use num_bigint::{BigInt, BigUint};
32use num_traits::{ToPrimitive, Zero};
33use petgraph::{graph::NodeIndex, prelude::EdgeRef};
34use tracing::{debug, instrument, trace, warn};
35use tycho_simulation::{
36 tycho_common::models::Address,
37 tycho_core::{models::token::Token, simulation::protocol_sim::Price},
38};
39
40use super::{
41 split_primitives::MarketOverrides, Algorithm, AlgorithmConfig, AlgorithmError, NoPathReason,
42};
43use crate::{
44 algorithm::sim_guard::GuardedProtocolSim,
45 derived::{
46 computation::ComputationRequirements,
47 types::{SpotPrices, TokenGasPrices},
48 SharedDerivedDataRef,
49 },
50 feed::market_data::{MarketData, MarketState, StateLabel},
51 graph::{petgraph::StableDiGraph, PetgraphStableDiGraphManager},
52 types::{ComponentId, Order, Route, RouteResult, Swap},
53};
54
55type Subgraph =
57 (HashMap<NodeIndex, Vec<(NodeIndex, ComponentId)>>, HashSet<NodeIndex>, HashSet<ComponentId>);
58
59pub(crate) struct BellmanFordContext {
65 pub(crate) token_in_node: NodeIndex,
66 pub(crate) token_out_node: NodeIndex,
67 pub(crate) adj: HashMap<NodeIndex, Vec<(NodeIndex, ComponentId)>>,
68 pub(crate) token_map: HashMap<NodeIndex, Token>,
69 pub(crate) market_data: MarketState,
70 pub(crate) gas_price_wei: Option<BigUint>,
71 pub(crate) token_prices: Option<TokenGasPrices>,
72 pub(crate) spot_prices: Option<SpotPrices>,
73 pub(crate) node_address: HashMap<NodeIndex, Address>,
74 pub(crate) max_idx: usize,
75 pub(crate) scoring: RouteScoringMode,
76}
77
78pub(crate) enum RouteScoringMode {
80 GrossOutput,
82 NetOutput,
84}
85
86#[derive(Default)]
88pub(crate) struct FindRouteOptions {
89 pub(crate) overrides: MarketOverrides,
91}
92
93struct SPFAResult {
95 amount: Vec<BigUint>,
97 predecessor: Vec<Option<(NodeIndex, ComponentId)>>,
99 edge_gas: Vec<BigUint>,
101 spot_product: Vec<f64>,
103 input_below_hop_gas: bool,
106}
107
108pub struct BellmanFordAlgorithm {
114 max_hops: usize,
115 timeout: Duration,
116 gas_aware: bool,
117 connector_tokens: Option<HashSet<Address>>,
118}
119
120impl Default for BellmanFordAlgorithm {
121 fn default() -> Self {
122 Self::with_config(AlgorithmConfig::default())
123 }
124}
125
126impl BellmanFordAlgorithm {
127 pub(crate) fn with_config(config: AlgorithmConfig) -> Self {
128 Self {
129 max_hops: config.max_hops(),
130 timeout: config.timeout(),
131 gas_aware: config.gas_aware(),
132 connector_tokens: config.connector_tokens().cloned(),
133 }
134 }
135
136 pub(crate) async fn build_context(
143 &self,
144 graph: &StableDiGraph<()>,
145 market: MarketData,
146 label: Option<StateLabel>,
147 derived: Option<SharedDerivedDataRef>,
148 order: &Order,
149 ) -> Result<BellmanFordContext, AlgorithmError> {
150 if !order.is_sell() {
151 return Err(AlgorithmError::ExactOutNotSupported);
152 }
153
154 let (token_prices, spot_prices) = if let Some(ref d) = derived {
155 let guard = d.read().await;
156 (guard.token_prices().cloned(), guard.spot_prices().cloned())
157 } else {
158 (None, None)
159 };
160
161 let token_in_node = graph
162 .node_indices()
163 .find(|&n| &graph[n] == order.token_in())
164 .ok_or(AlgorithmError::NoPath {
165 from: order.token_in().clone(),
166 to: order.token_out().clone(),
167 reason: NoPathReason::SourceTokenNotInGraph,
168 })?;
169 let token_out_node = graph
170 .node_indices()
171 .find(|&n| &graph[n] == order.token_out())
172 .ok_or(AlgorithmError::NoPath {
173 from: order.token_in().clone(),
174 to: order.token_out().clone(),
175 reason: NoPathReason::DestinationTokenNotInGraph,
176 })?;
177
178 if token_in_node == token_out_node {
179 return Err(AlgorithmError::NoPath {
180 from: order.token_in().clone(),
181 to: order.token_out().clone(),
182 reason: NoPathReason::NoGraphPath,
183 });
184 }
185
186 let (adj, token_nodes, component_ids) =
188 Self::get_subgraph(graph, token_in_node, self.max_hops, order)?;
189
190 let market_view = match label.as_ref() {
191 Some(l) => market
192 .read_labeled(l)
193 .await
194 .map_err(|e| AlgorithmError::Other(e.to_string()))?,
195 None => market.read().await,
196 };
197 let token_map: HashMap<NodeIndex, Token> = token_nodes
198 .iter()
199 .filter_map(|&node| {
200 market_view
201 .get_token(&graph[node])
202 .cloned()
203 .map(|t| (node, t))
204 })
205 .collect();
206 let market_data = market_view.extract_subset_with_overlay(&component_ids);
207 let gas_price_wei = market_data
208 .gas_price()
209 .map(|gp| gp.effective_gas_price().clone());
210 drop(market_view);
211
212 let node_address: HashMap<NodeIndex, Address> = token_map
213 .iter()
214 .map(|(&node, token)| (node, token.address.clone()))
215 .collect();
216
217 let max_idx = graph
218 .node_indices()
219 .map(|n| n.index())
220 .max()
221 .unwrap_or(0) +
222 1;
223
224 let scoring = if self.gas_aware {
225 RouteScoringMode::NetOutput
226 } else {
227 RouteScoringMode::GrossOutput
228 };
229
230 debug!(
231 edges = adj
232 .values()
233 .map(Vec::len)
234 .sum::<usize>(),
235 tokens = token_map.len(),
236 "subgraph extracted"
237 );
238
239 Ok(BellmanFordContext {
240 token_in_node,
241 token_out_node,
242 adj,
243 token_map,
244 market_data,
245 gas_price_wei,
246 token_prices,
247 spot_prices,
248 node_address,
249 max_idx,
250 scoring,
251 })
252 }
253
254 pub(crate) fn find_single_route(
261 &self,
262 ctx: &BellmanFordContext,
263 order: &Order,
264 opts: FindRouteOptions,
265 ) -> Result<RouteResult, AlgorithmError> {
266 let start = Instant::now();
267
268 let spfa = self.run_spfa(ctx, order, &opts.overrides, start);
269
270 let out_idx = ctx.token_out_node.index();
271 if spfa.amount[out_idx].is_zero() {
272 let reason = if spfa.input_below_hop_gas {
276 NoPathReason::AmountTooSmall
277 } else {
278 NoPathReason::NoGraphPath
279 };
280 return Err(AlgorithmError::NoPath {
281 from: order.token_in().clone(),
282 to: order.token_out().clone(),
283 reason,
284 });
285 }
286
287 let path_edges =
291 Self::reconstruct_path(ctx.token_out_node, ctx.token_in_node, &spfa.predecessor)?;
292
293 let route =
294 Self::build_route(ctx, &path_edges, &spfa.amount, &spfa.edge_gas, &opts.overrides)?;
295
296 let final_amount_out = spfa.amount[out_idx].clone();
297 let gas_price = ctx
298 .gas_price_wei
299 .clone()
300 .unwrap_or_default();
301
302 let net_amount_out = Self::compute_net_amount_out(
303 &final_amount_out,
304 &route,
305 &gas_price,
306 ctx.token_prices.as_ref(),
307 &spfa.spot_product,
308 &ctx.node_address,
309 ctx.token_in_node,
310 )?;
311
312 let result = RouteResult::new(route, net_amount_out, gas_price);
313
314 let solve_time_ms = start.elapsed().as_millis() as u64;
315 debug!(
316 solve_time_ms,
317 hops = result.route().swaps().len(),
318 amount_in = %order.amount(),
319 amount_out = %final_amount_out,
320 net_amount_out = %result.net_amount_out(),
321 "bellman_ford route found"
322 );
323
324 Ok(result)
325 }
326
327 fn run_spfa(
333 &self,
334 ctx: &BellmanFordContext,
335 order: &Order,
336 overrides: &MarketOverrides,
337 start: Instant,
338 ) -> SPFAResult {
339 let mut amount: Vec<BigUint> = vec![BigUint::ZERO; ctx.max_idx];
343 let mut predecessor: Vec<Option<(NodeIndex, ComponentId)>> = vec![None; ctx.max_idx];
344 let mut edge_gas: Vec<BigUint> = vec![BigUint::ZERO; ctx.max_idx];
345 let mut cumul_gas: Vec<BigUint> = vec![BigUint::ZERO; ctx.max_idx];
346
347 amount[ctx.token_in_node.index()] = order.amount().clone();
348
349 let mut spot_product: Vec<f64> = vec![0.0; ctx.max_idx];
352 spot_product[ctx.token_in_node.index()] = 1.0;
353
354 let mut input_below_hop_gas = false;
355
356 let gas_aware = matches!(ctx.scoring, RouteScoringMode::NetOutput) &&
357 ctx.gas_price_wei.is_some() &&
358 ctx.token_prices.is_some();
359 if !gas_aware && matches!(ctx.scoring, RouteScoringMode::NetOutput) {
360 debug!("gas-aware comparison disabled (missing gas_price or token_prices)");
361 } else if matches!(ctx.scoring, RouteScoringMode::GrossOutput) {
362 debug!("gas-aware comparison disabled by config");
363 }
364
365 let mut active_nodes: Vec<NodeIndex> = vec![ctx.token_in_node];
366
367 for round in 0..self.max_hops {
368 if start.elapsed() >= self.timeout {
369 debug!(round, "timeout during relaxation");
370 break;
371 }
372 if active_nodes.is_empty() {
373 debug!(round, "no active nodes, stopping early");
374 break;
375 }
376
377 let mut next_active: HashSet<NodeIndex> = HashSet::new();
378
379 for &u in &active_nodes {
380 let u_idx = u.index();
381 if amount[u_idx].is_zero() {
382 continue;
383 }
384
385 let Some(token_u) = ctx.token_map.get(&u) else { continue };
386 let Some(edges) = ctx.adj.get(&u) else { continue };
387
388 for (v, component_id) in edges {
389 let v_idx = v.index();
390
391 if Self::path_has_conflict(u, *v, component_id, &predecessor) {
393 continue;
394 }
395
396 if !self.connector_allows(ctx, order, *v) {
399 continue;
400 }
401
402 let Some(token_v) = ctx.token_map.get(v) else { continue };
403
404 let sim: &dyn tycho_simulation::tycho_common::simulation::protocol_sim::ProtocolSim =
406 if let Some(s) = overrides.get(component_id) {
407 s
408 } else if let Some(s) = ctx.market_data.get_simulation_state(component_id) {
409 s
410 } else {
411 continue;
412 };
413
414 let result =
415 match sim.get_amount_out_guarded(amount[u_idx].clone(), token_u, token_v) {
416 Ok(r) => r,
417 Err(e) => {
418 trace!(
419 component_id,
420 error = %e,
421 "simulation failed, skipping edge"
422 );
423 continue;
424 }
425 };
426
427 let candidate_cumul_gas = &cumul_gas[u_idx] + &result.gas;
428
429 let candidate_spot = Self::compute_edge_spot_product(
432 spot_product[u_idx],
433 component_id,
434 ctx.node_address.get(&u),
435 ctx.node_address.get(v),
436 ctx.spot_prices.as_ref(),
437 );
438
439 let is_better = if gas_aware {
441 let v_price = Self::resolve_token_price(
442 ctx.node_address.get(v),
443 ctx.token_prices.as_ref(),
444 candidate_spot,
445 ctx.node_address.get(&ctx.token_in_node),
446 );
447 let net_candidate = Self::gas_adjusted_amount(
448 &result.amount,
449 &candidate_cumul_gas,
450 ctx.gas_price_wei.as_ref().unwrap(),
451 v_price.as_ref(),
452 );
453 if !input_below_hop_gas && net_candidate <= BigInt::ZERO {
458 let u_price = Self::resolve_token_price(
459 ctx.node_address.get(&u),
460 ctx.token_prices.as_ref(),
461 spot_product[u_idx],
462 ctx.node_address.get(&ctx.token_in_node),
463 );
464 if Self::gas_adjusted_amount(
465 &amount[u_idx],
466 &result.gas,
467 ctx.gas_price_wei.as_ref().unwrap(),
468 u_price.as_ref(),
469 ) <= BigInt::ZERO
470 {
471 input_below_hop_gas = true;
472 }
473 }
474 let net_existing = Self::gas_adjusted_amount(
475 &amount[v_idx],
476 &cumul_gas[v_idx],
477 ctx.gas_price_wei.as_ref().unwrap(),
478 v_price.as_ref(),
479 );
480 net_candidate > net_existing
481 } else {
482 if result.amount.is_zero() {
483 input_below_hop_gas = true;
484 }
485 result.amount > amount[v_idx]
486 };
487
488 if is_better {
489 spot_product[v_idx] = candidate_spot;
490 amount[v_idx] = result.amount;
491 predecessor[v_idx] = Some((u, component_id.clone()));
492 edge_gas[v_idx] = result.gas;
493 cumul_gas[v_idx] = candidate_cumul_gas;
494 next_active.insert(*v);
495 }
496 }
497 }
498
499 active_nodes = next_active.into_iter().collect();
500 active_nodes.sort_unstable();
505 }
506
507 SPFAResult { amount, predecessor, edge_gas, spot_product, input_below_hop_gas }
508 }
509
510 fn connector_allows(&self, ctx: &BellmanFordContext, order: &Order, v: NodeIndex) -> bool {
513 let (Some(tokens), Some(v_addr)) = (&self.connector_tokens, ctx.node_address.get(&v))
514 else {
515 return true;
516 };
517 v_addr == order.token_in() || v_addr == order.token_out() || tokens.contains(v_addr)
518 }
519
520 fn build_route(
522 ctx: &BellmanFordContext,
523 path_edges: &[(NodeIndex, NodeIndex, ComponentId)],
524 amount: &[BigUint],
525 edge_gas: &[BigUint],
526 overrides: &MarketOverrides,
527 ) -> Result<Route, AlgorithmError> {
528 let mut swaps = Vec::with_capacity(path_edges.len());
529 let mut tokens: HashMap<Address, Token> = HashMap::new();
530
531 for (from_node, to_node, component_id) in path_edges {
532 let token_in = ctx
533 .token_map
534 .get(from_node)
535 .ok_or_else(|| AlgorithmError::DataNotFound {
536 kind: "token",
537 id: Some(format!("{:?}", from_node)),
538 })?;
539 let token_out = ctx
540 .token_map
541 .get(to_node)
542 .ok_or_else(|| AlgorithmError::DataNotFound {
543 kind: "token",
544 id: Some(format!("{:?}", to_node)),
545 })?;
546 let component = ctx
547 .market_data
548 .get_component(component_id)
549 .ok_or_else(|| AlgorithmError::DataNotFound {
550 kind: "component",
551 id: Some(component_id.clone()),
552 })?;
553 let sim_state = overrides
555 .get(component_id)
556 .or_else(|| {
557 ctx.market_data
558 .get_simulation_state(component_id)
559 })
560 .ok_or_else(|| AlgorithmError::DataNotFound {
561 kind: "simulation state",
562 id: Some(component_id.clone()),
563 })?;
564
565 swaps.push(Swap::new(
566 component_id.clone(),
567 component.protocol_system.clone(),
568 token_in.address.clone(),
569 token_out.address.clone(),
570 amount[from_node.index()].clone(),
571 amount[to_node.index()].clone(),
572 edge_gas[to_node.index()].clone(),
573 component.clone(),
574 sim_state.clone_box(),
575 ));
576 tokens
577 .entry(token_in.address.clone())
578 .or_insert_with(|| token_in.clone());
579 tokens
580 .entry(token_out.address.clone())
581 .or_insert_with(|| token_out.clone());
582 }
583
584 Ok(Route::new(swaps, tokens)?)
585 }
586
587 fn gas_adjusted_amount(
592 gross: &BigUint,
593 cumul_gas: &BigUint,
594 gas_price_wei: &BigUint,
595 token_price: Option<&Price>,
596 ) -> BigInt {
597 match token_price {
598 Some(price) if !price.denominator.is_zero() => {
599 let gas_cost = cumul_gas * gas_price_wei * &price.numerator / &price.denominator;
600 BigInt::from(gross.clone()) - BigInt::from(gas_cost)
601 }
602 _ => BigInt::from(gross.clone()),
603 }
604 }
605
606 fn compute_edge_spot_product(
611 parent_spot: f64,
612 component_id: &ComponentId,
613 u_addr: Option<&Address>,
614 v_addr: Option<&Address>,
615 spot_prices: Option<&SpotPrices>,
616 ) -> f64 {
617 if parent_spot == 0.0 {
618 return 0.0;
619 }
620 let (Some(u), Some(v), Some(prices)) = (u_addr, v_addr, spot_prices) else {
621 return 0.0;
622 };
623 let key = (component_id.clone(), u.clone(), v.clone());
624 match prices.get(&key) {
625 Some(&spot) if spot > 0.0 => parent_spot * spot,
626 _ => 0.0,
627 }
628 }
629
630 fn resolve_token_price(
637 v_addr: Option<&Address>,
638 token_prices: Option<&TokenGasPrices>,
639 spot_product: f64,
640 token_in_addr: Option<&Address>,
641 ) -> Option<Price> {
642 let prices = token_prices?;
643 let addr = v_addr?;
644
645 if let Some(price) = prices.get(addr) {
647 return Some(price.clone());
648 }
649
650 if spot_product > 0.0 {
652 if let Some(in_price) = token_in_addr.and_then(|a| prices.get(a)) {
653 let in_rate_f64 = in_price.numerator.to_f64()? / in_price.denominator.to_f64()?;
654 let estimated_rate = in_rate_f64 * spot_product;
655 let denom = BigUint::from(10u64).pow(18);
656 let numer_f64 = estimated_rate * 1e18;
657 if numer_f64.is_finite() && numer_f64 > 0.0 {
658 return Some(Price {
659 numerator: BigUint::from(numer_f64 as u128),
660 denominator: denom,
661 });
662 }
663 }
664 }
665
666 None
667 }
668
669 pub(crate) fn path_has_conflict(
672 from: NodeIndex,
673 target_node: NodeIndex,
674 target_pool: &ComponentId,
675 predecessor: &[Option<(NodeIndex, ComponentId)>],
676 ) -> bool {
677 let mut current = from;
678 loop {
679 if current == target_node {
680 return true;
681 }
682 match &predecessor[current.index()] {
683 Some((prev, cid)) => {
684 if cid == target_pool {
685 return true;
686 }
687 current = *prev;
688 }
689 None => return false,
690 }
691 }
692 }
693
694 pub(crate) fn reconstruct_path(
697 token_out: NodeIndex,
698 token_in: NodeIndex,
699 predecessor: &[Option<(NodeIndex, ComponentId)>],
700 ) -> Result<Vec<(NodeIndex, NodeIndex, ComponentId)>, AlgorithmError> {
701 let mut path = Vec::new();
702 let mut current = token_out;
703 let mut visited = HashSet::new();
704
705 while current != token_in {
706 if !visited.insert(current) {
707 return Err(AlgorithmError::Other("cycle in predecessor chain".to_string()));
708 }
709
710 let idx = current.index();
711 match &predecessor
712 .get(idx)
713 .and_then(|p| p.as_ref())
714 {
715 Some((prev_node, component_id)) => {
716 path.push((*prev_node, current, component_id.clone()));
717 current = *prev_node;
718 }
719 None => {
720 return Err(AlgorithmError::Other(format!(
721 "broken predecessor chain at node {idx}"
722 )));
723 }
724 }
725 }
726
727 path.reverse();
728 Ok(path)
729 }
730
731 pub(crate) fn get_subgraph(
736 graph: &StableDiGraph<()>,
737 token_in_node: NodeIndex,
738 max_hops: usize,
739 order: &Order,
740 ) -> Result<Subgraph, AlgorithmError> {
741 let mut adj: HashMap<NodeIndex, Vec<(NodeIndex, ComponentId)>> = HashMap::new();
742 let mut token_nodes: HashSet<NodeIndex> = HashSet::new();
743 let mut component_ids: HashSet<ComponentId> = HashSet::new();
744 let mut visited_nodes = HashSet::new();
745 let mut queued_nodes = VecDeque::new();
746
747 visited_nodes.insert(token_in_node);
748 token_nodes.insert(token_in_node);
749 queued_nodes.push_back((token_in_node, 0usize));
750
751 while let Some((node, depth)) = queued_nodes.pop_front() {
752 if depth >= max_hops {
753 continue;
754 }
755 for edge in graph.edges(node) {
756 let target = edge.target();
757 let cid = edge.weight().component_id.clone();
758
759 adj.entry(node)
760 .or_default()
761 .push((target, cid.clone()));
762 component_ids.insert(cid);
763 token_nodes.insert(target);
764
765 if visited_nodes.insert(target) {
766 queued_nodes.push_back((target, depth + 1));
767 }
768 }
769 }
770
771 if adj.is_empty() {
772 return Err(AlgorithmError::NoPath {
773 from: order.token_in().clone(),
774 to: order.token_out().clone(),
775 reason: NoPathReason::NoGraphPath,
776 });
777 }
778
779 Ok((adj, token_nodes, component_ids))
780 }
781
782 #[allow(clippy::too_many_arguments)]
788 fn compute_net_amount_out(
789 amount_out: &BigUint,
790 route: &Route,
791 gas_price: &BigUint,
792 token_prices: Option<&TokenGasPrices>,
793 spot_product: &[f64],
794 node_address: &HashMap<NodeIndex, Address>,
795 token_in_node: NodeIndex,
796 ) -> Result<BigInt, AlgorithmError> {
797 let last_swap = route.swaps().last().ok_or_else(|| {
798 AlgorithmError::Other("compute_net_amount_out called with empty route".to_string())
799 })?;
800
801 let total_gas = route.total_gas();
802
803 if gas_price.is_zero() {
804 warn!("missing gas price, returning gross amount_out");
805 return Ok(BigInt::from(amount_out.clone()));
806 }
807
808 let gas_cost_wei = &total_gas * gas_price;
809
810 let out_addr = last_swap.token_out();
812 let out_node_spot = node_address
813 .iter()
814 .find(|(_, addr)| *addr == out_addr)
815 .and_then(|(node, _)| spot_product.get(node.index()).copied())
816 .unwrap_or(0.0);
817
818 let output_price = Self::resolve_token_price(
819 Some(out_addr),
820 token_prices,
821 out_node_spot,
822 node_address.get(&token_in_node),
823 );
824
825 Ok(match output_price {
826 Some(price) if !price.denominator.is_zero() => {
827 let gas_cost = &gas_cost_wei * &price.numerator / &price.denominator;
828 BigInt::from(amount_out.clone()) - BigInt::from(gas_cost)
829 }
830 _ => {
831 warn!("no gas price for output token, returning gross amount_out");
832 BigInt::from(amount_out.clone())
833 }
834 })
835 }
836}
837
838impl Algorithm for BellmanFordAlgorithm {
839 type GraphType = StableDiGraph<()>;
840 type GraphManager = PetgraphStableDiGraphManager<()>;
841
842 fn name(&self) -> &str {
843 "bellman_ford"
844 }
845
846 #[instrument(level = "debug", skip_all, fields(order_id = %order.id()))]
847 async fn find_best_route(
848 &self,
849 graph: &Self::GraphType,
850 market: MarketData,
851 label: Option<StateLabel>,
852 derived: Option<SharedDerivedDataRef>,
853 order: &Order,
854 ) -> Result<RouteResult, AlgorithmError> {
855 let ctx = self
856 .build_context(graph, market, label, derived, order)
857 .await?;
858 self.find_single_route(&ctx, order, FindRouteOptions::default())
859 }
860
861 fn computation_requirements(&self) -> ComputationRequirements {
862 ComputationRequirements::none()
866 .allow_stale("token_prices")
867 .expect("token_prices requirement conflicts (bug)")
868 .allow_stale("spot_prices")
869 .expect("spot_prices requirement conflicts (bug)")
870 }
871
872 fn timeout(&self) -> Duration {
873 self.timeout
874 }
875}
876
877#[cfg(test)]
878mod tests {
879 use std::sync::Arc;
880
881 use num_bigint::BigInt;
882 use tokio::sync::RwLock;
883 use tycho_simulation::{
884 tycho_common::{models::Address, simulation::protocol_sim::ProtocolSim},
885 tycho_ethereum::gas::{BlockGasPrice, GasPrice},
886 };
887
888 use super::*;
889 use crate::{
890 algorithm::test_utils::{component, order, token, MockProtocolSim},
891 derived::{types::TokenGasPrices, DerivedData},
892 feed::market_data::{MarketData, MarketState},
893 graph::GraphManager,
894 types::quote::OrderSide,
895 };
896
897 fn setup_market_bf(
901 pools: Vec<(&str, &Token, &Token, MockProtocolSim)>,
902 ) -> (MarketData, PetgraphStableDiGraphManager<()>) {
903 let mut market = MarketState::new();
904
905 market.update_gas_price(BlockGasPrice {
906 block_number: 1,
907 block_hash: Default::default(),
908 block_timestamp: 0,
909 pricing: GasPrice::Legacy { gas_price: BigUint::from(100u64) },
910 });
911 market.update_last_updated(crate::types::BlockInfo::new(1, "0x00".into(), 0));
912
913 for (pool_id, token_in, token_out, state) in pools {
914 let tokens = vec![token_in.clone(), token_out.clone()];
915 let comp = component(pool_id, &tokens);
916 market.upsert_components(std::iter::once(comp));
917 market.update_states([(pool_id.to_string(), Box::new(state) as Box<dyn ProtocolSim>)]);
918 market.upsert_tokens(tokens);
919 }
920
921 let mut graph_manager = PetgraphStableDiGraphManager::default();
922 graph_manager.initialize_graph(&market.component_topology());
923
924 (MarketData::new(Arc::new(RwLock::new(market))), graph_manager)
925 }
926
927 fn setup_derived_with_token_prices(
928 token_addresses: &[Address],
929 ) -> crate::derived::SharedDerivedDataRef {
930 use tycho_simulation::tycho_core::simulation::protocol_sim::Price;
931
932 let mut token_prices: TokenGasPrices = HashMap::new();
933 for address in token_addresses {
934 token_prices.insert(
935 address.clone(),
936 Price { numerator: BigUint::from(1u64), denominator: BigUint::from(1u64) },
937 );
938 }
939
940 let mut derived_data = DerivedData::new();
941 derived_data.set_token_prices(token_prices, vec![], 1, true);
942 Arc::new(RwLock::new(derived_data))
943 }
944
945 fn bf_algorithm(max_hops: usize, timeout_ms: u64) -> BellmanFordAlgorithm {
946 BellmanFordAlgorithm::with_config(
947 AlgorithmConfig::new(1, max_hops, Duration::from_millis(timeout_ms), None).unwrap(),
948 )
949 }
950
951 #[tokio::test]
954 async fn test_linear_path_found() {
955 let token_a = token(0x01, "A");
956 let token_b = token(0x02, "B");
957 let token_c = token(0x03, "C");
958 let token_d = token(0x04, "D");
959
960 let (market, manager) = setup_market_bf(vec![
961 ("pool_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
962 ("pool_bc", &token_b, &token_c, MockProtocolSim::new(3.0)),
963 ("pool_cd", &token_c, &token_d, MockProtocolSim::new(4.0)),
964 ]);
965
966 let algo = bf_algorithm(4, 1000);
967 let ord = order(&token_a, &token_d, 100, OrderSide::Sell);
968
969 let result = algo
970 .find_best_route(manager.graph(), market, None, None, &ord)
971 .await
972 .unwrap();
973
974 assert_eq!(result.route().swaps().len(), 3);
975 assert_eq!(result.route().swaps()[0].amount_out(), &BigUint::from(200u64));
977 assert_eq!(result.route().swaps()[1].amount_out(), &BigUint::from(600u64));
978 assert_eq!(result.route().swaps()[2].amount_out(), &BigUint::from(2400u64));
979 }
980
981 #[tokio::test]
982 async fn test_picks_better_of_two_paths() {
983 let token_a = token(0x01, "A");
985 let token_b = token(0x02, "B");
986 let token_c = token(0x03, "C");
987 let token_d = token(0x04, "D");
988
989 let (market, manager) = setup_market_bf(vec![
990 ("pool_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
991 ("pool_bd", &token_b, &token_d, MockProtocolSim::new(3.0)),
992 ("pool_ac", &token_a, &token_c, MockProtocolSim::new(4.0)),
993 ("pool_cd", &token_c, &token_d, MockProtocolSim::new(1.0)),
994 ]);
995
996 let algo = bf_algorithm(3, 1000);
997 let ord = order(&token_a, &token_d, 100, OrderSide::Sell);
998
999 let result = algo
1000 .find_best_route(manager.graph(), market, None, None, &ord)
1001 .await
1002 .unwrap();
1003
1004 assert_eq!(result.route().swaps().len(), 2);
1006 assert_eq!(result.route().swaps()[0].component_id(), "pool_ab");
1007 assert_eq!(result.route().swaps()[1].component_id(), "pool_bd");
1008 assert_eq!(result.route().swaps()[1].amount_out(), &BigUint::from(600u64));
1009 }
1010
1011 #[tokio::test]
1012 async fn test_parallel_pools() {
1013 let token_a = token(0x01, "A");
1015 let token_b = token(0x02, "B");
1016
1017 let (market, manager) = setup_market_bf(vec![
1018 ("pool1", &token_a, &token_b, MockProtocolSim::new(2.0)),
1019 ("pool2", &token_a, &token_b, MockProtocolSim::new(5.0)),
1020 ]);
1021
1022 let algo = bf_algorithm(2, 1000);
1023 let ord = order(&token_a, &token_b, 100, OrderSide::Sell);
1024
1025 let result = algo
1026 .find_best_route(manager.graph(), market, None, None, &ord)
1027 .await
1028 .unwrap();
1029
1030 assert_eq!(result.route().swaps().len(), 1);
1031 assert_eq!(result.route().swaps()[0].component_id(), "pool2");
1032 assert_eq!(result.route().swaps()[0].amount_out(), &BigUint::from(500u64));
1033 }
1034
1035 #[tokio::test]
1036 async fn test_no_path_returns_error() {
1037 let token_a = token(0x01, "A");
1038 let token_b = token(0x02, "B");
1039 let token_c = token(0x03, "C");
1040
1041 let (market, manager) =
1043 setup_market_bf(vec![("pool_ab", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1044
1045 {
1047 let mut m = market.write().await;
1048 m.upsert_tokens(vec![token_c.clone()]);
1049 }
1050
1051 let algo = bf_algorithm(3, 1000);
1052 let ord = order(&token_a, &token_c, 100, OrderSide::Sell);
1053
1054 let result = algo
1055 .find_best_route(manager.graph(), market, None, None, &ord)
1056 .await;
1057 assert!(matches!(result, Err(AlgorithmError::NoPath { .. })));
1058 }
1059
1060 #[tokio::test]
1061 async fn test_source_not_in_graph() {
1062 let token_a = token(0x01, "A");
1063 let token_b = token(0x02, "B");
1064 let token_x = token(0x99, "X");
1065
1066 let (market, manager) =
1067 setup_market_bf(vec![("pool_ab", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1068
1069 let algo = bf_algorithm(3, 1000);
1070 let ord = order(&token_x, &token_b, 100, OrderSide::Sell);
1071
1072 let result = algo
1073 .find_best_route(manager.graph(), market, None, None, &ord)
1074 .await;
1075 assert!(matches!(
1076 result,
1077 Err(AlgorithmError::NoPath { reason: NoPathReason::SourceTokenNotInGraph, .. })
1078 ));
1079 }
1080
1081 #[tokio::test]
1082 async fn test_amount_too_small_when_reachable_but_zero_output() {
1083 let token_a = token(0x01, "A");
1084 let token_b = token(0x02, "B");
1085 let (market, manager) =
1087 setup_market_bf(vec![("pool_ab", &token_a, &token_b, MockProtocolSim::new(0.5))]);
1088 let algo = bf_algorithm(3, 1000);
1089 let ord = order(&token_a, &token_b, 1, OrderSide::Sell);
1090
1091 let result = algo
1092 .find_best_route(manager.graph(), market, None, None, &ord)
1093 .await;
1094 assert!(matches!(
1095 result,
1096 Err(AlgorithmError::NoPath { reason: NoPathReason::AmountTooSmall, .. })
1097 ));
1098 }
1099
1100 #[tokio::test]
1101 async fn test_amount_too_small_when_dust_occurs_mid_route() {
1102 let token_a = token(0x01, "A");
1105 let token_b = token(0x02, "B");
1106 let token_c = token(0x03, "C");
1107 let (market, manager) = setup_market_bf(vec![
1108 ("pool_ab", &token_a, &token_b, MockProtocolSim::new(0.5)),
1109 ("pool_bc", &token_b, &token_c, MockProtocolSim::new(2.0)),
1110 ]);
1111 let algo = bf_algorithm(3, 1000);
1112 let ord = order(&token_a, &token_c, 1, OrderSide::Sell);
1113
1114 let result = algo
1115 .find_best_route(manager.graph(), market, None, None, &ord)
1116 .await;
1117 assert!(matches!(
1118 result,
1119 Err(AlgorithmError::NoPath { reason: NoPathReason::AmountTooSmall, .. })
1120 ));
1121 }
1122
1123 #[tokio::test]
1124 async fn test_no_graph_path_when_amount_too_large_for_liquidity() {
1125 let token_a = token(0x01, "A");
1128 let token_b = token(0x02, "B");
1129 let (market, manager) = setup_market_bf(vec![(
1130 "pool_ab",
1131 &token_a,
1132 &token_b,
1133 MockProtocolSim::new(2.0).with_liquidity(500),
1134 )]);
1135 let algo = bf_algorithm(2, 1000);
1136 let ord = order(&token_a, &token_b, 1000, OrderSide::Sell);
1137
1138 let result = algo
1139 .find_best_route(manager.graph(), market, None, None, &ord)
1140 .await;
1141 assert!(matches!(
1142 result,
1143 Err(AlgorithmError::NoPath { reason: NoPathReason::NoGraphPath, .. })
1144 ));
1145 }
1146
1147 #[tokio::test]
1148 async fn test_no_graph_path_when_unreachable_within_hops() {
1149 let token_a = token(0x01, "A");
1151 let token_b = token(0x02, "B");
1152 let token_c = token(0x03, "C");
1153 let (market, manager) = setup_market_bf(vec![
1154 ("pool_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
1155 ("pool_bc", &token_b, &token_c, MockProtocolSim::new(2.0)),
1156 ]);
1157 let algo = bf_algorithm(1, 1000);
1158 let ord = order(&token_a, &token_c, 100, OrderSide::Sell);
1159
1160 let result = algo
1161 .find_best_route(manager.graph(), market, None, None, &ord)
1162 .await;
1163 assert!(matches!(
1164 result,
1165 Err(AlgorithmError::NoPath { reason: NoPathReason::NoGraphPath, .. })
1166 ));
1167 }
1168
1169 #[tokio::test]
1170 async fn test_no_graph_path_when_connector_tokens_exclude_intermediate() {
1171 let token_a = token(0x01, "A");
1174 let token_b = token(0x02, "B");
1175 let token_c = token(0x03, "C");
1176
1177 let (market, manager) = setup_market_bf(vec![
1178 ("pool_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
1179 ("pool_bc", &token_b, &token_c, MockProtocolSim::new(2.0)),
1180 ]);
1181
1182 let algo = BellmanFordAlgorithm::with_config(
1183 AlgorithmConfig::new(1, 3, Duration::from_millis(1000), None)
1184 .unwrap()
1185 .with_connector_tokens(HashSet::new()),
1186 );
1187 let ord = order(&token_a, &token_c, 100, OrderSide::Sell);
1188
1189 let result = algo
1190 .find_best_route(manager.graph(), market, None, None, &ord)
1191 .await;
1192 assert!(matches!(
1193 result,
1194 Err(AlgorithmError::NoPath { reason: NoPathReason::NoGraphPath, .. })
1195 ));
1196 }
1197
1198 #[tokio::test]
1199 async fn test_destination_not_in_graph() {
1200 let token_a = token(0x01, "A");
1201 let token_b = token(0x02, "B");
1202 let token_x = token(0x99, "X");
1203
1204 let (market, manager) =
1205 setup_market_bf(vec![("pool_ab", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1206
1207 let algo = bf_algorithm(3, 1000);
1208 let ord = order(&token_a, &token_x, 100, OrderSide::Sell);
1209
1210 let result = algo
1211 .find_best_route(manager.graph(), market, None, None, &ord)
1212 .await;
1213 assert!(matches!(
1214 result,
1215 Err(AlgorithmError::NoPath { reason: NoPathReason::DestinationTokenNotInGraph, .. })
1216 ));
1217 }
1218
1219 #[tokio::test]
1220 async fn test_respects_max_hops() {
1221 let token_a = token(0x01, "A");
1223 let token_b = token(0x02, "B");
1224 let token_c = token(0x03, "C");
1225 let token_d = token(0x04, "D");
1226
1227 let (market, manager) = setup_market_bf(vec![
1228 ("pool_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
1229 ("pool_bc", &token_b, &token_c, MockProtocolSim::new(3.0)),
1230 ("pool_cd", &token_c, &token_d, MockProtocolSim::new(4.0)),
1231 ]);
1232
1233 let algo = bf_algorithm(2, 1000);
1234 let ord = order(&token_a, &token_d, 100, OrderSide::Sell);
1235
1236 let result = algo
1237 .find_best_route(manager.graph(), market, None, None, &ord)
1238 .await;
1239 assert!(
1240 matches!(result, Err(AlgorithmError::NoPath { .. })),
1241 "Should not find 3-hop path with max_hops=2"
1242 );
1243 }
1244
1245 #[tokio::test]
1246 async fn test_source_token_revisit_blocked() {
1247 let token_a = token(0x01, "A");
1250 let token_b = token(0x02, "B");
1251 let token_c = token(0x03, "C");
1252
1253 let (market, manager) = setup_market_bf(vec![
1254 ("pool_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
1255 ("pool_bc", &token_b, &token_c, MockProtocolSim::new(3.0)),
1256 ]);
1257
1258 let algo = bf_algorithm(4, 1000);
1259 let ord = order(&token_a, &token_c, 100, OrderSide::Sell);
1260
1261 let result = algo
1262 .find_best_route(manager.graph(), market, None, None, &ord)
1263 .await
1264 .unwrap();
1265
1266 assert_eq!(result.route().swaps().len(), 2);
1268 assert_eq!(result.route().swaps()[0].component_id(), "pool_ab");
1269 assert_eq!(result.route().swaps()[1].component_id(), "pool_bc");
1270 assert_eq!(result.route().swaps()[1].amount_out(), &BigUint::from(600u64));
1271 }
1272
1273 #[tokio::test]
1274 async fn test_hub_token_revisit_blocked() {
1275 let token_a = token(0x01, "A");
1278 let token_c = token(0x02, "C");
1279 let token_b = token(0x03, "B");
1280 let token_d = token(0x04, "D");
1281
1282 let (market, manager) = setup_market_bf(vec![
1283 ("pool_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
1284 ("pool_bc", &token_b, &token_c, MockProtocolSim::new(3.0)),
1285 ("pool_cb", &token_c, &token_b, MockProtocolSim::new(100.0)),
1286 ("pool_bd", &token_b, &token_d, MockProtocolSim::new(2.0)),
1287 ]);
1288
1289 let algo = bf_algorithm(4, 1000);
1290 let ord = order(&token_a, &token_d, 100, OrderSide::Sell);
1291
1292 let result = algo
1293 .find_best_route(manager.graph(), market, None, None, &ord)
1294 .await
1295 .unwrap();
1296
1297 assert_eq!(result.route().swaps().len(), 2, "should use direct 2-hop path");
1300 assert_eq!(result.route().swaps()[0].component_id(), "pool_ab");
1301 assert_eq!(result.route().swaps()[1].component_id(), "pool_bd");
1302 assert_eq!(result.route().swaps()[1].amount_out(), &BigUint::from(400u64));
1303 }
1304
1305 #[tokio::test]
1306 async fn test_route_amounts_are_sequential() {
1307 let token_a = token(0x01, "A");
1309 let token_b = token(0x02, "B");
1310 let token_c = token(0x03, "C");
1311
1312 let (market, manager) = setup_market_bf(vec![
1313 ("pool_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
1314 ("pool_bc", &token_b, &token_c, MockProtocolSim::new(3.0)),
1315 ]);
1316
1317 let algo = bf_algorithm(3, 1000);
1318 let ord = order(&token_a, &token_c, 100, OrderSide::Sell);
1319
1320 let result = algo
1321 .find_best_route(manager.graph(), market, None, None, &ord)
1322 .await
1323 .unwrap();
1324
1325 assert_eq!(result.route().swaps().len(), 2);
1326 assert_eq!(result.route().swaps()[1].amount_in(), result.route().swaps()[0].amount_out());
1328 }
1329
1330 #[tokio::test]
1331 async fn test_gas_deduction() {
1332 let token_a = token(0x01, "A");
1333 let token_b = token(0x02, "B");
1334
1335 let (market, manager) = setup_market_bf(vec![(
1336 "pool1",
1337 &token_a,
1338 &token_b,
1339 MockProtocolSim::new(2.0).with_gas(10),
1340 )]);
1341
1342 let algo = bf_algorithm(2, 1000);
1343 let ord = order(&token_a, &token_b, 1000, OrderSide::Sell);
1344
1345 let derived = setup_derived_with_token_prices(std::slice::from_ref(&token_b.address));
1346
1347 let result = algo
1348 .find_best_route(manager.graph(), market, None, Some(derived), &ord)
1349 .await
1350 .unwrap();
1351
1352 assert_eq!(result.route().swaps()[0].amount_out(), &BigUint::from(2000u64));
1356 assert_eq!(result.net_amount_out(), &BigInt::from(1000));
1357 }
1358
1359 #[tokio::test]
1360 async fn test_timeout_respected() {
1361 let token_a = token(0x01, "A");
1362 let token_b = token(0x02, "B");
1363 let token_c = token(0x03, "C");
1364
1365 let (market, manager) = setup_market_bf(vec![
1366 ("pool_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
1367 ("pool_bc", &token_b, &token_c, MockProtocolSim::new(3.0)),
1368 ]);
1369
1370 let algo = bf_algorithm(3, 0);
1372 let ord = order(&token_a, &token_c, 100, OrderSide::Sell);
1373
1374 let result = algo
1375 .find_best_route(manager.graph(), market, None, None, &ord)
1376 .await;
1377
1378 match result {
1383 Ok(r) => {
1384 assert!(!r.route().swaps().is_empty());
1385 }
1386 Err(AlgorithmError::Timeout { .. }) | Err(AlgorithmError::NoPath { .. }) => {
1387 }
1389 Err(e) => panic!("Unexpected error: {:?}", e),
1390 }
1391 }
1392
1393 #[tokio::test]
1396 async fn test_with_fees() {
1397 let token_a = token(0x01, "A");
1398 let token_b = token(0x02, "B");
1399
1400 let (market, manager) = setup_market_bf(vec![(
1402 "pool1",
1403 &token_a,
1404 &token_b,
1405 MockProtocolSim::new(2.0).with_fee(0.1),
1406 )]);
1407
1408 let algo = bf_algorithm(2, 1000);
1409 let ord = order(&token_a, &token_b, 1000, OrderSide::Sell);
1410
1411 let result = algo
1412 .find_best_route(manager.graph(), market, None, None, &ord)
1413 .await
1414 .unwrap();
1415
1416 assert_eq!(result.route().swaps()[0].amount_out(), &BigUint::from(1800u64));
1418 }
1419
1420 #[tokio::test]
1421 async fn test_large_trade_slippage() {
1422 let token_a = token(0x01, "A");
1423 let token_b = token(0x02, "B");
1424
1425 let (market, manager) = setup_market_bf(vec![(
1427 "pool1",
1428 &token_a,
1429 &token_b,
1430 MockProtocolSim::new(2.0).with_liquidity(500),
1431 )]);
1432
1433 let algo = bf_algorithm(2, 1000);
1434 let ord = order(&token_a, &token_b, 1000, OrderSide::Sell);
1435
1436 let result = algo
1438 .find_best_route(manager.graph(), market, None, None, &ord)
1439 .await;
1440 assert!(
1441 matches!(result, Err(AlgorithmError::NoPath { .. })),
1442 "Should fail when trade exceeds pool liquidity"
1443 );
1444 }
1445
1446 #[tokio::test]
1447 async fn test_disconnected_tokens_return_no_path() {
1448 let token_a = token(0x01, "A");
1450 let token_b = token(0x02, "B");
1451 let token_d = token(0x04, "D");
1452 let token_e = token(0x05, "E");
1453
1454 let (market, manager) = setup_market_bf(vec![
1455 ("pool_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
1456 ("pool_de", &token_d, &token_e, MockProtocolSim::new(4.0)),
1457 ]);
1458
1459 let algo = bf_algorithm(3, 1000);
1460 let ord = order(&token_a, &token_e, 100, OrderSide::Sell);
1461
1462 let result = algo
1463 .find_best_route(manager.graph(), market, None, None, &ord)
1464 .await;
1465 assert!(
1466 matches!(result, Err(AlgorithmError::NoPath { .. })),
1467 "should not find path to disconnected component"
1468 );
1469 }
1470
1471 #[tokio::test]
1472 async fn test_spfa_skips_failed_simulations() {
1473 let token_a = token(0x01, "A");
1475 let token_b = token(0x02, "B");
1476 let token_c = token(0x03, "C");
1477
1478 let (market, manager) = setup_market_bf(vec![
1479 ("pool_ab_bad", &token_a, &token_b, MockProtocolSim::new(2.0).with_liquidity(0)),
1481 ("pool_ac", &token_a, &token_c, MockProtocolSim::new(2.0)),
1483 ("pool_cb", &token_c, &token_b, MockProtocolSim::new(3.0)),
1484 ]);
1485
1486 let algo = bf_algorithm(3, 1000);
1487 let ord = order(&token_a, &token_b, 100, OrderSide::Sell);
1488
1489 let result = algo
1490 .find_best_route(manager.graph(), market, None, None, &ord)
1491 .await;
1492
1493 match result {
1497 Ok(r) => {
1498 assert!(!r.route().swaps().is_empty());
1500 }
1501 Err(AlgorithmError::NoPath { .. }) => {
1502 }
1505 Err(e) => panic!("Unexpected error: {:?}", e),
1506 }
1507 }
1508
1509 #[tokio::test]
1510 async fn test_resimulation_produces_correct_amounts() {
1511 let token_a = token(0x01, "A");
1513 let token_b = token(0x02, "B");
1514 let token_c = token(0x03, "C");
1515
1516 let (market, manager) = setup_market_bf(vec![
1517 ("pool_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
1518 ("pool_bc", &token_b, &token_c, MockProtocolSim::new(3.0)),
1519 ]);
1520
1521 let algo = bf_algorithm(3, 1000);
1522 let ord = order(&token_a, &token_c, 100, OrderSide::Sell);
1523
1524 let result = algo
1525 .find_best_route(manager.graph(), market, None, None, &ord)
1526 .await
1527 .unwrap();
1528
1529 assert_eq!(result.route().swaps()[0].amount_in(), &BigUint::from(100u64));
1532 assert_eq!(result.route().swaps()[0].amount_out(), &BigUint::from(200u64));
1533 assert_eq!(result.route().swaps()[1].amount_in(), &BigUint::from(200u64));
1534 assert_eq!(result.route().swaps()[1].amount_out(), &BigUint::from(600u64));
1535 }
1536
1537 #[test]
1540 fn algorithm_name() {
1541 let algo = bf_algorithm(4, 200);
1542 assert_eq!(algo.name(), "bellman_ford");
1543 }
1544
1545 #[test]
1546 fn algorithm_timeout() {
1547 let algo = bf_algorithm(4, 200);
1548 assert_eq!(algo.timeout(), Duration::from_millis(200));
1549 }
1550
1551 #[tokio::test]
1554 async fn test_gas_aware_relaxation_picks_cheaper_path() {
1555 let token_a = token(0x01, "A");
1570 let token_b = token(0x02, "B");
1571 let token_c = token(0x03, "C");
1572 let token_d = token(0x04, "D");
1573
1574 let high_gas: u64 = 100_000_000;
1575 let low_gas: u64 = 100;
1576
1577 let (market, manager) = setup_market_bf(vec![
1578 ("pool_ab", &token_a, &token_b, MockProtocolSim::new(3.0).with_gas(high_gas)),
1579 ("pool_bd", &token_b, &token_d, MockProtocolSim::new(2.0).with_gas(high_gas)),
1580 ("pool_ac", &token_a, &token_c, MockProtocolSim::new(2.0).with_gas(low_gas)),
1581 ("pool_cd", &token_c, &token_d, MockProtocolSim::new(2.0).with_gas(low_gas)),
1582 ]);
1583
1584 let algo = bf_algorithm(3, 1000);
1585 let ord = order(&token_a, &token_d, 1_000_000_000, OrderSide::Sell);
1586
1587 let derived = setup_derived_with_token_prices(&[
1589 token_a.address.clone(),
1590 token_b.address.clone(),
1591 token_c.address.clone(),
1592 token_d.address.clone(),
1593 ]);
1594
1595 let result = algo
1596 .find_best_route(manager.graph(), market, None, Some(derived), &ord)
1597 .await
1598 .unwrap();
1599
1600 assert_eq!(result.route().swaps().len(), 2);
1602 assert_eq!(result.route().swaps()[0].component_id(), "pool_ac");
1603 assert_eq!(result.route().swaps()[1].component_id(), "pool_cd");
1604 }
1605
1606 #[tokio::test]
1607 async fn test_gas_aware_falls_back_to_gross_without_derived() {
1608 let token_a = token(0x01, "A");
1611 let token_b = token(0x02, "B");
1612 let token_c = token(0x03, "C");
1613 let token_d = token(0x04, "D");
1614
1615 let high_gas: u64 = 100_000_000;
1616 let low_gas: u64 = 100;
1617
1618 let (market, manager) = setup_market_bf(vec![
1619 ("pool_ab", &token_a, &token_b, MockProtocolSim::new(3.0).with_gas(high_gas)),
1620 ("pool_bd", &token_b, &token_d, MockProtocolSim::new(2.0).with_gas(high_gas)),
1621 ("pool_ac", &token_a, &token_c, MockProtocolSim::new(2.0).with_gas(low_gas)),
1622 ("pool_cd", &token_c, &token_d, MockProtocolSim::new(2.0).with_gas(low_gas)),
1623 ]);
1624
1625 let algo = bf_algorithm(3, 1000);
1626 let ord = order(&token_a, &token_d, 1_000_000_000, OrderSide::Sell);
1627
1628 let result = algo
1630 .find_best_route(manager.graph(), market, None, None, &ord)
1631 .await
1632 .unwrap();
1633
1634 assert_eq!(result.route().swaps().len(), 2);
1636 assert_eq!(result.route().swaps()[0].component_id(), "pool_ab");
1637 assert_eq!(result.route().swaps()[1].component_id(), "pool_bd");
1638 }
1639
1640 #[tokio::test]
1641 async fn test_amount_too_small_when_net_uneconomic_after_gas() {
1642 let token_a = token(0x01, "A");
1645 let token_b = token(0x02, "B");
1646
1647 let (market, manager) = setup_market_bf(vec![(
1648 "pool_ab",
1649 &token_a,
1650 &token_b,
1651 MockProtocolSim::new(2.0).with_gas(10),
1652 )]);
1653
1654 let algo = bf_algorithm(2, 1000);
1655 let ord = order(&token_a, &token_b, 1, OrderSide::Sell);
1656 let derived =
1657 setup_derived_with_token_prices(&[token_a.address.clone(), token_b.address.clone()]);
1658
1659 let result = algo
1660 .find_best_route(manager.graph(), market, None, Some(derived), &ord)
1661 .await;
1662 assert!(matches!(
1663 result,
1664 Err(AlgorithmError::NoPath { reason: NoPathReason::AmountTooSmall, .. })
1665 ));
1666 }
1667
1668 #[tokio::test]
1669 async fn test_no_graph_path_when_output_uneconomic_but_input_economic() {
1670 let token_a = token(0x01, "A");
1674 let token_b = token(0x02, "B");
1675
1676 let (market, manager) = setup_market_bf(vec![(
1677 "pool_ab",
1678 &token_a,
1679 &token_b,
1680 MockProtocolSim::new(0.05).with_gas(10),
1681 )]);
1682
1683 let algo = bf_algorithm(1, 1000);
1684 let ord = order(&token_a, &token_b, 10_000, OrderSide::Sell);
1685 let derived =
1686 setup_derived_with_token_prices(&[token_a.address.clone(), token_b.address.clone()]);
1687
1688 let result = algo
1689 .find_best_route(manager.graph(), market, None, Some(derived), &ord)
1690 .await;
1691 assert!(matches!(
1692 result,
1693 Err(AlgorithmError::NoPath { reason: NoPathReason::NoGraphPath, .. })
1694 ));
1695 }
1696
1697 fn bf_algorithm_with_connectors(
1701 max_hops: usize,
1702 timeout_ms: u64,
1703 connector_tokens: HashSet<Address>,
1704 ) -> BellmanFordAlgorithm {
1705 BellmanFordAlgorithm::with_config(
1706 AlgorithmConfig::new(1, max_hops, Duration::from_millis(timeout_ms), None)
1707 .unwrap()
1708 .with_connector_tokens(connector_tokens),
1709 )
1710 }
1711
1712 #[tokio::test]
1713 async fn test_connector_tokens_blocks_disallowed_intermediate() {
1714 let token_a = token(0x01, "A");
1721 let token_b = token(0x02, "B");
1722 let token_c = token(0x03, "C");
1723 let token_d = token(0x04, "D");
1724
1725 let (market, manager) = setup_market_bf(vec![
1726 ("pool_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
1727 ("pool_bd", &token_b, &token_d, MockProtocolSim::new(2.0)),
1728 ("pool_ac", &token_a, &token_c, MockProtocolSim::new(3.0)),
1729 ("pool_cd", &token_c, &token_d, MockProtocolSim::new(3.0)),
1730 ]);
1731
1732 let connectors: HashSet<Address> = [token_c.address.clone()].into();
1733 let algo = bf_algorithm_with_connectors(3, 1000, connectors);
1734 let ord = order(&token_a, &token_d, 100, OrderSide::Sell);
1735
1736 let result = algo
1737 .find_best_route(manager.graph(), market, None, None, &ord)
1738 .await
1739 .unwrap();
1740
1741 assert_eq!(result.route().swaps().len(), 2);
1743 assert_eq!(result.route().swaps()[0].component_id(), "pool_ac");
1744 assert_eq!(result.route().swaps()[1].component_id(), "pool_cd");
1745 }
1746
1747 #[tokio::test]
1748 async fn test_connector_tokens_allows_endpoints_even_if_not_listed() {
1749 let token_a = token(0x01, "A");
1751 let token_b = token(0x02, "B");
1752
1753 let (market, manager) =
1754 setup_market_bf(vec![("pool_ab", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1755
1756 let algo = bf_algorithm_with_connectors(1, 1000, HashSet::new());
1758 let ord = order(&token_a, &token_b, 100, OrderSide::Sell);
1759
1760 let result = algo
1761 .find_best_route(manager.graph(), market, None, None, &ord)
1762 .await
1763 .unwrap();
1764
1765 assert_eq!(result.route().swaps().len(), 1);
1766 assert_eq!(result.route().swaps()[0].amount_out(), &BigUint::from(200u64));
1767 }
1768
1769 #[tokio::test]
1770 async fn test_connector_tokens_none_is_unrestricted() {
1771 let token_a = token(0x01, "A");
1773 let token_b = token(0x02, "B");
1774 let token_c = token(0x03, "C");
1775 let token_d = token(0x04, "D");
1776
1777 let (market, manager) = setup_market_bf(vec![
1778 ("pool_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
1779 ("pool_bd", &token_b, &token_d, MockProtocolSim::new(3.0)),
1780 ("pool_ac", &token_a, &token_c, MockProtocolSim::new(1.0)),
1781 ("pool_cd", &token_c, &token_d, MockProtocolSim::new(1.0)),
1782 ]);
1783
1784 let algo = bf_algorithm(3, 1000);
1785 let ord = order(&token_a, &token_d, 100, OrderSide::Sell);
1786
1787 let result = algo
1788 .find_best_route(manager.graph(), market, None, None, &ord)
1789 .await
1790 .unwrap();
1791
1792 assert_eq!(result.route().swaps()[0].component_id(), "pool_ab");
1794 assert_eq!(result.route().swaps()[1].component_id(), "pool_bd");
1795 assert_eq!(result.route().swaps()[1].amount_out(), &BigUint::from(600u64));
1796 }
1797
1798 #[test]
1799 fn test_path_has_conflict_detects_node_and_pool() {
1800 let mut pred: Vec<Option<(NodeIndex, ComponentId)>> = vec![None; 4];
1802 pred[1] = Some((NodeIndex::new(0), "pool_a".into()));
1803 pred[2] = Some((NodeIndex::new(1), "pool_b".into()));
1804
1805 assert!(BellmanFordAlgorithm::path_has_conflict(
1807 NodeIndex::new(2),
1808 NodeIndex::new(0),
1809 &"any".into(),
1810 &pred
1811 ));
1812 assert!(!BellmanFordAlgorithm::path_has_conflict(
1813 NodeIndex::new(2),
1814 NodeIndex::new(3),
1815 &"any".into(),
1816 &pred
1817 ));
1818 assert!(BellmanFordAlgorithm::path_has_conflict(
1820 NodeIndex::new(2),
1821 NodeIndex::new(2),
1822 &"any".into(),
1823 &pred
1824 ));
1825
1826 assert!(BellmanFordAlgorithm::path_has_conflict(
1828 NodeIndex::new(2),
1829 NodeIndex::new(3),
1830 &"pool_a".into(),
1831 &pred
1832 ));
1833 assert!(BellmanFordAlgorithm::path_has_conflict(
1834 NodeIndex::new(2),
1835 NodeIndex::new(3),
1836 &"pool_b".into(),
1837 &pred
1838 ));
1839 assert!(!BellmanFordAlgorithm::path_has_conflict(
1840 NodeIndex::new(2),
1841 NodeIndex::new(3),
1842 &"pool_c".into(),
1843 &pred
1844 ));
1845 }
1846
1847 #[tokio::test]
1848 async fn test_find_single_route_with_state_overrides() {
1849 let token_a = token(0x01, "A");
1850 let token_b = token(0x02, "B");
1851
1852 let (market, manager) =
1853 setup_market_bf(vec![("pool_ab", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1854
1855 let algo = bf_algorithm(2, 1000);
1856 let ord = order(&token_a, &token_b, 1000, OrderSide::Sell);
1857
1858 let ctx = algo
1859 .build_context(manager.graph(), market, None, None, &ord)
1860 .await
1861 .unwrap();
1862
1863 let normal = algo
1865 .find_single_route(&ctx, &ord, FindRouteOptions::default())
1866 .unwrap();
1867 assert_eq!(normal.route().swaps()[0].amount_out(), &BigUint::from(2000u64));
1868
1869 let opts = FindRouteOptions {
1871 overrides: MarketOverrides::empty()
1872 .with_override("pool_ab".to_string(), Box::new(MockProtocolSim::new(1.0))),
1873 };
1874 let overridden = algo
1875 .find_single_route(&ctx, &ord, opts)
1876 .unwrap();
1877 assert_eq!(overridden.route().swaps()[0].amount_out(), &BigUint::from(1000u64));
1878
1879 assert!(
1880 overridden.route().swaps()[0].amount_out() < normal.route().swaps()[0].amount_out()
1881 );
1882 }
1883
1884 #[tokio::test]
1885 async fn test_single_find_route_options_default() {
1886 use super::super::split_primitives::MarketOverrides;
1887
1888 let token_a = token(0x01, "A");
1889 let token_b = token(0x02, "B");
1890
1891 let (market, manager) =
1892 setup_market_bf(vec![("pool_ab", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1893
1894 let algo = bf_algorithm(2, 1000);
1895 let ord = order(&token_a, &token_b, 1000, OrderSide::Sell);
1896
1897 let ctx = algo
1898 .build_context(manager.graph(), market, None, None, &ord)
1899 .await
1900 .unwrap();
1901
1902 let with_default = algo
1903 .find_single_route(&ctx, &ord, FindRouteOptions::default())
1904 .unwrap();
1905 let with_empty = algo
1906 .find_single_route(&ctx, &ord, FindRouteOptions { overrides: MarketOverrides::empty() })
1907 .unwrap();
1908
1909 assert_eq!(
1910 with_default.route().swaps()[0].amount_out(),
1911 with_empty.route().swaps()[0].amount_out()
1912 );
1913 assert_eq!(with_default.route().swaps()[0].amount_out(), &BigUint::from(2000u64));
1914 }
1915}