pub mod bellman_ford;
pub mod most_liquid;
#[cfg(test)]
pub mod test_utils;
use std::{collections::HashSet, time::Duration};
pub use bellman_ford::BellmanFordAlgorithm;
pub use most_liquid::MostLiquidAlgorithm;
use tycho_simulation::tycho_core::models::Address;
use crate::{
derived::{computation::ComputationRequirements, SharedDerivedDataRef},
feed::market_data::SharedMarketDataRef,
graph::GraphManager,
types::{quote::Order, RouteResult},
};
#[must_use]
#[derive(Debug, Clone)]
pub struct AlgorithmConfig {
min_hops: usize,
max_hops: usize,
timeout: Duration,
max_routes: Option<usize>,
gas_aware: bool,
connector_tokens: Option<HashSet<Address>>,
}
impl AlgorithmConfig {
pub fn new(
min_hops: usize,
max_hops: usize,
timeout: Duration,
max_routes: Option<usize>,
) -> Result<Self, AlgorithmError> {
if min_hops == 0 {
return Err(AlgorithmError::InvalidConfiguration {
reason: "min_hops must be at least 1".to_string(),
});
}
if min_hops > max_hops {
return Err(AlgorithmError::InvalidConfiguration {
reason: format!("min_hops ({}) cannot exceed max_hops ({})", min_hops, max_hops),
});
}
if max_routes == Some(0) {
return Err(AlgorithmError::InvalidConfiguration {
reason: "max_routes must be at least 1".to_string(),
});
}
Ok(Self {
min_hops,
max_hops,
timeout,
max_routes,
gas_aware: true,
connector_tokens: None,
})
}
pub fn min_hops(&self) -> usize {
self.min_hops
}
pub fn max_hops(&self) -> usize {
self.max_hops
}
pub fn max_routes(&self) -> Option<usize> {
self.max_routes
}
pub fn timeout(&self) -> Duration {
self.timeout
}
pub fn gas_aware(&self) -> bool {
self.gas_aware
}
pub fn with_gas_aware(mut self, enabled: bool) -> Self {
self.gas_aware = enabled;
self
}
pub fn with_connector_tokens(mut self, tokens: HashSet<Address>) -> Self {
self.connector_tokens = Some(tokens);
self
}
pub fn connector_tokens(&self) -> Option<&HashSet<Address>> {
self.connector_tokens.as_ref()
}
}
impl Default for AlgorithmConfig {
fn default() -> Self {
Self::new(1, 3, Duration::from_millis(100), None).unwrap()
}
}
#[allow(async_fn_in_trait)]
pub trait Algorithm: Send + Sync {
type GraphType: Send + Sync;
type GraphManager: GraphManager<Self::GraphType> + Default;
fn name(&self) -> &str;
async fn find_best_route(
&self,
graph: &Self::GraphType,
market: SharedMarketDataRef,
derived: Option<SharedDerivedDataRef>,
order: &Order,
) -> Result<RouteResult, AlgorithmError>;
fn computation_requirements(&self) -> ComputationRequirements;
fn timeout(&self) -> Duration;
}
#[non_exhaustive]
#[derive(Debug, Clone, thiserror::Error, PartialEq)]
pub enum AlgorithmError {
#[non_exhaustive]
#[error("invalid configuration: {reason}")]
InvalidConfiguration {
reason: String,
},
#[non_exhaustive]
#[error("no path from {from:?} to {to:?}: {reason}")]
NoPath {
from: Address,
to: Address,
reason: NoPathReason,
},
#[error("insufficient liquidity on all paths")]
InsufficientLiquidity,
#[non_exhaustive]
#[error("timeout after {elapsed_ms}ms")]
Timeout {
elapsed_ms: u64,
},
#[error("exact-out orders not supported")]
ExactOutNotSupported,
#[non_exhaustive]
#[error("simulation failed for {component_id}: {error}")]
SimulationFailed {
component_id: String,
error: String,
},
#[non_exhaustive]
#[error("{kind} not found{}", id.as_ref().map(|i| format!(": {i}")).unwrap_or_default())]
DataNotFound {
kind: &'static str,
id: Option<String>,
},
#[error("{0}")]
Other(String),
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NoPathReason {
SourceTokenNotInGraph,
DestinationTokenNotInGraph,
NoGraphPath,
NoScorablePaths,
}
impl std::fmt::Display for NoPathReason {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::SourceTokenNotInGraph => write!(f, "source token not in graph"),
Self::DestinationTokenNotInGraph => write!(f, "destination token not in graph"),
Self::NoGraphPath => write!(f, "no connecting path in graph"),
Self::NoScorablePaths => write!(f, "no paths with valid scores"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_connector_tokens_default_is_none() {
assert!(AlgorithmConfig::default()
.connector_tokens()
.is_none());
}
#[test]
fn test_with_connector_tokens_sets_field() {
let addr = Address::from([0x01u8; 20]);
let tokens: HashSet<Address> = [addr.clone()].into();
let config = AlgorithmConfig::default().with_connector_tokens(tokens);
let stored = config
.connector_tokens()
.expect("should be Some");
assert!(stored.contains(&addr));
assert_eq!(stored.len(), 1);
}
#[test]
fn test_with_connector_tokens_empty_set() {
let config = AlgorithmConfig::default().with_connector_tokens(HashSet::new());
assert_eq!(
config
.connector_tokens()
.map(|s| s.len()),
Some(0)
);
}
}