use std::{
collections::VecDeque,
sync::Arc,
time::{Duration, Instant},
};
use num_bigint::{BigInt, BigUint};
use num_traits::{ToPrimitive, Zero};
use petgraph::{graph::NodeIndex, prelude::EdgeRef, stable_graph::EdgeReference};
use rustc_hash::{FxHashMap, FxHashSet};
use tracing::{debug, instrument, trace, warn};
use tycho_simulation::{
tycho_common::models::Address,
tycho_core::{models::token::Token, simulation::protocol_sim::Price},
};
use super::{
split_primitives::MarketOverrides, Algorithm, AlgorithmConfig, AlgorithmError, NoPathReason,
};
use crate::{
algorithm::{
paths,
request::{SolveParts, SolveRequest},
sim_guard::GuardedProtocolSim,
},
derived::{
computation::ComputationRequirements,
types::{SpotPrices, TokenGasPrices},
},
feed::market_data::{MarketData, MarketState},
graph::{petgraph::StableDiGraph, EdgeData, PetgraphStableDiGraphManager},
types::{ComponentId, Order, Route, RouteExclusions, RouteResult, Swap},
};
struct Subgraph<'a> {
adjacency: FxHashMap<NodeIndex, Vec<(NodeIndex, ComponentId)>>,
token_nodes: FxHashSet<NodeIndex>,
component_ids: FxHashSet<&'a ComponentId>,
}
pub(crate) struct BellmanFordContext {
pub(crate) token_in_node: NodeIndex,
pub(crate) token_out_node: Option<NodeIndex>,
pub(crate) adj: FxHashMap<NodeIndex, Vec<(NodeIndex, ComponentId)>>,
pub(crate) token_map: FxHashMap<NodeIndex, Arc<Token>>,
pub(crate) market_data: MarketState,
pub(crate) gas_price_wei: Option<BigUint>,
pub(crate) token_prices: Option<TokenGasPrices>,
pub(crate) spot_prices: Option<SpotPrices>,
pub(crate) node_address: FxHashMap<NodeIndex, Address>,
pub(crate) max_idx: usize,
pub(crate) scoring: RouteScoringMode,
}
impl BellmanFordContext {
pub(crate) fn reroot_toward<'a>(
&mut self,
graph: &'a StableDiGraph<()>,
token_in_node: NodeIndex,
token_out_node: NodeIndex,
hops_to_token_out: &FxHashMap<NodeIndex, usize>,
max_hops: usize,
) -> Option<FxHashSet<&'a ComponentId>> {
let subgraph = BellmanFordAlgorithm::get_subgraph_with_hop_map(
graph,
(token_in_node, Some(token_out_node)),
Some(hops_to_token_out),
max_hops,
&RouteExclusions::default(),
)?;
self.adj = subgraph.adjacency;
self.token_in_node = token_in_node;
self.token_out_node = Some(token_out_node);
Some(subgraph.component_ids)
}
}
pub(crate) struct ReachOutcome {
pub(crate) reached: FxHashMap<Address, ReachedToken>,
pub(crate) timed_out: bool,
}
pub(crate) struct ReachedToken {
pub(crate) amount_out: BigUint,
pub(crate) components: Vec<ComponentId>,
}
pub(crate) enum RouteScoringMode {
GrossOutput,
NetOutput,
}
#[derive(Default)]
pub(crate) struct FindRouteOptions {
pub(crate) overrides: MarketOverrides,
}
struct SPFAResult {
amount: Vec<BigUint>,
predecessor: Vec<Option<(NodeIndex, ComponentId)>>,
edge_gas: Vec<BigUint>,
spot_product: Vec<f64>,
input_below_hop_gas: bool,
timed_out: bool,
}
pub struct BellmanFordAlgorithm {
max_hops: usize,
timeout: Duration,
gas_aware: bool,
connector_tokens: Option<FxHashSet<Address>>,
}
impl Default for BellmanFordAlgorithm {
fn default() -> Self {
Self::with_config(AlgorithmConfig::default())
}
}
impl BellmanFordAlgorithm {
pub(crate) fn with_config(config: AlgorithmConfig) -> Self {
Self {
max_hops: config.max_hops(),
timeout: config.timeout(),
gas_aware: config.gas_aware(),
connector_tokens: config.connector_tokens().cloned(),
}
}
pub(crate) fn max_hops(&self) -> usize {
self.max_hops
}
pub(crate) async fn build_context(
&self,
request: SolveRequest<'_, StableDiGraph<()>>,
) -> Result<BellmanFordContext, AlgorithmError> {
let SolveParts { graph, order, market, label, derived, exclusions } = request.into_parts();
if !order.is_sell() {
return Err(AlgorithmError::ExactOutNotSupported);
}
let (token_prices, spot_prices) = if let Some(ref d) = derived {
let guard = d.read().await;
(guard.token_prices().cloned(), guard.spot_prices().cloned())
} else {
(None, None)
};
let token_in_node = graph
.node_indices()
.find(|&n| &graph[n] == order.token_in())
.ok_or(AlgorithmError::NoPath {
from: order.token_in().clone(),
to: order.token_out().clone(),
reason: NoPathReason::SourceTokenNotInGraph,
})?;
let token_out_node = graph
.node_indices()
.find(|&n| &graph[n] == order.token_out())
.ok_or(AlgorithmError::NoPath {
from: order.token_in().clone(),
to: order.token_out().clone(),
reason: NoPathReason::DestinationTokenNotInGraph,
})?;
if token_in_node == token_out_node {
return Err(AlgorithmError::NoPath {
from: order.token_in().clone(),
to: order.token_out().clone(),
reason: NoPathReason::NoGraphPath,
});
}
let subgraph =
Self::get_subgraph(graph, token_in_node, token_out_node, self.max_hops, &exclusions)
.ok_or_else(|| AlgorithmError::NoPath {
from: order.token_in().clone(),
to: order.token_out().clone(),
reason: NoPathReason::NoGraphPath,
})?;
let market_view = paths::read_market(&market, label).await?;
let market_data = market_view.extract_subset_with_overlay(&subgraph.component_ids);
drop(market_view);
let mut ctx = self.context_from_snapshot(
graph,
market_data,
subgraph,
token_in_node,
Some(token_out_node),
);
ctx.token_prices = token_prices;
ctx.spot_prices = spot_prices;
Ok(ctx)
}
pub(crate) async fn build_context_from_source_token(
&self,
graph: &StableDiGraph<()>,
market: MarketData,
token_in: &Address,
walk_hops: usize,
prune_toward: Option<&FxHashSet<Address>>,
) -> Option<BellmanFordContext> {
let mut token_in_node = None;
let mut target_nodes = Vec::new();
for node in graph.node_indices() {
let address = &graph[node];
if address == token_in {
token_in_node = Some(node);
}
if prune_toward.is_some_and(|targets| targets.contains(address)) {
target_nodes.push(node);
}
}
let token_in_node = token_in_node?;
let exclusions = RouteExclusions::default();
let hops_to_targets = prune_toward.map(|_| {
Self::get_hops_to_reach_any(
graph,
target_nodes,
(token_in_node, token_in_node),
walk_hops,
&exclusions,
)
});
let subgraph = Self::get_subgraph_with_hop_map(
graph,
(token_in_node, None),
hops_to_targets.as_ref(),
walk_hops,
&exclusions,
)?;
let market_data = market
.extract_subset_batched(&subgraph.component_ids)
.await;
Some(self.context_from_snapshot(graph, market_data, subgraph, token_in_node, None))
}
fn context_from_snapshot(
&self,
graph: &StableDiGraph<()>,
market_data: MarketState,
subgraph: Subgraph<'_>,
token_in_node: NodeIndex,
token_out_node: Option<NodeIndex>,
) -> BellmanFordContext {
let Subgraph { adjacency: adj, token_nodes, component_ids: _ } = subgraph;
let token_map: FxHashMap<NodeIndex, Arc<Token>> = token_nodes
.iter()
.filter_map(|&node| {
market_data
.get_token_shared(&graph[node])
.map(|token| (node, Arc::clone(token)))
})
.collect();
let gas_price_wei = market_data
.gas_price()
.map(|gp| gp.effective_gas_price().clone());
let node_address: FxHashMap<NodeIndex, Address> = token_map
.iter()
.map(|(&node, token)| (node, token.address.clone()))
.collect();
let max_idx = graph
.node_indices()
.map(|n| n.index())
.max()
.unwrap_or(0) +
1;
let scoring = if self.gas_aware {
RouteScoringMode::NetOutput
} else {
RouteScoringMode::GrossOutput
};
debug!(
edges = adj
.values()
.map(Vec::len)
.sum::<usize>(),
tokens = token_map.len(),
"subgraph extracted"
);
BellmanFordContext {
token_in_node,
token_out_node,
adj,
token_map,
market_data,
gas_price_wei,
token_prices: None,
spot_prices: None,
node_address,
max_idx,
scoring,
}
}
pub(crate) fn reach_from_source_token(
&self,
ctx: &BellmanFordContext,
amount_in: &BigUint,
) -> ReachOutcome {
let spfa = self.run_spfa(ctx, amount_in, &MarketOverrides::default(), Instant::now());
let mut reached = FxHashMap::default();
let mut dropped = 0usize;
for (idx, amount) in spfa.amount.iter().enumerate() {
if amount.is_zero() || idx == ctx.token_in_node.index() {
continue;
}
let node = NodeIndex::new(idx);
let Some(address) = ctx.node_address.get(&node) else {
trace!(node = idx, "destination dropped: no token metadata for node");
dropped += 1;
continue;
};
let path_edges = match Self::reconstruct_path(
node,
ctx.token_in_node,
&spfa.predecessor,
) {
Ok(path_edges) => path_edges,
Err(error) => {
trace!(node = idx, token = %address, %error, "destination dropped: path reconstruction failed");
dropped += 1;
continue;
}
};
let components = path_edges
.into_iter()
.map(|(_, _, component_id)| component_id)
.collect();
reached
.insert(address.clone(), ReachedToken { amount_out: amount.clone(), components });
}
debug!(
reached = reached.len(),
dropped,
timed_out = spfa.timed_out,
"found a route to every reachable destination from one relaxation"
);
ReachOutcome { reached, timed_out: spfa.timed_out }
}
pub(crate) fn find_single_route(
&self,
ctx: &BellmanFordContext,
order: &Order,
opts: FindRouteOptions,
) -> Result<RouteResult, AlgorithmError> {
let start = Instant::now();
let Some(token_out_node) = ctx.token_out_node else {
return Err(AlgorithmError::Other(
"find_single_route needs a context built with a destination".to_string(),
));
};
debug_assert!(
ctx.node_address.get(&ctx.token_in_node) == Some(order.token_in()) &&
ctx.node_address.get(&token_out_node) == Some(order.token_out()),
"context endpoints do not match the order's token pair"
);
let spfa = self.run_spfa(ctx, order.amount(), &opts.overrides, start);
let out_idx = token_out_node.index();
if spfa.amount[out_idx].is_zero() {
let reason = if spfa.input_below_hop_gas {
NoPathReason::AmountTooSmall
} else {
NoPathReason::NoGraphPath
};
return Err(AlgorithmError::NoPath {
from: order.token_in().clone(),
to: order.token_out().clone(),
reason,
});
}
let path_edges =
Self::reconstruct_path(token_out_node, ctx.token_in_node, &spfa.predecessor)?;
let route =
Self::build_route(ctx, &path_edges, &spfa.amount, &spfa.edge_gas, &opts.overrides)?;
let final_amount_out = spfa.amount[out_idx].clone();
let gas_price = ctx
.gas_price_wei
.clone()
.unwrap_or_default();
let net_amount_out = match ctx.scoring {
RouteScoringMode::GrossOutput => BigInt::from(final_amount_out.clone()),
RouteScoringMode::NetOutput => Self::compute_net_amount_out(
&final_amount_out,
&route,
&gas_price,
ctx.token_prices.as_ref(),
&spfa.spot_product,
&ctx.node_address,
ctx.token_in_node,
)?,
};
let result = RouteResult::new(route, net_amount_out, gas_price);
let solve_time_ms = start.elapsed().as_millis() as u64;
debug!(
solve_time_ms,
hops = result.route().swaps().len(),
amount_in = %order.amount(),
amount_out = %final_amount_out,
net_amount_out = %result.net_amount_out(),
"bellman_ford route found"
);
Ok(result)
}
fn run_spfa(
&self,
ctx: &BellmanFordContext,
amount_in: &BigUint,
overrides: &MarketOverrides,
start: Instant,
) -> SPFAResult {
let mut amount: Vec<BigUint> = vec![BigUint::ZERO; ctx.max_idx];
let mut predecessor: Vec<Option<(NodeIndex, ComponentId)>> = vec![None; ctx.max_idx];
let mut edge_gas: Vec<BigUint> = vec![BigUint::ZERO; ctx.max_idx];
let mut cumul_gas: Vec<BigUint> = vec![BigUint::ZERO; ctx.max_idx];
amount[ctx.token_in_node.index()] = amount_in.clone();
let mut spot_product: Vec<f64> = vec![0.0; ctx.max_idx];
spot_product[ctx.token_in_node.index()] = 1.0;
let mut input_below_hop_gas = false;
let gas_aware = matches!(ctx.scoring, RouteScoringMode::NetOutput) &&
ctx.gas_price_wei.is_some() &&
ctx.token_prices.is_some();
if !gas_aware && matches!(ctx.scoring, RouteScoringMode::NetOutput) {
debug!("gas-aware comparison disabled (missing gas_price or token_prices)");
} else if matches!(ctx.scoring, RouteScoringMode::GrossOutput) {
debug!("gas-aware comparison disabled by config");
}
let mut active_nodes: Vec<NodeIndex> = vec![ctx.token_in_node];
let mut timed_out = false;
for round in 0..self.max_hops {
if start.elapsed() >= self.timeout {
debug!(round, "timeout during relaxation");
timed_out = true;
break;
}
if active_nodes.is_empty() {
debug!(round, "no active nodes, stopping early");
break;
}
let mut next_active: FxHashSet<NodeIndex> = FxHashSet::default();
for &u in &active_nodes {
let u_idx = u.index();
if amount[u_idx].is_zero() {
continue;
}
let Some(token_u) = ctx.token_map.get(&u) else { continue };
let Some(edges) = ctx.adj.get(&u) else { continue };
for (v, component_id) in edges {
let v_idx = v.index();
if Self::path_has_conflict(u, *v, component_id, &predecessor) {
continue;
}
if !self.connector_allows(ctx, *v) {
continue;
}
let Some(token_v) = ctx.token_map.get(v) else { continue };
let sim: &dyn tycho_simulation::tycho_common::simulation::protocol_sim::ProtocolSim =
if let Some(s) = overrides.get(component_id) {
s
} else if let Some(s) = ctx.market_data.get_simulation_state(component_id) {
s
} else {
continue;
};
let result =
match sim.get_amount_out_guarded(amount[u_idx].clone(), token_u, token_v) {
Ok(r) => r,
Err(e) => {
trace!(
component_id,
error = %e,
"simulation failed, skipping edge"
);
continue;
}
};
let candidate_cumul_gas = &cumul_gas[u_idx] + &result.gas;
let candidate_spot = Self::compute_edge_spot_product(
spot_product[u_idx],
component_id,
ctx.node_address.get(&u),
ctx.node_address.get(v),
ctx.spot_prices.as_ref(),
);
let is_better = if gas_aware {
let v_price = Self::resolve_token_price(
ctx.node_address.get(v),
ctx.token_prices.as_ref(),
candidate_spot,
ctx.node_address.get(&ctx.token_in_node),
);
let net_candidate = Self::gas_adjusted_amount(
&result.amount,
&candidate_cumul_gas,
ctx.gas_price_wei.as_ref().unwrap(),
v_price.as_ref(),
);
if !input_below_hop_gas && net_candidate <= BigInt::ZERO {
let u_price = Self::resolve_token_price(
ctx.node_address.get(&u),
ctx.token_prices.as_ref(),
spot_product[u_idx],
ctx.node_address.get(&ctx.token_in_node),
);
if Self::gas_adjusted_amount(
&amount[u_idx],
&result.gas,
ctx.gas_price_wei.as_ref().unwrap(),
u_price.as_ref(),
) <= BigInt::ZERO
{
input_below_hop_gas = true;
}
}
let net_existing = Self::gas_adjusted_amount(
&amount[v_idx],
&cumul_gas[v_idx],
ctx.gas_price_wei.as_ref().unwrap(),
v_price.as_ref(),
);
net_candidate > net_existing
} else {
if result.amount.is_zero() {
input_below_hop_gas = true;
}
result.amount > amount[v_idx]
};
if is_better {
spot_product[v_idx] = candidate_spot;
amount[v_idx] = result.amount;
predecessor[v_idx] = Some((u, component_id.clone()));
edge_gas[v_idx] = result.gas;
cumul_gas[v_idx] = candidate_cumul_gas;
next_active.insert(*v);
}
}
}
active_nodes = next_active.into_iter().collect();
active_nodes.sort_unstable();
}
SPFAResult { amount, predecessor, edge_gas, spot_product, input_below_hop_gas, timed_out }
}
fn connector_allows(&self, ctx: &BellmanFordContext, v: NodeIndex) -> bool {
let (Some(tokens), Some(v_addr)) = (&self.connector_tokens, ctx.node_address.get(&v))
else {
return true;
};
v == ctx.token_in_node || ctx.token_out_node == Some(v) || tokens.contains(v_addr)
}
fn build_route(
ctx: &BellmanFordContext,
path_edges: &[(NodeIndex, NodeIndex, ComponentId)],
amount: &[BigUint],
edge_gas: &[BigUint],
overrides: &MarketOverrides,
) -> Result<Route, AlgorithmError> {
let mut swaps = Vec::with_capacity(path_edges.len());
let mut tokens: FxHashMap<Address, Token> = FxHashMap::default();
for (from_node, to_node, component_id) in path_edges {
let token_in = ctx
.token_map
.get(from_node)
.ok_or_else(|| AlgorithmError::DataNotFound {
kind: "token",
id: Some(format!("{:?}", from_node)),
})?;
let token_out = ctx
.token_map
.get(to_node)
.ok_or_else(|| AlgorithmError::DataNotFound {
kind: "token",
id: Some(format!("{:?}", to_node)),
})?;
let component = ctx
.market_data
.get_component(component_id)
.ok_or_else(|| AlgorithmError::DataNotFound {
kind: "component",
id: Some(component_id.clone()),
})?;
let sim_state = overrides
.get(component_id)
.or_else(|| {
ctx.market_data
.get_simulation_state(component_id)
})
.ok_or_else(|| AlgorithmError::DataNotFound {
kind: "simulation state",
id: Some(component_id.clone()),
})?;
swaps.push(Swap::new(
component_id.clone(),
component.protocol_system.clone(),
token_in.address.clone(),
token_out.address.clone(),
amount[from_node.index()].clone(),
amount[to_node.index()].clone(),
edge_gas[to_node.index()].clone(),
component.clone(),
sim_state.clone_box(),
));
tokens
.entry(token_in.address.clone())
.or_insert_with(|| Token::clone(token_in));
tokens
.entry(token_out.address.clone())
.or_insert_with(|| Token::clone(token_out));
}
Ok(Route::new(swaps, tokens)?)
}
fn gas_adjusted_amount(
gross: &BigUint,
cumul_gas: &BigUint,
gas_price_wei: &BigUint,
token_price: Option<&Price>,
) -> BigInt {
match token_price {
Some(price) if !price.denominator.is_zero() => {
let gas_cost = cumul_gas * gas_price_wei * &price.numerator / &price.denominator;
BigInt::from(gross.clone()) - BigInt::from(gas_cost)
}
_ => BigInt::from(gross.clone()),
}
}
fn compute_edge_spot_product(
parent_spot: f64,
component_id: &ComponentId,
u_addr: Option<&Address>,
v_addr: Option<&Address>,
spot_prices: Option<&SpotPrices>,
) -> f64 {
if parent_spot == 0.0 {
return 0.0;
}
let (Some(u), Some(v), Some(prices)) = (u_addr, v_addr, spot_prices) else {
return 0.0;
};
let key = (component_id.clone(), u.clone(), v.clone());
match prices.get(&key) {
Some(&spot) if spot > 0.0 => parent_spot * spot,
_ => 0.0,
}
}
fn resolve_token_price(
v_addr: Option<&Address>,
token_prices: Option<&TokenGasPrices>,
spot_product: f64,
token_in_addr: Option<&Address>,
) -> Option<Price> {
let prices = token_prices?;
let addr = v_addr?;
if let Some(price) = prices.get(addr) {
return Some(price.clone());
}
if spot_product > 0.0 {
if let Some(in_price) = token_in_addr.and_then(|a| prices.get(a)) {
let in_rate_f64 = in_price.numerator.to_f64()? / in_price.denominator.to_f64()?;
let estimated_rate = in_rate_f64 * spot_product;
let denom = BigUint::from(10u64).pow(18);
let numer_f64 = estimated_rate * 1e18;
if numer_f64.is_finite() && numer_f64 > 0.0 {
return Some(Price {
numerator: BigUint::from(numer_f64 as u128),
denominator: denom,
});
}
}
}
None
}
pub(crate) fn path_has_conflict(
from: NodeIndex,
target_node: NodeIndex,
target_component: &ComponentId,
predecessor: &[Option<(NodeIndex, ComponentId)>],
) -> bool {
let mut current = from;
loop {
if current == target_node {
return true;
}
match &predecessor[current.index()] {
Some((prev, cid)) => {
if cid == target_component {
return true;
}
current = *prev;
}
None => return false,
}
}
}
pub(crate) fn reconstruct_path(
token_out: NodeIndex,
token_in: NodeIndex,
predecessor: &[Option<(NodeIndex, ComponentId)>],
) -> Result<Vec<(NodeIndex, NodeIndex, ComponentId)>, AlgorithmError> {
let mut path = Vec::new();
let mut current = token_out;
let mut visited = FxHashSet::default();
while current != token_in {
if !visited.insert(current) {
return Err(AlgorithmError::Other("cycle in predecessor chain".to_string()));
}
let idx = current.index();
match &predecessor
.get(idx)
.and_then(|p| p.as_ref())
{
Some((prev_node, component_id)) => {
path.push((*prev_node, current, component_id.clone()));
current = *prev_node;
}
None => {
return Err(AlgorithmError::Other(format!(
"broken predecessor chain at node {idx}"
)));
}
}
}
path.reverse();
Ok(path)
}
fn get_subgraph<'a>(
graph: &'a StableDiGraph<()>,
token_in: NodeIndex,
token_out: NodeIndex,
max_hops: usize,
exclusions: &RouteExclusions,
) -> Option<Subgraph<'a>> {
let hops_to_token_out =
Self::get_hops_to_reach(graph, token_in, token_out, max_hops, exclusions);
Self::get_subgraph_with_hop_map(
graph,
(token_in, Some(token_out)),
Some(&hops_to_token_out),
max_hops,
exclusions,
)
}
fn get_subgraph_with_hop_map<'a>(
graph: &'a StableDiGraph<()>,
endpoints: (NodeIndex, Option<NodeIndex>),
hops_to_token_out: Option<&FxHashMap<NodeIndex, usize>>,
max_hops: usize,
exclusions: &RouteExclusions,
) -> Option<Subgraph<'a>> {
let (token_in, token_out) = endpoints;
let mut adj: FxHashMap<NodeIndex, Vec<(NodeIndex, ComponentId)>> = FxHashMap::default();
let mut token_nodes: FxHashSet<NodeIndex> = FxHashSet::default();
let mut component_ids: FxHashSet<&ComponentId> = FxHashSet::default();
let mut visited_nodes = FxHashSet::default();
let mut queued_nodes = VecDeque::new();
visited_nodes.insert(token_in);
token_nodes.insert(token_in);
queued_nodes.push_back((token_in, 0usize));
while let Some((node, depth_walked)) = queued_nodes.pop_front() {
if depth_walked >= max_hops {
continue;
}
for edge in graph.edges(node) {
let next_token = edge.target();
if !Self::can_cross(
graph,
edge,
(token_in, token_out.unwrap_or(token_in)),
exclusions,
) {
continue;
}
if let Some(hops_to_token_out) = &hops_to_token_out {
let Some(&hops_left) = hops_to_token_out.get(&next_token) else {
continue;
};
if depth_walked + 1 + hops_left > max_hops {
continue;
}
}
let component_id = &edge.weight().component_id;
adj.entry(node)
.or_default()
.push((next_token, component_id.clone()));
component_ids.insert(component_id);
token_nodes.insert(next_token);
if visited_nodes.insert(next_token) {
queued_nodes.push_back((next_token, depth_walked + 1));
}
}
}
if adj.is_empty() {
return None;
}
Some(Subgraph { adjacency: adj, token_nodes, component_ids })
}
fn can_cross(
graph: &StableDiGraph<()>,
edge: EdgeReference<'_, EdgeData<()>>,
endpoints: (NodeIndex, NodeIndex),
exclusions: &RouteExclusions,
) -> bool {
!exclusions.excludes_pool(&edge.weight().component_id) &&
exclusions
.allows_token(&graph[edge.target()], (&graph[endpoints.0], &graph[endpoints.1]))
}
pub(crate) fn get_hops_to_reach(
graph: &StableDiGraph<()>,
token_in: NodeIndex,
token_out: NodeIndex,
max_hops: usize,
exclusions: &RouteExclusions,
) -> FxHashMap<NodeIndex, usize> {
Self::get_hops_to_reach_any(graph, [token_out], (token_in, token_out), max_hops, exclusions)
}
fn get_hops_to_reach_any(
graph: &StableDiGraph<()>,
sources: impl IntoIterator<Item = NodeIndex>,
endpoints: (NodeIndex, NodeIndex),
max_hops: usize,
exclusions: &RouteExclusions,
) -> FxHashMap<NodeIndex, usize> {
let mut hops_to_reach: FxHashMap<NodeIndex, usize> = FxHashMap::default();
let mut frontier = Vec::new();
for source in sources {
hops_to_reach.insert(source, 0);
frontier.push(source);
}
for depth in 1..=max_hops {
let mut next = Vec::new();
for node in frontier {
for edge in graph.edges(node) {
let neighbor = edge.target();
if hops_to_reach.contains_key(&neighbor) ||
!Self::can_cross(graph, edge, endpoints, exclusions)
{
continue;
}
hops_to_reach.insert(neighbor, depth);
next.push(neighbor);
}
}
frontier = next;
}
hops_to_reach
}
#[allow(clippy::too_many_arguments)]
fn compute_net_amount_out(
amount_out: &BigUint,
route: &Route,
gas_price: &BigUint,
token_prices: Option<&TokenGasPrices>,
spot_product: &[f64],
node_address: &FxHashMap<NodeIndex, Address>,
token_in_node: NodeIndex,
) -> Result<BigInt, AlgorithmError> {
let last_swap = route.swaps().last().ok_or_else(|| {
AlgorithmError::Other("compute_net_amount_out called with empty route".to_string())
})?;
let total_gas = route.total_gas();
if gas_price.is_zero() {
warn!("missing gas price, returning gross amount_out");
return Ok(BigInt::from(amount_out.clone()));
}
let gas_cost_wei = &total_gas * gas_price;
let out_addr = last_swap.token_out();
let out_node_spot = node_address
.iter()
.find(|(_, addr)| *addr == out_addr)
.and_then(|(node, _)| spot_product.get(node.index()).copied())
.unwrap_or(0.0);
let output_price = Self::resolve_token_price(
Some(out_addr),
token_prices,
out_node_spot,
node_address.get(&token_in_node),
);
Ok(match output_price {
Some(price) if !price.denominator.is_zero() => {
let gas_cost = &gas_cost_wei * &price.numerator / &price.denominator;
BigInt::from(amount_out.clone()) - BigInt::from(gas_cost)
}
_ => {
debug!("no gas price for output token, returning gross amount_out");
BigInt::from(amount_out.clone())
}
})
}
}
impl Algorithm for BellmanFordAlgorithm {
type GraphType = StableDiGraph<()>;
type GraphManager = PetgraphStableDiGraphManager<()>;
fn name(&self) -> &str {
"bellman_ford"
}
#[instrument(level = "debug", skip_all, fields(order_id = %request.order().id()))]
async fn find_best_route(
&self,
request: SolveRequest<'_, Self::GraphType>,
) -> Result<RouteResult, AlgorithmError> {
let order = request.order();
let ctx = self.build_context(request).await?;
self.find_single_route(&ctx, order, FindRouteOptions::default())
}
fn computation_requirements(&self) -> ComputationRequirements {
ComputationRequirements::none()
.allow_stale("token_prices")
.expect("token_prices requirement conflicts (bug)")
.allow_stale("spot_prices")
.expect("spot_prices requirement conflicts (bug)")
}
fn timeout(&self) -> Duration {
self.timeout
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use num_bigint::BigInt;
use tokio::sync::RwLock;
use tycho_simulation::{
tycho_common::{models::Address, simulation::protocol_sim::ProtocolSim},
tycho_ethereum::gas::{BlockGasPrice, GasPrice},
};
use super::*;
use crate::{
algorithm::test_utils::{component, order, token, MockProtocolSim},
derived::{types::TokenGasPrices, DerivedData},
feed::market_data::{MarketData, MarketState},
graph::GraphManager,
types::quote::OrderSide,
};
fn setup_market_bf(
components: Vec<(&str, &Token, &Token, MockProtocolSim)>,
) -> (MarketData, PetgraphStableDiGraphManager<()>) {
let mut market = MarketState::new();
market.update_gas_price(BlockGasPrice {
block_number: 1,
block_hash: Default::default(),
block_timestamp: 0,
pricing: GasPrice::Legacy { gas_price: BigUint::from(100u64) },
});
market.update_last_updated(crate::types::BlockInfo::new(1, "0x00".into(), 0));
for (component_id, token_in, token_out, state) in components {
let tokens = vec![token_in.clone(), token_out.clone()];
let comp = component(component_id, &tokens);
market.upsert_components(std::iter::once(comp));
market.update_states([(
component_id.to_string(),
Box::new(state) as Box<dyn ProtocolSim>,
)]);
market.upsert_tokens(tokens);
}
let mut graph_manager = PetgraphStableDiGraphManager::default();
graph_manager.initialize_graph(&market.component_topology());
(MarketData::new(Arc::new(RwLock::new(market))), graph_manager)
}
fn setup_derived_with_token_prices(
token_addresses: &[Address],
) -> crate::derived::SharedDerivedDataRef {
use tycho_simulation::tycho_core::simulation::protocol_sim::Price;
let mut token_prices: TokenGasPrices = FxHashMap::default();
for address in token_addresses {
token_prices.insert(
address.clone(),
Price { numerator: BigUint::from(1u64), denominator: BigUint::from(1u64) },
);
}
let mut derived_data = DerivedData::new();
derived_data.set_token_prices(token_prices, vec![], 1, true);
Arc::new(RwLock::new(derived_data))
}
fn bf_algorithm(max_hops: usize, timeout_ms: u64) -> BellmanFordAlgorithm {
BellmanFordAlgorithm::with_config(
AlgorithmConfig::new(1, max_hops, Duration::from_millis(timeout_ms), None).unwrap(),
)
}
#[tokio::test]
async fn test_find_best_route_with_excluded_endpoints() {
let a = token(0x01, "A");
let b = token(0x02, "B");
let c = token(0x03, "C");
let (market, manager) = setup_market_bf(vec![
("ab", &a, &b, MockProtocolSim::new(2.0)),
("bc", &b, &c, MockProtocolSim::new(2.0)),
]);
let order = order(&a, &c, 1000, OrderSide::Sell);
let result = bf_algorithm(2, 1000)
.find_best_route(SolveRequest::new(manager.graph(), market, &order).with_exclusions(
RouteExclusions::default().with_tokens([a.address.clone(), c.address.clone()]),
))
.await
.unwrap();
assert_eq!(result.route().swaps().len(), 2);
assert_eq!(result.route().swaps()[0].component_id(), "ab");
assert_eq!(result.route().swaps()[1].component_id(), "bc");
}
#[tokio::test]
async fn test_find_best_route_with_excluded_pool() {
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let (market, manager) = setup_market_bf(vec![
("best", &token_a, &token_b, MockProtocolSim::new(3.0)),
("second", &token_a, &token_b, MockProtocolSim::new(2.0)),
]);
let ord = order(&token_a, &token_b, 1000, OrderSide::Sell);
let result = bf_algorithm(2, 1000)
.find_best_route(
SolveRequest::new(manager.graph(), market, &ord)
.with_exclusions(RouteExclusions::default().with_pools(["best".to_string()])),
)
.await
.unwrap();
assert_eq!(result.route().swaps()[0].component_id(), "second");
}
#[test]
fn test_get_subgraph_keeps_full_length_routes_and_drops_dead_ends() {
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let token_c = token(0x03, "C");
let token_d = token(0x04, "D");
let (_, manager) = setup_market_bf(vec![
("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
("component_bc", &token_b, &token_c, MockProtocolSim::new(3.0)),
("component_ad", &token_a, &token_d, MockProtocolSim::new(5.0)),
]);
let graph = manager.graph();
let node = |address: &Address| {
graph
.node_indices()
.find(|&n| &graph[n] == address)
.expect("token in graph")
};
let Subgraph { adjacency: adj, component_ids, .. } = BellmanFordAlgorithm::get_subgraph(
graph,
node(&token_a.address),
node(&token_c.address),
2,
&RouteExclusions::default(),
)
.unwrap();
let kept = |id: &str| {
component_ids
.iter()
.any(|component_id| *component_id == id)
};
assert!(kept("component_ab"), "the route's first hop must survive");
assert!(kept("component_bc"), "the route's second hop must survive");
assert!(!kept("component_ad"), "a dead end must not be kept");
let from_b = adj
.get(&node(&token_b.address))
.map(Vec::as_slice)
.unwrap_or_default();
assert!(
from_b
.iter()
.all(|(target, _)| *target != node(&token_a.address)),
"B -> A cannot finish the route and must not be kept"
);
}
#[test]
fn test_get_subgraph_drops_detours_that_cannot_finish_in_budget() {
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let token_c = token(0x03, "C");
let token_d = token(0x04, "D");
let token_e = token(0x05, "E");
let (_, manager) = setup_market_bf(vec![
("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
("component_bc", &token_b, &token_c, MockProtocolSim::new(3.0)),
("component_ad", &token_a, &token_d, MockProtocolSim::new(5.0)),
("component_de", &token_d, &token_e, MockProtocolSim::new(5.0)),
("component_ec", &token_e, &token_c, MockProtocolSim::new(5.0)),
]);
let graph = manager.graph();
let node = |address: &Address| {
graph
.node_indices()
.find(|&n| &graph[n] == address)
.expect("token in graph")
};
let Subgraph { token_nodes, component_ids, .. } = BellmanFordAlgorithm::get_subgraph(
graph,
node(&token_a.address),
node(&token_c.address),
2,
&RouteExclusions::default(),
)
.unwrap();
let kept = |id: &str| {
component_ids
.iter()
.any(|component_id| *component_id == id)
};
assert!(kept("component_ab"), "the two-hop route's first leg must survive");
assert!(kept("component_bc"), "the two-hop route's second leg must survive");
assert!(!kept("component_ad"), "the step into a detour must not be kept");
assert!(!kept("component_de"), "nor anything further along it");
assert!(!kept("component_ec"), "nor its last leg into the destination");
assert!(
!token_nodes.contains(&node(&token_d.address)),
"a token no legal route reaches must not be kept"
);
}
#[test]
fn test_subgraph_without_destination() {
let token_g = token(0x01, "G");
let token_b = token(0x02, "B");
let token_c = token(0x03, "C");
let token_d = token(0x04, "D");
let (_, manager) = setup_market_bf(vec![
("component_gb", &token_g, &token_b, MockProtocolSim::new(2.0)),
("component_bc", &token_b, &token_c, MockProtocolSim::new(2.0)),
("component_cd", &token_c, &token_d, MockProtocolSim::new(2.0)),
]);
let graph = manager.graph();
let node = |address: &Address| {
graph
.node_indices()
.find(|&n| &graph[n] == address)
.expect("token in graph")
};
let Subgraph { token_nodes, component_ids, .. } =
BellmanFordAlgorithm::get_subgraph_with_hop_map(
graph,
(node(&token_g.address), None),
None,
2,
&RouteExclusions::default(),
)
.unwrap();
let kept = |id: &str| {
component_ids
.iter()
.any(|component_id| *component_id == id)
};
assert!(kept("component_gb"), "the first hop is within budget");
assert!(kept("component_bc"), "the second hop spends the budget exactly");
assert!(!kept("component_cd"), "an edge past the hop budget must not be kept");
assert!(
!token_nodes.contains(&node(&token_d.address)),
"a token past the hop budget must not be kept"
);
}
#[tokio::test]
async fn test_linear_path_found() {
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let token_c = token(0x03, "C");
let token_d = token(0x04, "D");
let (market, manager) = setup_market_bf(vec![
("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
("component_bc", &token_b, &token_c, MockProtocolSim::new(3.0)),
("component_cd", &token_c, &token_d, MockProtocolSim::new(4.0)),
]);
let algo = bf_algorithm(4, 1000);
let ord = order(&token_a, &token_d, 100, OrderSide::Sell);
let result = algo
.find_best_route(SolveRequest::new(manager.graph(), market, &ord))
.await
.unwrap();
assert_eq!(result.route().swaps().len(), 3);
assert_eq!(result.route().swaps()[0].amount_out(), &BigUint::from(200u64));
assert_eq!(result.route().swaps()[1].amount_out(), &BigUint::from(600u64));
assert_eq!(result.route().swaps()[2].amount_out(), &BigUint::from(2400u64));
}
#[tokio::test]
async fn test_picks_better_of_two_paths() {
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let token_c = token(0x03, "C");
let token_d = token(0x04, "D");
let (market, manager) = setup_market_bf(vec![
("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
("component_bd", &token_b, &token_d, MockProtocolSim::new(3.0)),
("component_ac", &token_a, &token_c, MockProtocolSim::new(4.0)),
("component_cd", &token_c, &token_d, MockProtocolSim::new(1.0)),
]);
let algo = bf_algorithm(3, 1000);
let ord = order(&token_a, &token_d, 100, OrderSide::Sell);
let result = algo
.find_best_route(SolveRequest::new(manager.graph(), market, &ord))
.await
.unwrap();
assert_eq!(result.route().swaps().len(), 2);
assert_eq!(result.route().swaps()[0].component_id(), "component_ab");
assert_eq!(result.route().swaps()[1].component_id(), "component_bd");
assert_eq!(result.route().swaps()[1].amount_out(), &BigUint::from(600u64));
}
#[tokio::test]
async fn test_parallel_components() {
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let (market, manager) = setup_market_bf(vec![
("component1", &token_a, &token_b, MockProtocolSim::new(2.0)),
("component2", &token_a, &token_b, MockProtocolSim::new(5.0)),
]);
let algo = bf_algorithm(2, 1000);
let ord = order(&token_a, &token_b, 100, OrderSide::Sell);
let result = algo
.find_best_route(SolveRequest::new(manager.graph(), market, &ord))
.await
.unwrap();
assert_eq!(result.route().swaps().len(), 1);
assert_eq!(result.route().swaps()[0].component_id(), "component2");
assert_eq!(result.route().swaps()[0].amount_out(), &BigUint::from(500u64));
}
#[tokio::test]
async fn test_no_path_returns_error() {
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let token_c = token(0x03, "C");
let (market, manager) =
setup_market_bf(vec![("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0))]);
{
let mut m = market.write().await;
m.upsert_tokens(vec![token_c.clone()]);
}
let algo = bf_algorithm(3, 1000);
let ord = order(&token_a, &token_c, 100, OrderSide::Sell);
let result = algo
.find_best_route(SolveRequest::new(manager.graph(), market, &ord))
.await;
assert!(matches!(result, Err(AlgorithmError::NoPath { .. })));
}
#[tokio::test]
async fn test_source_not_in_graph() {
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let token_x = token(0x99, "X");
let (market, manager) =
setup_market_bf(vec![("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0))]);
let algo = bf_algorithm(3, 1000);
let ord = order(&token_x, &token_b, 100, OrderSide::Sell);
let result = algo
.find_best_route(SolveRequest::new(manager.graph(), market, &ord))
.await;
assert!(matches!(
result,
Err(AlgorithmError::NoPath { reason: NoPathReason::SourceTokenNotInGraph, .. })
));
}
#[tokio::test]
async fn test_amount_too_small_when_reachable_but_zero_output() {
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let (market, manager) =
setup_market_bf(vec![("component_ab", &token_a, &token_b, MockProtocolSim::new(0.5))]);
let algo = bf_algorithm(3, 1000);
let ord = order(&token_a, &token_b, 1, OrderSide::Sell);
let result = algo
.find_best_route(SolveRequest::new(manager.graph(), market, &ord))
.await;
assert!(matches!(
result,
Err(AlgorithmError::NoPath { reason: NoPathReason::AmountTooSmall, .. })
));
}
#[tokio::test]
async fn test_amount_too_small_when_dust_occurs_mid_route() {
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let token_c = token(0x03, "C");
let (market, manager) = setup_market_bf(vec![
("component_ab", &token_a, &token_b, MockProtocolSim::new(0.5)),
("component_bc", &token_b, &token_c, MockProtocolSim::new(2.0)),
]);
let algo = bf_algorithm(3, 1000);
let ord = order(&token_a, &token_c, 1, OrderSide::Sell);
let result = algo
.find_best_route(SolveRequest::new(manager.graph(), market, &ord))
.await;
assert!(matches!(
result,
Err(AlgorithmError::NoPath { reason: NoPathReason::AmountTooSmall, .. })
));
}
#[tokio::test]
async fn test_no_graph_path_when_amount_too_large_for_liquidity() {
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let (market, manager) = setup_market_bf(vec![(
"component_ab",
&token_a,
&token_b,
MockProtocolSim::new(2.0).with_liquidity(500),
)]);
let algo = bf_algorithm(2, 1000);
let ord = order(&token_a, &token_b, 1000, OrderSide::Sell);
let result = algo
.find_best_route(SolveRequest::new(manager.graph(), market, &ord))
.await;
assert!(matches!(
result,
Err(AlgorithmError::NoPath { reason: NoPathReason::NoGraphPath, .. })
));
}
#[tokio::test]
async fn test_no_graph_path_when_unreachable_within_hops() {
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let token_c = token(0x03, "C");
let (market, manager) = setup_market_bf(vec![
("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
("component_bc", &token_b, &token_c, MockProtocolSim::new(2.0)),
]);
let algo = bf_algorithm(1, 1000);
let ord = order(&token_a, &token_c, 100, OrderSide::Sell);
let result = algo
.find_best_route(SolveRequest::new(manager.graph(), market, &ord))
.await;
assert!(matches!(
result,
Err(AlgorithmError::NoPath { reason: NoPathReason::NoGraphPath, .. })
));
}
#[tokio::test]
async fn test_reach_from_source_token_covers_branches_off_any_pair() {
let token_g = token(0x01, "G");
let token_a = token(0x02, "A");
let token_b = token(0x03, "B");
let token_c = token(0x04, "C");
let (market, manager) = setup_market_bf(vec![
("component_ga", &token_g, &token_a, MockProtocolSim::new(2.0)),
("component_gb", &token_g, &token_b, MockProtocolSim::new(2.0)),
("component_bc", &token_b, &token_c, MockProtocolSim::new(2.0)),
]);
let algo = bf_algorithm(3, 1000);
let ctx = algo
.build_context_from_source_token(manager.graph(), market, &token_g.address, 3, None)
.await
.expect("gas token has outgoing edges");
let routes = algo.reach_from_source_token(&ctx, &BigUint::from(100u64));
let reached: FxHashSet<Address> = routes.reached.keys().cloned().collect();
let expected: FxHashSet<Address> = [&token_a, &token_b, &token_c]
.into_iter()
.map(|t| t.address.clone())
.collect();
assert_eq!(reached, expected);
}
#[tokio::test]
async fn test_reach_from_source_token_zero_timeout() {
let token_g = token(0x01, "G");
let token_a = token(0x02, "A");
let (market, manager) =
setup_market_bf(vec![("component_ga", &token_g, &token_a, MockProtocolSim::new(2.0))]);
let algo = bf_algorithm(3, 0);
let ctx = algo
.build_context_from_source_token(manager.graph(), market, &token_g.address, 3, None)
.await
.expect("source has outgoing edges");
let routes = algo.reach_from_source_token(&ctx, &BigUint::from(100u64));
assert!(routes.timed_out, "a zero timeout must be reported as a cut-short relaxation");
assert!(routes.reached.is_empty());
}
#[tokio::test]
async fn test_context_pruned_toward_filter_tokens() {
let token_g = token(0x01, "G");
let token_a = token(0x02, "A");
let token_b = token(0x03, "B");
let token_c = token(0x04, "C");
let (market, manager) = setup_market_bf(vec![
("component_ga", &token_g, &token_a, MockProtocolSim::new(2.0)),
("component_gb", &token_g, &token_b, MockProtocolSim::new(2.0)),
("component_bc", &token_b, &token_c, MockProtocolSim::new(2.0)),
]);
let algo = bf_algorithm(3, 1000);
let filter: FxHashSet<Address> = [token_a.address.clone()]
.into_iter()
.collect();
let ctx = algo
.build_context_from_source_token(
manager.graph(),
market,
&token_g.address,
2,
Some(&filter),
)
.await
.expect("the filter token is reachable");
assert!(ctx
.market_data
.get_simulation_state("component_ga")
.is_some());
assert!(
ctx.market_data
.get_simulation_state("component_gb")
.is_none(),
"the B branch sits on no G-A candidate path"
);
assert!(ctx
.market_data
.get_simulation_state("component_bc")
.is_none());
let routes = algo.reach_from_source_token(&ctx, &BigUint::from(100u64));
let reached: FxHashSet<Address> = routes.reached.keys().cloned().collect();
assert_eq!(reached, filter);
}
#[tokio::test]
async fn test_find_single_route_rejects_a_context_built_without_destination() {
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let (market, manager) =
setup_market_bf(vec![("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0))]);
let algo = bf_algorithm(3, 1000);
let ctx = algo
.build_context_from_source_token(manager.graph(), market, &token_a.address, 3, None)
.await
.expect("source has outgoing edges");
let ord = order(&token_a, &token_b, 100, OrderSide::Sell);
let result = algo.find_single_route(&ctx, &ord, FindRouteOptions::default());
assert!(
matches!(result, Err(AlgorithmError::Other(_))),
"expected an Other error, got {result:?}"
);
}
#[tokio::test]
async fn test_find_single_route_after_reroot() {
let token_g = token(0x01, "G");
let token_b = token(0x02, "B");
let token_c = token(0x03, "C");
let (market, manager) = setup_market_bf(vec![
("component_gb", &token_g, &token_b, MockProtocolSim::new(2.0)),
("component_bc", &token_b, &token_c, MockProtocolSim::new(2.0)),
]);
let graph = manager.graph();
let algo = bf_algorithm(3, 1000);
let mut ctx = algo
.build_context_from_source_token(graph, market, &token_g.address, 3, None)
.await
.expect("source has outgoing edges");
let node_of = |address: &Address| {
graph
.node_indices()
.find(|&n| &graph[n] == address)
.expect("token is in the graph")
};
let gas_node = ctx.token_in_node;
let hops_to_gas = BellmanFordAlgorithm::get_hops_to_reach(
graph,
gas_node,
gas_node,
3,
&RouteExclusions::default(),
);
ctx.reroot_toward(graph, node_of(&token_c.address), gas_node, &hops_to_gas, 3)
.expect("a C-to-G path exists");
let ord = order(&token_c, &token_g, 100, OrderSide::Sell);
let result = algo
.find_single_route(&ctx, &ord, FindRouteOptions::default())
.expect("re-rooted context solves back to its source");
assert_eq!(
result
.route()
.amount_out(&token_g.address),
BigUint::from(25u64)
);
}
#[tokio::test]
async fn test_no_graph_path_when_connector_tokens_exclude_intermediate() {
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let token_c = token(0x03, "C");
let (market, manager) = setup_market_bf(vec![
("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
("component_bc", &token_b, &token_c, MockProtocolSim::new(2.0)),
]);
let algo = BellmanFordAlgorithm::with_config(
AlgorithmConfig::new(1, 3, Duration::from_millis(1000), None)
.unwrap()
.with_connector_tokens(FxHashSet::default()),
);
let ord = order(&token_a, &token_c, 100, OrderSide::Sell);
let result = algo
.find_best_route(SolveRequest::new(manager.graph(), market, &ord))
.await;
assert!(matches!(
result,
Err(AlgorithmError::NoPath { reason: NoPathReason::NoGraphPath, .. })
));
}
#[tokio::test]
async fn test_destination_not_in_graph() {
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let token_x = token(0x99, "X");
let (market, manager) =
setup_market_bf(vec![("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0))]);
let algo = bf_algorithm(3, 1000);
let ord = order(&token_a, &token_x, 100, OrderSide::Sell);
let result = algo
.find_best_route(SolveRequest::new(manager.graph(), market, &ord))
.await;
assert!(matches!(
result,
Err(AlgorithmError::NoPath { reason: NoPathReason::DestinationTokenNotInGraph, .. })
));
}
#[tokio::test]
async fn test_respects_max_hops() {
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let token_c = token(0x03, "C");
let token_d = token(0x04, "D");
let (market, manager) = setup_market_bf(vec![
("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
("component_bc", &token_b, &token_c, MockProtocolSim::new(3.0)),
("component_cd", &token_c, &token_d, MockProtocolSim::new(4.0)),
]);
let algo = bf_algorithm(2, 1000);
let ord = order(&token_a, &token_d, 100, OrderSide::Sell);
let result = algo
.find_best_route(SolveRequest::new(manager.graph(), market, &ord))
.await;
assert!(
matches!(result, Err(AlgorithmError::NoPath { .. })),
"Should not find 3-hop path with max_hops=2"
);
}
#[tokio::test]
async fn test_source_token_revisit_blocked() {
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let token_c = token(0x03, "C");
let (market, manager) = setup_market_bf(vec![
("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
("component_bc", &token_b, &token_c, MockProtocolSim::new(3.0)),
]);
let algo = bf_algorithm(4, 1000);
let ord = order(&token_a, &token_c, 100, OrderSide::Sell);
let result = algo
.find_best_route(SolveRequest::new(manager.graph(), market, &ord))
.await
.unwrap();
assert_eq!(result.route().swaps().len(), 2);
assert_eq!(result.route().swaps()[0].component_id(), "component_ab");
assert_eq!(result.route().swaps()[1].component_id(), "component_bc");
assert_eq!(result.route().swaps()[1].amount_out(), &BigUint::from(600u64));
}
#[tokio::test]
async fn test_hub_token_revisit_blocked() {
let token_a = token(0x01, "A");
let token_c = token(0x02, "C");
let token_b = token(0x03, "B");
let token_d = token(0x04, "D");
let (market, manager) = setup_market_bf(vec![
("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
("component_bc", &token_b, &token_c, MockProtocolSim::new(3.0)),
("component_cb", &token_c, &token_b, MockProtocolSim::new(100.0)),
("component_bd", &token_b, &token_d, MockProtocolSim::new(2.0)),
]);
let algo = bf_algorithm(4, 1000);
let ord = order(&token_a, &token_d, 100, OrderSide::Sell);
let result = algo
.find_best_route(SolveRequest::new(manager.graph(), market, &ord))
.await
.unwrap();
assert_eq!(result.route().swaps().len(), 2, "should use direct 2-hop path");
assert_eq!(result.route().swaps()[0].component_id(), "component_ab");
assert_eq!(result.route().swaps()[1].component_id(), "component_bd");
assert_eq!(result.route().swaps()[1].amount_out(), &BigUint::from(400u64));
}
#[tokio::test]
async fn test_route_amounts_are_sequential() {
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let token_c = token(0x03, "C");
let (market, manager) = setup_market_bf(vec![
("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
("component_bc", &token_b, &token_c, MockProtocolSim::new(3.0)),
]);
let algo = bf_algorithm(3, 1000);
let ord = order(&token_a, &token_c, 100, OrderSide::Sell);
let result = algo
.find_best_route(SolveRequest::new(manager.graph(), market, &ord))
.await
.unwrap();
assert_eq!(result.route().swaps().len(), 2);
assert_eq!(result.route().swaps()[1].amount_in(), result.route().swaps()[0].amount_out());
}
#[tokio::test]
async fn test_gas_deduction() {
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let (market, manager) = setup_market_bf(vec![(
"component1",
&token_a,
&token_b,
MockProtocolSim::new(2.0).with_gas(10),
)]);
let algo = bf_algorithm(2, 1000);
let ord = order(&token_a, &token_b, 1000, OrderSide::Sell);
let derived = setup_derived_with_token_prices(std::slice::from_ref(&token_b.address));
let result = algo
.find_best_route(SolveRequest::new(manager.graph(), market, &ord).with_derived(derived))
.await
.unwrap();
assert_eq!(result.route().swaps()[0].amount_out(), &BigUint::from(2000u64));
assert_eq!(result.net_amount_out(), &BigInt::from(1000));
}
#[tokio::test]
async fn test_timeout_respected() {
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let token_c = token(0x03, "C");
let (market, manager) = setup_market_bf(vec![
("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
("component_bc", &token_b, &token_c, MockProtocolSim::new(3.0)),
]);
let algo = bf_algorithm(3, 0);
let ord = order(&token_a, &token_c, 100, OrderSide::Sell);
let result = algo
.find_best_route(SolveRequest::new(manager.graph(), market, &ord))
.await;
match result {
Ok(r) => {
assert!(!r.route().swaps().is_empty());
}
Err(AlgorithmError::Timeout { .. }) | Err(AlgorithmError::NoPath { .. }) => {
}
Err(e) => panic!("Unexpected error: {:?}", e),
}
}
#[tokio::test]
async fn test_with_fees() {
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let (market, manager) = setup_market_bf(vec![(
"component1",
&token_a,
&token_b,
MockProtocolSim::new(2.0).with_fee(0.1),
)]);
let algo = bf_algorithm(2, 1000);
let ord = order(&token_a, &token_b, 1000, OrderSide::Sell);
let result = algo
.find_best_route(SolveRequest::new(manager.graph(), market, &ord))
.await
.unwrap();
assert_eq!(result.route().swaps()[0].amount_out(), &BigUint::from(1800u64));
}
#[tokio::test]
async fn test_large_trade_slippage() {
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let (market, manager) = setup_market_bf(vec![(
"component1",
&token_a,
&token_b,
MockProtocolSim::new(2.0).with_liquidity(500),
)]);
let algo = bf_algorithm(2, 1000);
let ord = order(&token_a, &token_b, 1000, OrderSide::Sell);
let result = algo
.find_best_route(SolveRequest::new(manager.graph(), market, &ord))
.await;
assert!(
matches!(result, Err(AlgorithmError::NoPath { .. })),
"Should fail when trade exceeds component liquidity"
);
}
#[tokio::test]
async fn test_disconnected_tokens_return_no_path() {
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let token_d = token(0x04, "D");
let token_e = token(0x05, "E");
let (market, manager) = setup_market_bf(vec![
("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
("component_de", &token_d, &token_e, MockProtocolSim::new(4.0)),
]);
let algo = bf_algorithm(3, 1000);
let ord = order(&token_a, &token_e, 100, OrderSide::Sell);
let result = algo
.find_best_route(SolveRequest::new(manager.graph(), market, &ord))
.await;
assert!(
matches!(result, Err(AlgorithmError::NoPath { .. })),
"should not find path to disconnected component"
);
}
#[tokio::test]
async fn test_spfa_skips_failed_simulations() {
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let token_c = token(0x03, "C");
let (market, manager) = setup_market_bf(vec![
("component_ab_bad", &token_a, &token_b, MockProtocolSim::new(2.0).with_liquidity(0)),
("component_ac", &token_a, &token_c, MockProtocolSim::new(2.0)),
("component_cb", &token_c, &token_b, MockProtocolSim::new(3.0)),
]);
let algo = bf_algorithm(3, 1000);
let ord = order(&token_a, &token_b, 100, OrderSide::Sell);
let result = algo
.find_best_route(SolveRequest::new(manager.graph(), market, &ord))
.await;
match result {
Ok(r) => {
assert!(!r.route().swaps().is_empty());
}
Err(AlgorithmError::NoPath { .. }) => {
}
Err(e) => panic!("Unexpected error: {:?}", e),
}
}
#[tokio::test]
async fn test_resimulation_produces_correct_amounts() {
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let token_c = token(0x03, "C");
let (market, manager) = setup_market_bf(vec![
("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
("component_bc", &token_b, &token_c, MockProtocolSim::new(3.0)),
]);
let algo = bf_algorithm(3, 1000);
let ord = order(&token_a, &token_c, 100, OrderSide::Sell);
let result = algo
.find_best_route(SolveRequest::new(manager.graph(), market, &ord))
.await
.unwrap();
assert_eq!(result.route().swaps()[0].amount_in(), &BigUint::from(100u64));
assert_eq!(result.route().swaps()[0].amount_out(), &BigUint::from(200u64));
assert_eq!(result.route().swaps()[1].amount_in(), &BigUint::from(200u64));
assert_eq!(result.route().swaps()[1].amount_out(), &BigUint::from(600u64));
}
#[test]
fn algorithm_name() {
let algo = bf_algorithm(4, 200);
assert_eq!(algo.name(), "bellman_ford");
}
#[test]
fn algorithm_timeout() {
let algo = bf_algorithm(4, 200);
assert_eq!(algo.timeout(), Duration::from_millis(200));
}
#[tokio::test]
async fn test_gas_aware_relaxation_picks_cheaper_path() {
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let token_c = token(0x03, "C");
let token_d = token(0x04, "D");
let high_gas: u64 = 100_000_000;
let low_gas: u64 = 100;
let (market, manager) = setup_market_bf(vec![
("component_ab", &token_a, &token_b, MockProtocolSim::new(3.0).with_gas(high_gas)),
("component_bd", &token_b, &token_d, MockProtocolSim::new(2.0).with_gas(high_gas)),
("component_ac", &token_a, &token_c, MockProtocolSim::new(2.0).with_gas(low_gas)),
("component_cd", &token_c, &token_d, MockProtocolSim::new(2.0).with_gas(low_gas)),
]);
let algo = bf_algorithm(3, 1000);
let ord = order(&token_a, &token_d, 1_000_000_000, OrderSide::Sell);
let derived = setup_derived_with_token_prices(&[
token_a.address.clone(),
token_b.address.clone(),
token_c.address.clone(),
token_d.address.clone(),
]);
let result = algo
.find_best_route(SolveRequest::new(manager.graph(), market, &ord).with_derived(derived))
.await
.unwrap();
assert_eq!(result.route().swaps().len(), 2);
assert_eq!(result.route().swaps()[0].component_id(), "component_ac");
assert_eq!(result.route().swaps()[1].component_id(), "component_cd");
}
#[tokio::test]
async fn test_gas_aware_falls_back_to_gross_without_derived() {
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let token_c = token(0x03, "C");
let token_d = token(0x04, "D");
let high_gas: u64 = 100_000_000;
let low_gas: u64 = 100;
let (market, manager) = setup_market_bf(vec![
("component_ab", &token_a, &token_b, MockProtocolSim::new(3.0).with_gas(high_gas)),
("component_bd", &token_b, &token_d, MockProtocolSim::new(2.0).with_gas(high_gas)),
("component_ac", &token_a, &token_c, MockProtocolSim::new(2.0).with_gas(low_gas)),
("component_cd", &token_c, &token_d, MockProtocolSim::new(2.0).with_gas(low_gas)),
]);
let algo = bf_algorithm(3, 1000);
let ord = order(&token_a, &token_d, 1_000_000_000, OrderSide::Sell);
let result = algo
.find_best_route(SolveRequest::new(manager.graph(), market, &ord))
.await
.unwrap();
assert_eq!(result.route().swaps().len(), 2);
assert_eq!(result.route().swaps()[0].component_id(), "component_ab");
assert_eq!(result.route().swaps()[1].component_id(), "component_bd");
}
#[tokio::test]
async fn test_amount_too_small_when_net_uneconomic_after_gas() {
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let (market, manager) = setup_market_bf(vec![(
"component_ab",
&token_a,
&token_b,
MockProtocolSim::new(2.0).with_gas(10),
)]);
let algo = bf_algorithm(2, 1000);
let ord = order(&token_a, &token_b, 1, OrderSide::Sell);
let derived =
setup_derived_with_token_prices(&[token_a.address.clone(), token_b.address.clone()]);
let result = algo
.find_best_route(SolveRequest::new(manager.graph(), market, &ord).with_derived(derived))
.await;
assert!(matches!(
result,
Err(AlgorithmError::NoPath { reason: NoPathReason::AmountTooSmall, .. })
));
}
#[tokio::test]
async fn test_no_graph_path_when_output_uneconomic_but_input_economic() {
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let (market, manager) = setup_market_bf(vec![(
"component_ab",
&token_a,
&token_b,
MockProtocolSim::new(0.05).with_gas(10),
)]);
let algo = bf_algorithm(1, 1000);
let ord = order(&token_a, &token_b, 10_000, OrderSide::Sell);
let derived =
setup_derived_with_token_prices(&[token_a.address.clone(), token_b.address.clone()]);
let result = algo
.find_best_route(SolveRequest::new(manager.graph(), market, &ord).with_derived(derived))
.await;
assert!(matches!(
result,
Err(AlgorithmError::NoPath { reason: NoPathReason::NoGraphPath, .. })
));
}
fn bf_algorithm_with_connectors(
max_hops: usize,
timeout_ms: u64,
connector_tokens: FxHashSet<Address>,
) -> BellmanFordAlgorithm {
BellmanFordAlgorithm::with_config(
AlgorithmConfig::new(1, max_hops, Duration::from_millis(timeout_ms), None)
.unwrap()
.with_connector_tokens(connector_tokens),
)
}
#[tokio::test]
async fn test_connector_tokens_blocks_disallowed_intermediate() {
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let token_c = token(0x03, "C");
let token_d = token(0x04, "D");
let (market, manager) = setup_market_bf(vec![
("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
("component_bd", &token_b, &token_d, MockProtocolSim::new(2.0)),
("component_ac", &token_a, &token_c, MockProtocolSim::new(3.0)),
("component_cd", &token_c, &token_d, MockProtocolSim::new(3.0)),
]);
let connectors: FxHashSet<Address> = FxHashSet::from_iter([token_c.address.clone()]);
let algo = bf_algorithm_with_connectors(3, 1000, connectors);
let ord = order(&token_a, &token_d, 100, OrderSide::Sell);
let result = algo
.find_best_route(SolveRequest::new(manager.graph(), market, &ord))
.await
.unwrap();
assert_eq!(result.route().swaps().len(), 2);
assert_eq!(result.route().swaps()[0].component_id(), "component_ac");
assert_eq!(result.route().swaps()[1].component_id(), "component_cd");
}
#[tokio::test]
async fn test_connector_tokens_allows_endpoints_even_if_not_listed() {
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let (market, manager) =
setup_market_bf(vec![("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0))]);
let algo = bf_algorithm_with_connectors(1, 1000, FxHashSet::default());
let ord = order(&token_a, &token_b, 100, OrderSide::Sell);
let result = algo
.find_best_route(SolveRequest::new(manager.graph(), market, &ord))
.await
.unwrap();
assert_eq!(result.route().swaps().len(), 1);
assert_eq!(result.route().swaps()[0].amount_out(), &BigUint::from(200u64));
}
#[tokio::test]
async fn test_connector_tokens_none_is_unrestricted() {
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let token_c = token(0x03, "C");
let token_d = token(0x04, "D");
let (market, manager) = setup_market_bf(vec![
("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0)),
("component_bd", &token_b, &token_d, MockProtocolSim::new(3.0)),
("component_ac", &token_a, &token_c, MockProtocolSim::new(1.0)),
("component_cd", &token_c, &token_d, MockProtocolSim::new(1.0)),
]);
let algo = bf_algorithm(3, 1000);
let ord = order(&token_a, &token_d, 100, OrderSide::Sell);
let result = algo
.find_best_route(SolveRequest::new(manager.graph(), market, &ord))
.await
.unwrap();
assert_eq!(result.route().swaps()[0].component_id(), "component_ab");
assert_eq!(result.route().swaps()[1].component_id(), "component_bd");
assert_eq!(result.route().swaps()[1].amount_out(), &BigUint::from(600u64));
}
#[test]
fn test_path_has_conflict_detects_node_and_component() {
let mut pred: Vec<Option<(NodeIndex, ComponentId)>> = vec![None; 4];
pred[1] = Some((NodeIndex::new(0), "component_a".into()));
pred[2] = Some((NodeIndex::new(1), "component_b".into()));
assert!(BellmanFordAlgorithm::path_has_conflict(
NodeIndex::new(2),
NodeIndex::new(0),
&"any".into(),
&pred
));
assert!(!BellmanFordAlgorithm::path_has_conflict(
NodeIndex::new(2),
NodeIndex::new(3),
&"any".into(),
&pred
));
assert!(BellmanFordAlgorithm::path_has_conflict(
NodeIndex::new(2),
NodeIndex::new(2),
&"any".into(),
&pred
));
assert!(BellmanFordAlgorithm::path_has_conflict(
NodeIndex::new(2),
NodeIndex::new(3),
&"component_a".into(),
&pred
));
assert!(BellmanFordAlgorithm::path_has_conflict(
NodeIndex::new(2),
NodeIndex::new(3),
&"component_b".into(),
&pred
));
assert!(!BellmanFordAlgorithm::path_has_conflict(
NodeIndex::new(2),
NodeIndex::new(3),
&"component_c".into(),
&pred
));
}
#[tokio::test]
async fn test_find_single_route_with_state_overrides() {
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let (market, manager) =
setup_market_bf(vec![("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0))]);
let algo = bf_algorithm(2, 1000);
let ord = order(&token_a, &token_b, 1000, OrderSide::Sell);
let ctx = algo
.build_context(SolveRequest::new(manager.graph(), market, &ord))
.await
.unwrap();
let normal = algo
.find_single_route(&ctx, &ord, FindRouteOptions::default())
.unwrap();
assert_eq!(normal.route().swaps()[0].amount_out(), &BigUint::from(2000u64));
let opts = FindRouteOptions {
overrides: MarketOverrides::empty()
.with_override("component_ab".to_string(), Box::new(MockProtocolSim::new(1.0))),
};
let overridden = algo
.find_single_route(&ctx, &ord, opts)
.unwrap();
assert_eq!(overridden.route().swaps()[0].amount_out(), &BigUint::from(1000u64));
assert!(
overridden.route().swaps()[0].amount_out() < normal.route().swaps()[0].amount_out()
);
}
#[tokio::test]
async fn test_single_find_route_options_default() {
use super::super::split_primitives::MarketOverrides;
let token_a = token(0x01, "A");
let token_b = token(0x02, "B");
let (market, manager) =
setup_market_bf(vec![("component_ab", &token_a, &token_b, MockProtocolSim::new(2.0))]);
let algo = bf_algorithm(2, 1000);
let ord = order(&token_a, &token_b, 1000, OrderSide::Sell);
let ctx = algo
.build_context(SolveRequest::new(manager.graph(), market, &ord))
.await
.unwrap();
let with_default = algo
.find_single_route(&ctx, &ord, FindRouteOptions::default())
.unwrap();
let with_empty = algo
.find_single_route(&ctx, &ord, FindRouteOptions { overrides: MarketOverrides::empty() })
.unwrap();
assert_eq!(
with_default.route().swaps()[0].amount_out(),
with_empty.route().swaps()[0].amount_out()
);
assert_eq!(with_default.route().swaps()[0].amount_out(), &BigUint::from(2000u64));
}
}