1use std::{
2 collections::{HashMap, HashSet},
3 fmt::Debug,
4 num::ParseIntError,
5 time::Duration,
6};
7
8use alloy::primitives::Address;
9use chrono::NaiveDateTime;
10use metrom_commons::{
11 clients::get_retryable_http_client,
12 types::{amm::Amm, amm_pool_id::AmmPoolId, LightBlock},
13};
14use serde::{de::DeserializeOwned, Deserialize, Serialize};
15use thiserror::Error;
16
17pub use metrom_resolver_commons::{AmmPool, LiquityV2Collateral, Token, TokenWithAddress};
19
20#[derive(Serialize, Clone, Debug)]
21#[serde(rename_all = "camelCase")]
22pub struct ResolveTokensQuery {
23 pub with_usd_prices: bool,
24}
25
26#[derive(Serialize, Clone, Debug)]
27#[serde(rename_all = "camelCase")]
28pub struct ResolveAmmPoolsQuery {
29 pub with_usd_tvls: bool,
30}
31
32#[derive(Serialize, Deserialize, Clone, Debug)]
33#[serde(rename_all = "camelCase")]
34pub struct PricedToken {
35 pub decimals: i32,
36 pub symbol: String,
37 pub name: String,
38 pub usd_price: f64,
39}
40
41#[derive(Serialize, Deserialize, Clone, Debug)]
42#[serde(rename_all = "camelCase")]
43pub struct AmmPoolWithTvl {
44 pub dex: String,
45 pub amm: Amm,
46 pub tokens: Vec<TokenWithAddress>,
47 pub usd_tvl: f64,
48 pub fee: Option<f64>,
49}
50
51#[derive(Error, Debug)]
52pub enum ResolveError {
53 #[error("An error occurred while serializing the query string")]
54 SerializeQuery(#[source] serde_qs::Error),
55 #[error("An error occurred sending the resolve tokens request")]
56 Network(#[source] reqwest_middleware::Error),
57 #[error("Could not deserialize the given response")]
58 Deserialize(#[source] reqwest::Error),
59 #[error("Could not decode the given response")]
60 Decode(#[source] ParseIntError),
61 #[error("Could not find any searched item in response")]
62 Missing,
63}
64
65pub struct ResolverClient {
66 base_url: String,
67 client: reqwest_middleware::ClientWithMiddleware,
68}
69
70impl ResolverClient {
71 pub fn new(url: String, timeout: Duration) -> Result<Self, reqwest::Error> {
72 Ok(Self {
73 base_url: format!("{url}/v1/resolvers"),
74 client: get_retryable_http_client(timeout)?,
75 })
76 }
77
78 async fn resolve_multiple<I: Serialize + Clone, O: DeserializeOwned, Q: Serialize>(
79 &self,
80 resource: &str,
81 selector: HashMap<i32, I>,
82 query: Option<Q>,
83 ) -> Result<HashMap<i32, O>, ResolveError> {
84 if selector.is_empty() {
85 return Ok(HashMap::new());
86 }
87
88 let mut endpoint = format!("{}/{}", self.base_url, resource);
89 if let Some(query) = query {
90 endpoint.push('?');
91 endpoint.push_str(&serde_qs::to_string(&query).map_err(ResolveError::SerializeQuery)?);
92 }
93
94 self.client
95 .post(endpoint)
96 .json(&selector)
97 .send()
98 .await
99 .map_err(ResolveError::Network)?
100 .json::<HashMap<i32, O>>()
101 .await
102 .map_err(ResolveError::Deserialize)
103 }
104
105 async fn resolve_single<I: Serialize + Debug + Clone, O: DeserializeOwned, Q: Serialize>(
106 &self,
107 resource: &str,
108 chain_id: i32,
109 selector: I,
110 query: Option<Q>,
111 ) -> Result<O, ResolveError> {
112 let mut filter = HashMap::new();
113 filter.insert(chain_id, selector);
114 self.resolve_multiple(resource, filter, query)
115 .await?
116 .remove(&chain_id)
117 .ok_or(ResolveError::Missing)
118 }
119
120 pub async fn resolve_unpriced_tokens(
121 &self,
122 token_addresses_by_chain: HashMap<i32, HashSet<Address>>,
123 ) -> Result<HashMap<i32, HashMap<Address, Token>>, ResolveError> {
124 self.resolve_multiple("tokens", token_addresses_by_chain, None::<()>)
125 .await
126 }
127
128 pub async fn resolve_priced_tokens(
129 &self,
130 token_addresses_by_chain: HashMap<i32, HashSet<Address>>,
131 ) -> Result<HashMap<i32, HashMap<Address, PricedToken>>, ResolveError> {
132 self.resolve_multiple(
133 "tokens",
134 token_addresses_by_chain,
135 Some(ResolveTokensQuery {
136 with_usd_prices: true,
137 }),
138 )
139 .await
140 }
141
142 pub async fn resolve_unpriced_token(
143 &self,
144 chain_id: i32,
145 token_address: Address,
146 ) -> Result<Token, ResolveError> {
147 self.resolve_single::<_, HashMap<Address, Token>, _>(
148 "tokens",
149 chain_id,
150 vec![token_address],
151 None::<()>,
152 )
153 .await?
154 .remove(&token_address)
155 .ok_or(ResolveError::Missing)
156 }
157
158 pub async fn resolve_priced_token(
159 &self,
160 chain_id: i32,
161 token_address: Address,
162 ) -> Result<PricedToken, ResolveError> {
163 self.resolve_single::<_, HashMap<Address, PricedToken>, _>(
164 "tokens",
165 chain_id,
166 vec![token_address],
167 Some(ResolveTokensQuery {
168 with_usd_prices: true,
169 }),
170 )
171 .await?
172 .remove(&token_address)
173 .ok_or(ResolveError::Missing)
174 }
175
176 pub async fn resolve_amm_pools_without_tvl(
177 &self,
178 pool_ids_by_chain: HashMap<i32, HashSet<AmmPoolId>>,
179 ) -> Result<HashMap<i32, HashMap<AmmPoolId, AmmPool>>, ResolveError> {
180 self.resolve_multiple("amms/pools", pool_ids_by_chain, None::<()>)
181 .await
182 }
183
184 pub async fn resolve_amm_pools_with_tvl(
185 &self,
186 pool_ids_by_chain: HashMap<i32, HashSet<AmmPoolId>>,
187 ) -> Result<HashMap<i32, HashMap<AmmPoolId, AmmPoolWithTvl>>, ResolveError> {
188 self.resolve_multiple(
189 "amms/pools",
190 pool_ids_by_chain,
191 Some(ResolveAmmPoolsQuery {
192 with_usd_tvls: true,
193 }),
194 )
195 .await
196 }
197
198 pub async fn resolve_amm_pool_without_tvl(
199 &self,
200 chain_id: i32,
201 pool_id: AmmPoolId,
202 ) -> Result<AmmPool, ResolveError> {
203 self.resolve_single::<_, HashMap<AmmPoolId, AmmPool>, ()>(
204 "amms/pools",
205 chain_id,
206 vec![pool_id],
207 None,
208 )
209 .await?
210 .remove(&pool_id)
211 .ok_or(ResolveError::Missing)
212 }
213
214 pub async fn resolve_amm_pool_with_tvl(
215 &self,
216 chain_id: i32,
217 pool_id: AmmPoolId,
218 ) -> Result<AmmPoolWithTvl, ResolveError> {
219 self.resolve_single::<_, HashMap<AmmPoolId, AmmPoolWithTvl>, ResolveAmmPoolsQuery>(
220 "amms/pools",
221 chain_id,
222 vec![pool_id],
223 Some(ResolveAmmPoolsQuery {
224 with_usd_tvls: true,
225 }),
226 )
227 .await?
228 .remove(&pool_id)
229 .ok_or(ResolveError::Missing)
230 }
231
232 pub async fn resolve_prices(
233 &self,
234 token_addresses_by_chain: HashMap<i32, HashSet<Address>>,
235 ) -> Result<HashMap<i32, HashMap<Address, f64>>, ResolveError> {
236 self.resolve_multiple("prices", token_addresses_by_chain, None::<()>)
237 .await
238 }
239
240 pub async fn resolve_price(
241 &self,
242 chain_id: i32,
243 token_address: Address,
244 ) -> Result<f64, ResolveError> {
245 self.resolve_single::<_, HashMap<Address, f64>, ()>(
246 "prices",
247 chain_id,
248 vec![token_address],
249 None,
250 )
251 .await?
252 .remove(&token_address)
253 .ok_or(ResolveError::Missing)
254 }
255
256 pub async fn resolve_amm_pool_tvls(
257 &self,
258 pool_ids_by_chain: HashMap<i32, HashSet<AmmPoolId>>,
259 ) -> Result<HashMap<i32, HashMap<AmmPoolId, f64>>, ResolveError> {
260 self.resolve_multiple("amms/tvls", pool_ids_by_chain, None::<()>)
261 .await
262 }
263
264 pub async fn resolve_amm_pool_tvl(
265 &self,
266 chain_id: i32,
267 pool_id: AmmPoolId,
268 ) -> Result<f64, ResolveError> {
269 self.resolve_single::<_, HashMap<AmmPoolId, f64>, ()>(
270 "amms/tvls",
271 chain_id,
272 vec![pool_id],
273 None,
274 )
275 .await?
276 .remove(&pool_id)
277 .ok_or(ResolveError::Missing)
278 }
279
280 pub async fn get_amm_pools_with_usd_tvl(
281 &self,
282 chain_id: i32,
283 dex: String,
284 ) -> Result<HashMap<AmmPoolId, AmmPoolWithTvl>, ResolveError> {
285 self.client
286 .get(format!(
287 "{}/amms/pools-with-usd-tvls/{}/{}",
288 self.base_url, chain_id, dex
289 ))
290 .send()
291 .await
292 .map_err(ResolveError::Network)?
293 .json::<HashMap<AmmPoolId, AmmPoolWithTvl>>()
294 .await
295 .map_err(ResolveError::Deserialize)
296 }
297
298 pub async fn resolve_all_liquity_v2_collaterals_in_chain(
299 &self,
300 chain_id: i32,
301 brands: HashSet<String>,
302 ) -> Result<HashMap<String, HashMap<Address, LiquityV2Collateral>>, ResolveError> {
303 let mut selector: HashMap<String, HashSet<Address>> = HashMap::new();
304 for brand in brands.into_iter() {
305 selector.insert(brand, HashSet::new());
306 }
307
308 self.resolve_single::<_, HashMap<String, HashMap<Address, LiquityV2Collateral>>, ()>(
309 "liquity-v2/collaterals",
310 chain_id,
311 selector,
312 None::<()>,
313 )
314 .await
315 }
316
317 pub async fn resolve_all_liquity_v2_collaterals_in_chain_for_brand(
318 &self,
319 chain_id: i32,
320 brand: String,
321 ) -> Result<HashMap<Address, LiquityV2Collateral>, ResolveError> {
322 let mut selector: HashMap<String, HashSet<Address>> = HashMap::new();
323 selector.insert(brand.clone(), HashSet::new());
324
325 self.resolve_single::<_, HashMap<String, HashMap<Address, LiquityV2Collateral>>, ()>(
326 "liquity-v2/collaterals",
327 chain_id,
328 selector,
329 None::<()>,
330 )
331 .await?
332 .remove(&brand)
333 .ok_or(ResolveError::Missing)
334 }
335
336 pub async fn resolve_liquity_v2_collaterals_in_chain_for_brand(
337 &self,
338 chain_id: i32,
339 brand: String,
340 collaterals: HashSet<Address>,
341 ) -> Result<HashMap<Address, LiquityV2Collateral>, ResolveError> {
342 let mut brands: HashMap<String, HashSet<Address>> = HashMap::new();
343 brands.insert(brand.clone(), collaterals);
344
345 self.resolve_single::<_, HashMap<String, HashMap<Address, LiquityV2Collateral>>, ()>(
346 "liquity-v2/collaterals",
347 chain_id,
348 brands,
349 None::<()>,
350 )
351 .await?
352 .remove(&brand)
353 .ok_or(ResolveError::Missing)
354 }
355
356 pub async fn resolve_all_liquity_v2_collaterals(
357 &self,
358 brands_by_chain: HashMap<i32, HashSet<String>>,
359 ) -> Result<HashMap<i32, HashMap<String, HashMap<Address, LiquityV2Collateral>>>, ResolveError>
360 {
361 let mut selector: HashMap<i32, HashMap<String, HashSet<Address>>> = HashMap::new();
362 for (chain_id, brands) in brands_by_chain.into_iter() {
363 for brand in brands.into_iter() {
364 selector
365 .entry(chain_id)
366 .or_default()
367 .insert(brand, HashSet::new());
368 }
369 }
370
371 self.resolve_multiple("liquity-v2/collaterals", selector, None::<()>)
372 .await
373 }
374
375 pub async fn resolve_liquity_v2_collaterals(
376 &self,
377 brands_by_chain: HashMap<i32, HashMap<String, HashSet<Address>>>,
378 ) -> Result<HashMap<i32, HashMap<String, HashMap<Address, LiquityV2Collateral>>>, ResolveError>
379 {
380 let mut selector: HashMap<i32, HashMap<String, HashSet<Address>>> = HashMap::new();
381 for (chain_id, brands) in brands_by_chain.into_iter() {
382 for (brand, collaterals) in brands.into_iter() {
383 selector
384 .entry(chain_id)
385 .or_default()
386 .insert(brand, collaterals);
387 }
388 }
389
390 self.resolve_multiple("liquity-v2/collaterals", selector, None::<()>)
391 .await
392 }
393
394 pub async fn resolve_latest_safe_block(
395 &self,
396 chain_id: i32,
397 ) -> Result<LightBlock, ResolveError> {
398 self.client
399 .get(format!("{}/blocks/{}/latest-safe", self.base_url, chain_id))
400 .send()
401 .await
402 .map_err(ResolveError::Network)?
403 .json::<LightBlock>()
404 .await
405 .map_err(ResolveError::Deserialize)
406 }
407
408 pub async fn resolve_block_at(
409 &self,
410 chain_id: i32,
411 timestamp: NaiveDateTime,
412 ) -> Result<LightBlock, ResolveError> {
413 self.client
414 .get(format!(
415 "{}/blocks/{}/{}",
416 self.base_url,
417 chain_id,
418 timestamp.and_utc().timestamp()
419 ))
420 .send()
421 .await
422 .map_err(ResolveError::Network)?
423 .json::<LightBlock>()
424 .await
425 .map_err(ResolveError::Deserialize)
426 }
427}
428
429#[cfg(test)]
430mod test {
431 use serde_json::json;
432 use wiremock::{
433 matchers::{body_json, method},
434 Mock, MockServer, ResponseTemplate,
435 };
436
437 use super::*;
438
439 #[tokio::test]
440 async fn test_resolve_multiple_serde() {
441 let mock_server = MockServer::start().await;
442
443 Mock::given(method("POST"))
444 .and(body_json(json!({
445 "17000": ["0x0000000000000000000000000000000000000001"]
446 })))
447 .respond_with(ResponseTemplate::new(200).set_body_json(json!(
448 {
449 "17000": {
450 "0x0000000000000000000000000000000000000001": {
451 "decimals": 18,
452 "name": "Mocked",
453 "symbol": "MCKD"
454 }
455 }
456 }
457 )))
458 .up_to_n_times(1)
459 .mount(&mock_server)
460 .await;
461
462 let client = ResolverClient::new(mock_server.uri(), Duration::from_secs(5)).unwrap();
463 let resolved_token = client
464 .resolve_unpriced_token(
465 17000,
466 "0x0000000000000000000000000000000000000001"
467 .parse::<Address>()
468 .unwrap(),
469 )
470 .await
471 .unwrap();
472
473 assert_eq!(resolved_token.decimals, 18);
474 assert_eq!(resolved_token.name, "Mocked");
475 assert_eq!(resolved_token.symbol, "MCKD");
476 }
477}