Skip to main content

eth_prices/router/
mod.rs

1use std::collections::HashMap;
2
3use petgraph::{
4    dot::Dot,
5    graph::{NodeIndex, UnGraph},
6};
7use tracing::info;
8
9use crate::{
10    Result,
11    asset::AssetIdentifier,
12    quoter::{AnyQuoter, RateDirection},
13};
14use route::{Route, RouteStep};
15
16pub use auto::AutoRouter;
17
18pub mod auto;
19pub mod route;
20
21const MAX_CONFIDENCE: u64 = 100;
22
23#[derive(Debug, Clone)]
24pub struct Router {
25    pub quoters: Vec<AnyQuoter>,
26    pub graph: UnGraph<String, String>,
27    pub token_map: HashMap<String, NodeIndex<u32>>,
28    confidences: HashMap<String, u64>,
29}
30
31impl Default for Router {
32    fn default() -> Self {
33        Self {
34            quoters: Vec::new(),
35            graph: UnGraph::new_undirected(),
36            token_map: HashMap::new(),
37            confidences: HashMap::new(),
38        }
39    }
40}
41
42impl FromIterator<AnyQuoter> for Router {
43    fn from_iter<T: IntoIterator<Item = AnyQuoter>>(iter: T) -> Self {
44        let mut graph = Self::default();
45        for quoter in iter {
46            graph.add_quoter(quoter);
47        }
48        graph
49    }
50}
51
52impl Router {
53    pub fn get_token_index(&self, token: &AssetIdentifier) -> Option<NodeIndex<u32>> {
54        self.token_map.get(&token.to_string()).copied()
55    }
56
57    pub fn get_token_by_index(&self, index: NodeIndex<u32>) -> Option<AssetIdentifier> {
58        self.token_map
59            .iter()
60            .find(|x| *x.1 == index)
61            .map(|(token, _)| token.clone())
62            .map(AssetIdentifier::try_from)
63            .and_then(|x| x.ok())
64    }
65
66    pub fn add_token(&mut self, token: &AssetIdentifier) -> NodeIndex<u32> {
67        match self.token_map.get(&token.to_string()) {
68            Some(node_index) => *node_index,
69            None => {
70                let slug = token.to_string();
71                let node_index = self.graph.add_node(slug.to_owned());
72                self.token_map.insert(slug, node_index);
73                node_index
74            }
75        }
76    }
77
78    pub fn add_quoter(&mut self, quoter: AnyQuoter) {
79        let slug = quoter.to_string();
80        let confidence = quoter.confidence;
81        let (token_in, token_out) = quoter.tokens();
82        self.quoters.push(quoter);
83        self.confidences.insert(slug.clone(), confidence);
84
85        let token_in_index = self.add_token(&token_in);
86        let token_out_index = self.add_token(&token_out);
87
88        self.graph
89            .extend_with_edges([(token_in_index, token_out_index, slug)]);
90    }
91
92    #[cfg(feature = "ecb")]
93    pub fn with_ecb(mut self) -> Self {
94        use crate::quoter::ecb::EcbRateSource;
95
96        let fiat = EcbRateSource::default();
97        self.merge_with(fiat.graph());
98        self
99    }
100
101    /// Merge two routers together.
102    ///
103    /// This can be useful when leveraging [`crate::quoter::ecb::EcbRateSource`] to build a router.
104    /// ```
105    /// use eth_prices::{quoter::ecb::EcbRateSource, router::Router};
106    ///
107    /// let ecb_rate_source = EcbRateSource::default();
108    /// let ecb_graph = ecb_rate_source.graph();
109    ///
110    /// let router = Router::default().merge_with(ecb_graph);
111    /// ```
112    pub fn merge_with(&mut self, other: Self) -> &mut Self {
113        for quoter in other.quoters {
114            self.add_quoter(quoter);
115        }
116        self
117    }
118
119    pub fn to_graphviz(&self) -> String {
120        Dot::new(&self.graph).to_string()
121    }
122
123    /// compute a route given an input and output token
124    pub fn compute(
125        &self,
126        input_token: &AssetIdentifier,
127        output_token: &AssetIdentifier,
128    ) -> Result<Route> {
129        let token_a_index = self
130            .get_token_index(input_token)
131            .ok_or_else(|| crate::error::EthPricesError::AssetNotFound(input_token.to_string()))?;
132        let token_b_index = self
133            .get_token_index(output_token)
134            .ok_or_else(|| crate::error::EthPricesError::AssetNotFound(output_token.to_string()))?;
135
136        info!(
137            target: "router::compute_start",
138            input_token = %input_token,
139            output_token = %output_token,
140        );
141
142        let confidences = &self.confidences;
143        let path = petgraph::algo::astar(
144            &self.graph,
145            token_a_index,
146            |x| x == token_b_index,
147            |edge| {
148                let slug = edge.weight();
149                let conf = confidences.get(slug.as_str()).copied().unwrap_or(0);
150                (MAX_CONFIDENCE + 1 - conf.min(MAX_CONFIDENCE)) as u32
151            },
152            |_| 0,
153        );
154
155        match path {
156            None => Err(crate::error::EthPricesError::NoRouteFound(
157                input_token.to_string(),
158                output_token.to_string(),
159            )),
160            Some((_cost, node_path)) => {
161                info!(
162                    target: "router::compute_end",
163                    node_path = ?node_path,
164                );
165                let token_route = node_path
166                    .iter()
167                    .map(|x| {
168                        self.get_token_by_index(*x)
169                            .ok_or_else(|| crate::error::EthPricesError::MissingTokenInRoute)
170                    })
171                    .collect::<Result<Vec<AssetIdentifier>>>()?;
172
173                let mut path = Vec::new();
174
175                let mut previous_token = input_token;
176                for next_token in token_route.iter() {
177                    if *previous_token == *next_token {
178                        continue;
179                    };
180
181                    let quoter = self
182                        .quoters
183                        .iter()
184                        .find(|x| {
185                            let (token_in, token_out) = x.tokens();
186
187                            (token_in == *previous_token && token_out == *next_token)
188                                || (token_in == *next_token && token_out == *previous_token)
189                        })
190                        .ok_or_else(|| crate::error::EthPricesError::MissingQuoterInRoute)?;
191
192                    path.push(RouteStep {
193                        quoter: quoter.clone(),
194                        direction: if *previous_token == quoter.tokens().0 {
195                            RateDirection::Forward
196                        } else {
197                            RateDirection::Reverse
198                        },
199                    });
200                    previous_token = next_token;
201                }
202
203                if path.len() != node_path.len() - 1 {
204                    return Err(crate::error::EthPricesError::PathLengthMismatch {
205                        expected: node_path.len() - 1,
206                        actual: path.len(),
207                    });
208                }
209
210                Ok(Route {
211                    path,
212                    input_token: input_token.clone(),
213                    output_token: output_token.clone(),
214                })
215            }
216        }
217    }
218}