Skip to main content

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