pub mod bellman_ford;
pub mod most_liquid;
pub mod path_frank_wolfe;
pub(crate) mod path_scoring;
pub mod paths;
pub mod registry;
pub mod request;
pub(crate) mod sim_guard;
pub mod sim_meter;
pub mod split_primitives;
pub mod water_fill;
#[cfg(any(test, feature = "test-utils"))]
pub mod split_test_harness;
pub mod swap_cache;
#[cfg(any(test, feature = "test-utils"))]
pub mod test_utils;
use std::time::Duration;
pub use bellman_ford::BellmanFordAlgorithm;
pub use most_liquid::MostLiquidAlgorithm;
pub use path_frank_wolfe::PathFrankWolfeAlgorithm;
pub use registry::{AlgorithmRegistry, RegisterAlgorithmError};
pub use request::{SolveParts, SolveRequest};
use rustc_hash::FxHashSet;
use tycho_simulation::tycho_core::models::Address;
pub use water_fill::WaterFillAlgorithm;
use crate::{
derived::computation::ComputationRequirements, graph::GraphManager, types::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<FxHashSet<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: impl IntoIterator<Item = Address>) -> Self {
self.connector_tokens = Some(tokens.into_iter().collect());
self
}
pub fn connector_tokens(&self) -> Option<&FxHashSet<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,
request: SolveRequest<'_, Self::GraphType>,
) -> 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),
}
impl From<crate::types::RouteValidationError> for AlgorithmError {
fn from(error: crate::types::RouteValidationError) -> Self {
Self::Other(error.to_string())
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NoPathReason {
SourceTokenNotInGraph,
DestinationTokenNotInGraph,
NoGraphPath,
NoScorablePaths,
AmountTooSmall,
}
impl AlgorithmError {
#[must_use]
pub fn no_path(from: Address, to: Address, reason: NoPathReason) -> Self {
Self::NoPath { from, to, reason }
}
#[must_use]
pub fn timeout(elapsed_ms: u64) -> Self {
Self::Timeout { elapsed_ms }
}
#[must_use]
pub fn simulation_failed(component_id: impl Into<String>, error: impl Into<String>) -> Self {
Self::SimulationFailed { component_id: component_id.into(), error: error.into() }
}
#[must_use]
pub fn data_not_found(kind: &'static str, id: impl Into<Option<String>>) -> Self {
Self::DataNotFound { kind, id: id.into() }
}
#[must_use]
pub fn invalid_configuration(reason: impl Into<String>) -> Self {
Self::InvalidConfiguration { reason: reason.into() }
}
}
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"),
Self::AmountTooSmall => write!(f, "amount too small to route"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_constructors_put_each_argument_where_its_name_says() {
let from = Address::from(vec![0x0Au8]);
let to = Address::from(vec![0x0Bu8]);
match AlgorithmError::no_path(from.clone(), to.clone(), NoPathReason::NoGraphPath) {
AlgorithmError::NoPath { from: f, to: t, reason } => {
assert_eq!((f, t, reason), (from, to, NoPathReason::NoGraphPath));
}
other => panic!("expected NoPath, got {other:?}"),
}
match AlgorithmError::simulation_failed("pool-1", "reverted") {
AlgorithmError::SimulationFailed { component_id, error } => {
assert_eq!((component_id.as_str(), error.as_str()), ("pool-1", "reverted"));
}
other => panic!("expected SimulationFailed, got {other:?}"),
}
match AlgorithmError::data_not_found("token", "0x0a".to_string()) {
AlgorithmError::DataNotFound { kind, id } => {
assert_eq!((kind, id.as_deref()), ("token", Some("0x0a")));
}
other => panic!("expected DataNotFound, got {other:?}"),
}
assert!(matches!(AlgorithmError::timeout(42), AlgorithmError::Timeout { elapsed_ms: 42 }));
assert!(matches!(
AlgorithmError::invalid_configuration("bad"),
AlgorithmError::InvalidConfiguration { .. }
));
}
#[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: FxHashSet<Address> = FxHashSet::from_iter([addr.clone()]);
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(FxHashSet::default());
assert_eq!(
config
.connector_tokens()
.map(|s| s.len()),
Some(0)
);
}
}