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)
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        // Check if contract ids are specified
518        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            // Pure VM protocols will return empty states
554            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    // TODO: remove once deprecated ProtocolId struct is removed
641    #[allow(deprecated)]
642    use tycho_common::dto::ProtocolId;
643
644    use super::*;
645
646    // Dummy implementation of `get_protocol_states_paginated` for backwards compatibility testing
647    // purposes
648    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    // TODO: remove once deprecated ProtocolId struct is removed
682    #[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        // Verify that the request bodies have been created correctly
712        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        // test that the response is deserialized correctly
751        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        // test that the response is deserialized correctly
814        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        // test that the response is deserialized correctly
869        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        // test that the response is deserialized correctly
942        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        // test that the response is deserialized correctly
1000        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}