metrom_resolver_client/
lib.rs

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
20// reexport this stuff
21pub 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}