Skip to main content

fynd_core/algorithm/
mod.rs

1//! Route-finding algorithms.
2//!
3//! This module defines the Algorithm trait and built-in implementations.
4//! New algorithms can be added by implementing the trait.
5//!
6//! Algorithms are generic over their preferred graph type, allowing them to use
7//! different graph crates (petgraph, custom, etc.) and leverage built-in algorithms.
8//!
9//! # Adding a New Algorithm
10//!
11//! **External:** Implement the `Algorithm` trait in your own crate and plug it
12//! into a [`WorkerPoolBuilder`](crate::worker_pool::pool::WorkerPoolBuilder) via
13//! [`with_algorithm`](crate::worker_pool::pool::WorkerPoolBuilder::with_algorithm). No changes
14//! to fynd-core required. See the `custom_algorithm` example.
15//!
16//! **Built-in:** To add an algorithm to the built-in registry:
17//! 1. Create a new module with your algorithm implementation
18//! 2. Implement the `Algorithm` trait
19//! 3. Register it in `registry.rs`
20
21pub mod bellman_ford;
22pub mod most_liquid;
23
24#[cfg(test)]
25pub mod test_utils;
26
27use std::time::Duration;
28
29pub use bellman_ford::BellmanFordAlgorithm;
30pub use most_liquid::MostLiquidAlgorithm;
31use tycho_simulation::tycho_core::models::Address;
32
33use crate::{
34    derived::{computation::ComputationRequirements, SharedDerivedDataRef},
35    feed::market_data::SharedMarketDataRef,
36    graph::GraphManager,
37    types::{quote::Order, RouteResult},
38};
39
40/// Configuration for an Algorithm instance.
41#[derive(Debug, Clone)]
42pub struct AlgorithmConfig {
43    /// Minimum hops to search (must be >= 1).
44    min_hops: usize,
45    /// Maximum hops to search.
46    max_hops: usize,
47    /// Timeout for solving.
48    timeout: Duration,
49    /// Maximum number of paths to simulate. `None` means no cap.
50    max_routes: Option<usize>,
51    /// Enable gas-aware comparison (compares net amounts instead of gross during path selection).
52    /// Currently used by Bellman-Ford; ignored by other algorithms. Defaults to true.
53    gas_aware: bool,
54}
55
56impl AlgorithmConfig {
57    /// Creates a new `AlgorithmConfig` with validation.
58    ///
59    /// # Errors
60    ///
61    /// Returns `InvalidConfiguration` if:
62    /// - `min_hops == 0` (at least one hop is required)
63    /// - `min_hops > max_hops`
64    /// - `max_routes` is `Some(0)`
65    pub fn new(
66        min_hops: usize,
67        max_hops: usize,
68        timeout: Duration,
69        max_routes: Option<usize>,
70    ) -> Result<Self, AlgorithmError> {
71        if min_hops == 0 {
72            return Err(AlgorithmError::InvalidConfiguration {
73                reason: "min_hops must be at least 1".to_string(),
74            });
75        }
76        if min_hops > max_hops {
77            return Err(AlgorithmError::InvalidConfiguration {
78                reason: format!("min_hops ({}) cannot exceed max_hops ({})", min_hops, max_hops),
79            });
80        }
81        if max_routes == Some(0) {
82            return Err(AlgorithmError::InvalidConfiguration {
83                reason: "max_routes must be at least 1".to_string(),
84            });
85        }
86        Ok(Self { min_hops, max_hops, timeout, max_routes, gas_aware: true })
87    }
88
89    /// Returns the minimum number of hops to search.
90    pub fn min_hops(&self) -> usize {
91        self.min_hops
92    }
93
94    /// Returns the maximum number of hops to search.
95    pub fn max_hops(&self) -> usize {
96        self.max_hops
97    }
98
99    /// Returns the maximum number of paths to simulate.
100    pub fn max_routes(&self) -> Option<usize> {
101        self.max_routes
102    }
103
104    /// Returns the timeout for solving.
105    pub fn timeout(&self) -> Duration {
106        self.timeout
107    }
108
109    /// Returns whether gas-aware comparison is enabled.
110    pub fn gas_aware(&self) -> bool {
111        self.gas_aware
112    }
113
114    /// Sets gas-aware comparison.
115    pub fn with_gas_aware(mut self, enabled: bool) -> Self {
116        self.gas_aware = enabled;
117        self
118    }
119}
120
121impl Default for AlgorithmConfig {
122    fn default() -> Self {
123        // Default values are valid, so we can unwrap safely
124        Self::new(1, 3, Duration::from_millis(100), None).unwrap()
125    }
126}
127
128/// Trait for route-finding algorithms.
129///
130/// Algorithms are generic over their preferred graph type `G`, allowing them to:
131/// - Use different graph crates (petgraph, custom, etc.)
132/// - Leverage built-in algorithms from graph libraries
133/// - Optimize their graph representation for their specific needs
134///
135/// # Implementation Notes
136///
137/// - Algorithms should respect the timeout from `timeout()`
138/// - They should use `graph` for path finding (BFS/etc)
139/// - They should use `market` to read component states for simulation
140/// - They should NOT modify the graph or market data
141#[allow(async_fn_in_trait)]
142pub trait Algorithm: Send + Sync {
143    /// The graph type this algorithm uses.
144    type GraphType: Send + Sync;
145
146    /// The graph manager type for this algorithm.
147    /// This allows the solver to automatically create the appropriate graph manager.
148    type GraphManager: GraphManager<Self::GraphType> + Default;
149
150    /// Returns the algorithm's name.
151    fn name(&self) -> &str;
152
153    /// Finds the best route for the given order.
154    ///
155    /// # Arguments
156    ///
157    /// * `graph` - The algorithm's preferred graph type (e.g., petgraph::Graph)
158    /// * `market` - Shared reference to market data for state lookups (algorithms acquire their own
159    ///   locks)
160    /// * `derived` - Optional shared reference to derived data (token prices, etc.)
161    /// * `order` - The order to solve
162    ///
163    /// # Returns
164    ///
165    /// The best route and its gas-adjusted net output amount, or an error if no route could be
166    /// found.
167    async fn find_best_route(
168        &self,
169        graph: &Self::GraphType,
170        market: SharedMarketDataRef,
171        derived: Option<SharedDerivedDataRef>,
172        order: &Order,
173    ) -> Result<RouteResult, AlgorithmError>;
174
175    /// Returns the derived data computation requirements for this algorithm.
176    ///
177    /// Algorithms declare freshness requirements for derived data:
178    /// - `require_fresh`: Data must be from the current block (same as SharedMarketData)
179    /// - `allow_stale`: Data can be from any past block, as long as it exists
180    ///
181    /// Workers use this to determine when they can safely solve.
182    ///
183    /// Default implementation returns no requirements - algorithm works without
184    /// any derived data.
185    fn computation_requirements(&self) -> ComputationRequirements;
186
187    /// Returns the timeout for solving.
188    ///
189    /// Workers use this to set the maximum time to wait for derived data
190    /// before failing a solve request.
191    fn timeout(&self) -> Duration;
192}
193
194/// Errors that can occur during route finding.
195#[non_exhaustive]
196#[derive(Debug, Clone, thiserror::Error, PartialEq)]
197pub enum AlgorithmError {
198    /// Invalid algorithm configuration (programmer error).
199    #[non_exhaustive]
200    #[error("invalid configuration: {reason}")]
201    InvalidConfiguration { reason: String },
202
203    /// No path exists between the tokens.
204    #[non_exhaustive]
205    #[error("no path from {from:?} to {to:?}: {reason}")]
206    NoPath { from: Address, to: Address, reason: NoPathReason },
207
208    /// Paths exist but none have sufficient liquidity.
209    #[error("insufficient liquidity on all paths")]
210    InsufficientLiquidity,
211
212    /// Route finding timed out.
213    #[non_exhaustive]
214    #[error("timeout after {elapsed_ms}ms")]
215    Timeout { elapsed_ms: u64 },
216
217    /// Exact-out not supported by this algorithm.
218    #[error("exact-out orders not supported")]
219    ExactOutNotSupported,
220
221    /// Simulation failed for a specific component.
222    #[non_exhaustive]
223    #[error("simulation failed for {component_id}: {error}")]
224    SimulationFailed { component_id: String, error: String },
225
226    /// Required data not found in market.
227    #[non_exhaustive]
228    #[error("{kind} not found{}", id.as_ref().map(|i| format!(": {i}")).unwrap_or_default())]
229    DataNotFound { kind: &'static str, id: Option<String> },
230
231    /// Other algorithm-specific error.
232    #[error("{0}")]
233    Other(String),
234}
235
236/// Reason why no path was found between tokens.
237#[derive(Debug, Clone, Copy, PartialEq, Eq)]
238pub enum NoPathReason {
239    /// Source token not present in the routing graph.
240    SourceTokenNotInGraph,
241    /// Destination token not present in the routing graph.
242    DestinationTokenNotInGraph,
243    /// Both tokens exist but no edges connect them within hop limits.
244    NoGraphPath,
245    /// Paths exist but none could be scored (e.g., missing edge weights).
246    NoScorablePaths,
247}
248
249impl std::fmt::Display for NoPathReason {
250    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
251        match self {
252            Self::SourceTokenNotInGraph => write!(f, "source token not in graph"),
253            Self::DestinationTokenNotInGraph => write!(f, "destination token not in graph"),
254            Self::NoGraphPath => write!(f, "no connecting path in graph"),
255            Self::NoScorablePaths => write!(f, "no paths with valid scores"),
256        }
257    }
258}