1use std::{
17 collections::{HashMap, HashSet, VecDeque},
18 time::{Duration, Instant},
19};
20
21use num_bigint::{BigInt, BigUint};
22use num_traits::{ToPrimitive, Zero};
23use petgraph::{graph::NodeIndex, prelude::EdgeRef};
24use tracing::{debug, instrument, trace, warn};
25use tycho_simulation::{
26 tycho_common::models::Address,
27 tycho_core::{models::token::Token, simulation::protocol_sim::Price},
28};
29
30use super::{Algorithm, AlgorithmConfig, AlgorithmError, NoPathReason};
31use crate::{
32 derived::{
33 computation::ComputationRequirements,
34 types::{SpotPrices, TokenGasPrices},
35 SharedDerivedDataRef,
36 },
37 feed::market_data::SharedMarketDataRef,
38 graph::{petgraph::StableDiGraph, PetgraphStableDiGraphManager},
39 types::{ComponentId, Order, Route, RouteResult, Swap},
40};
41
42type Subgraph =
44 (HashMap<NodeIndex, Vec<(NodeIndex, ComponentId)>>, HashSet<NodeIndex>, HashSet<ComponentId>);
45
46pub struct BellmanFordAlgorithm {
47 max_hops: usize,
48 timeout: Duration,
49 gas_aware: bool,
50}
51
52impl BellmanFordAlgorithm {
53 pub(crate) fn with_config(config: AlgorithmConfig) -> Result<Self, AlgorithmError> {
54 Ok(Self {
55 max_hops: config.max_hops(),
56 timeout: config.timeout(),
57 gas_aware: config.gas_aware(),
58 })
59 }
60
61 fn gas_adjusted_amount(
66 gross: &BigUint,
67 cumul_gas: &BigUint,
68 gas_price_wei: &BigUint,
69 token_price: Option<&Price>,
70 ) -> BigInt {
71 match token_price {
72 Some(price) if !price.denominator.is_zero() => {
73 let gas_cost = cumul_gas * gas_price_wei * &price.numerator / &price.denominator;
74 BigInt::from(gross.clone()) - BigInt::from(gas_cost)
75 }
76 _ => BigInt::from(gross.clone()),
77 }
78 }
79
80 fn compute_edge_spot_product(
85 parent_spot: f64,
86 component_id: &ComponentId,
87 u_addr: Option<&Address>,
88 v_addr: Option<&Address>,
89 spot_prices: Option<&SpotPrices>,
90 ) -> f64 {
91 if parent_spot == 0.0 {
92 return 0.0;
93 }
94 let (Some(u), Some(v), Some(prices)) = (u_addr, v_addr, spot_prices) else {
95 return 0.0;
96 };
97 let key = (component_id.clone(), u.clone(), v.clone());
98 match prices.get(&key) {
99 Some(&spot) if spot > 0.0 => parent_spot * spot,
100 _ => 0.0,
101 }
102 }
103
104 fn resolve_token_price(
111 v_addr: Option<&Address>,
112 token_prices: Option<&TokenGasPrices>,
113 spot_product: f64,
114 token_in_addr: Option<&Address>,
115 ) -> Option<Price> {
116 let prices = token_prices?;
117 let addr = v_addr?;
118
119 if let Some(price) = prices.get(addr) {
121 return Some(price.clone());
122 }
123
124 if spot_product > 0.0 {
126 if let Some(in_price) = token_in_addr.and_then(|a| prices.get(a)) {
127 let in_rate_f64 = in_price.numerator.to_f64()? / in_price.denominator.to_f64()?;
128 let estimated_rate = in_rate_f64 * spot_product;
129 let denom = BigUint::from(10u64).pow(18);
130 let numer_f64 = estimated_rate * 1e18;
131 if numer_f64.is_finite() && numer_f64 > 0.0 {
132 return Some(Price {
133 numerator: BigUint::from(numer_f64 as u128),
134 denominator: denom,
135 });
136 }
137 }
138 }
139
140 None
141 }
142
143 fn path_has_conflict(
146 from: NodeIndex,
147 target_node: NodeIndex,
148 target_pool: &ComponentId,
149 predecessor: &[Option<(NodeIndex, ComponentId)>],
150 ) -> bool {
151 let mut current = from;
152 loop {
153 if current == target_node {
154 return true;
155 }
156 match &predecessor[current.index()] {
157 Some((prev, cid)) => {
158 if cid == target_pool {
159 return true;
160 }
161 current = *prev;
162 }
163 None => return false,
164 }
165 }
166 }
167
168 fn reconstruct_path(
171 token_out: NodeIndex,
172 token_in: NodeIndex,
173 predecessor: &[Option<(NodeIndex, ComponentId)>],
174 ) -> Result<Vec<(NodeIndex, NodeIndex, ComponentId)>, AlgorithmError> {
175 let mut path = Vec::new();
176 let mut current = token_out;
177 let mut visited = HashSet::new();
178
179 while current != token_in {
180 if !visited.insert(current) {
181 return Err(AlgorithmError::Other("cycle in predecessor chain".to_string()));
182 }
183
184 let idx = current.index();
185 match &predecessor
186 .get(idx)
187 .and_then(|p| p.as_ref())
188 {
189 Some((prev_node, component_id)) => {
190 path.push((*prev_node, current, component_id.clone()));
191 current = *prev_node;
192 }
193 None => {
194 return Err(AlgorithmError::Other(format!(
195 "broken predecessor chain at node {idx}"
196 )));
197 }
198 }
199 }
200
201 path.reverse();
202 Ok(path)
203 }
204
205 fn get_subgraph(
210 graph: &StableDiGraph<()>,
211 token_in_node: NodeIndex,
212 max_hops: usize,
213 order: &Order,
214 ) -> Result<Subgraph, AlgorithmError> {
215 let mut adj: HashMap<NodeIndex, Vec<(NodeIndex, ComponentId)>> = HashMap::new();
216 let mut token_nodes: HashSet<NodeIndex> = HashSet::new();
217 let mut component_ids: HashSet<ComponentId> = HashSet::new();
218 let mut visited = HashSet::new();
219 let mut queue = VecDeque::new();
220
221 visited.insert(token_in_node);
222 token_nodes.insert(token_in_node);
223 queue.push_back((token_in_node, 0usize));
224
225 while let Some((node, depth)) = queue.pop_front() {
226 if depth >= max_hops {
227 continue;
228 }
229 for edge in graph.edges(node) {
230 let target = edge.target();
231 let cid = edge.weight().component_id.clone();
232
233 adj.entry(node)
234 .or_default()
235 .push((target, cid.clone()));
236 component_ids.insert(cid);
237 token_nodes.insert(target);
238
239 if visited.insert(target) {
240 queue.push_back((target, depth + 1));
241 }
242 }
243 }
244
245 if adj.is_empty() {
246 return Err(AlgorithmError::NoPath {
247 from: order.token_in().clone(),
248 to: order.token_out().clone(),
249 reason: NoPathReason::NoGraphPath,
250 });
251 }
252
253 Ok((adj, token_nodes, component_ids))
254 }
255
256 #[allow(clippy::too_many_arguments)]
262 fn compute_net_amount_out(
263 amount_out: &BigUint,
264 route: &Route,
265 gas_price: &BigUint,
266 token_prices: Option<&TokenGasPrices>,
267 spot_product: &[f64],
268 node_address: &HashMap<NodeIndex, Address>,
269 token_in_node: NodeIndex,
270 ) -> BigInt {
271 let Some(last_swap) = route.swaps().last() else {
272 return BigInt::from(amount_out.clone());
273 };
274
275 let total_gas = route.total_gas();
276
277 if gas_price.is_zero() {
278 warn!("missing gas price, returning gross amount_out");
279 return BigInt::from(amount_out.clone());
280 }
281
282 let gas_cost_wei = &total_gas * gas_price;
283
284 let out_addr = last_swap.token_out();
286 let out_node_spot = node_address
287 .iter()
288 .find(|(_, addr)| *addr == out_addr)
289 .and_then(|(node, _)| spot_product.get(node.index()).copied())
290 .unwrap_or(0.0);
291
292 let output_price = Self::resolve_token_price(
293 Some(out_addr),
294 token_prices,
295 out_node_spot,
296 node_address.get(&token_in_node),
297 );
298
299 match output_price {
300 Some(price) if !price.denominator.is_zero() => {
301 let gas_cost = &gas_cost_wei * &price.numerator / &price.denominator;
302 BigInt::from(amount_out.clone()) - BigInt::from(gas_cost)
303 }
304 _ => {
305 warn!("no gas price for output token, returning gross amount_out");
306 BigInt::from(amount_out.clone())
307 }
308 }
309 }
310}
311
312impl Algorithm for BellmanFordAlgorithm {
313 type GraphType = StableDiGraph<()>;
314 type GraphManager = PetgraphStableDiGraphManager<()>;
315
316 fn name(&self) -> &str {
317 "bellman_ford"
318 }
319
320 #[instrument(level = "debug", skip_all, fields(order_id = %order.id()))]
321 async fn find_best_route(
322 &self,
323 graph: &Self::GraphType,
324 market: SharedMarketDataRef,
325 derived: Option<SharedDerivedDataRef>,
326 order: &Order,
327 ) -> Result<RouteResult, AlgorithmError> {
328 let start = Instant::now();
329
330 if !order.is_sell() {
331 return Err(AlgorithmError::ExactOutNotSupported);
332 }
333
334 let (token_prices, spot_prices) = if let Some(ref derived) = derived {
335 let guard = derived.read().await;
336 (guard.token_prices().cloned(), guard.spot_prices().cloned())
337 } else {
338 (None, None)
339 };
340
341 let token_in_node = graph
342 .node_indices()
343 .find(|&n| &graph[n] == order.token_in())
344 .ok_or(AlgorithmError::NoPath {
345 from: order.token_in().clone(),
346 to: order.token_out().clone(),
347 reason: NoPathReason::SourceTokenNotInGraph,
348 })?;
349 let token_out_node = graph
350 .node_indices()
351 .find(|&n| &graph[n] == order.token_out())
352 .ok_or(AlgorithmError::NoPath {
353 from: order.token_in().clone(),
354 to: order.token_out().clone(),
355 reason: NoPathReason::DestinationTokenNotInGraph,
356 })?;
357
358 let (adj, token_nodes, component_ids) =
360 Self::get_subgraph(graph, token_in_node, self.max_hops, order)?;
361
362 let (token_map, market_subset) = {
364 let market = market.read().await;
365
366 let token_map: HashMap<NodeIndex, Token> = token_nodes
367 .iter()
368 .filter_map(|&node| {
369 market
370 .get_token(&graph[node])
371 .cloned()
372 .map(|t| (node, t))
373 })
374 .collect();
375
376 let market_subset = market.extract_subset(&component_ids);
377
378 (token_map, market_subset)
379 };
380
381 debug!(
382 edges = adj
383 .values()
384 .map(Vec::len)
385 .sum::<usize>(),
386 tokens = token_map.len(),
387 "subgraph extracted"
388 );
389
390 let max_idx = graph
395 .node_indices()
396 .map(|n| n.index())
397 .max()
398 .unwrap_or(0) +
399 1;
400
401 let mut amount: Vec<BigUint> = vec![BigUint::ZERO; max_idx];
402 let mut predecessor: Vec<Option<(NodeIndex, ComponentId)>> = vec![None; max_idx];
403 let mut edge_gas: Vec<BigUint> = vec![BigUint::ZERO; max_idx];
404 let mut cumul_gas: Vec<BigUint> = vec![BigUint::ZERO; max_idx];
405
406 amount[token_in_node.index()] = order.amount().clone();
407
408 let gas_price_wei = market_subset
410 .gas_price()
411 .map(|gp| gp.effective_gas_price().clone());
412
413 let node_address: HashMap<NodeIndex, Address> = token_map
415 .iter()
416 .map(|(&node, token)| (node, token.address.clone()))
417 .collect();
418
419 let mut spot_product: Vec<f64> = vec![0.0; max_idx];
422 spot_product[token_in_node.index()] = 1.0;
423
424 let gas_aware = self.gas_aware && gas_price_wei.is_some() && token_prices.is_some();
425 if !gas_aware && self.gas_aware {
426 debug!("gas-aware comparison disabled (missing gas_price or token_prices)");
427 } else if !self.gas_aware {
428 debug!("gas-aware comparison disabled by config");
429 }
430
431 let mut active_nodes: Vec<NodeIndex> = vec![token_in_node];
432
433 for round in 0..self.max_hops {
434 if start.elapsed() >= self.timeout {
435 debug!(round, "timeout during relaxation");
436 break;
437 }
438 if active_nodes.is_empty() {
439 debug!(round, "no active nodes, stopping early");
440 break;
441 }
442
443 let mut next_active: HashSet<NodeIndex> = HashSet::new();
444
445 for &u in &active_nodes {
446 let u_idx = u.index();
447 if amount[u_idx].is_zero() {
448 continue;
449 }
450
451 let Some(token_u) = token_map.get(&u) else { continue };
452 let Some(edges) = adj.get(&u) else { continue };
453
454 for (v, component_id) in edges {
455 let v_idx = v.index();
456
457 if Self::path_has_conflict(u, *v, component_id, &predecessor) {
459 continue;
460 }
461
462 let Some(token_v) = token_map.get(v) else { continue };
463 let Some(sim) = market_subset.get_simulation_state(component_id) else {
464 continue;
465 };
466
467 let result = match sim.get_amount_out(amount[u_idx].clone(), token_u, token_v) {
468 Ok(r) => r,
469 Err(e) => {
470 trace!(
471 component_id,
472 error = %e,
473 "simulation failed, skipping edge"
474 );
475 continue;
476 }
477 };
478
479 let candidate_cumul_gas = &cumul_gas[u_idx] + &result.gas;
480
481 let candidate_spot = Self::compute_edge_spot_product(
484 spot_product[u_idx],
485 component_id,
486 node_address.get(&u),
487 node_address.get(v),
488 spot_prices.as_ref(),
489 );
490
491 let is_better = if gas_aware {
493 let v_price = Self::resolve_token_price(
494 node_address.get(v),
495 token_prices.as_ref(),
496 candidate_spot,
497 node_address.get(&token_in_node),
498 );
499
500 let net_candidate = Self::gas_adjusted_amount(
501 &result.amount,
502 &candidate_cumul_gas,
503 gas_price_wei.as_ref().unwrap(),
504 v_price.as_ref(),
505 );
506 let net_existing = Self::gas_adjusted_amount(
507 &amount[v_idx],
508 &cumul_gas[v_idx],
509 gas_price_wei.as_ref().unwrap(),
510 v_price.as_ref(),
511 );
512 net_candidate > net_existing
513 } else {
514 result.amount > amount[v_idx]
515 };
516
517 if is_better {
518 spot_product[v_idx] = candidate_spot;
519 amount[v_idx] = result.amount;
520 predecessor[v_idx] = Some((u, component_id.clone()));
521 edge_gas[v_idx] = result.gas;
522 cumul_gas[v_idx] = candidate_cumul_gas;
523 next_active.insert(*v);
524 }
525 }
526 }
527
528 active_nodes = next_active.into_iter().collect();
529 }
530
531 let out_idx = token_out_node.index();
533 if amount[out_idx].is_zero() {
534 return Err(AlgorithmError::NoPath {
535 from: order.token_in().clone(),
536 to: order.token_out().clone(),
537 reason: NoPathReason::NoGraphPath,
538 });
539 }
540
541 let path_edges = Self::reconstruct_path(token_out_node, token_in_node, &predecessor)?;
545
546 let mut swaps = Vec::with_capacity(path_edges.len());
547 for (from_node, to_node, component_id) in &path_edges {
548 let token_in = token_map
549 .get(from_node)
550 .ok_or_else(|| AlgorithmError::DataNotFound {
551 kind: "token",
552 id: Some(format!("{:?}", from_node)),
553 })?;
554 let token_out = token_map
555 .get(to_node)
556 .ok_or_else(|| AlgorithmError::DataNotFound {
557 kind: "token",
558 id: Some(format!("{:?}", to_node)),
559 })?;
560 let component = market_subset
561 .get_component(component_id)
562 .ok_or_else(|| AlgorithmError::DataNotFound {
563 kind: "component",
564 id: Some(component_id.clone()),
565 })?;
566 let sim_state = market_subset
567 .get_simulation_state(component_id)
568 .ok_or_else(|| AlgorithmError::DataNotFound {
569 kind: "simulation state",
570 id: Some(component_id.clone()),
571 })?;
572
573 swaps.push(Swap::new(
574 component_id.clone(),
575 component.protocol_system.clone(),
576 token_in.address.clone(),
577 token_out.address.clone(),
578 amount[from_node.index()].clone(),
579 amount[to_node.index()].clone(),
580 edge_gas[to_node.index()].clone(),
581 component.clone(),
582 sim_state.clone_box(),
583 ));
584 }
585
586 let route = Route::new(swaps);
587 let final_amount_out = amount[out_idx].clone();
588
589 let gas_price = gas_price_wei.unwrap_or_default();
590
591 let net_amount_out = Self::compute_net_amount_out(
592 &final_amount_out,
593 &route,
594 &gas_price,
595 token_prices.as_ref(),
596 &spot_product,
597 &node_address,
598 token_in_node,
599 );
600
601 let result = RouteResult::new(route, net_amount_out, gas_price);
602
603 let solve_time_ms = start.elapsed().as_millis() as u64;
604 debug!(
605 solve_time_ms,
606 hops = result.route().swaps().len(),
607 amount_in = %order.amount(),
608 amount_out = %final_amount_out,
609 net_amount_out = %result.net_amount_out(),
610 "bellman_ford route found"
611 );
612
613 Ok(result)
614 }
615
616 fn computation_requirements(&self) -> ComputationRequirements {
617 ComputationRequirements::none()
621 .allow_stale("token_prices")
622 .expect("token_prices requirement conflicts (bug)")
623 .allow_stale("spot_prices")
624 .expect("spot_prices requirement conflicts (bug)")
625 }
626
627 fn timeout(&self) -> Duration {
628 self.timeout
629 }
630}
631
632#[cfg(test)]
633mod tests {
634 use std::sync::Arc;
635
636 use num_bigint::BigInt;
637 use tokio::sync::RwLock;
638 use tycho_simulation::{
639 tycho_common::{models::Address, simulation::protocol_sim::ProtocolSim},
640 tycho_ethereum::gas::{BlockGasPrice, GasPrice},
641 };
642
643 use super::*;
644 use crate::{
645 algorithm::test_utils::{component, order, token, MockProtocolSim},
646 derived::{types::TokenGasPrices, DerivedData},
647 feed::market_data::SharedMarketData,
648 graph::GraphManager,
649 types::quote::OrderSide,
650 };
651
652 fn setup_market_bf(
656 pools: Vec<(&str, &Token, &Token, MockProtocolSim)>,
657 ) -> (Arc<RwLock<SharedMarketData>>, PetgraphStableDiGraphManager<()>) {
658 let mut market = SharedMarketData::new();
659
660 market.update_gas_price(BlockGasPrice {
661 block_number: 1,
662 block_hash: Default::default(),
663 block_timestamp: 0,
664 pricing: GasPrice::Legacy { gas_price: BigUint::from(100u64) },
665 });
666 market.update_last_updated(crate::types::BlockInfo::new(1, "0x00".into(), 0));
667
668 for (pool_id, token_in, token_out, state) in pools {
669 let tokens = vec![token_in.clone(), token_out.clone()];
670 let comp = component(pool_id, &tokens);
671 market.upsert_components(std::iter::once(comp));
672 market.update_states([(pool_id.to_string(), Box::new(state) as Box<dyn ProtocolSim>)]);
673 market.upsert_tokens(tokens);
674 }
675
676 let mut graph_manager = PetgraphStableDiGraphManager::default();
677 graph_manager.initialize_graph(&market.component_topology());
678
679 (Arc::new(RwLock::new(market)), graph_manager)
680 }
681
682 fn setup_derived_with_token_prices(
683 token_addresses: &[Address],
684 ) -> crate::derived::SharedDerivedDataRef {
685 use tycho_simulation::tycho_core::simulation::protocol_sim::Price;
686
687 let mut token_prices: TokenGasPrices = HashMap::new();
688 for address in token_addresses {
689 token_prices.insert(
690 address.clone(),
691 Price { numerator: BigUint::from(1u64), denominator: BigUint::from(1u64) },
692 );
693 }
694
695 let mut derived_data = DerivedData::new();
696 derived_data.set_token_prices(token_prices, vec![], 1, true);
697 Arc::new(RwLock::new(derived_data))
698 }
699
700 fn bf_algorithm(max_hops: usize, timeout_ms: u64) -> BellmanFordAlgorithm {
701 BellmanFordAlgorithm::with_config(
702 AlgorithmConfig::new(1, max_hops, Duration::from_millis(timeout_ms), None).unwrap(),
703 )
704 .unwrap()
705 }
706
707 #[tokio::test]
710 async fn test_linear_path_found() {
711 let token_a = token(0x01, "A");
712 let token_b = token(0x02, "B");
713 let token_c = token(0x03, "C");
714 let token_d = token(0x04, "D");
715
716 let (market, manager) = setup_market_bf(vec![
717 ("pool_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
718 ("pool_bc", &token_b, &token_c, MockProtocolSim::new(3.0)),
719 ("pool_cd", &token_c, &token_d, MockProtocolSim::new(4.0)),
720 ]);
721
722 let algo = bf_algorithm(4, 1000);
723 let ord = order(&token_a, &token_d, 100, OrderSide::Sell);
724
725 let result = algo
726 .find_best_route(manager.graph(), market, None, &ord)
727 .await
728 .unwrap();
729
730 assert_eq!(result.route().swaps().len(), 3);
731 assert_eq!(result.route().swaps()[0].amount_out(), &BigUint::from(200u64));
733 assert_eq!(result.route().swaps()[1].amount_out(), &BigUint::from(600u64));
734 assert_eq!(result.route().swaps()[2].amount_out(), &BigUint::from(2400u64));
735 }
736
737 #[tokio::test]
738 async fn test_picks_better_of_two_paths() {
739 let token_a = token(0x01, "A");
741 let token_b = token(0x02, "B");
742 let token_c = token(0x03, "C");
743 let token_d = token(0x04, "D");
744
745 let (market, manager) = setup_market_bf(vec![
746 ("pool_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
747 ("pool_bd", &token_b, &token_d, MockProtocolSim::new(3.0)),
748 ("pool_ac", &token_a, &token_c, MockProtocolSim::new(4.0)),
749 ("pool_cd", &token_c, &token_d, MockProtocolSim::new(1.0)),
750 ]);
751
752 let algo = bf_algorithm(3, 1000);
753 let ord = order(&token_a, &token_d, 100, OrderSide::Sell);
754
755 let result = algo
756 .find_best_route(manager.graph(), market, None, &ord)
757 .await
758 .unwrap();
759
760 assert_eq!(result.route().swaps().len(), 2);
762 assert_eq!(result.route().swaps()[0].component_id(), "pool_ab");
763 assert_eq!(result.route().swaps()[1].component_id(), "pool_bd");
764 assert_eq!(result.route().swaps()[1].amount_out(), &BigUint::from(600u64));
765 }
766
767 #[tokio::test]
768 async fn test_parallel_pools() {
769 let token_a = token(0x01, "A");
771 let token_b = token(0x02, "B");
772
773 let (market, manager) = setup_market_bf(vec![
774 ("pool1", &token_a, &token_b, MockProtocolSim::new(2.0)),
775 ("pool2", &token_a, &token_b, MockProtocolSim::new(5.0)),
776 ]);
777
778 let algo = bf_algorithm(2, 1000);
779 let ord = order(&token_a, &token_b, 100, OrderSide::Sell);
780
781 let result = algo
782 .find_best_route(manager.graph(), market, None, &ord)
783 .await
784 .unwrap();
785
786 assert_eq!(result.route().swaps().len(), 1);
787 assert_eq!(result.route().swaps()[0].component_id(), "pool2");
788 assert_eq!(result.route().swaps()[0].amount_out(), &BigUint::from(500u64));
789 }
790
791 #[tokio::test]
792 async fn test_no_path_returns_error() {
793 let token_a = token(0x01, "A");
794 let token_b = token(0x02, "B");
795 let token_c = token(0x03, "C");
796
797 let (market, manager) =
799 setup_market_bf(vec![("pool_ab", &token_a, &token_b, MockProtocolSim::new(2.0))]);
800
801 {
803 let mut m = market.write().await;
804 m.upsert_tokens(vec![token_c.clone()]);
805 }
806
807 let algo = bf_algorithm(3, 1000);
808 let ord = order(&token_a, &token_c, 100, OrderSide::Sell);
809
810 let result = algo
811 .find_best_route(manager.graph(), market, None, &ord)
812 .await;
813 assert!(matches!(result, Err(AlgorithmError::NoPath { .. })));
814 }
815
816 #[tokio::test]
817 async fn test_source_not_in_graph() {
818 let token_a = token(0x01, "A");
819 let token_b = token(0x02, "B");
820 let token_x = token(0x99, "X");
821
822 let (market, manager) =
823 setup_market_bf(vec![("pool_ab", &token_a, &token_b, MockProtocolSim::new(2.0))]);
824
825 let algo = bf_algorithm(3, 1000);
826 let ord = order(&token_x, &token_b, 100, OrderSide::Sell);
827
828 let result = algo
829 .find_best_route(manager.graph(), market, None, &ord)
830 .await;
831 assert!(matches!(
832 result,
833 Err(AlgorithmError::NoPath { reason: NoPathReason::SourceTokenNotInGraph, .. })
834 ));
835 }
836
837 #[tokio::test]
838 async fn test_destination_not_in_graph() {
839 let token_a = token(0x01, "A");
840 let token_b = token(0x02, "B");
841 let token_x = token(0x99, "X");
842
843 let (market, manager) =
844 setup_market_bf(vec![("pool_ab", &token_a, &token_b, MockProtocolSim::new(2.0))]);
845
846 let algo = bf_algorithm(3, 1000);
847 let ord = order(&token_a, &token_x, 100, OrderSide::Sell);
848
849 let result = algo
850 .find_best_route(manager.graph(), market, None, &ord)
851 .await;
852 assert!(matches!(
853 result,
854 Err(AlgorithmError::NoPath { reason: NoPathReason::DestinationTokenNotInGraph, .. })
855 ));
856 }
857
858 #[tokio::test]
859 async fn test_respects_max_hops() {
860 let token_a = token(0x01, "A");
862 let token_b = token(0x02, "B");
863 let token_c = token(0x03, "C");
864 let token_d = token(0x04, "D");
865
866 let (market, manager) = setup_market_bf(vec![
867 ("pool_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
868 ("pool_bc", &token_b, &token_c, MockProtocolSim::new(3.0)),
869 ("pool_cd", &token_c, &token_d, MockProtocolSim::new(4.0)),
870 ]);
871
872 let algo = bf_algorithm(2, 1000);
873 let ord = order(&token_a, &token_d, 100, OrderSide::Sell);
874
875 let result = algo
876 .find_best_route(manager.graph(), market, None, &ord)
877 .await;
878 assert!(
879 matches!(result, Err(AlgorithmError::NoPath { .. })),
880 "Should not find 3-hop path with max_hops=2"
881 );
882 }
883
884 #[tokio::test]
885 async fn test_source_token_revisit_blocked() {
886 let token_a = token(0x01, "A");
889 let token_b = token(0x02, "B");
890 let token_c = token(0x03, "C");
891
892 let (market, manager) = setup_market_bf(vec![
893 ("pool_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
894 ("pool_bc", &token_b, &token_c, MockProtocolSim::new(3.0)),
895 ]);
896
897 let algo = bf_algorithm(4, 1000);
898 let ord = order(&token_a, &token_c, 100, OrderSide::Sell);
899
900 let result = algo
901 .find_best_route(manager.graph(), market, None, &ord)
902 .await
903 .unwrap();
904
905 assert_eq!(result.route().swaps().len(), 2);
907 assert_eq!(result.route().swaps()[0].component_id(), "pool_ab");
908 assert_eq!(result.route().swaps()[1].component_id(), "pool_bc");
909 assert_eq!(result.route().swaps()[1].amount_out(), &BigUint::from(600u64));
910 }
911
912 #[tokio::test]
913 async fn test_hub_token_revisit_blocked() {
914 let token_a = token(0x01, "A");
917 let token_c = token(0x02, "C");
918 let token_b = token(0x03, "B");
919 let token_d = token(0x04, "D");
920
921 let (market, manager) = setup_market_bf(vec![
922 ("pool_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
923 ("pool_bc", &token_b, &token_c, MockProtocolSim::new(3.0)),
924 ("pool_cb", &token_c, &token_b, MockProtocolSim::new(100.0)),
925 ("pool_bd", &token_b, &token_d, MockProtocolSim::new(2.0)),
926 ]);
927
928 let algo = bf_algorithm(4, 1000);
929 let ord = order(&token_a, &token_d, 100, OrderSide::Sell);
930
931 let result = algo
932 .find_best_route(manager.graph(), market, None, &ord)
933 .await
934 .unwrap();
935
936 assert_eq!(result.route().swaps().len(), 2, "should use direct 2-hop path");
939 assert_eq!(result.route().swaps()[0].component_id(), "pool_ab");
940 assert_eq!(result.route().swaps()[1].component_id(), "pool_bd");
941 assert_eq!(result.route().swaps()[1].amount_out(), &BigUint::from(400u64));
942 }
943
944 #[tokio::test]
945 async fn test_route_amounts_are_sequential() {
946 let token_a = token(0x01, "A");
948 let token_b = token(0x02, "B");
949 let token_c = token(0x03, "C");
950
951 let (market, manager) = setup_market_bf(vec![
952 ("pool_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
953 ("pool_bc", &token_b, &token_c, MockProtocolSim::new(3.0)),
954 ]);
955
956 let algo = bf_algorithm(3, 1000);
957 let ord = order(&token_a, &token_c, 100, OrderSide::Sell);
958
959 let result = algo
960 .find_best_route(manager.graph(), market, None, &ord)
961 .await
962 .unwrap();
963
964 assert_eq!(result.route().swaps().len(), 2);
965 assert_eq!(result.route().swaps()[1].amount_in(), result.route().swaps()[0].amount_out());
967 }
968
969 #[tokio::test]
970 async fn test_gas_deduction() {
971 let token_a = token(0x01, "A");
972 let token_b = token(0x02, "B");
973
974 let (market, manager) = setup_market_bf(vec![(
975 "pool1",
976 &token_a,
977 &token_b,
978 MockProtocolSim::new(2.0).with_gas(10),
979 )]);
980
981 let algo = bf_algorithm(2, 1000);
982 let ord = order(&token_a, &token_b, 1000, OrderSide::Sell);
983
984 let derived = setup_derived_with_token_prices(std::slice::from_ref(&token_b.address));
985
986 let result = algo
987 .find_best_route(manager.graph(), market, Some(derived), &ord)
988 .await
989 .unwrap();
990
991 assert_eq!(result.route().swaps()[0].amount_out(), &BigUint::from(2000u64));
995 assert_eq!(result.net_amount_out(), &BigInt::from(1000));
996 }
997
998 #[tokio::test]
999 async fn test_timeout_respected() {
1000 let token_a = token(0x01, "A");
1001 let token_b = token(0x02, "B");
1002 let token_c = token(0x03, "C");
1003
1004 let (market, manager) = setup_market_bf(vec![
1005 ("pool_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
1006 ("pool_bc", &token_b, &token_c, MockProtocolSim::new(3.0)),
1007 ]);
1008
1009 let algo = bf_algorithm(3, 0);
1011 let ord = order(&token_a, &token_c, 100, OrderSide::Sell);
1012
1013 let result = algo
1014 .find_best_route(manager.graph(), market, None, &ord)
1015 .await;
1016
1017 match result {
1022 Ok(r) => {
1023 assert!(!r.route().swaps().is_empty());
1024 }
1025 Err(AlgorithmError::Timeout { .. }) | Err(AlgorithmError::NoPath { .. }) => {
1026 }
1028 Err(e) => panic!("Unexpected error: {:?}", e),
1029 }
1030 }
1031
1032 #[tokio::test]
1035 async fn test_with_fees() {
1036 let token_a = token(0x01, "A");
1037 let token_b = token(0x02, "B");
1038
1039 let (market, manager) = setup_market_bf(vec![(
1041 "pool1",
1042 &token_a,
1043 &token_b,
1044 MockProtocolSim::new(2.0).with_fee(0.1),
1045 )]);
1046
1047 let algo = bf_algorithm(2, 1000);
1048 let ord = order(&token_a, &token_b, 1000, OrderSide::Sell);
1049
1050 let result = algo
1051 .find_best_route(manager.graph(), market, None, &ord)
1052 .await
1053 .unwrap();
1054
1055 assert_eq!(result.route().swaps()[0].amount_out(), &BigUint::from(1800u64));
1057 }
1058
1059 #[tokio::test]
1060 async fn test_large_trade_slippage() {
1061 let token_a = token(0x01, "A");
1062 let token_b = token(0x02, "B");
1063
1064 let (market, manager) = setup_market_bf(vec![(
1066 "pool1",
1067 &token_a,
1068 &token_b,
1069 MockProtocolSim::new(2.0).with_liquidity(500),
1070 )]);
1071
1072 let algo = bf_algorithm(2, 1000);
1073 let ord = order(&token_a, &token_b, 1000, OrderSide::Sell);
1074
1075 let result = algo
1077 .find_best_route(manager.graph(), market, None, &ord)
1078 .await;
1079 assert!(
1080 matches!(result, Err(AlgorithmError::NoPath { .. })),
1081 "Should fail when trade exceeds pool liquidity"
1082 );
1083 }
1084
1085 #[tokio::test]
1086 async fn test_disconnected_tokens_return_no_path() {
1087 let token_a = token(0x01, "A");
1089 let token_b = token(0x02, "B");
1090 let token_d = token(0x04, "D");
1091 let token_e = token(0x05, "E");
1092
1093 let (market, manager) = setup_market_bf(vec![
1094 ("pool_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
1095 ("pool_de", &token_d, &token_e, MockProtocolSim::new(4.0)),
1096 ]);
1097
1098 let algo = bf_algorithm(3, 1000);
1099 let ord = order(&token_a, &token_e, 100, OrderSide::Sell);
1100
1101 let result = algo
1102 .find_best_route(manager.graph(), market, None, &ord)
1103 .await;
1104 assert!(
1105 matches!(result, Err(AlgorithmError::NoPath { .. })),
1106 "should not find path to disconnected component"
1107 );
1108 }
1109
1110 #[tokio::test]
1111 async fn test_spfa_skips_failed_simulations() {
1112 let token_a = token(0x01, "A");
1114 let token_b = token(0x02, "B");
1115 let token_c = token(0x03, "C");
1116
1117 let (market, manager) = setup_market_bf(vec![
1118 ("pool_ab_bad", &token_a, &token_b, MockProtocolSim::new(2.0).with_liquidity(0)),
1120 ("pool_ac", &token_a, &token_c, MockProtocolSim::new(2.0)),
1122 ("pool_cb", &token_c, &token_b, MockProtocolSim::new(3.0)),
1123 ]);
1124
1125 let algo = bf_algorithm(3, 1000);
1126 let ord = order(&token_a, &token_b, 100, OrderSide::Sell);
1127
1128 let result = algo
1129 .find_best_route(manager.graph(), market, None, &ord)
1130 .await;
1131
1132 match result {
1136 Ok(r) => {
1137 assert!(!r.route().swaps().is_empty());
1139 }
1140 Err(AlgorithmError::NoPath { .. }) => {
1141 }
1144 Err(e) => panic!("Unexpected error: {:?}", e),
1145 }
1146 }
1147
1148 #[tokio::test]
1149 async fn test_resimulation_produces_correct_amounts() {
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, &ord)
1165 .await
1166 .unwrap();
1167
1168 assert_eq!(result.route().swaps()[0].amount_in(), &BigUint::from(100u64));
1171 assert_eq!(result.route().swaps()[0].amount_out(), &BigUint::from(200u64));
1172 assert_eq!(result.route().swaps()[1].amount_in(), &BigUint::from(200u64));
1173 assert_eq!(result.route().swaps()[1].amount_out(), &BigUint::from(600u64));
1174 }
1175
1176 #[test]
1179 fn algorithm_name() {
1180 let algo = bf_algorithm(4, 200);
1181 assert_eq!(algo.name(), "bellman_ford");
1182 }
1183
1184 #[test]
1185 fn algorithm_timeout() {
1186 let algo = bf_algorithm(4, 200);
1187 assert_eq!(algo.timeout(), Duration::from_millis(200));
1188 }
1189
1190 #[tokio::test]
1193 async fn test_gas_aware_relaxation_picks_cheaper_path() {
1194 let token_a = token(0x01, "A");
1209 let token_b = token(0x02, "B");
1210 let token_c = token(0x03, "C");
1211 let token_d = token(0x04, "D");
1212
1213 let high_gas: u64 = 100_000_000;
1214 let low_gas: u64 = 100;
1215
1216 let (market, manager) = setup_market_bf(vec![
1217 ("pool_ab", &token_a, &token_b, MockProtocolSim::new(3.0).with_gas(high_gas)),
1218 ("pool_bd", &token_b, &token_d, MockProtocolSim::new(2.0).with_gas(high_gas)),
1219 ("pool_ac", &token_a, &token_c, MockProtocolSim::new(2.0).with_gas(low_gas)),
1220 ("pool_cd", &token_c, &token_d, MockProtocolSim::new(2.0).with_gas(low_gas)),
1221 ]);
1222
1223 let algo = bf_algorithm(3, 1000);
1224 let ord = order(&token_a, &token_d, 1_000_000_000, OrderSide::Sell);
1225
1226 let derived = setup_derived_with_token_prices(&[
1228 token_a.address.clone(),
1229 token_b.address.clone(),
1230 token_c.address.clone(),
1231 token_d.address.clone(),
1232 ]);
1233
1234 let result = algo
1235 .find_best_route(manager.graph(), market, Some(derived), &ord)
1236 .await
1237 .unwrap();
1238
1239 assert_eq!(result.route().swaps().len(), 2);
1241 assert_eq!(result.route().swaps()[0].component_id(), "pool_ac");
1242 assert_eq!(result.route().swaps()[1].component_id(), "pool_cd");
1243 }
1244
1245 #[tokio::test]
1246 async fn test_gas_aware_falls_back_to_gross_without_derived() {
1247 let token_a = token(0x01, "A");
1250 let token_b = token(0x02, "B");
1251 let token_c = token(0x03, "C");
1252 let token_d = token(0x04, "D");
1253
1254 let high_gas: u64 = 100_000_000;
1255 let low_gas: u64 = 100;
1256
1257 let (market, manager) = setup_market_bf(vec![
1258 ("pool_ab", &token_a, &token_b, MockProtocolSim::new(3.0).with_gas(high_gas)),
1259 ("pool_bd", &token_b, &token_d, MockProtocolSim::new(2.0).with_gas(high_gas)),
1260 ("pool_ac", &token_a, &token_c, MockProtocolSim::new(2.0).with_gas(low_gas)),
1261 ("pool_cd", &token_c, &token_d, MockProtocolSim::new(2.0).with_gas(low_gas)),
1262 ]);
1263
1264 let algo = bf_algorithm(3, 1000);
1265 let ord = order(&token_a, &token_d, 1_000_000_000, OrderSide::Sell);
1266
1267 let result = algo
1269 .find_best_route(manager.graph(), market, None, &ord)
1270 .await
1271 .unwrap();
1272
1273 assert_eq!(result.route().swaps().len(), 2);
1275 assert_eq!(result.route().swaps()[0].component_id(), "pool_ab");
1276 assert_eq!(result.route().swaps()[1].component_id(), "pool_bd");
1277 }
1278
1279 #[test]
1280 fn test_path_has_conflict_detects_node_and_pool() {
1281 let mut pred: Vec<Option<(NodeIndex, ComponentId)>> = vec![None; 4];
1283 pred[1] = Some((NodeIndex::new(0), "pool_a".into()));
1284 pred[2] = Some((NodeIndex::new(1), "pool_b".into()));
1285
1286 assert!(BellmanFordAlgorithm::path_has_conflict(
1288 NodeIndex::new(2),
1289 NodeIndex::new(0),
1290 &"any".into(),
1291 &pred
1292 ));
1293 assert!(!BellmanFordAlgorithm::path_has_conflict(
1294 NodeIndex::new(2),
1295 NodeIndex::new(3),
1296 &"any".into(),
1297 &pred
1298 ));
1299 assert!(BellmanFordAlgorithm::path_has_conflict(
1301 NodeIndex::new(2),
1302 NodeIndex::new(2),
1303 &"any".into(),
1304 &pred
1305 ));
1306
1307 assert!(BellmanFordAlgorithm::path_has_conflict(
1309 NodeIndex::new(2),
1310 NodeIndex::new(3),
1311 &"pool_a".into(),
1312 &pred
1313 ));
1314 assert!(BellmanFordAlgorithm::path_has_conflict(
1315 NodeIndex::new(2),
1316 NodeIndex::new(3),
1317 &"pool_b".into(),
1318 &pred
1319 ));
1320 assert!(!BellmanFordAlgorithm::path_has_conflict(
1321 NodeIndex::new(2),
1322 NodeIndex::new(3),
1323 &"pool_c".into(),
1324 &pred
1325 ));
1326 }
1327}