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
22pub 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 pub decimals: i32,
33 pub symbol: String,
35 pub name: String,
37 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 pub dex: String,
47 pub amm: Amm,
49 pub liquidity_type: AmmPoolLiquidityType,
51 pub tokens: Vec<TokenWithAddress>,
53 pub usd_tvl: f64,
55 #[serde(skip_serializing_if = "Option::is_none")]
56 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 pub dex: String,
66 pub amm: Amm,
68 pub liquidity_type: AmmPoolLiquidityType,
70 #[cfg_attr(feature = "utoipa", schema(value_type = String, example = "1655626"))]
71 pub liquidity: U256,
73 pub tokens: Vec<TokenWithAddress>,
75 pub usd_tvl: f64,
77 #[serde(skip_serializing_if = "Option::is_none")]
78 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}