tycho_client/
rpc.rs

1//! # Tycho RPC Client
2//!
3//! The objective of this module is to provide swift and simplified access to the Remote Procedure
4//! Call (RPC) endpoints of Tycho. These endpoints are chiefly responsible for facilitating data
5//! queries, especially querying snapshots of data.
6use 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    /// The passed tycho url failed to parse.
32    #[error("Failed to parse URL: {0}. Error: {1}")]
33    UrlParsing(String, String),
34    /// The request data is not correctly formed.
35    #[error("Failed to format request: {0}")]
36    FormatRequest(String),
37    /// Errors forwarded from the HTTP protocol.
38    #[error("Unexpected HTTP client error: {0}")]
39    HttpClient(String),
40    /// The response from the server could not be parsed correctly.
41    #[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    /// Retrieves a snapshot of contract state.
51    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        // Execute all tasks concurrently with the defined concurrency limit.
91        let responses = try_join_all(tasks).await?;
92
93        // Aggregate the responses into a single result.
94        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        // If a set of component IDs is specified, the maximum return size is already known,
123        // allowing us to pre-compute the number of requests to be made.
124        match request.component_ids {
125            Some(ref ids) => {
126                // We can divide the component_ids into chunks of size chunk_size
127                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                // If no component ids are specified, we need to make requests based on the total
170                // number of results from the first response.
171
172                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                // Initialize the final response accumulator
188                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                    // Create request bodies for parallel requests, respecting the concurrency limit
202                    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                    // Update the accumulated response or set the initial response
246                    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    /// This function returns only one chunk of tokens. To get all tokens please call
330    /// get_all_tokens.
331    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        // Add default headers
393        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        // Add Authorization if one is given
402        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        // Check if contract ids are specified
425        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            // Pure native protocols will return empty contract states
460            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        // Check if contract ids are specified
523        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            // Pure VM protocols will return empty states
559            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    // TODO: remove once deprecated ProtocolId struct is removed
654    #[allow(deprecated)]
655    use tycho_common::dto::ProtocolId;
656
657    use super::*;
658
659    // Dummy implementation of `get_protocol_states_paginated` for backwards compatibility testing
660    // purposes
661    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    // TODO: remove once deprecated ProtocolId struct is removed
695    #[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        // Verify that the request bodies have been created correctly
725        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        // test that the response is deserialized correctly
764        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        // test that the response is deserialized correctly
827        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        // test that the response is deserialized correctly
882        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        // test that the response is deserialized correctly
955        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        // test that the response is deserialized correctly
1013        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}