1use std::sync::Arc;
7
8use async_trait::async_trait;
9use futures03::future::try_join_all;
10#[cfg(test)]
11use mockall::automock;
12use reqwest::{header, Client, ClientBuilder, Url};
13use thiserror::Error;
14use tokio::sync::Semaphore;
15use tracing::{debug, error, instrument, trace, warn};
16use tycho_common::{
17 dto::{
18 Chain, PaginationParams, PaginationResponse, ProtocolComponentRequestResponse,
19 ProtocolComponentsRequestBody, ProtocolStateRequestBody, ProtocolStateRequestResponse,
20 ProtocolSystemsRequestBody, ProtocolSystemsRequestResponse, ResponseToken,
21 StateRequestBody, StateRequestResponse, TokensRequestBody, TokensRequestResponse,
22 VersionParam,
23 },
24 Bytes,
25};
26
27use crate::TYCHO_SERVER_VERSION;
28
29#[derive(Error, Debug)]
30pub enum RPCError {
31 #[error("Failed to parse URL: {0}. Error: {1}")]
33 UrlParsing(String, String),
34 #[error("Failed to format request: {0}")]
36 FormatRequest(String),
37 #[error("Unexpected HTTP client error: {0}")]
39 HttpClient(String),
40 #[error("Failed to parse response: {0}")]
42 ParseResponse(String),
43 #[error("Fatal error: {0}")]
44 Fatal(String),
45}
46
47#[cfg_attr(test, automock)]
48#[async_trait]
49pub trait RPCClient: Send + Sync {
50 async fn get_contract_state(
52 &self,
53 request: &StateRequestBody,
54 ) -> Result<StateRequestResponse, RPCError>;
55
56 async fn get_contract_state_paginated(
57 &self,
58 chain: Chain,
59 ids: &[Bytes],
60 protocol_system: &str,
61 version: &VersionParam,
62 chunk_size: usize,
63 concurrency: usize,
64 ) -> Result<StateRequestResponse, RPCError> {
65 let semaphore = Arc::new(Semaphore::new(concurrency));
66
67 let chunked_bodies = ids
68 .chunks(chunk_size)
69 .map(|chunk| StateRequestBody {
70 contract_ids: Some(chunk.to_vec()),
71 protocol_system: protocol_system.to_string(),
72 chain,
73 version: version.clone(),
74 pagination: PaginationParams { page: 0, page_size: chunk_size as i64 },
75 })
76 .collect::<Vec<_>>();
77
78 let mut tasks = Vec::new();
79 for body in chunked_bodies.iter() {
80 let sem = semaphore.clone();
81 tasks.push(async move {
82 let _permit = sem
83 .acquire()
84 .await
85 .map_err(|_| RPCError::Fatal("Semaphore dropped".to_string()))?;
86 self.get_contract_state(body).await
87 });
88 }
89
90 let responses = try_join_all(tasks).await?;
92
93 let accounts = responses
95 .iter()
96 .flat_map(|r| r.accounts.clone())
97 .collect();
98 let total: i64 = responses
99 .iter()
100 .map(|r| r.pagination.total)
101 .sum();
102
103 Ok(StateRequestResponse {
104 accounts,
105 pagination: PaginationResponse { page: 0, page_size: chunk_size as i64, total },
106 })
107 }
108
109 async fn get_protocol_components(
110 &self,
111 request: &ProtocolComponentsRequestBody,
112 ) -> Result<ProtocolComponentRequestResponse, RPCError>;
113
114 async fn get_protocol_components_paginated(
115 &self,
116 request: &ProtocolComponentsRequestBody,
117 chunk_size: usize,
118 concurrency: usize,
119 ) -> Result<ProtocolComponentRequestResponse, RPCError> {
120 let semaphore = Arc::new(Semaphore::new(concurrency));
121
122 match request.component_ids {
125 Some(ref ids) => {
126 let chunked_bodies = ids
128 .chunks(chunk_size)
129 .enumerate()
130 .map(|(index, _)| ProtocolComponentsRequestBody {
131 protocol_system: request.protocol_system.clone(),
132 component_ids: request.component_ids.clone(),
133 tvl_gt: request.tvl_gt,
134 chain: request.chain,
135 pagination: PaginationParams {
136 page: index as i64,
137 page_size: chunk_size as i64,
138 },
139 })
140 .collect::<Vec<_>>();
141
142 let mut tasks = Vec::new();
143 for body in chunked_bodies.iter() {
144 let sem = semaphore.clone();
145 tasks.push(async move {
146 let _permit = sem
147 .acquire()
148 .await
149 .map_err(|_| RPCError::Fatal("Semaphore dropped".to_string()))?;
150 self.get_protocol_components(body).await
151 });
152 }
153
154 try_join_all(tasks)
155 .await
156 .map(|responses| ProtocolComponentRequestResponse {
157 protocol_components: responses
158 .into_iter()
159 .flat_map(|r| r.protocol_components.into_iter())
160 .collect(),
161 pagination: PaginationResponse {
162 page: 0,
163 page_size: chunk_size as i64,
164 total: ids.len() as i64,
165 },
166 })
167 }
168 _ => {
169 let initial_request = ProtocolComponentsRequestBody {
173 protocol_system: request.protocol_system.clone(),
174 component_ids: request.component_ids.clone(),
175 tvl_gt: request.tvl_gt,
176 chain: request.chain,
177 pagination: PaginationParams { page: 0, page_size: chunk_size as i64 },
178 };
179 let first_response = self
180 .get_protocol_components(&initial_request)
181 .await
182 .map_err(|err| RPCError::Fatal(err.to_string()))?;
183
184 let total_items = first_response.pagination.total;
185 let total_pages = (total_items as f64 / chunk_size as f64).ceil() as i64;
186
187 let mut accumulated_response = ProtocolComponentRequestResponse {
189 protocol_components: first_response.protocol_components,
190 pagination: PaginationResponse {
191 page: 0,
192 page_size: chunk_size as i64,
193 total: total_items,
194 },
195 };
196
197 let mut page = 1;
198 while page < total_pages {
199 let requests_in_this_iteration = (total_pages - page).min(concurrency as i64);
200
201 let chunked_bodies = (0..requests_in_this_iteration)
203 .map(|iter| ProtocolComponentsRequestBody {
204 protocol_system: request.protocol_system.clone(),
205 component_ids: request.component_ids.clone(),
206 tvl_gt: request.tvl_gt,
207 chain: request.chain,
208 pagination: PaginationParams {
209 page: page + iter,
210 page_size: chunk_size as i64,
211 },
212 })
213 .collect::<Vec<_>>();
214
215 let tasks: Vec<_> = chunked_bodies
216 .iter()
217 .map(|body| {
218 let sem = semaphore.clone();
219 async move {
220 let _permit = sem.acquire().await.map_err(|_| {
221 RPCError::Fatal("Semaphore dropped".to_string())
222 })?;
223 self.get_protocol_components(body).await
224 }
225 })
226 .collect();
227
228 let responses = try_join_all(tasks)
229 .await
230 .map(|responses| {
231 let total = responses[0].pagination.total;
232 ProtocolComponentRequestResponse {
233 protocol_components: responses
234 .into_iter()
235 .flat_map(|r| r.protocol_components.into_iter())
236 .collect(),
237 pagination: PaginationResponse {
238 page,
239 page_size: chunk_size as i64,
240 total,
241 },
242 }
243 });
244
245 match responses {
247 Ok(mut resp) => {
248 accumulated_response
249 .protocol_components
250 .append(&mut resp.protocol_components);
251 }
252 Err(e) => return Err(e),
253 }
254
255 page += concurrency as i64;
256 }
257 Ok(accumulated_response)
258 }
259 }
260 }
261
262 async fn get_protocol_states(
263 &self,
264 request: &ProtocolStateRequestBody,
265 ) -> Result<ProtocolStateRequestResponse, RPCError>;
266
267 #[allow(clippy::too_many_arguments)]
268 async fn get_protocol_states_paginated<T>(
269 &self,
270 chain: Chain,
271 ids: &[T],
272 protocol_system: &str,
273 include_balances: bool,
274 version: &VersionParam,
275 chunk_size: usize,
276 concurrency: usize,
277 ) -> Result<ProtocolStateRequestResponse, RPCError>
278 where
279 T: AsRef<str> + Sync + 'static,
280 {
281 let semaphore = Arc::new(Semaphore::new(concurrency));
282 let chunked_bodies = ids
283 .chunks(chunk_size)
284 .map(|c| ProtocolStateRequestBody {
285 protocol_ids: Some(
286 c.iter()
287 .map(|id| id.as_ref().to_string())
288 .collect(),
289 ),
290 protocol_system: protocol_system.to_string(),
291 chain,
292 include_balances,
293 version: version.clone(),
294 pagination: PaginationParams { page: 0, page_size: chunk_size as i64 },
295 })
296 .collect::<Vec<_>>();
297
298 let mut tasks = Vec::new();
299 for body in chunked_bodies.iter() {
300 let sem = semaphore.clone();
301 tasks.push(async move {
302 let _permit = sem
303 .acquire()
304 .await
305 .map_err(|_| RPCError::Fatal("Semaphore dropped".to_string()))?;
306 self.get_protocol_states(body).await
307 });
308 }
309
310 try_join_all(tasks)
311 .await
312 .map(|responses| {
313 let states = responses
314 .clone()
315 .into_iter()
316 .flat_map(|r| r.states)
317 .collect();
318 let total = responses
319 .iter()
320 .map(|r| r.pagination.total)
321 .sum();
322 ProtocolStateRequestResponse {
323 states,
324 pagination: PaginationResponse { page: 0, page_size: chunk_size as i64, total },
325 }
326 })
327 }
328
329 async fn get_tokens(
332 &self,
333 request: &TokensRequestBody,
334 ) -> Result<TokensRequestResponse, RPCError>;
335
336 async fn get_all_tokens(
337 &self,
338 chain: Chain,
339 min_quality: Option<i32>,
340 traded_n_days_ago: Option<u64>,
341 chunk_size: usize,
342 ) -> Result<Vec<ResponseToken>, RPCError> {
343 let mut request_page = 0;
344 let mut all_tokens = Vec::new();
345 loop {
346 let mut response = self
347 .get_tokens(&TokensRequestBody {
348 token_addresses: None,
349 min_quality,
350 traded_n_days_ago,
351 pagination: PaginationParams {
352 page: request_page,
353 page_size: chunk_size.try_into().map_err(|_| {
354 RPCError::FormatRequest(
355 "Failed to convert chunk_size into i64".to_string(),
356 )
357 })?,
358 },
359 chain,
360 })
361 .await?;
362
363 let num_tokens = response.tokens.len();
364 all_tokens.append(&mut response.tokens);
365 request_page += 1;
366
367 if num_tokens < chunk_size {
368 break;
369 }
370 }
371 Ok(all_tokens)
372 }
373
374 async fn get_protocol_systems(
375 &self,
376 request: &ProtocolSystemsRequestBody,
377 ) -> Result<ProtocolSystemsRequestResponse, RPCError>;
378}
379
380#[derive(Debug, Clone)]
381pub struct HttpRPCClient {
382 http_client: Client,
383 url: Url,
384}
385
386impl HttpRPCClient {
387 pub fn new(base_uri: &str, auth_key: Option<&str>) -> Result<Self, RPCError> {
388 let uri = base_uri
389 .parse::<Url>()
390 .map_err(|e| RPCError::UrlParsing(base_uri.to_string(), e.to_string()))?;
391
392 let mut headers = header::HeaderMap::new();
394 headers.insert(header::CONTENT_TYPE, header::HeaderValue::from_static("application/json"));
395 let user_agent = format!("tycho-client-{}", env!("CARGO_PKG_VERSION"));
396 headers.insert(
397 header::USER_AGENT,
398 header::HeaderValue::from_str(&user_agent).expect("invalid user agent format"),
399 );
400
401 if let Some(key) = auth_key {
403 let mut auth_value = header::HeaderValue::from_str(key).expect("invalid key format");
404 auth_value.set_sensitive(true);
405 headers.insert(header::AUTHORIZATION, auth_value);
406 }
407
408 let client = ClientBuilder::new()
409 .default_headers(headers)
410 .http2_prior_knowledge()
411 .build()
412 .map_err(|e| RPCError::HttpClient(e.to_string()))?;
413 Ok(Self { http_client: client, url: uri })
414 }
415}
416
417#[async_trait]
418impl RPCClient for HttpRPCClient {
419 #[instrument(skip(self, request))]
420 async fn get_contract_state(
421 &self,
422 request: &StateRequestBody,
423 ) -> Result<StateRequestResponse, RPCError> {
424 if request.contract_ids.is_none() ||
426 request
427 .contract_ids
428 .as_ref()
429 .unwrap()
430 .is_empty()
431 {
432 warn!("No contract ids specified in request.");
433 }
434
435 let uri = format!(
436 "{}/{}/contract_state",
437 self.url
438 .to_string()
439 .trim_end_matches('/'),
440 TYCHO_SERVER_VERSION
441 );
442 debug!(%uri, "Sending contract_state request to Tycho server");
443 trace!(?request, "Sending request to Tycho server");
444
445 let response = self
446 .http_client
447 .post(&uri)
448 .json(request)
449 .send()
450 .await
451 .map_err(|e| RPCError::HttpClient(e.to_string()))?;
452 trace!(?response, "Received response from Tycho server");
453
454 let body = response
455 .text()
456 .await
457 .map_err(|e| RPCError::ParseResponse(e.to_string()))?;
458 if body.is_empty() {
459 return Ok(StateRequestResponse {
461 accounts: vec![],
462 pagination: PaginationResponse {
463 page: request.pagination.page,
464 page_size: request.pagination.page,
465 total: 0,
466 },
467 });
468 }
469
470 let accounts = serde_json::from_str::<StateRequestResponse>(&body)
471 .map_err(|err| RPCError::ParseResponse(format!("Error: {err}, Body: {body}")))?;
472 trace!(?accounts, "Received contract_state response from Tycho server");
473
474 Ok(accounts)
475 }
476
477 async fn get_protocol_components(
478 &self,
479 request: &ProtocolComponentsRequestBody,
480 ) -> Result<ProtocolComponentRequestResponse, RPCError> {
481 let uri = format!(
482 "{}/{}/protocol_components",
483 self.url
484 .to_string()
485 .trim_end_matches('/'),
486 TYCHO_SERVER_VERSION,
487 );
488 debug!(%uri, "Sending protocol_components request to Tycho server");
489 trace!(?request, "Sending request to Tycho server");
490
491 let response = self
492 .http_client
493 .post(uri)
494 .header(header::CONTENT_TYPE, "application/json")
495 .json(request)
496 .send()
497 .await
498 .map_err(|e| RPCError::HttpClient(e.to_string()))?;
499
500 trace!(?response, "Received response from Tycho server");
501
502 let body = response
503 .text()
504 .await
505 .map_err(|e| RPCError::ParseResponse(e.to_string()))?;
506 let components = serde_json::from_str::<ProtocolComponentRequestResponse>(&body)
507 .map_err(|err| RPCError::ParseResponse(format!("Error: {err}, Body: {body}")))?;
508 trace!(?components, "Received protocol_components response from Tycho server");
509
510 Ok(components)
511 }
512
513 async fn get_protocol_states(
514 &self,
515 request: &ProtocolStateRequestBody,
516 ) -> Result<ProtocolStateRequestResponse, RPCError> {
517 if request.protocol_ids.is_none() ||
519 request
520 .protocol_ids
521 .as_ref()
522 .unwrap()
523 .is_empty()
524 {
525 warn!("No protocol ids specified in request.");
526 }
527
528 let uri = format!(
529 "{}/{}/protocol_state",
530 self.url
531 .to_string()
532 .trim_end_matches('/'),
533 TYCHO_SERVER_VERSION
534 );
535 debug!(%uri, "Sending protocol_states request to Tycho server");
536 trace!(?request, "Sending request to Tycho server");
537
538 let response = self
539 .http_client
540 .post(&uri)
541 .json(request)
542 .send()
543 .await
544 .map_err(|e| RPCError::HttpClient(e.to_string()))?;
545 trace!(?response, "Received response from Tycho server");
546
547 let body = response
548 .text()
549 .await
550 .map_err(|e| RPCError::ParseResponse(e.to_string()))?;
551
552 if body.is_empty() {
553 return Ok(ProtocolStateRequestResponse {
555 states: vec![],
556 pagination: PaginationResponse {
557 page: request.pagination.page,
558 page_size: request.pagination.page_size,
559 total: 0,
560 },
561 });
562 }
563
564 let states = serde_json::from_str::<ProtocolStateRequestResponse>(&body)
565 .map_err(|err| RPCError::ParseResponse(format!("Error: {err}, Body: {body}")))?;
566 trace!(?states, "Received protocol_states response from Tycho server");
567
568 Ok(states)
569 }
570
571 async fn get_tokens(
572 &self,
573 request: &TokensRequestBody,
574 ) -> Result<TokensRequestResponse, RPCError> {
575 let uri = format!(
576 "{}/{}/tokens",
577 self.url
578 .to_string()
579 .trim_end_matches('/'),
580 TYCHO_SERVER_VERSION
581 );
582 debug!(%uri, "Sending tokens request to Tycho server");
583
584 let response = self
585 .http_client
586 .post(&uri)
587 .json(request)
588 .send()
589 .await
590 .map_err(|e| RPCError::HttpClient(e.to_string()))?;
591
592 let body = response
593 .text()
594 .await
595 .map_err(|e| RPCError::ParseResponse(e.to_string()))?;
596 let tokens = serde_json::from_str::<TokensRequestResponse>(&body)
597 .map_err(|err| RPCError::ParseResponse(format!("Error: {err}, Body: {body}")))?;
598
599 Ok(tokens)
600 }
601
602 async fn get_protocol_systems(
603 &self,
604 request: &ProtocolSystemsRequestBody,
605 ) -> Result<ProtocolSystemsRequestResponse, RPCError> {
606 let uri = format!(
607 "{}/{}/protocol_systems",
608 self.url
609 .to_string()
610 .trim_end_matches('/'),
611 TYCHO_SERVER_VERSION
612 );
613 debug!(%uri, "Sending protocol_systems request to Tycho server");
614 trace!(?request, "Sending request to Tycho server");
615 let response = self
616 .http_client
617 .post(&uri)
618 .json(request)
619 .send()
620 .await
621 .map_err(|e| RPCError::HttpClient(e.to_string()))?;
622 trace!(?response, "Received response from Tycho server");
623 let body = response
624 .text()
625 .await
626 .map_err(|e| RPCError::ParseResponse(e.to_string()))?;
627 let protocol_systems = serde_json::from_str::<ProtocolSystemsRequestResponse>(&body)
628 .map_err(|err| RPCError::ParseResponse(format!("Error: {err}, Body: {body}")))?;
629 trace!(?protocol_systems, "Received protocol_systems response from Tycho server");
630 Ok(protocol_systems)
631 }
632}
633
634#[cfg(test)]
635mod tests {
636 use std::{collections::HashMap, str::FromStr};
637
638 use mockito::Server;
639 use rstest::rstest;
640 #[allow(deprecated)]
642 use tycho_common::dto::ProtocolId;
643
644 use super::*;
645
646 impl MockRPCClient {
649 #[allow(clippy::too_many_arguments)]
650 async fn test_get_protocol_states_paginated<T>(
651 &self,
652 chain: Chain,
653 ids: &[T],
654 protocol_system: &str,
655 include_balances: bool,
656 version: &VersionParam,
657 chunk_size: usize,
658 _concurrency: usize,
659 ) -> Vec<ProtocolStateRequestBody>
660 where
661 T: AsRef<str> + Clone + Send + Sync + 'static,
662 {
663 ids.chunks(chunk_size)
664 .map(|chunk| ProtocolStateRequestBody {
665 protocol_ids: Some(
666 chunk
667 .iter()
668 .map(|id| id.as_ref().to_string())
669 .collect(),
670 ),
671 protocol_system: protocol_system.to_string(),
672 chain,
673 include_balances,
674 version: version.clone(),
675 pagination: PaginationParams { page: 0, page_size: chunk_size as i64 },
676 })
677 .collect()
678 }
679 }
680
681 #[allow(deprecated)]
683 #[rstest]
684 #[case::protocol_id_input(vec![
685 ProtocolId { id: "id1".to_string(), chain: Chain::Ethereum },
686 ProtocolId { id: "id2".to_string(), chain: Chain::Ethereum }
687 ])]
688 #[case::string_input(vec![
689 "id1".to_string(),
690 "id2".to_string()
691 ])]
692 #[tokio::test]
693 async fn test_get_protocol_states_paginated_backwards_compatibility<T>(#[case] ids: Vec<T>)
694 where
695 T: AsRef<str> + Clone + Send + Sync + 'static,
696 {
697 let mock_client = MockRPCClient::new();
698
699 let request_bodies = mock_client
700 .test_get_protocol_states_paginated(
701 Chain::Ethereum,
702 &ids,
703 "test_system",
704 true,
705 &VersionParam::default(),
706 2,
707 2,
708 )
709 .await;
710
711 assert_eq!(request_bodies.len(), 1);
713 assert_eq!(
714 request_bodies[0]
715 .protocol_ids
716 .as_ref()
717 .unwrap()
718 .len(),
719 2
720 );
721 }
722
723 #[tokio::test]
724 async fn test_get_contract_state() {
725 let mut server = Server::new_async().await;
726 let server_resp = r#"
727 {
728 "accounts": [
729 {
730 "chain": "ethereum",
731 "address": "0x0000000000000000000000000000000000000000",
732 "title": "",
733 "slots": {},
734 "native_balance": "0x01f4",
735 "token_balances": {},
736 "code": "0x00",
737 "code_hash": "0x5c06b7c5b3d910fd33bc2229846f9ddaf91d584d9b196e16636901ac3a77077e",
738 "balance_modify_tx": "0x0000000000000000000000000000000000000000000000000000000000000000",
739 "code_modify_tx": "0x0000000000000000000000000000000000000000000000000000000000000000",
740 "creation_tx": null
741 }
742 ],
743 "pagination": {
744 "page": 0,
745 "page_size": 20,
746 "total": 10
747 }
748 }
749 "#;
750 serde_json::from_str::<StateRequestResponse>(server_resp).expect("deserialize");
752
753 let mocked_server = server
754 .mock("POST", "/v1/contract_state")
755 .expect(1)
756 .with_body(server_resp)
757 .create_async()
758 .await;
759
760 let client = HttpRPCClient::new(server.url().as_str(), None).expect("create client");
761
762 let response = client
763 .get_contract_state(&Default::default())
764 .await
765 .expect("get state");
766 let accounts = response.accounts;
767
768 mocked_server.assert();
769 assert_eq!(accounts.len(), 1);
770 assert_eq!(accounts[0].slots, HashMap::new());
771 assert_eq!(accounts[0].native_balance, Bytes::from(500u16.to_be_bytes()));
772 assert_eq!(accounts[0].code, [0].to_vec());
773 assert_eq!(
774 accounts[0].code_hash,
775 hex::decode("5c06b7c5b3d910fd33bc2229846f9ddaf91d584d9b196e16636901ac3a77077e")
776 .unwrap()
777 );
778 }
779
780 #[tokio::test]
781 async fn test_get_protocol_components() {
782 let mut server = Server::new_async().await;
783 let server_resp = r#"
784 {
785 "protocol_components": [
786 {
787 "id": "State1",
788 "protocol_system": "ambient",
789 "protocol_type_name": "Pool",
790 "chain": "ethereum",
791 "tokens": [
792 "0x0000000000000000000000000000000000000000",
793 "0x0000000000000000000000000000000000000001"
794 ],
795 "contract_ids": [
796 "0x0000000000000000000000000000000000000000"
797 ],
798 "static_attributes": {
799 "attribute_1": "0x00000000000003e8"
800 },
801 "change": "Creation",
802 "creation_tx": "0x0000000000000000000000000000000000000000000000000000000000000000",
803 "created_at": "2022-01-01T00:00:00"
804 }
805 ],
806 "pagination": {
807 "page": 0,
808 "page_size": 20,
809 "total": 10
810 }
811 }
812 "#;
813 serde_json::from_str::<ProtocolComponentRequestResponse>(server_resp).expect("deserialize");
815
816 let mocked_server = server
817 .mock("POST", "/v1/protocol_components")
818 .expect(1)
819 .with_body(server_resp)
820 .create_async()
821 .await;
822
823 let client = HttpRPCClient::new(server.url().as_str(), None).expect("create client");
824
825 let response = client
826 .get_protocol_components(&Default::default())
827 .await
828 .expect("get state");
829 let components = response.protocol_components;
830
831 mocked_server.assert();
832 assert_eq!(components.len(), 1);
833 assert_eq!(components[0].id, "State1");
834 assert_eq!(components[0].protocol_system, "ambient");
835 assert_eq!(components[0].protocol_type_name, "Pool");
836 assert_eq!(components[0].tokens.len(), 2);
837 let expected_attributes =
838 [("attribute_1".to_string(), Bytes::from(1000_u64.to_be_bytes()))]
839 .iter()
840 .cloned()
841 .collect::<HashMap<String, Bytes>>();
842 assert_eq!(components[0].static_attributes, expected_attributes);
843 }
844
845 #[tokio::test]
846 async fn test_get_protocol_states() {
847 let mut server = Server::new_async().await;
848 let server_resp = r#"
849 {
850 "states": [
851 {
852 "component_id": "State1",
853 "attributes": {
854 "attribute_1": "0x00000000000003e8"
855 },
856 "balances": {
857 "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2": "0x01f4"
858 }
859 }
860 ],
861 "pagination": {
862 "page": 0,
863 "page_size": 20,
864 "total": 10
865 }
866 }
867 "#;
868 serde_json::from_str::<ProtocolStateRequestResponse>(server_resp).expect("deserialize");
870
871 let mocked_server = server
872 .mock("POST", "/v1/protocol_state")
873 .expect(1)
874 .with_body(server_resp)
875 .create_async()
876 .await;
877 let client = HttpRPCClient::new(server.url().as_str(), None).expect("create client");
878
879 let response = client
880 .get_protocol_states(&Default::default())
881 .await
882 .expect("get state");
883 let states = response.states;
884
885 mocked_server.assert();
886 assert_eq!(states.len(), 1);
887 assert_eq!(states[0].component_id, "State1");
888 let expected_attributes =
889 [("attribute_1".to_string(), Bytes::from(1000_u64.to_be_bytes()))]
890 .iter()
891 .cloned()
892 .collect::<HashMap<String, Bytes>>();
893 assert_eq!(states[0].attributes, expected_attributes);
894 let expected_balances = [(
895 Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2")
896 .expect("Unsupported address format"),
897 Bytes::from_str("0x01f4").unwrap(),
898 )]
899 .iter()
900 .cloned()
901 .collect::<HashMap<Bytes, Bytes>>();
902 assert_eq!(states[0].balances, expected_balances);
903 }
904
905 #[tokio::test]
906 async fn test_get_tokens() {
907 let mut server = Server::new_async().await;
908 let server_resp = r#"
909 {
910 "tokens": [
911 {
912 "chain": "ethereum",
913 "address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
914 "symbol": "WETH",
915 "decimals": 18,
916 "tax": 0,
917 "gas": [
918 29962
919 ],
920 "quality": 100
921 },
922 {
923 "chain": "ethereum",
924 "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
925 "symbol": "USDC",
926 "decimals": 6,
927 "tax": 0,
928 "gas": [
929 40652
930 ],
931 "quality": 100
932 }
933 ],
934 "pagination": {
935 "page": 0,
936 "page_size": 20,
937 "total": 10
938 }
939 }
940 "#;
941 serde_json::from_str::<TokensRequestResponse>(server_resp).expect("deserialize");
943
944 let mocked_server = server
945 .mock("POST", "/v1/tokens")
946 .expect(1)
947 .with_body(server_resp)
948 .create_async()
949 .await;
950 let client = HttpRPCClient::new(server.url().as_str(), None).expect("create client");
951
952 let response = client
953 .get_tokens(&Default::default())
954 .await
955 .expect("get tokens");
956
957 let expected = vec![
958 ResponseToken {
959 chain: Chain::Ethereum,
960 address: Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(),
961 symbol: "WETH".to_string(),
962 decimals: 18,
963 tax: 0,
964 gas: vec![Some(29962)],
965 quality: 100,
966 },
967 ResponseToken {
968 chain: Chain::Ethereum,
969 address: Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(),
970 symbol: "USDC".to_string(),
971 decimals: 6,
972 tax: 0,
973 gas: vec![Some(40652)],
974 quality: 100,
975 },
976 ];
977
978 mocked_server.assert();
979 assert_eq!(response.tokens, expected);
980 assert_eq!(response.pagination, PaginationResponse { page: 0, page_size: 20, total: 10 });
981 }
982
983 #[tokio::test]
984 async fn test_get_protocol_systems() {
985 let mut server = Server::new_async().await;
986 let server_resp = r#"
987 {
988 "protocol_systems": [
989 "system1",
990 "system2"
991 ],
992 "pagination": {
993 "page": 0,
994 "page_size": 20,
995 "total": 10
996 }
997 }
998 "#;
999 serde_json::from_str::<ProtocolSystemsRequestResponse>(server_resp).expect("deserialize");
1001
1002 let mocked_server = server
1003 .mock("POST", "/v1/protocol_systems")
1004 .expect(1)
1005 .with_body(server_resp)
1006 .create_async()
1007 .await;
1008 let client = HttpRPCClient::new(server.url().as_str(), None).expect("create client");
1009
1010 let response = client
1011 .get_protocol_systems(&Default::default())
1012 .await
1013 .expect("get protocol systems");
1014 let protocol_systems = response.protocol_systems;
1015
1016 mocked_server.assert();
1017 assert_eq!(protocol_systems, vec!["system1", "system2"]);
1018 }
1019}