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