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