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).map_err(|err| {
471 error!("Failed to parse contract state response: {:?}", &body);
472 RPCError::ParseResponse(format!("Error: {}, Body: {}", err, body))
473 })?;
474 trace!(?accounts, "Received contract_state response from Tycho server");
475
476 Ok(accounts)
477 }
478
479 async fn get_protocol_components(
480 &self,
481 request: &ProtocolComponentsRequestBody,
482 ) -> Result<ProtocolComponentRequestResponse, RPCError> {
483 let uri = format!(
484 "{}/{}/protocol_components",
485 self.url
486 .to_string()
487 .trim_end_matches('/'),
488 TYCHO_SERVER_VERSION,
489 );
490 debug!(%uri, "Sending protocol_components request to Tycho server");
491 trace!(?request, "Sending request to Tycho server");
492
493 let response = self
494 .http_client
495 .post(uri)
496 .header(header::CONTENT_TYPE, "application/json")
497 .json(request)
498 .send()
499 .await
500 .map_err(|e| RPCError::HttpClient(e.to_string()))?;
501
502 trace!(?response, "Received response from Tycho server");
503
504 let body = response
505 .text()
506 .await
507 .map_err(|e| RPCError::ParseResponse(e.to_string()))?;
508 let components =
509 serde_json::from_str::<ProtocolComponentRequestResponse>(&body).map_err(|err| {
510 error!("Failed to parse protocol component response: {:?}", &body);
511 RPCError::ParseResponse(format!("Error: {}, Body: {}", err, body))
512 })?;
513 trace!(?components, "Received protocol_components response from Tycho server");
514
515 Ok(components)
516 }
517
518 async fn get_protocol_states(
519 &self,
520 request: &ProtocolStateRequestBody,
521 ) -> Result<ProtocolStateRequestResponse, RPCError> {
522 if request.protocol_ids.is_none() ||
524 request
525 .protocol_ids
526 .as_ref()
527 .unwrap()
528 .is_empty()
529 {
530 warn!("No protocol ids specified in request.");
531 }
532
533 let uri = format!(
534 "{}/{}/protocol_state",
535 self.url
536 .to_string()
537 .trim_end_matches('/'),
538 TYCHO_SERVER_VERSION
539 );
540 debug!(%uri, "Sending protocol_states request to Tycho server");
541 trace!(?request, "Sending request to Tycho server");
542
543 let response = self
544 .http_client
545 .post(&uri)
546 .json(request)
547 .send()
548 .await
549 .map_err(|e| RPCError::HttpClient(e.to_string()))?;
550 trace!(?response, "Received response from Tycho server");
551
552 let body = response
553 .text()
554 .await
555 .map_err(|e| RPCError::ParseResponse(e.to_string()))?;
556
557 if body.is_empty() {
558 return Ok(ProtocolStateRequestResponse {
560 states: vec![],
561 pagination: PaginationResponse {
562 page: request.pagination.page,
563 page_size: request.pagination.page_size,
564 total: 0,
565 },
566 });
567 }
568
569 let states =
570 serde_json::from_str::<ProtocolStateRequestResponse>(&body).map_err(|err| {
571 error!("Failed to parse protocol state response: {:?}", &body);
572 RPCError::ParseResponse(format!("Error: {}, Body: {}", err, body))
573 })?;
574 trace!(?states, "Received protocol_states response from Tycho server");
575
576 Ok(states)
577 }
578
579 async fn get_tokens(
580 &self,
581 request: &TokensRequestBody,
582 ) -> Result<TokensRequestResponse, RPCError> {
583 let uri = format!(
584 "{}/{}/tokens",
585 self.url
586 .to_string()
587 .trim_end_matches('/'),
588 TYCHO_SERVER_VERSION
589 );
590 debug!(%uri, "Sending tokens request to Tycho server");
591
592 let response = self
593 .http_client
594 .post(&uri)
595 .json(request)
596 .send()
597 .await
598 .map_err(|e| RPCError::HttpClient(e.to_string()))?;
599
600 let body = response
601 .text()
602 .await
603 .map_err(|e| RPCError::ParseResponse(e.to_string()))?;
604 let tokens = serde_json::from_str::<TokensRequestResponse>(&body).map_err(|err| {
605 error!("Failed to parse tokens response: {:?}", &body);
606 RPCError::ParseResponse(format!("Error: {}, Body: {}", err, body))
607 })?;
608
609 Ok(tokens)
610 }
611
612 async fn get_protocol_systems(
613 &self,
614 request: &ProtocolSystemsRequestBody,
615 ) -> Result<ProtocolSystemsRequestResponse, RPCError> {
616 let uri = format!(
617 "{}/{}/protocol_systems",
618 self.url
619 .to_string()
620 .trim_end_matches('/'),
621 TYCHO_SERVER_VERSION
622 );
623 debug!(%uri, "Sending protocol_systems request to Tycho server");
624 trace!(?request, "Sending request to Tycho server");
625 let response = self
626 .http_client
627 .post(&uri)
628 .json(request)
629 .send()
630 .await
631 .map_err(|e| RPCError::HttpClient(e.to_string()))?;
632 trace!(?response, "Received response from Tycho server");
633 let body = response
634 .text()
635 .await
636 .map_err(|e| RPCError::ParseResponse(e.to_string()))?;
637 let protocol_systems = serde_json::from_str::<ProtocolSystemsRequestResponse>(&body)
638 .map_err(|err| {
639 error!("Failed to parse protocol systems response: {:?}", &body);
640 RPCError::ParseResponse(format!("Error: {}, Body: {}", err, body))
641 })?;
642 trace!(?protocol_systems, "Received protocol_systems response from Tycho server");
643 Ok(protocol_systems)
644 }
645}
646
647#[cfg(test)]
648mod tests {
649 use std::{collections::HashMap, str::FromStr};
650
651 use mockito::Server;
652 use rstest::rstest;
653 #[allow(deprecated)]
655 use tycho_common::dto::ProtocolId;
656
657 use super::*;
658
659 impl MockRPCClient {
662 #[allow(clippy::too_many_arguments)]
663 async fn test_get_protocol_states_paginated<T>(
664 &self,
665 chain: Chain,
666 ids: &[T],
667 protocol_system: &str,
668 include_balances: bool,
669 version: &VersionParam,
670 chunk_size: usize,
671 _concurrency: usize,
672 ) -> Vec<ProtocolStateRequestBody>
673 where
674 T: AsRef<str> + Clone + Send + Sync + 'static,
675 {
676 ids.chunks(chunk_size)
677 .map(|chunk| ProtocolStateRequestBody {
678 protocol_ids: Some(
679 chunk
680 .iter()
681 .map(|id| id.as_ref().to_string())
682 .collect(),
683 ),
684 protocol_system: protocol_system.to_string(),
685 chain,
686 include_balances,
687 version: version.clone(),
688 pagination: PaginationParams { page: 0, page_size: chunk_size as i64 },
689 })
690 .collect()
691 }
692 }
693
694 #[allow(deprecated)]
696 #[rstest]
697 #[case::protocol_id_input(vec![
698 ProtocolId { id: "id1".to_string(), chain: Chain::Ethereum },
699 ProtocolId { id: "id2".to_string(), chain: Chain::Ethereum }
700 ])]
701 #[case::string_input(vec![
702 "id1".to_string(),
703 "id2".to_string()
704 ])]
705 #[tokio::test]
706 async fn test_get_protocol_states_paginated_backwards_compatibility<T>(#[case] ids: Vec<T>)
707 where
708 T: AsRef<str> + Clone + Send + Sync + 'static,
709 {
710 let mock_client = MockRPCClient::new();
711
712 let request_bodies = mock_client
713 .test_get_protocol_states_paginated(
714 Chain::Ethereum,
715 &ids,
716 "test_system",
717 true,
718 &VersionParam::default(),
719 2,
720 2,
721 )
722 .await;
723
724 assert_eq!(request_bodies.len(), 1);
726 assert_eq!(
727 request_bodies[0]
728 .protocol_ids
729 .as_ref()
730 .unwrap()
731 .len(),
732 2
733 );
734 }
735
736 #[tokio::test]
737 async fn test_get_contract_state() {
738 let mut server = Server::new_async().await;
739 let server_resp = r#"
740 {
741 "accounts": [
742 {
743 "chain": "ethereum",
744 "address": "0x0000000000000000000000000000000000000000",
745 "title": "",
746 "slots": {},
747 "native_balance": "0x01f4",
748 "token_balances": {},
749 "code": "0x00",
750 "code_hash": "0x5c06b7c5b3d910fd33bc2229846f9ddaf91d584d9b196e16636901ac3a77077e",
751 "balance_modify_tx": "0x0000000000000000000000000000000000000000000000000000000000000000",
752 "code_modify_tx": "0x0000000000000000000000000000000000000000000000000000000000000000",
753 "creation_tx": null
754 }
755 ],
756 "pagination": {
757 "page": 0,
758 "page_size": 20,
759 "total": 10
760 }
761 }
762 "#;
763 serde_json::from_str::<StateRequestResponse>(server_resp).expect("deserialize");
765
766 let mocked_server = server
767 .mock("POST", "/v1/contract_state")
768 .expect(1)
769 .with_body(server_resp)
770 .create_async()
771 .await;
772
773 let client = HttpRPCClient::new(server.url().as_str(), None).expect("create client");
774
775 let response = client
776 .get_contract_state(&Default::default())
777 .await
778 .expect("get state");
779 let accounts = response.accounts;
780
781 mocked_server.assert();
782 assert_eq!(accounts.len(), 1);
783 assert_eq!(accounts[0].slots, HashMap::new());
784 assert_eq!(accounts[0].native_balance, Bytes::from(500u16.to_be_bytes()));
785 assert_eq!(accounts[0].code, [0].to_vec());
786 assert_eq!(
787 accounts[0].code_hash,
788 hex::decode("5c06b7c5b3d910fd33bc2229846f9ddaf91d584d9b196e16636901ac3a77077e")
789 .unwrap()
790 );
791 }
792
793 #[tokio::test]
794 async fn test_get_protocol_components() {
795 let mut server = Server::new_async().await;
796 let server_resp = r#"
797 {
798 "protocol_components": [
799 {
800 "id": "State1",
801 "protocol_system": "ambient",
802 "protocol_type_name": "Pool",
803 "chain": "ethereum",
804 "tokens": [
805 "0x0000000000000000000000000000000000000000",
806 "0x0000000000000000000000000000000000000001"
807 ],
808 "contract_ids": [
809 "0x0000000000000000000000000000000000000000"
810 ],
811 "static_attributes": {
812 "attribute_1": "0xe803000000000000"
813 },
814 "change": "Creation",
815 "creation_tx": "0x0000000000000000000000000000000000000000000000000000000000000000",
816 "created_at": "2022-01-01T00:00:00"
817 }
818 ],
819 "pagination": {
820 "page": 0,
821 "page_size": 20,
822 "total": 10
823 }
824 }
825 "#;
826 serde_json::from_str::<ProtocolComponentRequestResponse>(server_resp).expect("deserialize");
828
829 let mocked_server = server
830 .mock("POST", "/v1/protocol_components")
831 .expect(1)
832 .with_body(server_resp)
833 .create_async()
834 .await;
835
836 let client = HttpRPCClient::new(server.url().as_str(), None).expect("create client");
837
838 let response = client
839 .get_protocol_components(&Default::default())
840 .await
841 .expect("get state");
842 let components = response.protocol_components;
843
844 mocked_server.assert();
845 assert_eq!(components.len(), 1);
846 assert_eq!(components[0].id, "State1");
847 assert_eq!(components[0].protocol_system, "ambient");
848 assert_eq!(components[0].protocol_type_name, "Pool");
849 assert_eq!(components[0].tokens.len(), 2);
850 let expected_attributes =
851 [("attribute_1".to_string(), Bytes::from(1000_u64.to_le_bytes()))]
852 .iter()
853 .cloned()
854 .collect::<HashMap<String, Bytes>>();
855 assert_eq!(components[0].static_attributes, expected_attributes);
856 }
857
858 #[tokio::test]
859 async fn test_get_protocol_states() {
860 let mut server = Server::new_async().await;
861 let server_resp = r#"
862 {
863 "states": [
864 {
865 "component_id": "State1",
866 "attributes": {
867 "attribute_1": "0xe803000000000000"
868 },
869 "balances": {
870 "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2": "0x01f4"
871 }
872 }
873 ],
874 "pagination": {
875 "page": 0,
876 "page_size": 20,
877 "total": 10
878 }
879 }
880 "#;
881 serde_json::from_str::<ProtocolStateRequestResponse>(server_resp).expect("deserialize");
883
884 let mocked_server = server
885 .mock("POST", "/v1/protocol_state")
886 .expect(1)
887 .with_body(server_resp)
888 .create_async()
889 .await;
890 let client = HttpRPCClient::new(server.url().as_str(), None).expect("create client");
891
892 let response = client
893 .get_protocol_states(&Default::default())
894 .await
895 .expect("get state");
896 let states = response.states;
897
898 mocked_server.assert();
899 assert_eq!(states.len(), 1);
900 assert_eq!(states[0].component_id, "State1");
901 let expected_attributes =
902 [("attribute_1".to_string(), Bytes::from(1000_u64.to_le_bytes()))]
903 .iter()
904 .cloned()
905 .collect::<HashMap<String, Bytes>>();
906 assert_eq!(states[0].attributes, expected_attributes);
907 let expected_balances = [(
908 Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2")
909 .expect("Unsupported address format"),
910 Bytes::from_str("0x01f4").unwrap(),
911 )]
912 .iter()
913 .cloned()
914 .collect::<HashMap<Bytes, Bytes>>();
915 assert_eq!(states[0].balances, expected_balances);
916 }
917
918 #[tokio::test]
919 async fn test_get_tokens() {
920 let mut server = Server::new_async().await;
921 let server_resp = r#"
922 {
923 "tokens": [
924 {
925 "chain": "ethereum",
926 "address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
927 "symbol": "WETH",
928 "decimals": 18,
929 "tax": 0,
930 "gas": [
931 29962
932 ],
933 "quality": 100
934 },
935 {
936 "chain": "ethereum",
937 "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
938 "symbol": "USDC",
939 "decimals": 6,
940 "tax": 0,
941 "gas": [
942 40652
943 ],
944 "quality": 100
945 }
946 ],
947 "pagination": {
948 "page": 0,
949 "page_size": 20,
950 "total": 10
951 }
952 }
953 "#;
954 serde_json::from_str::<TokensRequestResponse>(server_resp).expect("deserialize");
956
957 let mocked_server = server
958 .mock("POST", "/v1/tokens")
959 .expect(1)
960 .with_body(server_resp)
961 .create_async()
962 .await;
963 let client = HttpRPCClient::new(server.url().as_str(), None).expect("create client");
964
965 let response = client
966 .get_tokens(&Default::default())
967 .await
968 .expect("get tokens");
969
970 let expected = vec![
971 ResponseToken {
972 chain: Chain::Ethereum,
973 address: Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(),
974 symbol: "WETH".to_string(),
975 decimals: 18,
976 tax: 0,
977 gas: vec![Some(29962)],
978 quality: 100,
979 },
980 ResponseToken {
981 chain: Chain::Ethereum,
982 address: Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(),
983 symbol: "USDC".to_string(),
984 decimals: 6,
985 tax: 0,
986 gas: vec![Some(40652)],
987 quality: 100,
988 },
989 ];
990
991 mocked_server.assert();
992 assert_eq!(response.tokens, expected);
993 assert_eq!(response.pagination, PaginationResponse { page: 0, page_size: 20, total: 10 });
994 }
995
996 #[tokio::test]
997 async fn test_get_protocol_systems() {
998 let mut server = Server::new_async().await;
999 let server_resp = r#"
1000 {
1001 "protocol_systems": [
1002 "system1",
1003 "system2"
1004 ],
1005 "pagination": {
1006 "page": 0,
1007 "page_size": 20,
1008 "total": 10
1009 }
1010 }
1011 "#;
1012 serde_json::from_str::<ProtocolSystemsRequestResponse>(server_resp).expect("deserialize");
1014
1015 let mocked_server = server
1016 .mock("POST", "/v1/protocol_systems")
1017 .expect(1)
1018 .with_body(server_resp)
1019 .create_async()
1020 .await;
1021 let client = HttpRPCClient::new(server.url().as_str(), None).expect("create client");
1022
1023 let response = client
1024 .get_protocol_systems(&Default::default())
1025 .await
1026 .expect("get protocol systems");
1027 let protocol_systems = response.protocol_systems;
1028
1029 mocked_server.assert();
1030 assert_eq!(protocol_systems, vec!["system1", "system2"]);
1031 }
1032}