1use std::{
2 collections::{HashMap, HashSet},
3 fmt::Debug,
4 num::ParseIntError,
5 time::Duration,
6};
7
8use alloy::primitives::{Address, U256};
9use chrono::NaiveDateTime;
10use metrom_commons::{
11 clients::get_retryable_http_client,
12 types::{
13 amm::Amm, amm_pool_id::AmmPoolId, amm_pool_liquidity_type::AmmPoolLiquidityType, LightBlock,
14 },
15};
16use metrom_resolver_commons::{ResolveAmmPoolsQuery, ResolveTokensQuery};
17use serde::{de::DeserializeOwned, Deserialize, Serialize};
18use thiserror::Error;
19
20pub use metrom_resolver_commons::{
22 AmmPool, GmxV1Collateral, LiquityV2Collateral, Token, TokenWithAddress,
23};
24
25#[derive(Serialize, Deserialize, Clone, Debug)]
26#[serde(rename_all = "camelCase")]
27pub struct PricedToken {
28 pub decimals: i32,
29 pub symbol: String,
30 pub name: String,
31 pub usd_price: f64,
32}
33
34#[derive(Serialize, Deserialize, Clone, Debug)]
35#[serde(rename_all = "camelCase")]
36pub struct AmmPoolWithTvl {
37 pub dex: String,
38 pub amm: Amm,
39 pub liquidity_type: AmmPoolLiquidityType,
40 pub tokens: Vec<TokenWithAddress>,
41 pub usd_tvl: f64,
42 pub fee: Option<f64>,
43}
44
45#[derive(Serialize, Deserialize, Clone, Debug)]
46#[serde(rename_all = "camelCase")]
47pub struct AmmPoolWithTvlAndLiquidity {
48 pub dex: String,
49 pub amm: Amm,
50 pub liquidity_type: AmmPoolLiquidityType,
51 pub liquidity: U256,
52 pub tokens: Vec<TokenWithAddress>,
53 pub usd_tvl: f64,
54 pub fee: Option<f64>,
55}
56
57trait Selector: Serialize {
58 fn is_empty(&self) -> bool;
59}
60
61impl<K: Serialize, V: Serialize> Selector for HashMap<K, V> {
62 fn is_empty(&self) -> bool {
63 self.is_empty()
64 }
65}
66
67impl<T: Serialize> Selector for HashSet<T> {
68 fn is_empty(&self) -> bool {
69 self.is_empty()
70 }
71}
72
73#[derive(Error, Debug)]
74pub enum ResolveError {
75 #[error("An error occurred while serializing the query string")]
76 SerializeQuery(#[source] serde_qs::Error),
77 #[error("An error occurred sending the resolve tokens request")]
78 Network(#[source] reqwest_middleware::Error),
79 #[error("Could not deserialize the given response")]
80 Deserialize(#[source] reqwest::Error),
81 #[error("Could not decode the given response")]
82 Decode(#[source] ParseIntError),
83 #[error("Could not find any searched item in response")]
84 Missing,
85}
86
87pub struct ResolverClient {
88 base_url: String,
89 client: reqwest_middleware::ClientWithMiddleware,
90}
91
92impl ResolverClient {
93 pub fn new(url: String, timeout: Duration) -> Result<Self, reqwest::Error> {
94 Ok(Self {
95 base_url: format!("{url}/v1/resolvers"),
96 client: get_retryable_http_client(timeout)?,
97 })
98 }
99
100 async fn resolve_multiple<S: Selector, O: DeserializeOwned, Q: Serialize>(
101 &self,
102 resource: &str,
103 selector: S,
104 query: Option<Q>,
105 ) -> Result<HashMap<i32, O>, ResolveError> {
106 if selector.is_empty() {
107 return Ok(HashMap::new());
108 }
109
110 let mut endpoint = format!("{}/{}", self.base_url, resource);
111 if let Some(query) = query {
112 endpoint.push('?');
113 endpoint.push_str(&serde_qs::to_string(&query).map_err(ResolveError::SerializeQuery)?);
114 }
115
116 self.client
117 .post(endpoint)
118 .json(&selector)
119 .send()
120 .await
121 .map_err(ResolveError::Network)?
122 .json::<HashMap<i32, O>>()
123 .await
124 .map_err(ResolveError::Deserialize)
125 }
126
127 async fn resolve_single<I: Serialize + Debug + Clone, O: DeserializeOwned, Q: Serialize>(
128 &self,
129 resource: &str,
130 chain_id: i32,
131 selector: I,
132 query: Option<Q>,
133 ) -> Result<O, ResolveError> {
134 let mut filter = HashMap::new();
135 filter.insert(chain_id, selector);
136 self.resolve_multiple(resource, filter, query)
137 .await?
138 .remove(&chain_id)
139 .ok_or(ResolveError::Missing)
140 }
141
142 pub async fn resolve_unpriced_tokens(
143 &self,
144 token_addresses_by_chain: HashMap<i32, HashSet<Address>>,
145 ) -> Result<HashMap<i32, HashMap<Address, Token>>, ResolveError> {
146 self.resolve_multiple("tokens", token_addresses_by_chain, None::<()>)
147 .await
148 }
149
150 pub async fn resolve_priced_tokens(
151 &self,
152 token_addresses_by_chain: HashMap<i32, HashSet<Address>>,
153 ) -> Result<HashMap<i32, HashMap<Address, PricedToken>>, ResolveError> {
154 self.resolve_multiple(
155 "tokens",
156 token_addresses_by_chain,
157 Some(ResolveTokensQuery {
158 with_usd_prices: Some(true),
159 }),
160 )
161 .await
162 }
163
164 pub async fn resolve_unpriced_token(
165 &self,
166 chain_id: i32,
167 token_address: Address,
168 ) -> Result<Token, ResolveError> {
169 self.resolve_single::<_, HashMap<Address, Token>, _>(
170 "tokens",
171 chain_id,
172 vec![token_address],
173 None::<()>,
174 )
175 .await?
176 .remove(&token_address)
177 .ok_or(ResolveError::Missing)
178 }
179
180 pub async fn resolve_priced_token(
181 &self,
182 chain_id: i32,
183 token_address: Address,
184 ) -> Result<PricedToken, ResolveError> {
185 self.resolve_single::<_, HashMap<Address, PricedToken>, _>(
186 "tokens",
187 chain_id,
188 vec![token_address],
189 Some(ResolveTokensQuery {
190 with_usd_prices: Some(true),
191 }),
192 )
193 .await?
194 .remove(&token_address)
195 .ok_or(ResolveError::Missing)
196 }
197
198 pub async fn resolve_amm_pools_without_tvl(
199 &self,
200 pool_ids_by_chain: HashMap<i32, HashSet<AmmPoolId>>,
201 ) -> Result<HashMap<i32, HashMap<AmmPoolId, AmmPool>>, ResolveError> {
202 self.resolve_multiple("amms/pools", pool_ids_by_chain, None::<()>)
203 .await
204 }
205
206 pub async fn resolve_amm_pools_with_tvl(
207 &self,
208 pool_ids_by_chain: HashMap<i32, HashSet<AmmPoolId>>,
209 ) -> Result<HashMap<i32, HashMap<AmmPoolId, AmmPoolWithTvl>>, ResolveError> {
210 self.resolve_multiple(
211 "amms/pools",
212 pool_ids_by_chain,
213 Some(ResolveAmmPoolsQuery {
214 with_usd_tvls: Some(true),
215 with_liquidity: Some(false),
216 }),
217 )
218 .await
219 }
220
221 pub async fn resolve_amm_pools_with_tvl_and_liquidity(
222 &self,
223 pool_ids_by_chain: HashMap<i32, HashSet<AmmPoolId>>,
224 ) -> Result<HashMap<i32, HashMap<AmmPoolId, AmmPoolWithTvlAndLiquidity>>, ResolveError> {
225 self.resolve_multiple(
226 "amms/pools",
227 pool_ids_by_chain,
228 Some(ResolveAmmPoolsQuery {
229 with_usd_tvls: Some(true),
230 with_liquidity: Some(true),
231 }),
232 )
233 .await
234 }
235
236 pub async fn resolve_amm_pool_without_tvl(
237 &self,
238 chain_id: i32,
239 pool_id: AmmPoolId,
240 ) -> Result<AmmPool, ResolveError> {
241 self.resolve_single::<_, HashMap<AmmPoolId, AmmPool>, ()>(
242 "amms/pools",
243 chain_id,
244 vec![pool_id],
245 None,
246 )
247 .await?
248 .remove(&pool_id)
249 .ok_or(ResolveError::Missing)
250 }
251
252 pub async fn resolve_amm_pool_with_tvl(
253 &self,
254 chain_id: i32,
255 pool_id: AmmPoolId,
256 ) -> Result<AmmPoolWithTvl, ResolveError> {
257 self.resolve_single::<_, HashMap<AmmPoolId, AmmPoolWithTvl>, ResolveAmmPoolsQuery>(
258 "amms/pools",
259 chain_id,
260 vec![pool_id],
261 Some(ResolveAmmPoolsQuery {
262 with_usd_tvls: Some(true),
263 with_liquidity: None,
264 }),
265 )
266 .await?
267 .remove(&pool_id)
268 .ok_or(ResolveError::Missing)
269 }
270
271 pub async fn resolve_amm_pool_with_tvl_and_liquidity(
272 &self,
273 chain_id: i32,
274 pool_id: AmmPoolId,
275 ) -> Result<AmmPoolWithTvlAndLiquidity, ResolveError> {
276 self.resolve_single::<_, HashMap<AmmPoolId, AmmPoolWithTvlAndLiquidity>, ResolveAmmPoolsQuery>(
277 "amms/pools",
278 chain_id,
279 vec![pool_id],
280 Some(ResolveAmmPoolsQuery {
281 with_usd_tvls: Some(true),
282 with_liquidity: Some(true),
283 }),
284 )
285 .await?
286 .remove(&pool_id)
287 .ok_or(ResolveError::Missing)
288 }
289
290 pub async fn resolve_prices(
291 &self,
292 token_addresses_by_chain: HashMap<i32, HashSet<Address>>,
293 ) -> Result<HashMap<i32, HashMap<Address, f64>>, ResolveError> {
294 self.resolve_multiple("prices", token_addresses_by_chain, None::<()>)
295 .await
296 }
297
298 pub async fn resolve_price(
299 &self,
300 chain_id: i32,
301 token_address: Address,
302 ) -> Result<f64, ResolveError> {
303 self.resolve_single::<_, HashMap<Address, f64>, ()>(
304 "prices",
305 chain_id,
306 vec![token_address],
307 None,
308 )
309 .await?
310 .remove(&token_address)
311 .ok_or(ResolveError::Missing)
312 }
313
314 pub async fn resolve_amm_pool_tvls(
315 &self,
316 pool_ids_by_chain: HashMap<i32, HashSet<AmmPoolId>>,
317 ) -> Result<HashMap<i32, HashMap<AmmPoolId, f64>>, ResolveError> {
318 self.resolve_multiple("amms/tvls", pool_ids_by_chain, None::<()>)
319 .await
320 }
321
322 pub async fn resolve_amm_pool_tvl(
323 &self,
324 chain_id: i32,
325 pool_id: AmmPoolId,
326 ) -> Result<f64, ResolveError> {
327 self.resolve_single::<_, HashMap<AmmPoolId, f64>, ()>(
328 "amms/tvls",
329 chain_id,
330 vec![pool_id],
331 None,
332 )
333 .await?
334 .remove(&pool_id)
335 .ok_or(ResolveError::Missing)
336 }
337
338 pub async fn get_amm_pools_with_usd_tvl_and_liquidity(
339 &self,
340 chain_id: i32,
341 dex: String,
342 ) -> Result<HashMap<AmmPoolId, AmmPoolWithTvlAndLiquidity>, ResolveError> {
343 self.client
344 .get(format!(
345 "{}/amms/pools-with-usd-tvls/{}/{}",
346 self.base_url, chain_id, dex
347 ))
348 .send()
349 .await
350 .map_err(ResolveError::Network)?
351 .json::<HashMap<AmmPoolId, AmmPoolWithTvlAndLiquidity>>()
352 .await
353 .map_err(ResolveError::Deserialize)
354 }
355
356 pub async fn resolve_amm_pool_liquidities_by_addresses(
357 &self,
358 addresses_by_pool_id_and_chains: HashMap<i32, HashMap<AmmPoolId, HashSet<Address>>>,
359 ) -> Result<HashMap<i32, HashMap<AmmPoolId, HashMap<Address, U256>>>, ResolveError> {
360 self.resolve_multiple(
361 "amms/liquidities-by-addresses",
362 addresses_by_pool_id_and_chains,
363 None::<()>,
364 )
365 .await
366 }
367
368 pub async fn resolve_amm_pool_liquidity_by_addresses(
369 &self,
370 chain_id: i32,
371 pool_id: AmmPoolId,
372 addresses: HashSet<Address>,
373 ) -> Result<HashMap<Address, U256>, ResolveError> {
374 let mut selector = HashMap::new();
375 selector.insert(pool_id, addresses);
376
377 self.resolve_single::<_, HashMap<AmmPoolId, HashMap<Address, U256>>, ()>(
378 "amms/liquidities-by-addresses",
379 chain_id,
380 selector,
381 None,
382 )
383 .await?
384 .remove(&pool_id)
385 .ok_or(ResolveError::Missing)
386 }
387
388 pub async fn resolve_all_liquity_v2_collaterals_in_chain(
389 &self,
390 chain_id: i32,
391 brands: HashSet<String>,
392 ) -> Result<HashMap<String, HashMap<Address, LiquityV2Collateral>>, ResolveError> {
393 let mut selector: HashMap<String, HashSet<Address>> = HashMap::new();
394 for brand in brands.into_iter() {
395 selector.insert(brand, HashSet::new());
396 }
397
398 self.resolve_single::<_, HashMap<String, HashMap<Address, LiquityV2Collateral>>, ()>(
399 "liquity-v2/collaterals",
400 chain_id,
401 selector,
402 None::<()>,
403 )
404 .await
405 }
406
407 pub async fn resolve_all_liquity_v2_collaterals_in_chain_for_brand(
408 &self,
409 chain_id: i32,
410 brand: String,
411 ) -> Result<HashMap<Address, LiquityV2Collateral>, ResolveError> {
412 let mut selector: HashMap<String, HashSet<Address>> = HashMap::new();
413 selector.insert(brand.clone(), HashSet::new());
414
415 self.resolve_single::<_, HashMap<String, HashMap<Address, LiquityV2Collateral>>, ()>(
416 "liquity-v2/collaterals",
417 chain_id,
418 selector,
419 None::<()>,
420 )
421 .await?
422 .remove(&brand)
423 .ok_or(ResolveError::Missing)
424 }
425
426 pub async fn resolve_liquity_v2_collaterals_in_chain_for_brand(
427 &self,
428 chain_id: i32,
429 brand: String,
430 collaterals: HashSet<Address>,
431 ) -> Result<HashMap<Address, LiquityV2Collateral>, ResolveError> {
432 let mut brands: HashMap<String, HashSet<Address>> = HashMap::new();
433 brands.insert(brand.clone(), collaterals);
434
435 self.resolve_single::<_, HashMap<String, HashMap<Address, LiquityV2Collateral>>, ()>(
436 "liquity-v2/collaterals",
437 chain_id,
438 brands,
439 None::<()>,
440 )
441 .await?
442 .remove(&brand)
443 .ok_or(ResolveError::Missing)
444 }
445
446 pub async fn resolve_all_liquity_v2_collaterals(
447 &self,
448 brands_by_chain: HashMap<i32, HashSet<String>>,
449 ) -> Result<HashMap<i32, HashMap<String, HashMap<Address, LiquityV2Collateral>>>, ResolveError>
450 {
451 let mut selector: HashMap<i32, HashMap<String, HashSet<Address>>> = HashMap::new();
452 for (chain_id, brands) in brands_by_chain.into_iter() {
453 for brand in brands.into_iter() {
454 selector
455 .entry(chain_id)
456 .or_default()
457 .insert(brand, HashSet::new());
458 }
459 }
460
461 self.resolve_multiple("liquity-v2/collaterals", selector, None::<()>)
462 .await
463 }
464
465 pub async fn resolve_liquity_v2_collaterals(
466 &self,
467 brands_by_chain: HashMap<i32, HashMap<String, HashSet<Address>>>,
468 ) -> Result<HashMap<i32, HashMap<String, HashMap<Address, LiquityV2Collateral>>>, ResolveError>
469 {
470 let mut selector: HashMap<i32, HashMap<String, HashSet<Address>>> = HashMap::new();
471 for (chain_id, brands) in brands_by_chain.into_iter() {
472 for (brand, collaterals) in brands.into_iter() {
473 selector
474 .entry(chain_id)
475 .or_default()
476 .insert(brand, collaterals);
477 }
478 }
479
480 self.resolve_multiple("liquity-v2/collaterals", selector, None::<()>)
481 .await
482 }
483
484 pub async fn resolve_all_gmx_v1_collaterals_in_chain(
485 &self,
486 chain_id: i32,
487 brands: HashSet<String>,
488 ) -> Result<HashMap<String, HashMap<Address, GmxV1Collateral>>, ResolveError> {
489 let mut selector: HashMap<String, HashSet<Address>> = HashMap::new();
490 for brand in brands.into_iter() {
491 selector.insert(brand, HashSet::new());
492 }
493
494 self.resolve_single::<_, HashMap<String, HashMap<Address, GmxV1Collateral>>, ()>(
495 "gmx-v1/collaterals",
496 chain_id,
497 selector,
498 None::<()>,
499 )
500 .await
501 }
502
503 pub async fn resolve_all_gmx_v1_collaterals_in_chain_for_brand(
504 &self,
505 chain_id: i32,
506 brand: String,
507 ) -> Result<HashMap<Address, GmxV1Collateral>, ResolveError> {
508 let mut selector: HashMap<String, HashSet<Address>> = HashMap::new();
509 selector.insert(brand.clone(), HashSet::new());
510
511 self.resolve_single::<_, HashMap<String, HashMap<Address, GmxV1Collateral>>, ()>(
512 "gmx-v1/collaterals",
513 chain_id,
514 selector,
515 None::<()>,
516 )
517 .await?
518 .remove(&brand)
519 .ok_or(ResolveError::Missing)
520 }
521
522 pub async fn resolve_gmx_v1_collaterals_in_chain_for_brand(
523 &self,
524 chain_id: i32,
525 brand: String,
526 collaterals: HashSet<Address>,
527 ) -> Result<HashMap<Address, GmxV1Collateral>, ResolveError> {
528 let mut brands: HashMap<String, HashSet<Address>> = HashMap::new();
529 brands.insert(brand.clone(), collaterals);
530
531 self.resolve_single::<_, HashMap<String, HashMap<Address, GmxV1Collateral>>, ()>(
532 "gmx-v1/collaterals",
533 chain_id,
534 brands,
535 None::<()>,
536 )
537 .await?
538 .remove(&brand)
539 .ok_or(ResolveError::Missing)
540 }
541
542 pub async fn resolve_all_gmx_v1_collaterals(
543 &self,
544 brands_by_chain: HashMap<i32, HashSet<String>>,
545 ) -> Result<HashMap<i32, HashMap<String, HashMap<Address, GmxV1Collateral>>>, ResolveError>
546 {
547 let mut selector: HashMap<i32, HashMap<String, HashSet<Address>>> = HashMap::new();
548 for (chain_id, brands) in brands_by_chain.into_iter() {
549 for brand in brands.into_iter() {
550 selector
551 .entry(chain_id)
552 .or_default()
553 .insert(brand, HashSet::new());
554 }
555 }
556
557 self.resolve_multiple("gmx-v1/collaterals", selector, None::<()>)
558 .await
559 }
560
561 pub async fn resolve_gmx_v1_collaterals(
562 &self,
563 brands_by_chain: HashMap<i32, HashMap<String, HashSet<Address>>>,
564 ) -> Result<HashMap<i32, HashMap<String, HashMap<Address, GmxV1Collateral>>>, ResolveError>
565 {
566 let mut selector: HashMap<i32, HashMap<String, HashSet<Address>>> = HashMap::new();
567 for (chain_id, brands) in brands_by_chain.into_iter() {
568 for (brand, collaterals) in brands.into_iter() {
569 selector
570 .entry(chain_id)
571 .or_default()
572 .insert(brand, collaterals);
573 }
574 }
575
576 self.resolve_multiple("gmx-v1/collaterals", selector, None::<()>)
577 .await
578 }
579
580 pub async fn resolve_latest_safe_block(
581 &self,
582 chain_id: i32,
583 ) -> Result<LightBlock, ResolveError> {
584 self.client
585 .get(format!("{}/blocks/{}/latest-safe", self.base_url, chain_id))
586 .send()
587 .await
588 .map_err(ResolveError::Network)?
589 .json::<LightBlock>()
590 .await
591 .map_err(ResolveError::Deserialize)
592 }
593
594 pub async fn resolve_block_at(
595 &self,
596 chain_id: i32,
597 timestamp: NaiveDateTime,
598 ) -> Result<LightBlock, ResolveError> {
599 self.client
600 .get(format!(
601 "{}/blocks/{}/{}",
602 self.base_url,
603 chain_id,
604 timestamp.and_utc().timestamp()
605 ))
606 .send()
607 .await
608 .map_err(ResolveError::Network)?
609 .json::<LightBlock>()
610 .await
611 .map_err(ResolveError::Deserialize)
612 }
613}
614
615#[cfg(test)]
616mod test {
617 use serde_json::json;
618 use wiremock::{
619 matchers::{body_json, method},
620 Mock, MockServer, ResponseTemplate,
621 };
622
623 use super::*;
624
625 #[tokio::test]
626 async fn test_resolve_multiple_serde() {
627 let mock_server = MockServer::start().await;
628
629 Mock::given(method("POST"))
630 .and(body_json(json!({
631 "17000": ["0x0000000000000000000000000000000000000001"]
632 })))
633 .respond_with(ResponseTemplate::new(200).set_body_json(json!(
634 {
635 "17000": {
636 "0x0000000000000000000000000000000000000001": {
637 "decimals": 18,
638 "name": "Mocked",
639 "symbol": "MCKD"
640 }
641 }
642 }
643 )))
644 .up_to_n_times(1)
645 .mount(&mock_server)
646 .await;
647
648 let client = ResolverClient::new(mock_server.uri(), Duration::from_secs(5)).unwrap();
649 let resolved_token = client
650 .resolve_unpriced_token(
651 17000,
652 "0x0000000000000000000000000000000000000001"
653 .parse::<Address>()
654 .unwrap(),
655 )
656 .await
657 .unwrap();
658
659 assert_eq!(resolved_token.decimals, 18);
660 assert_eq!(resolved_token.name, "Mocked");
661 assert_eq!(resolved_token.symbol, "MCKD");
662 }
663}