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