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 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 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}