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 derived::{
45 computation::ComputationRequirements,
46 types::{SpotPrices, TokenGasPrices},
47 SharedDerivedDataRef,
48 },
49 feed::market_data::{MarketData, MarketState, StateLabel},
50 graph::{petgraph::StableDiGraph, PetgraphStableDiGraphManager},
51 types::{ComponentId, Order, Route, RouteResult, Swap},
52};
53
54type Subgraph =
56 (HashMap<NodeIndex, Vec<(NodeIndex, ComponentId)>>, HashSet<NodeIndex>, HashSet<ComponentId>);
57
58pub(crate) struct BellmanFordContext {
64 pub(crate) token_in_node: NodeIndex,
65 pub(crate) token_out_node: NodeIndex,
66 pub(crate) adj: HashMap<NodeIndex, Vec<(NodeIndex, ComponentId)>>,
67 pub(crate) token_map: HashMap<NodeIndex, Token>,
68 pub(crate) market_data: MarketState,
69 pub(crate) gas_price_wei: Option<BigUint>,
70 pub(crate) token_prices: Option<TokenGasPrices>,
71 pub(crate) spot_prices: Option<SpotPrices>,
72 pub(crate) node_address: HashMap<NodeIndex, Address>,
73 pub(crate) max_idx: usize,
74 pub(crate) scoring: RouteScoringMode,
75}
76
77pub(crate) enum RouteScoringMode {
79 GrossOutput,
81 NetOutput,
83}
84
85#[derive(Default)]
87pub(crate) struct FindRouteOptions {
88 pub(crate) overrides: MarketOverrides,
90}
91
92struct SPFAResult {
94 amount: Vec<BigUint>,
96 predecessor: Vec<Option<(NodeIndex, ComponentId)>>,
98 edge_gas: Vec<BigUint>,
100 spot_product: Vec<f64>,
102}
103
104pub struct BellmanFordAlgorithm {
110 max_hops: usize,
111 timeout: Duration,
112 gas_aware: bool,
113 connector_tokens: Option<HashSet<Address>>,
114}
115
116impl Default for BellmanFordAlgorithm {
117 fn default() -> Self {
118 Self::with_config(AlgorithmConfig::default())
119 }
120}
121
122impl BellmanFordAlgorithm {
123 pub(crate) fn with_config(config: AlgorithmConfig) -> Self {
124 Self {
125 max_hops: config.max_hops(),
126 timeout: config.timeout(),
127 gas_aware: config.gas_aware(),
128 connector_tokens: config.connector_tokens().cloned(),
129 }
130 }
131
132 pub(crate) async fn build_context(
139 &self,
140 graph: &StableDiGraph<()>,
141 market: MarketData,
142 label: Option<StateLabel>,
143 derived: Option<SharedDerivedDataRef>,
144 order: &Order,
145 ) -> Result<BellmanFordContext, AlgorithmError> {
146 if !order.is_sell() {
147 return Err(AlgorithmError::ExactOutNotSupported);
148 }
149
150 let (token_prices, spot_prices) = if let Some(ref d) = derived {
151 let guard = d.read().await;
152 (guard.token_prices().cloned(), guard.spot_prices().cloned())
153 } else {
154 (None, None)
155 };
156
157 let token_in_node = graph
158 .node_indices()
159 .find(|&n| &graph[n] == order.token_in())
160 .ok_or(AlgorithmError::NoPath {
161 from: order.token_in().clone(),
162 to: order.token_out().clone(),
163 reason: NoPathReason::SourceTokenNotInGraph,
164 })?;
165 let token_out_node = graph
166 .node_indices()
167 .find(|&n| &graph[n] == order.token_out())
168 .ok_or(AlgorithmError::NoPath {
169 from: order.token_in().clone(),
170 to: order.token_out().clone(),
171 reason: NoPathReason::DestinationTokenNotInGraph,
172 })?;
173
174 if token_in_node == token_out_node {
175 return Err(AlgorithmError::NoPath {
176 from: order.token_in().clone(),
177 to: order.token_out().clone(),
178 reason: NoPathReason::NoGraphPath,
179 });
180 }
181
182 let (adj, token_nodes, component_ids) =
184 Self::get_subgraph(graph, token_in_node, self.max_hops, order)?;
185
186 let market_view = match label.as_ref() {
187 Some(l) => market
188 .read_labeled(l)
189 .await
190 .map_err(|e| AlgorithmError::Other(e.to_string()))?,
191 None => market.read().await,
192 };
193 let token_map: HashMap<NodeIndex, Token> = token_nodes
194 .iter()
195 .filter_map(|&node| {
196 market_view
197 .get_token(&graph[node])
198 .cloned()
199 .map(|t| (node, t))
200 })
201 .collect();
202 let market_data = market_view.extract_subset_with_overlay(&component_ids);
203 let gas_price_wei = market_data
204 .gas_price()
205 .map(|gp| gp.effective_gas_price().clone());
206 drop(market_view);
207
208 let node_address: HashMap<NodeIndex, Address> = token_map
209 .iter()
210 .map(|(&node, token)| (node, token.address.clone()))
211 .collect();
212
213 let max_idx = graph
214 .node_indices()
215 .map(|n| n.index())
216 .max()
217 .unwrap_or(0) +
218 1;
219
220 let scoring = if self.gas_aware {
221 RouteScoringMode::NetOutput
222 } else {
223 RouteScoringMode::GrossOutput
224 };
225
226 debug!(
227 edges = adj
228 .values()
229 .map(Vec::len)
230 .sum::<usize>(),
231 tokens = token_map.len(),
232 "subgraph extracted"
233 );
234
235 Ok(BellmanFordContext {
236 token_in_node,
237 token_out_node,
238 adj,
239 token_map,
240 market_data,
241 gas_price_wei,
242 token_prices,
243 spot_prices,
244 node_address,
245 max_idx,
246 scoring,
247 })
248 }
249
250 pub(crate) fn find_single_route(
257 &self,
258 ctx: &BellmanFordContext,
259 order: &Order,
260 opts: FindRouteOptions,
261 ) -> Result<RouteResult, AlgorithmError> {
262 let start = Instant::now();
263
264 let spfa = self.run_spfa(ctx, order, &opts.overrides, start);
265
266 let out_idx = ctx.token_out_node.index();
267 if spfa.amount[out_idx].is_zero() {
268 return Err(AlgorithmError::NoPath {
269 from: order.token_in().clone(),
270 to: order.token_out().clone(),
271 reason: NoPathReason::NoGraphPath,
272 });
273 }
274
275 let path_edges =
279 Self::reconstruct_path(ctx.token_out_node, ctx.token_in_node, &spfa.predecessor)?;
280
281 let route =
282 Self::build_route(ctx, &path_edges, &spfa.amount, &spfa.edge_gas, &opts.overrides)?;
283
284 let final_amount_out = spfa.amount[out_idx].clone();
285 let gas_price = ctx
286 .gas_price_wei
287 .clone()
288 .unwrap_or_default();
289
290 let net_amount_out = Self::compute_net_amount_out(
291 &final_amount_out,
292 &route,
293 &gas_price,
294 ctx.token_prices.as_ref(),
295 &spfa.spot_product,
296 &ctx.node_address,
297 ctx.token_in_node,
298 )?;
299
300 let result = RouteResult::new(route, net_amount_out, gas_price);
301
302 let solve_time_ms = start.elapsed().as_millis() as u64;
303 debug!(
304 solve_time_ms,
305 hops = result.route().swaps().len(),
306 amount_in = %order.amount(),
307 amount_out = %final_amount_out,
308 net_amount_out = %result.net_amount_out(),
309 "bellman_ford route found"
310 );
311
312 Ok(result)
313 }
314
315 fn run_spfa(
321 &self,
322 ctx: &BellmanFordContext,
323 order: &Order,
324 overrides: &MarketOverrides,
325 start: Instant,
326 ) -> SPFAResult {
327 let mut amount: Vec<BigUint> = vec![BigUint::ZERO; ctx.max_idx];
331 let mut predecessor: Vec<Option<(NodeIndex, ComponentId)>> = vec![None; ctx.max_idx];
332 let mut edge_gas: Vec<BigUint> = vec![BigUint::ZERO; ctx.max_idx];
333 let mut cumul_gas: Vec<BigUint> = vec![BigUint::ZERO; ctx.max_idx];
334
335 amount[ctx.token_in_node.index()] = order.amount().clone();
336
337 let mut spot_product: Vec<f64> = vec![0.0; ctx.max_idx];
340 spot_product[ctx.token_in_node.index()] = 1.0;
341
342 let gas_aware = matches!(ctx.scoring, RouteScoringMode::NetOutput) &&
343 ctx.gas_price_wei.is_some() &&
344 ctx.token_prices.is_some();
345 if !gas_aware && matches!(ctx.scoring, RouteScoringMode::NetOutput) {
346 debug!("gas-aware comparison disabled (missing gas_price or token_prices)");
347 } else if matches!(ctx.scoring, RouteScoringMode::GrossOutput) {
348 debug!("gas-aware comparison disabled by config");
349 }
350
351 let mut active_nodes: Vec<NodeIndex> = vec![ctx.token_in_node];
352
353 for round in 0..self.max_hops {
354 if start.elapsed() >= self.timeout {
355 debug!(round, "timeout during relaxation");
356 break;
357 }
358 if active_nodes.is_empty() {
359 debug!(round, "no active nodes, stopping early");
360 break;
361 }
362
363 let mut next_active: HashSet<NodeIndex> = HashSet::new();
364
365 for &u in &active_nodes {
366 let u_idx = u.index();
367 if amount[u_idx].is_zero() {
368 continue;
369 }
370
371 let Some(token_u) = ctx.token_map.get(&u) else { continue };
372 let Some(edges) = ctx.adj.get(&u) else { continue };
373
374 for (v, component_id) in edges {
375 let v_idx = v.index();
376
377 if Self::path_has_conflict(u, *v, component_id, &predecessor) {
379 continue;
380 }
381
382 if let (Some(tokens), Some(v_addr)) =
385 (&self.connector_tokens, ctx.node_address.get(v))
386 {
387 if v_addr != order.token_in() &&
388 v_addr != order.token_out() &&
389 !tokens.contains(v_addr)
390 {
391 continue;
392 }
393 }
394
395 let Some(token_v) = ctx.token_map.get(v) else { continue };
396
397 let sim: &dyn tycho_simulation::tycho_common::simulation::protocol_sim::ProtocolSim =
399 if let Some(s) = overrides.get(component_id) {
400 s
401 } else if let Some(s) = ctx.market_data.get_simulation_state(component_id) {
402 s
403 } else {
404 continue;
405 };
406
407 let result = match sim.get_amount_out(amount[u_idx].clone(), token_u, token_v) {
408 Ok(r) => r,
409 Err(e) => {
410 trace!(
411 component_id,
412 error = %e,
413 "simulation failed, skipping edge"
414 );
415 continue;
416 }
417 };
418
419 let candidate_cumul_gas = &cumul_gas[u_idx] + &result.gas;
420
421 let candidate_spot = Self::compute_edge_spot_product(
424 spot_product[u_idx],
425 component_id,
426 ctx.node_address.get(&u),
427 ctx.node_address.get(v),
428 ctx.spot_prices.as_ref(),
429 );
430
431 let is_better = if gas_aware {
433 let v_price = Self::resolve_token_price(
434 ctx.node_address.get(v),
435 ctx.token_prices.as_ref(),
436 candidate_spot,
437 ctx.node_address.get(&ctx.token_in_node),
438 );
439 let net_candidate = Self::gas_adjusted_amount(
440 &result.amount,
441 &candidate_cumul_gas,
442 ctx.gas_price_wei.as_ref().unwrap(),
443 v_price.as_ref(),
444 );
445 let net_existing = Self::gas_adjusted_amount(
446 &amount[v_idx],
447 &cumul_gas[v_idx],
448 ctx.gas_price_wei.as_ref().unwrap(),
449 v_price.as_ref(),
450 );
451 net_candidate > net_existing
452 } else {
453 result.amount > amount[v_idx]
454 };
455
456 if is_better {
457 spot_product[v_idx] = candidate_spot;
458 amount[v_idx] = result.amount;
459 predecessor[v_idx] = Some((u, component_id.clone()));
460 edge_gas[v_idx] = result.gas;
461 cumul_gas[v_idx] = candidate_cumul_gas;
462 next_active.insert(*v);
463 }
464 }
465 }
466
467 active_nodes = next_active.into_iter().collect();
468 active_nodes.sort_unstable();
473 }
474
475 SPFAResult { amount, predecessor, edge_gas, spot_product }
476 }
477
478 fn build_route(
480 ctx: &BellmanFordContext,
481 path_edges: &[(NodeIndex, NodeIndex, ComponentId)],
482 amount: &[BigUint],
483 edge_gas: &[BigUint],
484 overrides: &MarketOverrides,
485 ) -> Result<Route, AlgorithmError> {
486 let mut swaps = Vec::with_capacity(path_edges.len());
487 let mut tokens: HashMap<Address, Token> = HashMap::new();
488
489 for (from_node, to_node, component_id) in path_edges {
490 let token_in = ctx
491 .token_map
492 .get(from_node)
493 .ok_or_else(|| AlgorithmError::DataNotFound {
494 kind: "token",
495 id: Some(format!("{:?}", from_node)),
496 })?;
497 let token_out = ctx
498 .token_map
499 .get(to_node)
500 .ok_or_else(|| AlgorithmError::DataNotFound {
501 kind: "token",
502 id: Some(format!("{:?}", to_node)),
503 })?;
504 let component = ctx
505 .market_data
506 .get_component(component_id)
507 .ok_or_else(|| AlgorithmError::DataNotFound {
508 kind: "component",
509 id: Some(component_id.clone()),
510 })?;
511 let sim_state = overrides
513 .get(component_id)
514 .or_else(|| {
515 ctx.market_data
516 .get_simulation_state(component_id)
517 })
518 .ok_or_else(|| AlgorithmError::DataNotFound {
519 kind: "simulation state",
520 id: Some(component_id.clone()),
521 })?;
522
523 swaps.push(Swap::new(
524 component_id.clone(),
525 component.protocol_system.clone(),
526 token_in.address.clone(),
527 token_out.address.clone(),
528 amount[from_node.index()].clone(),
529 amount[to_node.index()].clone(),
530 edge_gas[to_node.index()].clone(),
531 component.clone(),
532 sim_state.clone_box(),
533 ));
534 tokens
535 .entry(token_in.address.clone())
536 .or_insert_with(|| token_in.clone());
537 tokens
538 .entry(token_out.address.clone())
539 .or_insert_with(|| token_out.clone());
540 }
541
542 Ok(Route::new(swaps, tokens))
543 }
544
545 fn gas_adjusted_amount(
550 gross: &BigUint,
551 cumul_gas: &BigUint,
552 gas_price_wei: &BigUint,
553 token_price: Option<&Price>,
554 ) -> BigInt {
555 match token_price {
556 Some(price) if !price.denominator.is_zero() => {
557 let gas_cost = cumul_gas * gas_price_wei * &price.numerator / &price.denominator;
558 BigInt::from(gross.clone()) - BigInt::from(gas_cost)
559 }
560 _ => BigInt::from(gross.clone()),
561 }
562 }
563
564 fn compute_edge_spot_product(
569 parent_spot: f64,
570 component_id: &ComponentId,
571 u_addr: Option<&Address>,
572 v_addr: Option<&Address>,
573 spot_prices: Option<&SpotPrices>,
574 ) -> f64 {
575 if parent_spot == 0.0 {
576 return 0.0;
577 }
578 let (Some(u), Some(v), Some(prices)) = (u_addr, v_addr, spot_prices) else {
579 return 0.0;
580 };
581 let key = (component_id.clone(), u.clone(), v.clone());
582 match prices.get(&key) {
583 Some(&spot) if spot > 0.0 => parent_spot * spot,
584 _ => 0.0,
585 }
586 }
587
588 fn resolve_token_price(
595 v_addr: Option<&Address>,
596 token_prices: Option<&TokenGasPrices>,
597 spot_product: f64,
598 token_in_addr: Option<&Address>,
599 ) -> Option<Price> {
600 let prices = token_prices?;
601 let addr = v_addr?;
602
603 if let Some(price) = prices.get(addr) {
605 return Some(price.clone());
606 }
607
608 if spot_product > 0.0 {
610 if let Some(in_price) = token_in_addr.and_then(|a| prices.get(a)) {
611 let in_rate_f64 = in_price.numerator.to_f64()? / in_price.denominator.to_f64()?;
612 let estimated_rate = in_rate_f64 * spot_product;
613 let denom = BigUint::from(10u64).pow(18);
614 let numer_f64 = estimated_rate * 1e18;
615 if numer_f64.is_finite() && numer_f64 > 0.0 {
616 return Some(Price {
617 numerator: BigUint::from(numer_f64 as u128),
618 denominator: denom,
619 });
620 }
621 }
622 }
623
624 None
625 }
626
627 pub(crate) fn path_has_conflict(
630 from: NodeIndex,
631 target_node: NodeIndex,
632 target_pool: &ComponentId,
633 predecessor: &[Option<(NodeIndex, ComponentId)>],
634 ) -> bool {
635 let mut current = from;
636 loop {
637 if current == target_node {
638 return true;
639 }
640 match &predecessor[current.index()] {
641 Some((prev, cid)) => {
642 if cid == target_pool {
643 return true;
644 }
645 current = *prev;
646 }
647 None => return false,
648 }
649 }
650 }
651
652 pub(crate) fn reconstruct_path(
655 token_out: NodeIndex,
656 token_in: NodeIndex,
657 predecessor: &[Option<(NodeIndex, ComponentId)>],
658 ) -> Result<Vec<(NodeIndex, NodeIndex, ComponentId)>, AlgorithmError> {
659 let mut path = Vec::new();
660 let mut current = token_out;
661 let mut visited = HashSet::new();
662
663 while current != token_in {
664 if !visited.insert(current) {
665 return Err(AlgorithmError::Other("cycle in predecessor chain".to_string()));
666 }
667
668 let idx = current.index();
669 match &predecessor
670 .get(idx)
671 .and_then(|p| p.as_ref())
672 {
673 Some((prev_node, component_id)) => {
674 path.push((*prev_node, current, component_id.clone()));
675 current = *prev_node;
676 }
677 None => {
678 return Err(AlgorithmError::Other(format!(
679 "broken predecessor chain at node {idx}"
680 )));
681 }
682 }
683 }
684
685 path.reverse();
686 Ok(path)
687 }
688
689 pub(crate) fn get_subgraph(
694 graph: &StableDiGraph<()>,
695 token_in_node: NodeIndex,
696 max_hops: usize,
697 order: &Order,
698 ) -> Result<Subgraph, AlgorithmError> {
699 let mut adj: HashMap<NodeIndex, Vec<(NodeIndex, ComponentId)>> = HashMap::new();
700 let mut token_nodes: HashSet<NodeIndex> = HashSet::new();
701 let mut component_ids: HashSet<ComponentId> = HashSet::new();
702 let mut visited_nodes = HashSet::new();
703 let mut queued_nodes = VecDeque::new();
704
705 visited_nodes.insert(token_in_node);
706 token_nodes.insert(token_in_node);
707 queued_nodes.push_back((token_in_node, 0usize));
708
709 while let Some((node, depth)) = queued_nodes.pop_front() {
710 if depth >= max_hops {
711 continue;
712 }
713 for edge in graph.edges(node) {
714 let target = edge.target();
715 let cid = edge.weight().component_id.clone();
716
717 adj.entry(node)
718 .or_default()
719 .push((target, cid.clone()));
720 component_ids.insert(cid);
721 token_nodes.insert(target);
722
723 if visited_nodes.insert(target) {
724 queued_nodes.push_back((target, depth + 1));
725 }
726 }
727 }
728
729 if adj.is_empty() {
730 return Err(AlgorithmError::NoPath {
731 from: order.token_in().clone(),
732 to: order.token_out().clone(),
733 reason: NoPathReason::NoGraphPath,
734 });
735 }
736
737 Ok((adj, token_nodes, component_ids))
738 }
739
740 #[allow(clippy::too_many_arguments)]
746 fn compute_net_amount_out(
747 amount_out: &BigUint,
748 route: &Route,
749 gas_price: &BigUint,
750 token_prices: Option<&TokenGasPrices>,
751 spot_product: &[f64],
752 node_address: &HashMap<NodeIndex, Address>,
753 token_in_node: NodeIndex,
754 ) -> Result<BigInt, AlgorithmError> {
755 let last_swap = route.swaps().last().ok_or_else(|| {
756 AlgorithmError::Other("compute_net_amount_out called with empty route".to_string())
757 })?;
758
759 let total_gas = route.total_gas();
760
761 if gas_price.is_zero() {
762 warn!("missing gas price, returning gross amount_out");
763 return Ok(BigInt::from(amount_out.clone()));
764 }
765
766 let gas_cost_wei = &total_gas * gas_price;
767
768 let out_addr = last_swap.token_out();
770 let out_node_spot = node_address
771 .iter()
772 .find(|(_, addr)| *addr == out_addr)
773 .and_then(|(node, _)| spot_product.get(node.index()).copied())
774 .unwrap_or(0.0);
775
776 let output_price = Self::resolve_token_price(
777 Some(out_addr),
778 token_prices,
779 out_node_spot,
780 node_address.get(&token_in_node),
781 );
782
783 Ok(match output_price {
784 Some(price) if !price.denominator.is_zero() => {
785 let gas_cost = &gas_cost_wei * &price.numerator / &price.denominator;
786 BigInt::from(amount_out.clone()) - BigInt::from(gas_cost)
787 }
788 _ => {
789 warn!("no gas price for output token, returning gross amount_out");
790 BigInt::from(amount_out.clone())
791 }
792 })
793 }
794}
795
796impl Algorithm for BellmanFordAlgorithm {
797 type GraphType = StableDiGraph<()>;
798 type GraphManager = PetgraphStableDiGraphManager<()>;
799
800 fn name(&self) -> &str {
801 "bellman_ford"
802 }
803
804 #[instrument(level = "debug", skip_all, fields(order_id = %order.id()))]
805 async fn find_best_route(
806 &self,
807 graph: &Self::GraphType,
808 market: MarketData,
809 label: Option<StateLabel>,
810 derived: Option<SharedDerivedDataRef>,
811 order: &Order,
812 ) -> Result<RouteResult, AlgorithmError> {
813 let ctx = self
814 .build_context(graph, market, label, derived, order)
815 .await?;
816 self.find_single_route(&ctx, order, FindRouteOptions::default())
817 }
818
819 fn computation_requirements(&self) -> ComputationRequirements {
820 ComputationRequirements::none()
824 .allow_stale("token_prices")
825 .expect("token_prices requirement conflicts (bug)")
826 .allow_stale("spot_prices")
827 .expect("spot_prices requirement conflicts (bug)")
828 }
829
830 fn timeout(&self) -> Duration {
831 self.timeout
832 }
833}
834
835#[cfg(test)]
836mod tests {
837 use std::sync::Arc;
838
839 use num_bigint::BigInt;
840 use tokio::sync::RwLock;
841 use tycho_simulation::{
842 tycho_common::{models::Address, simulation::protocol_sim::ProtocolSim},
843 tycho_ethereum::gas::{BlockGasPrice, GasPrice},
844 };
845
846 use super::*;
847 use crate::{
848 algorithm::test_utils::{component, order, token, MockProtocolSim},
849 derived::{types::TokenGasPrices, DerivedData},
850 feed::market_data::{MarketData, MarketState},
851 graph::GraphManager,
852 types::quote::OrderSide,
853 };
854
855 fn setup_market_bf(
859 pools: Vec<(&str, &Token, &Token, MockProtocolSim)>,
860 ) -> (MarketData, PetgraphStableDiGraphManager<()>) {
861 let mut market = MarketState::new();
862
863 market.update_gas_price(BlockGasPrice {
864 block_number: 1,
865 block_hash: Default::default(),
866 block_timestamp: 0,
867 pricing: GasPrice::Legacy { gas_price: BigUint::from(100u64) },
868 });
869 market.update_last_updated(crate::types::BlockInfo::new(1, "0x00".into(), 0));
870
871 for (pool_id, token_in, token_out, state) in pools {
872 let tokens = vec![token_in.clone(), token_out.clone()];
873 let comp = component(pool_id, &tokens);
874 market.upsert_components(std::iter::once(comp));
875 market.update_states([(pool_id.to_string(), Box::new(state) as Box<dyn ProtocolSim>)]);
876 market.upsert_tokens(tokens);
877 }
878
879 let mut graph_manager = PetgraphStableDiGraphManager::default();
880 graph_manager.initialize_graph(&market.component_topology());
881
882 (MarketData::new(Arc::new(RwLock::new(market))), graph_manager)
883 }
884
885 fn setup_derived_with_token_prices(
886 token_addresses: &[Address],
887 ) -> crate::derived::SharedDerivedDataRef {
888 use tycho_simulation::tycho_core::simulation::protocol_sim::Price;
889
890 let mut token_prices: TokenGasPrices = HashMap::new();
891 for address in token_addresses {
892 token_prices.insert(
893 address.clone(),
894 Price { numerator: BigUint::from(1u64), denominator: BigUint::from(1u64) },
895 );
896 }
897
898 let mut derived_data = DerivedData::new();
899 derived_data.set_token_prices(token_prices, vec![], 1, true);
900 Arc::new(RwLock::new(derived_data))
901 }
902
903 fn bf_algorithm(max_hops: usize, timeout_ms: u64) -> BellmanFordAlgorithm {
904 BellmanFordAlgorithm::with_config(
905 AlgorithmConfig::new(1, max_hops, Duration::from_millis(timeout_ms), None).unwrap(),
906 )
907 }
908
909 #[tokio::test]
912 async fn test_linear_path_found() {
913 let token_a = token(0x01, "A");
914 let token_b = token(0x02, "B");
915 let token_c = token(0x03, "C");
916 let token_d = token(0x04, "D");
917
918 let (market, manager) = setup_market_bf(vec![
919 ("pool_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
920 ("pool_bc", &token_b, &token_c, MockProtocolSim::new(3.0)),
921 ("pool_cd", &token_c, &token_d, MockProtocolSim::new(4.0)),
922 ]);
923
924 let algo = bf_algorithm(4, 1000);
925 let ord = order(&token_a, &token_d, 100, OrderSide::Sell);
926
927 let result = algo
928 .find_best_route(manager.graph(), market, None, None, &ord)
929 .await
930 .unwrap();
931
932 assert_eq!(result.route().swaps().len(), 3);
933 assert_eq!(result.route().swaps()[0].amount_out(), &BigUint::from(200u64));
935 assert_eq!(result.route().swaps()[1].amount_out(), &BigUint::from(600u64));
936 assert_eq!(result.route().swaps()[2].amount_out(), &BigUint::from(2400u64));
937 }
938
939 #[tokio::test]
940 async fn test_picks_better_of_two_paths() {
941 let token_a = token(0x01, "A");
943 let token_b = token(0x02, "B");
944 let token_c = token(0x03, "C");
945 let token_d = token(0x04, "D");
946
947 let (market, manager) = setup_market_bf(vec![
948 ("pool_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
949 ("pool_bd", &token_b, &token_d, MockProtocolSim::new(3.0)),
950 ("pool_ac", &token_a, &token_c, MockProtocolSim::new(4.0)),
951 ("pool_cd", &token_c, &token_d, MockProtocolSim::new(1.0)),
952 ]);
953
954 let algo = bf_algorithm(3, 1000);
955 let ord = order(&token_a, &token_d, 100, OrderSide::Sell);
956
957 let result = algo
958 .find_best_route(manager.graph(), market, None, None, &ord)
959 .await
960 .unwrap();
961
962 assert_eq!(result.route().swaps().len(), 2);
964 assert_eq!(result.route().swaps()[0].component_id(), "pool_ab");
965 assert_eq!(result.route().swaps()[1].component_id(), "pool_bd");
966 assert_eq!(result.route().swaps()[1].amount_out(), &BigUint::from(600u64));
967 }
968
969 #[tokio::test]
970 async fn test_parallel_pools() {
971 let token_a = token(0x01, "A");
973 let token_b = token(0x02, "B");
974
975 let (market, manager) = setup_market_bf(vec![
976 ("pool1", &token_a, &token_b, MockProtocolSim::new(2.0)),
977 ("pool2", &token_a, &token_b, MockProtocolSim::new(5.0)),
978 ]);
979
980 let algo = bf_algorithm(2, 1000);
981 let ord = order(&token_a, &token_b, 100, OrderSide::Sell);
982
983 let result = algo
984 .find_best_route(manager.graph(), market, None, None, &ord)
985 .await
986 .unwrap();
987
988 assert_eq!(result.route().swaps().len(), 1);
989 assert_eq!(result.route().swaps()[0].component_id(), "pool2");
990 assert_eq!(result.route().swaps()[0].amount_out(), &BigUint::from(500u64));
991 }
992
993 #[tokio::test]
994 async fn test_no_path_returns_error() {
995 let token_a = token(0x01, "A");
996 let token_b = token(0x02, "B");
997 let token_c = token(0x03, "C");
998
999 let (market, manager) =
1001 setup_market_bf(vec![("pool_ab", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1002
1003 {
1005 let mut m = market.write().await;
1006 m.upsert_tokens(vec![token_c.clone()]);
1007 }
1008
1009 let algo = bf_algorithm(3, 1000);
1010 let ord = order(&token_a, &token_c, 100, OrderSide::Sell);
1011
1012 let result = algo
1013 .find_best_route(manager.graph(), market, None, None, &ord)
1014 .await;
1015 assert!(matches!(result, Err(AlgorithmError::NoPath { .. })));
1016 }
1017
1018 #[tokio::test]
1019 async fn test_source_not_in_graph() {
1020 let token_a = token(0x01, "A");
1021 let token_b = token(0x02, "B");
1022 let token_x = token(0x99, "X");
1023
1024 let (market, manager) =
1025 setup_market_bf(vec![("pool_ab", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1026
1027 let algo = bf_algorithm(3, 1000);
1028 let ord = order(&token_x, &token_b, 100, OrderSide::Sell);
1029
1030 let result = algo
1031 .find_best_route(manager.graph(), market, None, None, &ord)
1032 .await;
1033 assert!(matches!(
1034 result,
1035 Err(AlgorithmError::NoPath { reason: NoPathReason::SourceTokenNotInGraph, .. })
1036 ));
1037 }
1038
1039 #[tokio::test]
1040 async fn test_destination_not_in_graph() {
1041 let token_a = token(0x01, "A");
1042 let token_b = token(0x02, "B");
1043 let token_x = token(0x99, "X");
1044
1045 let (market, manager) =
1046 setup_market_bf(vec![("pool_ab", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1047
1048 let algo = bf_algorithm(3, 1000);
1049 let ord = order(&token_a, &token_x, 100, OrderSide::Sell);
1050
1051 let result = algo
1052 .find_best_route(manager.graph(), market, None, None, &ord)
1053 .await;
1054 assert!(matches!(
1055 result,
1056 Err(AlgorithmError::NoPath { reason: NoPathReason::DestinationTokenNotInGraph, .. })
1057 ));
1058 }
1059
1060 #[tokio::test]
1061 async fn test_respects_max_hops() {
1062 let token_a = token(0x01, "A");
1064 let token_b = token(0x02, "B");
1065 let token_c = token(0x03, "C");
1066 let token_d = token(0x04, "D");
1067
1068 let (market, manager) = setup_market_bf(vec![
1069 ("pool_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
1070 ("pool_bc", &token_b, &token_c, MockProtocolSim::new(3.0)),
1071 ("pool_cd", &token_c, &token_d, MockProtocolSim::new(4.0)),
1072 ]);
1073
1074 let algo = bf_algorithm(2, 1000);
1075 let ord = order(&token_a, &token_d, 100, OrderSide::Sell);
1076
1077 let result = algo
1078 .find_best_route(manager.graph(), market, None, None, &ord)
1079 .await;
1080 assert!(
1081 matches!(result, Err(AlgorithmError::NoPath { .. })),
1082 "Should not find 3-hop path with max_hops=2"
1083 );
1084 }
1085
1086 #[tokio::test]
1087 async fn test_source_token_revisit_blocked() {
1088 let token_a = token(0x01, "A");
1091 let token_b = token(0x02, "B");
1092 let token_c = token(0x03, "C");
1093
1094 let (market, manager) = setup_market_bf(vec![
1095 ("pool_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
1096 ("pool_bc", &token_b, &token_c, MockProtocolSim::new(3.0)),
1097 ]);
1098
1099 let algo = bf_algorithm(4, 1000);
1100 let ord = order(&token_a, &token_c, 100, OrderSide::Sell);
1101
1102 let result = algo
1103 .find_best_route(manager.graph(), market, None, None, &ord)
1104 .await
1105 .unwrap();
1106
1107 assert_eq!(result.route().swaps().len(), 2);
1109 assert_eq!(result.route().swaps()[0].component_id(), "pool_ab");
1110 assert_eq!(result.route().swaps()[1].component_id(), "pool_bc");
1111 assert_eq!(result.route().swaps()[1].amount_out(), &BigUint::from(600u64));
1112 }
1113
1114 #[tokio::test]
1115 async fn test_hub_token_revisit_blocked() {
1116 let token_a = token(0x01, "A");
1119 let token_c = token(0x02, "C");
1120 let token_b = token(0x03, "B");
1121 let token_d = token(0x04, "D");
1122
1123 let (market, manager) = setup_market_bf(vec![
1124 ("pool_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
1125 ("pool_bc", &token_b, &token_c, MockProtocolSim::new(3.0)),
1126 ("pool_cb", &token_c, &token_b, MockProtocolSim::new(100.0)),
1127 ("pool_bd", &token_b, &token_d, MockProtocolSim::new(2.0)),
1128 ]);
1129
1130 let algo = bf_algorithm(4, 1000);
1131 let ord = order(&token_a, &token_d, 100, OrderSide::Sell);
1132
1133 let result = algo
1134 .find_best_route(manager.graph(), market, None, None, &ord)
1135 .await
1136 .unwrap();
1137
1138 assert_eq!(result.route().swaps().len(), 2, "should use direct 2-hop path");
1141 assert_eq!(result.route().swaps()[0].component_id(), "pool_ab");
1142 assert_eq!(result.route().swaps()[1].component_id(), "pool_bd");
1143 assert_eq!(result.route().swaps()[1].amount_out(), &BigUint::from(400u64));
1144 }
1145
1146 #[tokio::test]
1147 async fn test_route_amounts_are_sequential() {
1148 let token_a = token(0x01, "A");
1150 let token_b = token(0x02, "B");
1151 let token_c = token(0x03, "C");
1152
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(3.0)),
1156 ]);
1157
1158 let algo = bf_algorithm(3, 1000);
1159 let ord = order(&token_a, &token_c, 100, OrderSide::Sell);
1160
1161 let result = algo
1162 .find_best_route(manager.graph(), market, None, None, &ord)
1163 .await
1164 .unwrap();
1165
1166 assert_eq!(result.route().swaps().len(), 2);
1167 assert_eq!(result.route().swaps()[1].amount_in(), result.route().swaps()[0].amount_out());
1169 }
1170
1171 #[tokio::test]
1172 async fn test_gas_deduction() {
1173 let token_a = token(0x01, "A");
1174 let token_b = token(0x02, "B");
1175
1176 let (market, manager) = setup_market_bf(vec![(
1177 "pool1",
1178 &token_a,
1179 &token_b,
1180 MockProtocolSim::new(2.0).with_gas(10),
1181 )]);
1182
1183 let algo = bf_algorithm(2, 1000);
1184 let ord = order(&token_a, &token_b, 1000, OrderSide::Sell);
1185
1186 let derived = setup_derived_with_token_prices(std::slice::from_ref(&token_b.address));
1187
1188 let result = algo
1189 .find_best_route(manager.graph(), market, None, Some(derived), &ord)
1190 .await
1191 .unwrap();
1192
1193 assert_eq!(result.route().swaps()[0].amount_out(), &BigUint::from(2000u64));
1197 assert_eq!(result.net_amount_out(), &BigInt::from(1000));
1198 }
1199
1200 #[tokio::test]
1201 async fn test_timeout_respected() {
1202 let token_a = token(0x01, "A");
1203 let token_b = token(0x02, "B");
1204 let token_c = token(0x03, "C");
1205
1206 let (market, manager) = setup_market_bf(vec![
1207 ("pool_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
1208 ("pool_bc", &token_b, &token_c, MockProtocolSim::new(3.0)),
1209 ]);
1210
1211 let algo = bf_algorithm(3, 0);
1213 let ord = order(&token_a, &token_c, 100, OrderSide::Sell);
1214
1215 let result = algo
1216 .find_best_route(manager.graph(), market, None, None, &ord)
1217 .await;
1218
1219 match result {
1224 Ok(r) => {
1225 assert!(!r.route().swaps().is_empty());
1226 }
1227 Err(AlgorithmError::Timeout { .. }) | Err(AlgorithmError::NoPath { .. }) => {
1228 }
1230 Err(e) => panic!("Unexpected error: {:?}", e),
1231 }
1232 }
1233
1234 #[tokio::test]
1237 async fn test_with_fees() {
1238 let token_a = token(0x01, "A");
1239 let token_b = token(0x02, "B");
1240
1241 let (market, manager) = setup_market_bf(vec![(
1243 "pool1",
1244 &token_a,
1245 &token_b,
1246 MockProtocolSim::new(2.0).with_fee(0.1),
1247 )]);
1248
1249 let algo = bf_algorithm(2, 1000);
1250 let ord = order(&token_a, &token_b, 1000, OrderSide::Sell);
1251
1252 let result = algo
1253 .find_best_route(manager.graph(), market, None, None, &ord)
1254 .await
1255 .unwrap();
1256
1257 assert_eq!(result.route().swaps()[0].amount_out(), &BigUint::from(1800u64));
1259 }
1260
1261 #[tokio::test]
1262 async fn test_large_trade_slippage() {
1263 let token_a = token(0x01, "A");
1264 let token_b = token(0x02, "B");
1265
1266 let (market, manager) = setup_market_bf(vec![(
1268 "pool1",
1269 &token_a,
1270 &token_b,
1271 MockProtocolSim::new(2.0).with_liquidity(500),
1272 )]);
1273
1274 let algo = bf_algorithm(2, 1000);
1275 let ord = order(&token_a, &token_b, 1000, OrderSide::Sell);
1276
1277 let result = algo
1279 .find_best_route(manager.graph(), market, None, None, &ord)
1280 .await;
1281 assert!(
1282 matches!(result, Err(AlgorithmError::NoPath { .. })),
1283 "Should fail when trade exceeds pool liquidity"
1284 );
1285 }
1286
1287 #[tokio::test]
1288 async fn test_disconnected_tokens_return_no_path() {
1289 let token_a = token(0x01, "A");
1291 let token_b = token(0x02, "B");
1292 let token_d = token(0x04, "D");
1293 let token_e = token(0x05, "E");
1294
1295 let (market, manager) = setup_market_bf(vec![
1296 ("pool_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
1297 ("pool_de", &token_d, &token_e, MockProtocolSim::new(4.0)),
1298 ]);
1299
1300 let algo = bf_algorithm(3, 1000);
1301 let ord = order(&token_a, &token_e, 100, OrderSide::Sell);
1302
1303 let result = algo
1304 .find_best_route(manager.graph(), market, None, None, &ord)
1305 .await;
1306 assert!(
1307 matches!(result, Err(AlgorithmError::NoPath { .. })),
1308 "should not find path to disconnected component"
1309 );
1310 }
1311
1312 #[tokio::test]
1313 async fn test_spfa_skips_failed_simulations() {
1314 let token_a = token(0x01, "A");
1316 let token_b = token(0x02, "B");
1317 let token_c = token(0x03, "C");
1318
1319 let (market, manager) = setup_market_bf(vec![
1320 ("pool_ab_bad", &token_a, &token_b, MockProtocolSim::new(2.0).with_liquidity(0)),
1322 ("pool_ac", &token_a, &token_c, MockProtocolSim::new(2.0)),
1324 ("pool_cb", &token_c, &token_b, MockProtocolSim::new(3.0)),
1325 ]);
1326
1327 let algo = bf_algorithm(3, 1000);
1328 let ord = order(&token_a, &token_b, 100, OrderSide::Sell);
1329
1330 let result = algo
1331 .find_best_route(manager.graph(), market, None, None, &ord)
1332 .await;
1333
1334 match result {
1338 Ok(r) => {
1339 assert!(!r.route().swaps().is_empty());
1341 }
1342 Err(AlgorithmError::NoPath { .. }) => {
1343 }
1346 Err(e) => panic!("Unexpected error: {:?}", e),
1347 }
1348 }
1349
1350 #[tokio::test]
1351 async fn test_resimulation_produces_correct_amounts() {
1352 let token_a = token(0x01, "A");
1354 let token_b = token(0x02, "B");
1355 let token_c = token(0x03, "C");
1356
1357 let (market, manager) = setup_market_bf(vec![
1358 ("pool_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
1359 ("pool_bc", &token_b, &token_c, MockProtocolSim::new(3.0)),
1360 ]);
1361
1362 let algo = bf_algorithm(3, 1000);
1363 let ord = order(&token_a, &token_c, 100, OrderSide::Sell);
1364
1365 let result = algo
1366 .find_best_route(manager.graph(), market, None, None, &ord)
1367 .await
1368 .unwrap();
1369
1370 assert_eq!(result.route().swaps()[0].amount_in(), &BigUint::from(100u64));
1373 assert_eq!(result.route().swaps()[0].amount_out(), &BigUint::from(200u64));
1374 assert_eq!(result.route().swaps()[1].amount_in(), &BigUint::from(200u64));
1375 assert_eq!(result.route().swaps()[1].amount_out(), &BigUint::from(600u64));
1376 }
1377
1378 #[test]
1381 fn algorithm_name() {
1382 let algo = bf_algorithm(4, 200);
1383 assert_eq!(algo.name(), "bellman_ford");
1384 }
1385
1386 #[test]
1387 fn algorithm_timeout() {
1388 let algo = bf_algorithm(4, 200);
1389 assert_eq!(algo.timeout(), Duration::from_millis(200));
1390 }
1391
1392 #[tokio::test]
1395 async fn test_gas_aware_relaxation_picks_cheaper_path() {
1396 let token_a = token(0x01, "A");
1411 let token_b = token(0x02, "B");
1412 let token_c = token(0x03, "C");
1413 let token_d = token(0x04, "D");
1414
1415 let high_gas: u64 = 100_000_000;
1416 let low_gas: u64 = 100;
1417
1418 let (market, manager) = setup_market_bf(vec![
1419 ("pool_ab", &token_a, &token_b, MockProtocolSim::new(3.0).with_gas(high_gas)),
1420 ("pool_bd", &token_b, &token_d, MockProtocolSim::new(2.0).with_gas(high_gas)),
1421 ("pool_ac", &token_a, &token_c, MockProtocolSim::new(2.0).with_gas(low_gas)),
1422 ("pool_cd", &token_c, &token_d, MockProtocolSim::new(2.0).with_gas(low_gas)),
1423 ]);
1424
1425 let algo = bf_algorithm(3, 1000);
1426 let ord = order(&token_a, &token_d, 1_000_000_000, OrderSide::Sell);
1427
1428 let derived = setup_derived_with_token_prices(&[
1430 token_a.address.clone(),
1431 token_b.address.clone(),
1432 token_c.address.clone(),
1433 token_d.address.clone(),
1434 ]);
1435
1436 let result = algo
1437 .find_best_route(manager.graph(), market, None, Some(derived), &ord)
1438 .await
1439 .unwrap();
1440
1441 assert_eq!(result.route().swaps().len(), 2);
1443 assert_eq!(result.route().swaps()[0].component_id(), "pool_ac");
1444 assert_eq!(result.route().swaps()[1].component_id(), "pool_cd");
1445 }
1446
1447 #[tokio::test]
1448 async fn test_gas_aware_falls_back_to_gross_without_derived() {
1449 let token_a = token(0x01, "A");
1452 let token_b = token(0x02, "B");
1453 let token_c = token(0x03, "C");
1454 let token_d = token(0x04, "D");
1455
1456 let high_gas: u64 = 100_000_000;
1457 let low_gas: u64 = 100;
1458
1459 let (market, manager) = setup_market_bf(vec![
1460 ("pool_ab", &token_a, &token_b, MockProtocolSim::new(3.0).with_gas(high_gas)),
1461 ("pool_bd", &token_b, &token_d, MockProtocolSim::new(2.0).with_gas(high_gas)),
1462 ("pool_ac", &token_a, &token_c, MockProtocolSim::new(2.0).with_gas(low_gas)),
1463 ("pool_cd", &token_c, &token_d, MockProtocolSim::new(2.0).with_gas(low_gas)),
1464 ]);
1465
1466 let algo = bf_algorithm(3, 1000);
1467 let ord = order(&token_a, &token_d, 1_000_000_000, OrderSide::Sell);
1468
1469 let result = algo
1471 .find_best_route(manager.graph(), market, None, None, &ord)
1472 .await
1473 .unwrap();
1474
1475 assert_eq!(result.route().swaps().len(), 2);
1477 assert_eq!(result.route().swaps()[0].component_id(), "pool_ab");
1478 assert_eq!(result.route().swaps()[1].component_id(), "pool_bd");
1479 }
1480
1481 fn bf_algorithm_with_connectors(
1485 max_hops: usize,
1486 timeout_ms: u64,
1487 connector_tokens: HashSet<Address>,
1488 ) -> BellmanFordAlgorithm {
1489 BellmanFordAlgorithm::with_config(
1490 AlgorithmConfig::new(1, max_hops, Duration::from_millis(timeout_ms), None)
1491 .unwrap()
1492 .with_connector_tokens(connector_tokens),
1493 )
1494 }
1495
1496 #[tokio::test]
1497 async fn test_connector_tokens_blocks_disallowed_intermediate() {
1498 let token_a = token(0x01, "A");
1505 let token_b = token(0x02, "B");
1506 let token_c = token(0x03, "C");
1507 let token_d = token(0x04, "D");
1508
1509 let (market, manager) = setup_market_bf(vec![
1510 ("pool_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
1511 ("pool_bd", &token_b, &token_d, MockProtocolSim::new(2.0)),
1512 ("pool_ac", &token_a, &token_c, MockProtocolSim::new(3.0)),
1513 ("pool_cd", &token_c, &token_d, MockProtocolSim::new(3.0)),
1514 ]);
1515
1516 let connectors: HashSet<Address> = [token_c.address.clone()].into();
1517 let algo = bf_algorithm_with_connectors(3, 1000, connectors);
1518 let ord = order(&token_a, &token_d, 100, OrderSide::Sell);
1519
1520 let result = algo
1521 .find_best_route(manager.graph(), market, None, None, &ord)
1522 .await
1523 .unwrap();
1524
1525 assert_eq!(result.route().swaps().len(), 2);
1527 assert_eq!(result.route().swaps()[0].component_id(), "pool_ac");
1528 assert_eq!(result.route().swaps()[1].component_id(), "pool_cd");
1529 }
1530
1531 #[tokio::test]
1532 async fn test_connector_tokens_allows_endpoints_even_if_not_listed() {
1533 let token_a = token(0x01, "A");
1535 let token_b = token(0x02, "B");
1536
1537 let (market, manager) =
1538 setup_market_bf(vec![("pool_ab", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1539
1540 let algo = bf_algorithm_with_connectors(1, 1000, HashSet::new());
1542 let ord = order(&token_a, &token_b, 100, OrderSide::Sell);
1543
1544 let result = algo
1545 .find_best_route(manager.graph(), market, None, None, &ord)
1546 .await
1547 .unwrap();
1548
1549 assert_eq!(result.route().swaps().len(), 1);
1550 assert_eq!(result.route().swaps()[0].amount_out(), &BigUint::from(200u64));
1551 }
1552
1553 #[tokio::test]
1554 async fn test_connector_tokens_none_is_unrestricted() {
1555 let token_a = token(0x01, "A");
1557 let token_b = token(0x02, "B");
1558 let token_c = token(0x03, "C");
1559 let token_d = token(0x04, "D");
1560
1561 let (market, manager) = setup_market_bf(vec![
1562 ("pool_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
1563 ("pool_bd", &token_b, &token_d, MockProtocolSim::new(3.0)),
1564 ("pool_ac", &token_a, &token_c, MockProtocolSim::new(1.0)),
1565 ("pool_cd", &token_c, &token_d, MockProtocolSim::new(1.0)),
1566 ]);
1567
1568 let algo = bf_algorithm(3, 1000);
1569 let ord = order(&token_a, &token_d, 100, OrderSide::Sell);
1570
1571 let result = algo
1572 .find_best_route(manager.graph(), market, None, None, &ord)
1573 .await
1574 .unwrap();
1575
1576 assert_eq!(result.route().swaps()[0].component_id(), "pool_ab");
1578 assert_eq!(result.route().swaps()[1].component_id(), "pool_bd");
1579 assert_eq!(result.route().swaps()[1].amount_out(), &BigUint::from(600u64));
1580 }
1581
1582 #[test]
1583 fn test_path_has_conflict_detects_node_and_pool() {
1584 let mut pred: Vec<Option<(NodeIndex, ComponentId)>> = vec![None; 4];
1586 pred[1] = Some((NodeIndex::new(0), "pool_a".into()));
1587 pred[2] = Some((NodeIndex::new(1), "pool_b".into()));
1588
1589 assert!(BellmanFordAlgorithm::path_has_conflict(
1591 NodeIndex::new(2),
1592 NodeIndex::new(0),
1593 &"any".into(),
1594 &pred
1595 ));
1596 assert!(!BellmanFordAlgorithm::path_has_conflict(
1597 NodeIndex::new(2),
1598 NodeIndex::new(3),
1599 &"any".into(),
1600 &pred
1601 ));
1602 assert!(BellmanFordAlgorithm::path_has_conflict(
1604 NodeIndex::new(2),
1605 NodeIndex::new(2),
1606 &"any".into(),
1607 &pred
1608 ));
1609
1610 assert!(BellmanFordAlgorithm::path_has_conflict(
1612 NodeIndex::new(2),
1613 NodeIndex::new(3),
1614 &"pool_a".into(),
1615 &pred
1616 ));
1617 assert!(BellmanFordAlgorithm::path_has_conflict(
1618 NodeIndex::new(2),
1619 NodeIndex::new(3),
1620 &"pool_b".into(),
1621 &pred
1622 ));
1623 assert!(!BellmanFordAlgorithm::path_has_conflict(
1624 NodeIndex::new(2),
1625 NodeIndex::new(3),
1626 &"pool_c".into(),
1627 &pred
1628 ));
1629 }
1630
1631 #[tokio::test]
1632 async fn test_find_single_route_with_state_overrides() {
1633 let token_a = token(0x01, "A");
1634 let token_b = token(0x02, "B");
1635
1636 let (market, manager) =
1637 setup_market_bf(vec![("pool_ab", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1638
1639 let algo = bf_algorithm(2, 1000);
1640 let ord = order(&token_a, &token_b, 1000, OrderSide::Sell);
1641
1642 let ctx = algo
1643 .build_context(manager.graph(), market, None, None, &ord)
1644 .await
1645 .unwrap();
1646
1647 let normal = algo
1649 .find_single_route(&ctx, &ord, FindRouteOptions::default())
1650 .unwrap();
1651 assert_eq!(normal.route().swaps()[0].amount_out(), &BigUint::from(2000u64));
1652
1653 let opts = FindRouteOptions {
1655 overrides: MarketOverrides::empty()
1656 .with_override("pool_ab".to_string(), Box::new(MockProtocolSim::new(1.0))),
1657 };
1658 let overridden = algo
1659 .find_single_route(&ctx, &ord, opts)
1660 .unwrap();
1661 assert_eq!(overridden.route().swaps()[0].amount_out(), &BigUint::from(1000u64));
1662
1663 assert!(
1664 overridden.route().swaps()[0].amount_out() < normal.route().swaps()[0].amount_out()
1665 );
1666 }
1667
1668 #[tokio::test]
1669 async fn test_single_find_route_options_default() {
1670 use super::super::split_primitives::MarketOverrides;
1671
1672 let token_a = token(0x01, "A");
1673 let token_b = token(0x02, "B");
1674
1675 let (market, manager) =
1676 setup_market_bf(vec![("pool_ab", &token_a, &token_b, MockProtocolSim::new(2.0))]);
1677
1678 let algo = bf_algorithm(2, 1000);
1679 let ord = order(&token_a, &token_b, 1000, OrderSide::Sell);
1680
1681 let ctx = algo
1682 .build_context(manager.graph(), market, None, None, &ord)
1683 .await
1684 .unwrap();
1685
1686 let with_default = algo
1687 .find_single_route(&ctx, &ord, FindRouteOptions::default())
1688 .unwrap();
1689 let with_empty = algo
1690 .find_single_route(&ctx, &ord, FindRouteOptions { overrides: MarketOverrides::empty() })
1691 .unwrap();
1692
1693 assert_eq!(
1694 with_default.route().swaps()[0].amount_out(),
1695 with_empty.route().swaps()[0].amount_out()
1696 );
1697 assert_eq!(with_default.route().swaps()[0].amount_out(), &BigUint::from(2000u64));
1698 }
1699}