1use crate::api::response::{
4 AccountData, AptosResponse, GasEstimation, LedgerInfo, MoveModule, PendingTransaction, Resource,
5};
6use crate::config::AptosConfig;
7use crate::error::{AptosError, AptosResult};
8use crate::retry::{RetryConfig, RetryExecutor};
9use crate::transaction::simulation::SimulateQueryOptions;
10use crate::transaction::types::SignedTransaction;
11use crate::types::{AccountAddress, HashValue};
12use reqwest::Client;
13use reqwest::header::{ACCEPT, CONTENT_TYPE};
14use std::sync::Arc;
15use std::time::Duration;
16use url::Url;
17
18const BCS_CONTENT_TYPE: &str = "application/x.aptos.signed_transaction+bcs";
19const BCS_VIEW_CONTENT_TYPE: &str = "application/x-bcs";
20const JSON_CONTENT_TYPE: &str = "application/json";
21const DEFAULT_TRANSACTION_WAIT_TIMEOUT_SECS: u64 = 30;
23const MAX_ERROR_BODY_SIZE: usize = 8 * 1024;
30
31#[derive(Debug, Clone)]
64pub struct FullnodeClient {
65 config: AptosConfig,
66 client: Client,
67 retry_config: Arc<RetryConfig>,
68}
69
70impl FullnodeClient {
71 pub fn new(config: AptosConfig) -> AptosResult<Self> {
91 let pool = config.pool_config();
92
93 let mut builder = Client::builder()
97 .timeout(config.timeout)
98 .pool_max_idle_per_host(pool.max_idle_per_host.unwrap_or(usize::MAX))
99 .pool_idle_timeout(pool.idle_timeout)
100 .tcp_nodelay(pool.tcp_nodelay);
101
102 if let Some(keepalive) = pool.tcp_keepalive {
103 builder = builder.tcp_keepalive(keepalive);
104 }
105
106 let client = builder.build().map_err(AptosError::Http)?;
107
108 let retry_config = Arc::new(config.retry_config().clone());
109
110 Ok(Self {
111 config,
112 client,
113 retry_config,
114 })
115 }
116
117 pub fn base_url(&self) -> &Url {
119 self.config.fullnode_url()
120 }
121
122 pub fn config(&self) -> &AptosConfig {
124 &self.config
125 }
126
127 pub async fn view_bcs_args(
157 &self,
158 function: &str,
159 type_args: Vec<crate::types::TypeTag>,
160 args: Vec<Vec<u8>>,
161 ) -> AptosResult<AptosResponse<Vec<serde_json::Value>>> {
162 const BCS_VIEW_REQUEST_CONTENT_TYPE: &str = "application/x.aptos.view_function+bcs";
166
167 let view_fn =
172 crate::transaction::EntryFunction::from_function_id(function, type_args, args)?;
173 let body = aptos_bcs::to_bytes(&view_fn).map_err(AptosError::bcs)?;
174 let url = self.build_url("view");
175
176 let client = self.client.clone();
177 let retry_config = self.retry_config.clone();
178 let max_response_size = self.config.pool_config().max_response_size;
179
180 let executor = RetryExecutor::from_shared(retry_config);
181 executor
182 .execute(|| {
183 let client = client.clone();
184 let url = url.clone();
185 let body = body.clone();
186 async move {
187 let response = client
188 .post(url)
189 .header(CONTENT_TYPE, BCS_VIEW_REQUEST_CONTENT_TYPE)
190 .header(ACCEPT, JSON_CONTENT_TYPE)
191 .body(body)
192 .send()
193 .await?;
194
195 Self::handle_response_static(response, max_response_size).await
196 }
197 })
198 .await
199 }
200
201 pub fn retry_config(&self) -> &RetryConfig {
203 &self.retry_config
204 }
205
206 pub async fn get_ledger_info(&self) -> AptosResult<AptosResponse<LedgerInfo>> {
215 let url = self.build_url("");
216 self.get_json(url).await
217 }
218
219 pub async fn get_account(
228 &self,
229 address: AccountAddress,
230 ) -> AptosResult<AptosResponse<AccountData>> {
231 let url = self.build_url(&format!("accounts/{address}"));
232 self.get_json(url).await
233 }
234
235 pub async fn get_sequence_number(&self, address: AccountAddress) -> AptosResult<u64> {
242 let account = self.get_account(address).await?;
243 account
244 .data
245 .sequence_number()
246 .map_err(|e| AptosError::Internal(format!("failed to parse sequence number: {e}")))
247 }
248
249 pub async fn get_account_resources(
260 &self,
261 address: AccountAddress,
262 ) -> AptosResult<AptosResponse<Vec<Resource>>> {
263 self.get_account_resources_paginated(address, None, None)
264 .await
265 }
266
267 pub async fn get_account_resources_paginated(
287 &self,
288 address: AccountAddress,
289 start: Option<&str>,
290 limit: Option<u16>,
291 ) -> AptosResult<AptosResponse<Vec<Resource>>> {
292 let mut url = self.build_url(&format!("accounts/{address}/resources"));
293 append_start_limit(&mut url, start, limit);
294 self.get_json(url).await
295 }
296
297 pub async fn get_account_resource(
304 &self,
305 address: AccountAddress,
306 resource_type: &str,
307 ) -> AptosResult<AptosResponse<Resource>> {
308 let url = self.build_url(&format!(
309 "accounts/{}/resource/{}",
310 address,
311 urlencoding::encode(resource_type)
312 ));
313 self.get_json(url).await
314 }
315
316 pub async fn get_account_modules(
328 &self,
329 address: AccountAddress,
330 ) -> AptosResult<AptosResponse<Vec<MoveModule>>> {
331 self.get_account_modules_paginated(address, None, None)
332 .await
333 }
334
335 pub async fn get_account_modules_paginated(
346 &self,
347 address: AccountAddress,
348 start: Option<&str>,
349 limit: Option<u16>,
350 ) -> AptosResult<AptosResponse<Vec<MoveModule>>> {
351 let mut url = self.build_url(&format!("accounts/{address}/modules"));
352 append_start_limit(&mut url, start, limit);
353 self.get_json(url).await
354 }
355
356 pub async fn get_account_module(
363 &self,
364 address: AccountAddress,
365 module_name: &str,
366 ) -> AptosResult<AptosResponse<MoveModule>> {
367 let url = self.build_url(&format!("accounts/{address}/module/{module_name}"));
368 self.get_json(url).await
369 }
370
371 pub async fn get_account_balance(&self, address: AccountAddress) -> AptosResult<u64> {
380 let result = self
383 .view(
384 "0x1::coin::balance",
385 vec!["0x1::aptos_coin::AptosCoin".to_string()],
386 vec![serde_json::json!(address.to_string())],
387 )
388 .await?;
389
390 let balance_str = result
392 .data
393 .first()
394 .and_then(|v| v.as_str())
395 .ok_or_else(|| AptosError::Internal("failed to parse balance response".into()))?;
396
397 balance_str
398 .parse()
399 .map_err(|_| AptosError::Internal("failed to parse balance as u64".into()))
400 }
401
402 pub async fn submit_transaction(
414 &self,
415 signed_txn: &SignedTransaction,
416 ) -> AptosResult<AptosResponse<PendingTransaction>> {
417 let url = self.build_url("transactions");
418 let bcs_bytes = signed_txn.to_bcs()?;
419 let client = self.client.clone();
420 let retry_config = self.retry_config.clone();
421 let max_response_size = self.config.pool_config().max_response_size;
422
423 let executor = RetryExecutor::from_shared(retry_config);
424 executor
425 .execute(|| {
426 let client = client.clone();
427 let url = url.clone();
428 let bcs_bytes = bcs_bytes.clone();
429 async move {
430 let response = client
431 .post(url)
432 .header(CONTENT_TYPE, BCS_CONTENT_TYPE)
433 .header(ACCEPT, JSON_CONTENT_TYPE)
434 .body(bcs_bytes)
435 .send()
436 .await?;
437
438 Self::handle_response_static(response, max_response_size).await
439 }
440 })
441 .await
442 }
443
444 pub async fn submit_and_wait(
451 &self,
452 signed_txn: &SignedTransaction,
453 timeout: Option<Duration>,
454 ) -> AptosResult<AptosResponse<serde_json::Value>> {
455 let pending = self.submit_transaction(signed_txn).await?;
456 self.wait_for_transaction(&pending.data.hash, timeout).await
457 }
458
459 pub async fn get_transaction_by_hash(
466 &self,
467 hash: &HashValue,
468 ) -> AptosResult<AptosResponse<serde_json::Value>> {
469 let url = self.build_url(&format!("transactions/by_hash/{hash}"));
470 self.get_json(url).await
471 }
472
473 pub async fn get_transaction_by_version(
480 &self,
481 version: u64,
482 ) -> AptosResult<AptosResponse<serde_json::Value>> {
483 let url = self.build_url(&format!("transactions/by_version/{version}"));
484 self.get_json(url).await
485 }
486
487 pub async fn get_transactions(
500 &self,
501 start: Option<u64>,
502 limit: Option<u16>,
503 ) -> AptosResult<AptosResponse<Vec<serde_json::Value>>> {
504 let mut url = self.build_url("transactions");
505 {
506 let mut query = url.query_pairs_mut();
507 if let Some(start) = start {
508 query.append_pair("start", &start.to_string());
509 }
510 if let Some(limit) = limit {
511 query.append_pair("limit", &limit.to_string());
512 }
513 }
514 self.get_json(url).await
515 }
516
517 pub async fn get_account_transactions(
528 &self,
529 address: AccountAddress,
530 start: Option<u64>,
531 limit: Option<u16>,
532 ) -> AptosResult<AptosResponse<Vec<serde_json::Value>>> {
533 let mut url = self.build_url(&format!("accounts/{address}/transactions"));
534 {
535 let mut query = url.query_pairs_mut();
536 if let Some(start) = start {
537 query.append_pair("start", &start.to_string());
538 }
539 if let Some(limit) = limit {
540 query.append_pair("limit", &limit.to_string());
541 }
542 }
543 self.get_json(url).await
544 }
545
546 pub async fn wait_for_transaction(
555 &self,
556 hash: &HashValue,
557 timeout: Option<Duration>,
558 ) -> AptosResult<AptosResponse<serde_json::Value>> {
559 let timeout = timeout.unwrap_or(Duration::from_secs(DEFAULT_TRANSACTION_WAIT_TIMEOUT_SECS));
560 let start = std::time::Instant::now();
561
562 let initial_interval = Duration::from_millis(200);
564 let max_interval = Duration::from_secs(2);
565 let mut current_interval = initial_interval;
566
567 loop {
568 match self.get_transaction_by_hash(hash).await {
569 Ok(response) => {
570 if response.data.get("version").is_some() {
572 let success = response
574 .data
575 .get("success")
576 .and_then(serde_json::Value::as_bool);
577 if success == Some(false) {
578 let vm_status = response
579 .data
580 .get("vm_status")
581 .and_then(|v| v.as_str())
582 .unwrap_or("unknown")
583 .to_string();
584 return Err(AptosError::ExecutionFailed { vm_status });
585 }
586 return Ok(response);
587 }
588 }
589 Err(AptosError::Api {
590 status_code: 404, ..
591 }) => {
592 }
594 Err(e) => return Err(e),
595 }
596
597 if start.elapsed() >= timeout {
598 return Err(AptosError::TransactionTimeout {
599 hash: hash.to_string(),
600 timeout_secs: timeout.as_secs(),
601 });
602 }
603
604 tokio::time::sleep(current_interval).await;
605
606 current_interval = std::cmp::min(current_interval * 2, max_interval);
608 }
609 }
610
611 pub async fn simulate_transaction(
623 &self,
624 signed_txn: &SignedTransaction,
625 ) -> AptosResult<AptosResponse<Vec<serde_json::Value>>> {
626 self.simulate_transaction_with_options(signed_txn, None as Option<SimulateQueryOptions>)
627 .await
628 }
629
630 pub async fn simulate_transaction_with_options(
646 &self,
647 signed_txn: &SignedTransaction,
648 options: impl Into<Option<SimulateQueryOptions>>,
649 ) -> AptosResult<AptosResponse<Vec<serde_json::Value>>> {
650 let mut url = self.build_url("transactions/simulate");
651 if let Some(opts) = options.into() {
652 let mut pairs = url.query_pairs_mut();
653 if opts.estimate_gas_unit_price {
654 pairs.append_pair("estimate_gas_unit_price", "true");
655 }
656 if opts.estimate_max_gas_amount {
657 pairs.append_pair("estimate_max_gas_amount", "true");
658 }
659 if opts.estimate_prioritized_gas_unit_price {
660 pairs.append_pair("estimate_prioritized_gas_unit_price", "true");
661 }
662 }
663 let bcs_bytes = signed_txn.for_simulate_endpoint().to_bcs()?;
664 let client = self.client.clone();
665 let retry_config = self.retry_config.clone();
666 let max_response_size = self.config.pool_config().max_response_size;
667
668 let executor = RetryExecutor::from_shared(retry_config);
669 executor
670 .execute(|| {
671 let client = client.clone();
672 let url = url.clone();
673 let bcs_bytes = bcs_bytes.clone();
674 async move {
675 let response = client
676 .post(url)
677 .header(CONTENT_TYPE, BCS_CONTENT_TYPE)
678 .header(ACCEPT, JSON_CONTENT_TYPE)
679 .body(bcs_bytes)
680 .send()
681 .await?;
682
683 Self::handle_response_static(response, max_response_size).await
684 }
685 })
686 .await
687 }
688
689 pub async fn estimate_gas_price(&self) -> AptosResult<AptosResponse<GasEstimation>> {
698 let url = self.build_url("estimate_gas_price");
699 self.get_json(url).await
700 }
701
702 pub async fn view(
711 &self,
712 function: &str,
713 type_args: Vec<String>,
714 args: Vec<serde_json::Value>,
715 ) -> AptosResult<AptosResponse<Vec<serde_json::Value>>> {
716 let url = self.build_url("view");
717
718 let body = serde_json::json!({
719 "function": function,
720 "type_arguments": type_args,
721 "arguments": args,
722 });
723
724 let client = self.client.clone();
725 let retry_config = self.retry_config.clone();
726 let max_response_size = self.config.pool_config().max_response_size;
727
728 let executor = RetryExecutor::from_shared(retry_config);
729 executor
730 .execute(|| {
731 let client = client.clone();
732 let url = url.clone();
733 let body = body.clone();
734 async move {
735 let response = client
736 .post(url)
737 .header(CONTENT_TYPE, JSON_CONTENT_TYPE)
738 .header(ACCEPT, JSON_CONTENT_TYPE)
739 .json(&body)
740 .send()
741 .await?;
742
743 Self::handle_response_static(response, max_response_size).await
744 }
745 })
746 .await
747 }
748
749 pub async fn view_bcs(
771 &self,
772 function: &str,
773 type_args: Vec<String>,
774 args: Vec<Vec<u8>>,
775 ) -> AptosResult<AptosResponse<Vec<u8>>> {
776 let url = self.build_url("view");
777
778 let hex_args: Vec<serde_json::Value> = args
781 .iter()
782 .map(|bytes| serde_json::json!(const_hex::encode_prefixed(bytes)))
783 .collect();
784
785 let body = serde_json::json!({
786 "function": function,
787 "type_arguments": type_args,
788 "arguments": hex_args,
789 });
790
791 let client = self.client.clone();
792 let retry_config = self.retry_config.clone();
793 let max_response_size = self.config.pool_config().max_response_size;
794
795 let executor = RetryExecutor::from_shared(retry_config);
796 executor
797 .execute(|| {
798 let client = client.clone();
799 let url = url.clone();
800 let body = body.clone();
801 async move {
802 let response = client
803 .post(url)
804 .header(CONTENT_TYPE, JSON_CONTENT_TYPE)
805 .header(ACCEPT, BCS_VIEW_CONTENT_TYPE)
806 .json(&body)
807 .send()
808 .await?;
809
810 let status = response.status();
812 if !status.is_success() {
813 let error_bytes =
816 crate::config::read_response_bounded(response, MAX_ERROR_BODY_SIZE)
817 .await
818 .ok();
819 let error_text = error_bytes
820 .and_then(|b| String::from_utf8(b).ok())
821 .unwrap_or_default();
822 return Err(AptosError::Api {
823 status_code: status.as_u16(),
824 message: Self::truncate_error_body(error_text),
825 error_code: None,
826 vm_error_code: None,
827 });
828 }
829
830 let bytes =
833 crate::config::read_response_bounded(response, max_response_size).await?;
834 Ok(AptosResponse::new(bytes))
835 }
836 })
837 .await
838 }
839
840 pub async fn get_table_item(
866 &self,
867 handle: &str,
868 key_type: &str,
869 value_type: &str,
870 key: serde_json::Value,
871 ) -> AptosResult<AptosResponse<serde_json::Value>> {
872 let url = self.build_url(&format!("tables/{}/item", urlencoding::encode(handle)));
873 let body = serde_json::json!({
874 "key_type": key_type,
875 "value_type": value_type,
876 "key": key,
877 });
878 self.post_json(url, &body).await
879 }
880
881 pub async fn get_events_by_creation_number(
896 &self,
897 address: AccountAddress,
898 creation_number: u64,
899 start: Option<u64>,
900 limit: Option<u64>,
901 ) -> AptosResult<AptosResponse<Vec<serde_json::Value>>> {
902 let mut url = self.build_url(&format!("accounts/{address}/events/{creation_number}"));
903 {
904 let mut query = url.query_pairs_mut();
905 if let Some(start) = start {
906 query.append_pair("start", &start.to_string());
907 }
908 if let Some(limit) = limit {
909 query.append_pair("limit", &limit.to_string());
910 }
911 }
912 self.get_json(url).await
913 }
914
915 pub async fn get_events_by_event_handle(
922 &self,
923 address: AccountAddress,
924 event_handle_struct: &str,
925 field_name: &str,
926 start: Option<u64>,
927 limit: Option<u64>,
928 ) -> AptosResult<AptosResponse<Vec<serde_json::Value>>> {
929 let mut url = self.build_url(&format!(
930 "accounts/{}/events/{}/{}",
931 address,
932 urlencoding::encode(event_handle_struct),
933 field_name
934 ));
935
936 {
937 let mut query = url.query_pairs_mut();
938 if let Some(start) = start {
939 query.append_pair("start", &start.to_string());
940 }
941 if let Some(limit) = limit {
942 query.append_pair("limit", &limit.to_string());
943 }
944 }
945
946 self.get_json(url).await
947 }
948
949 pub async fn get_block_by_height(
958 &self,
959 height: u64,
960 with_transactions: bool,
961 ) -> AptosResult<AptosResponse<serde_json::Value>> {
962 let mut url = self.build_url(&format!("blocks/by_height/{height}"));
963 url.query_pairs_mut()
964 .append_pair("with_transactions", &with_transactions.to_string());
965 self.get_json(url).await
966 }
967
968 pub async fn get_block_by_version(
975 &self,
976 version: u64,
977 with_transactions: bool,
978 ) -> AptosResult<AptosResponse<serde_json::Value>> {
979 let mut url = self.build_url(&format!("blocks/by_version/{version}"));
980 url.query_pairs_mut()
981 .append_pair("with_transactions", &with_transactions.to_string());
982 self.get_json(url).await
983 }
984
985 fn build_url(&self, path: &str) -> Url {
988 let mut url = self.config.fullnode_url().clone();
989 if !path.is_empty() {
990 let base_path = url.path();
992 let needs_slash = !base_path.ends_with('/');
993 let new_len = base_path.len() + path.len() + usize::from(needs_slash);
994 let mut new_path = String::with_capacity(new_len);
995 new_path.push_str(base_path);
996 if needs_slash {
997 new_path.push('/');
998 }
999 new_path.push_str(path);
1000 url.set_path(&new_path);
1001 }
1002 url
1003 }
1004
1005 async fn get_json<T: for<'de> serde::Deserialize<'de>>(
1006 &self,
1007 url: Url,
1008 ) -> AptosResult<AptosResponse<T>> {
1009 let client = self.client.clone();
1010 let url_clone = url.clone();
1011 let retry_config = self.retry_config.clone();
1012 let max_response_size = self.config.pool_config().max_response_size;
1013
1014 let executor = RetryExecutor::from_shared(retry_config);
1015 executor
1016 .execute(|| {
1017 let client = client.clone();
1018 let url = url_clone.clone();
1019 async move {
1020 let response = client
1021 .get(url)
1022 .header(ACCEPT, JSON_CONTENT_TYPE)
1023 .send()
1024 .await?;
1025
1026 Self::handle_response_static(response, max_response_size).await
1027 }
1028 })
1029 .await
1030 }
1031
1032 async fn post_json<B: serde::Serialize, T: for<'de> serde::Deserialize<'de>>(
1039 &self,
1040 url: Url,
1041 body: &B,
1042 ) -> AptosResult<AptosResponse<T>> {
1043 let body = serde_json::to_vec(body)?;
1044 let client = self.client.clone();
1045 let retry_config = self.retry_config.clone();
1046 let max_response_size = self.config.pool_config().max_response_size;
1047
1048 let executor = RetryExecutor::from_shared(retry_config);
1049 executor
1050 .execute(|| {
1051 let client = client.clone();
1052 let url = url.clone();
1053 let body = body.clone();
1054 async move {
1055 let response = client
1056 .post(url)
1057 .header(CONTENT_TYPE, JSON_CONTENT_TYPE)
1058 .header(ACCEPT, JSON_CONTENT_TYPE)
1059 .body(body)
1060 .send()
1061 .await?;
1062
1063 Self::handle_response_static(response, max_response_size).await
1064 }
1065 })
1066 .await
1067 }
1068
1069 fn truncate_error_body(body: String) -> String {
1075 if body.len() > MAX_ERROR_BODY_SIZE {
1076 let mut end = MAX_ERROR_BODY_SIZE;
1078 while end > 0 && !body.is_char_boundary(end) {
1079 end -= 1;
1080 }
1081 format!(
1082 "{}... [truncated, total: {} bytes]",
1083 &body[..end],
1084 body.len()
1085 )
1086 } else {
1087 body
1088 }
1089 }
1090
1091 async fn handle_response_static<T: for<'de> serde::Deserialize<'de>>(
1099 response: reqwest::Response,
1100 max_response_size: usize,
1101 ) -> AptosResult<AptosResponse<T>> {
1102 let status = response.status();
1103
1104 let ledger_version = response
1106 .headers()
1107 .get("x-aptos-ledger-version")
1108 .and_then(|v| v.to_str().ok())
1109 .and_then(|v| v.parse().ok());
1110 let ledger_timestamp = response
1111 .headers()
1112 .get("x-aptos-ledger-timestamp")
1113 .and_then(|v| v.to_str().ok())
1114 .and_then(|v| v.parse().ok());
1115 let epoch = response
1116 .headers()
1117 .get("x-aptos-epoch")
1118 .and_then(|v| v.to_str().ok())
1119 .and_then(|v| v.parse().ok());
1120 let block_height = response
1121 .headers()
1122 .get("x-aptos-block-height")
1123 .and_then(|v| v.to_str().ok())
1124 .and_then(|v| v.parse().ok());
1125 let oldest_ledger_version = response
1126 .headers()
1127 .get("x-aptos-oldest-ledger-version")
1128 .and_then(|v| v.to_str().ok())
1129 .and_then(|v| v.parse().ok());
1130 let cursor = response
1131 .headers()
1132 .get("x-aptos-cursor")
1133 .and_then(|v| v.to_str().ok())
1134 .map(ToString::to_string);
1135
1136 let retry_after_secs = response
1138 .headers()
1139 .get("retry-after")
1140 .and_then(|v| v.to_str().ok())
1141 .and_then(|v| v.parse().ok());
1142
1143 if status.is_success() {
1144 let bytes = crate::config::read_response_bounded(response, max_response_size).await?;
1147 let data: T = serde_json::from_slice(&bytes)?;
1148 Ok(AptosResponse {
1149 data,
1150 ledger_version,
1151 ledger_timestamp,
1152 epoch,
1153 block_height,
1154 oldest_ledger_version,
1155 cursor,
1156 })
1157 } else if status.as_u16() == 429 {
1158 Err(AptosError::RateLimited { retry_after_secs })
1161 } else {
1162 let error_bytes = crate::config::read_response_bounded(response, MAX_ERROR_BODY_SIZE)
1165 .await
1166 .ok();
1167 let error_text = error_bytes
1168 .and_then(|b| String::from_utf8(b).ok())
1169 .unwrap_or_default();
1170 let error_text = Self::truncate_error_body(error_text);
1171 let body: serde_json::Value = serde_json::from_str(&error_text).unwrap_or_default();
1172 let message = body
1173 .get("message")
1174 .and_then(|v| v.as_str())
1175 .unwrap_or("Unknown error")
1176 .to_string();
1177 let error_code = body
1178 .get("error_code")
1179 .and_then(|v| v.as_str())
1180 .map(ToString::to_string);
1181 let vm_error_code = body
1182 .get("vm_error_code")
1183 .and_then(serde_json::Value::as_u64);
1184
1185 Err(AptosError::api_with_details(
1186 status.as_u16(),
1187 message,
1188 error_code,
1189 vm_error_code,
1190 ))
1191 }
1192 }
1193
1194 #[allow(dead_code)]
1196 async fn handle_response<T: for<'de> serde::Deserialize<'de>>(
1197 &self,
1198 response: reqwest::Response,
1199 ) -> AptosResult<AptosResponse<T>> {
1200 let max_response_size = self.config.pool_config().max_response_size;
1201 Self::handle_response_static(response, max_response_size).await
1202 }
1203}
1204
1205fn append_start_limit(url: &mut Url, start: Option<&str>, limit: Option<u16>) {
1213 if start.is_none() && limit.is_none() {
1214 return;
1215 }
1216 let mut query = url.query_pairs_mut();
1217 if let Some(start) = start {
1218 query.append_pair("start", start);
1219 }
1220 if let Some(limit) = limit {
1221 query.append_pair("limit", &limit.to_string());
1222 }
1223}
1224
1225#[cfg(test)]
1226mod tests {
1227 use super::*;
1228 use crate::transaction::authenticator::{
1229 Ed25519PublicKey, Ed25519Signature, TransactionAuthenticator,
1230 };
1231 use crate::transaction::simulation::SimulateQueryOptions;
1232 use crate::transaction::types::{RawTransaction, SignedTransaction};
1233 use crate::types::ChainId;
1234 use wiremock::{
1235 Mock, MockServer, ResponseTemplate,
1236 matchers::{method, path, path_regex, query_param},
1237 };
1238
1239 #[test]
1240 fn test_build_url() {
1241 let client = FullnodeClient::new(AptosConfig::testnet()).unwrap();
1242 let url = client.build_url("accounts/0x1");
1243 assert!(url.as_str().contains("accounts/0x1"));
1244 }
1245
1246 fn create_mock_client(server: &MockServer) -> FullnodeClient {
1247 let url = format!("{}/v1", server.uri());
1249 let config = AptosConfig::custom(&url).unwrap().without_retry();
1250 FullnodeClient::new(config).unwrap()
1251 }
1252
1253 fn create_minimal_signed_transaction() -> SignedTransaction {
1255 use crate::transaction::payload::{EntryFunction, TransactionPayload};
1256
1257 let raw = RawTransaction::new(
1258 AccountAddress::ONE,
1259 0,
1260 TransactionPayload::EntryFunction(
1261 EntryFunction::apt_transfer(AccountAddress::ONE, 0).unwrap(),
1262 ),
1263 100_000,
1264 100,
1265 std::time::SystemTime::now()
1266 .duration_since(std::time::UNIX_EPOCH)
1267 .unwrap()
1268 .as_secs()
1269 .saturating_add(600),
1270 ChainId::testnet(),
1271 );
1272 let auth = TransactionAuthenticator::Ed25519 {
1273 public_key: Ed25519PublicKey([0u8; 32]),
1274 signature: Ed25519Signature([0u8; 64]),
1275 };
1276 SignedTransaction::new(raw, auth)
1277 }
1278
1279 fn simulate_response_json() -> serde_json::Value {
1280 serde_json::json!([{
1281 "success": true,
1282 "vm_status": "Executed successfully",
1283 "gas_used": "100",
1284 "max_gas_amount": "200000",
1285 "gas_unit_price": "100",
1286 "hash": "0x1",
1287 "changes": [],
1288 "events": []
1289 }])
1290 }
1291
1292 #[tokio::test]
1293 async fn test_get_ledger_info() {
1294 let server = MockServer::start().await;
1295
1296 Mock::given(method("GET"))
1297 .and(path("/v1"))
1298 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1299 "chain_id": 2,
1300 "epoch": "100",
1301 "ledger_version": "12345",
1302 "oldest_ledger_version": "0",
1303 "ledger_timestamp": "1000000",
1304 "node_role": "full_node",
1305 "oldest_block_height": "0",
1306 "block_height": "5000"
1307 })))
1308 .expect(1)
1309 .mount(&server)
1310 .await;
1311
1312 let client = create_mock_client(&server);
1313 let result = client.get_ledger_info().await.unwrap();
1314
1315 assert_eq!(result.data.chain_id, 2);
1316 assert_eq!(result.data.version().unwrap(), 12345);
1317 assert_eq!(result.data.height().unwrap(), 5000);
1318 }
1319
1320 #[tokio::test]
1321 async fn test_get_account() {
1322 let server = MockServer::start().await;
1323
1324 Mock::given(method("GET"))
1325 .and(path_regex(r"^/v1/accounts/0x[0-9a-f]+$"))
1326 .respond_with(
1327 ResponseTemplate::new(200)
1328 .set_body_json(serde_json::json!({
1329 "sequence_number": "42",
1330 "authentication_key": "0x0000000000000000000000000000000000000000000000000000000000000001"
1331 }))
1332 .insert_header("x-aptos-ledger-version", "12345"),
1333 )
1334 .expect(1)
1335 .mount(&server)
1336 .await;
1337
1338 let client = create_mock_client(&server);
1339 let result = client.get_account(AccountAddress::ONE).await.unwrap();
1340
1341 assert_eq!(result.data.sequence_number().unwrap(), 42);
1342 assert_eq!(result.ledger_version, Some(12345));
1343 }
1344
1345 #[tokio::test]
1346 async fn test_get_account_not_found() {
1347 let server = MockServer::start().await;
1348
1349 Mock::given(method("GET"))
1350 .and(path_regex(r"/v1/accounts/0x[0-9a-f]+"))
1351 .respond_with(ResponseTemplate::new(404).set_body_json(serde_json::json!({
1352 "message": "Account not found",
1353 "error_code": "account_not_found"
1354 })))
1355 .expect(1)
1356 .mount(&server)
1357 .await;
1358
1359 let client = create_mock_client(&server);
1360 let result = client.get_account(AccountAddress::ONE).await;
1361
1362 assert!(result.is_err());
1363 let err = result.unwrap_err();
1364 assert!(err.is_not_found());
1365 }
1366
1367 #[tokio::test]
1368 async fn test_get_account_resources() {
1369 let server = MockServer::start().await;
1370
1371 Mock::given(method("GET"))
1372 .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/resources"))
1373 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([
1374 {
1375 "type": "0x1::account::Account",
1376 "data": {"sequence_number": "10"}
1377 },
1378 {
1379 "type": "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>",
1380 "data": {"coin": {"value": "1000000"}}
1381 }
1382 ])))
1383 .expect(1)
1384 .mount(&server)
1385 .await;
1386
1387 let client = create_mock_client(&server);
1388 let result = client
1389 .get_account_resources(AccountAddress::ONE)
1390 .await
1391 .unwrap();
1392
1393 assert_eq!(result.data.len(), 2);
1394 assert!(result.data[0].typ.contains("Account"));
1395 }
1396
1397 #[tokio::test]
1398 async fn test_get_account_resource() {
1399 let server = MockServer::start().await;
1400
1401 Mock::given(method("GET"))
1402 .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/resource/.*"))
1403 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1404 "type": "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>",
1405 "data": {"coin": {"value": "5000000"}}
1406 })))
1407 .expect(1)
1408 .mount(&server)
1409 .await;
1410
1411 let client = create_mock_client(&server);
1412 let result = client
1413 .get_account_resource(
1414 AccountAddress::ONE,
1415 "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>",
1416 )
1417 .await
1418 .unwrap();
1419
1420 assert!(result.data.typ.contains("CoinStore"));
1421 }
1422
1423 #[tokio::test]
1424 async fn test_get_account_modules() {
1425 let server = MockServer::start().await;
1426
1427 Mock::given(method("GET"))
1428 .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/modules"))
1429 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([
1430 {
1431 "bytecode": "0xabc123",
1432 "abi": {
1433 "address": "0x1",
1434 "name": "coin",
1435 "exposed_functions": [],
1436 "structs": []
1437 }
1438 }
1439 ])))
1440 .expect(1)
1441 .mount(&server)
1442 .await;
1443
1444 let client = create_mock_client(&server);
1445 let result = client
1446 .get_account_modules(AccountAddress::ONE)
1447 .await
1448 .unwrap();
1449
1450 assert_eq!(result.data.len(), 1);
1451 assert!(result.data[0].abi.is_some());
1452 }
1453
1454 #[tokio::test]
1455 async fn test_get_account_resources_paginated_sends_start_and_limit() {
1456 let server = MockServer::start().await;
1457
1458 Mock::given(method("GET"))
1460 .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/resources"))
1461 .and(query_param("start", "42"))
1462 .and(query_param("limit", "9"))
1463 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([])))
1464 .expect(1)
1465 .mount(&server)
1466 .await;
1467
1468 let client = create_mock_client(&server);
1469 let result = client
1470 .get_account_resources_paginated(AccountAddress::ONE, Some("42"), Some(9))
1471 .await
1472 .unwrap();
1473 assert_eq!(result.data.len(), 0);
1474 }
1475
1476 #[tokio::test]
1477 async fn test_get_account_resources_paginated_round_trips_opaque_cursor() {
1478 let server = MockServer::start().await;
1482 let opaque = "0x0a1b2c3d_state_key_token";
1483 Mock::given(method("GET"))
1484 .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/resources"))
1485 .and(query_param("start", opaque))
1486 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([])))
1487 .expect(1)
1488 .mount(&server)
1489 .await;
1490
1491 let client = create_mock_client(&server);
1492 client
1493 .get_account_resources_paginated(AccountAddress::ONE, Some(opaque), None)
1494 .await
1495 .unwrap();
1496 }
1497
1498 #[tokio::test]
1499 async fn test_get_account_resources_no_pagination_omits_query() {
1500 let server = MockServer::start().await;
1501
1502 Mock::given(method("GET"))
1505 .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/resources$"))
1506 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([])))
1507 .expect(1)
1508 .mount(&server)
1509 .await;
1510
1511 let client = create_mock_client(&server);
1512 client
1513 .get_account_resources(AccountAddress::ONE)
1514 .await
1515 .unwrap();
1516 }
1517
1518 #[tokio::test]
1519 async fn test_get_account_resources_paginated_sends_start_only() {
1520 let server = MockServer::start().await;
1523 Mock::given(method("GET"))
1524 .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/resources"))
1525 .and(query_param("start", "1234"))
1526 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([])))
1527 .expect(1)
1528 .mount(&server)
1529 .await;
1530
1531 let client = create_mock_client(&server);
1532 client
1533 .get_account_resources_paginated(AccountAddress::ONE, Some("1234"), None)
1534 .await
1535 .unwrap();
1536 }
1537
1538 #[tokio::test]
1539 async fn test_get_account_modules_paginated_sends_start_and_limit() {
1540 let server = MockServer::start().await;
1541 Mock::given(method("GET"))
1542 .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/modules"))
1543 .and(query_param("start", "7"))
1544 .and(query_param("limit", "100"))
1545 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([])))
1546 .expect(1)
1547 .mount(&server)
1548 .await;
1549
1550 let client = create_mock_client(&server);
1551 client
1552 .get_account_modules_paginated(AccountAddress::ONE, Some("7"), Some(100))
1553 .await
1554 .unwrap();
1555 }
1556
1557 #[tokio::test]
1558 async fn test_get_account_modules_no_pagination_omits_query() {
1559 let server = MockServer::start().await;
1562 Mock::given(method("GET"))
1563 .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/modules$"))
1564 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([])))
1565 .expect(1)
1566 .mount(&server)
1567 .await;
1568
1569 let client = create_mock_client(&server);
1570 client
1571 .get_account_modules(AccountAddress::ONE)
1572 .await
1573 .unwrap();
1574 }
1575
1576 #[tokio::test]
1577 async fn test_get_account_modules_paginated_sends_limit_only() {
1578 let server = MockServer::start().await;
1579
1580 Mock::given(method("GET"))
1583 .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/modules"))
1584 .and(query_param("limit", "25"))
1585 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([])))
1586 .expect(1)
1587 .mount(&server)
1588 .await;
1589
1590 let client = create_mock_client(&server);
1591 client
1592 .get_account_modules_paginated(AccountAddress::ONE, None, Some(25))
1593 .await
1594 .unwrap();
1595 }
1596
1597 #[tokio::test]
1598 async fn test_estimate_gas_price() {
1599 let server = MockServer::start().await;
1600
1601 Mock::given(method("GET"))
1602 .and(path("/v1/estimate_gas_price"))
1603 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1604 "deprioritized_gas_estimate": 50,
1605 "gas_estimate": 100,
1606 "prioritized_gas_estimate": 150
1607 })))
1608 .expect(1)
1609 .mount(&server)
1610 .await;
1611
1612 let client = create_mock_client(&server);
1613 let result = client.estimate_gas_price().await.unwrap();
1614
1615 assert_eq!(result.data.gas_estimate, 100);
1616 assert_eq!(result.data.low(), 50);
1617 assert_eq!(result.data.high(), 150);
1618 }
1619
1620 #[tokio::test]
1621 async fn test_get_transaction_by_hash() {
1622 let server = MockServer::start().await;
1623
1624 Mock::given(method("GET"))
1625 .and(path_regex(r"/v1/transactions/by_hash/0x[0-9a-f]+"))
1626 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1627 "version": "12345",
1628 "hash": "0x0000000000000000000000000000000000000000000000000000000000000001",
1629 "success": true,
1630 "vm_status": "Executed successfully"
1631 })))
1632 .expect(1)
1633 .mount(&server)
1634 .await;
1635
1636 let client = create_mock_client(&server);
1637 let hash = HashValue::from_hex(
1638 "0x0000000000000000000000000000000000000000000000000000000000000001",
1639 )
1640 .unwrap();
1641 let result = client.get_transaction_by_hash(&hash).await.unwrap();
1642
1643 assert!(
1644 result
1645 .data
1646 .get("success")
1647 .and_then(serde_json::Value::as_bool)
1648 .unwrap()
1649 );
1650 }
1651
1652 #[tokio::test]
1653 async fn test_wait_for_transaction_success() {
1654 let server = MockServer::start().await;
1655
1656 Mock::given(method("GET"))
1657 .and(path_regex(r"/v1/transactions/by_hash/0x[0-9a-f]+"))
1658 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1659 "type": "user_transaction",
1660 "version": "12345",
1661 "hash": "0x0000000000000000000000000000000000000000000000000000000000000001",
1662 "success": true,
1663 "vm_status": "Executed successfully"
1664 })))
1665 .expect(1..)
1666 .mount(&server)
1667 .await;
1668
1669 let client = create_mock_client(&server);
1670 let hash = HashValue::from_hex(
1671 "0x0000000000000000000000000000000000000000000000000000000000000001",
1672 )
1673 .unwrap();
1674 let result = client
1675 .wait_for_transaction(&hash, Some(Duration::from_secs(5)))
1676 .await
1677 .unwrap();
1678
1679 assert!(
1680 result
1681 .data
1682 .get("success")
1683 .and_then(serde_json::Value::as_bool)
1684 .unwrap()
1685 );
1686 }
1687
1688 #[tokio::test]
1689 async fn test_server_error_retryable() {
1690 let server = MockServer::start().await;
1691
1692 Mock::given(method("GET"))
1693 .and(path("/v1"))
1694 .respond_with(ResponseTemplate::new(503).set_body_json(serde_json::json!({
1695 "message": "Service temporarily unavailable"
1696 })))
1697 .expect(1)
1698 .mount(&server)
1699 .await;
1700
1701 let url = format!("{}/v1", server.uri());
1702 let config = AptosConfig::custom(&url).unwrap().without_retry();
1703 let client = FullnodeClient::new(config).unwrap();
1704 let result = client.get_ledger_info().await;
1705
1706 assert!(result.is_err());
1707 assert!(result.unwrap_err().is_retryable());
1708 }
1709
1710 #[tokio::test]
1711 async fn test_rate_limited() {
1712 let server = MockServer::start().await;
1713
1714 Mock::given(method("GET"))
1715 .and(path("/v1"))
1716 .respond_with(
1717 ResponseTemplate::new(429)
1718 .set_body_json(serde_json::json!({
1719 "message": "Rate limited"
1720 }))
1721 .insert_header("retry-after", "30"),
1722 )
1723 .expect(1)
1724 .mount(&server)
1725 .await;
1726
1727 let url = format!("{}/v1", server.uri());
1728 let config = AptosConfig::custom(&url).unwrap().without_retry();
1729 let client = FullnodeClient::new(config).unwrap();
1730 let result = client.get_ledger_info().await;
1731
1732 assert!(result.is_err());
1733 assert!(result.unwrap_err().is_retryable());
1734 }
1735
1736 #[tokio::test]
1737 async fn test_get_block_by_height() {
1738 let server = MockServer::start().await;
1739
1740 Mock::given(method("GET"))
1741 .and(path_regex(r"/v1/blocks/by_height/\d+"))
1742 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1743 "block_height": "1000",
1744 "block_hash": "0xabc",
1745 "block_timestamp": "1234567890",
1746 "first_version": "100",
1747 "last_version": "200"
1748 })))
1749 .expect(1)
1750 .mount(&server)
1751 .await;
1752
1753 let client = create_mock_client(&server);
1754 let result = client.get_block_by_height(1000, false).await.unwrap();
1755
1756 assert!(result.data.get("block_height").is_some());
1757 }
1758
1759 #[tokio::test]
1760 async fn test_view() {
1761 let server = MockServer::start().await;
1762
1763 Mock::given(method("POST"))
1764 .and(path("/v1/view"))
1765 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!(["1000000"])))
1766 .expect(1)
1767 .mount(&server)
1768 .await;
1769
1770 let client = create_mock_client(&server);
1771 let result: AptosResponse<Vec<serde_json::Value>> = client
1772 .view(
1773 "0x1::coin::balance",
1774 vec!["0x1::aptos_coin::AptosCoin".to_string()],
1775 vec![serde_json::json!("0x1")],
1776 )
1777 .await
1778 .unwrap();
1779
1780 assert_eq!(result.data.len(), 1);
1781 }
1782
1783 #[tokio::test]
1784 async fn test_view_bcs_args_posts_bcs_request() {
1785 let server = MockServer::start().await;
1786
1787 let args = vec![aptos_bcs::to_bytes(&AccountAddress::ONE).unwrap()];
1792 let expected_body = aptos_bcs::to_bytes(
1793 &crate::transaction::EntryFunction::from_function_id(
1794 "0x1::coin::balance",
1795 vec![],
1796 args.clone(),
1797 )
1798 .unwrap(),
1799 )
1800 .unwrap();
1801
1802 Mock::given(method("POST"))
1803 .and(path("/v1/view"))
1804 .and(wiremock::matchers::header(
1805 "content-type",
1806 "application/x.aptos.view_function+bcs",
1807 ))
1808 .and(wiremock::matchers::body_bytes(expected_body))
1809 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!(["1000000"])))
1810 .expect(1)
1811 .mount(&server)
1812 .await;
1813
1814 let client = create_mock_client(&server);
1815 let result = client
1816 .view_bcs_args("0x1::coin::balance", vec![], args)
1817 .await
1818 .unwrap()
1819 .into_inner();
1820
1821 assert_eq!(result.len(), 1);
1822 assert_eq!(result[0].as_str().unwrap(), "1000000");
1823 }
1824
1825 #[tokio::test]
1826 async fn test_simulate_transaction_with_estimate_gas_unit_price() {
1827 let server = MockServer::start().await;
1828
1829 Mock::given(method("POST"))
1830 .and(path("/v1/transactions/simulate"))
1831 .and(|req: &wiremock::Request| {
1832 req.url
1833 .query()
1834 .is_some_and(|q| q.contains("estimate_gas_unit_price=true"))
1835 })
1836 .respond_with(ResponseTemplate::new(200).set_body_json(simulate_response_json()))
1837 .expect(1)
1838 .mount(&server)
1839 .await;
1840
1841 let client = create_mock_client(&server);
1842 let signed = create_minimal_signed_transaction();
1843 let opts = SimulateQueryOptions::new().estimate_gas_unit_price(true);
1844 let result = client
1845 .simulate_transaction_with_options(&signed, opts)
1846 .await
1847 .unwrap();
1848 assert!(!result.data.is_empty());
1849 }
1850
1851 #[tokio::test]
1852 async fn test_simulate_transaction_with_estimate_max_gas_amount() {
1853 let server = MockServer::start().await;
1854
1855 Mock::given(method("POST"))
1856 .and(path("/v1/transactions/simulate"))
1857 .and(|req: &wiremock::Request| {
1858 req.url
1859 .query()
1860 .is_some_and(|q| q.contains("estimate_max_gas_amount=true"))
1861 })
1862 .respond_with(ResponseTemplate::new(200).set_body_json(simulate_response_json()))
1863 .expect(1)
1864 .mount(&server)
1865 .await;
1866
1867 let client = create_mock_client(&server);
1868 let signed = create_minimal_signed_transaction();
1869 let opts = SimulateQueryOptions::new().estimate_max_gas_amount(true);
1870 let result = client
1871 .simulate_transaction_with_options(&signed, opts)
1872 .await
1873 .unwrap();
1874 assert!(!result.data.is_empty());
1875 }
1876
1877 #[tokio::test]
1878 async fn test_simulate_transaction_with_estimate_prioritized_gas_unit_price() {
1879 let server = MockServer::start().await;
1880
1881 Mock::given(method("POST"))
1882 .and(path("/v1/transactions/simulate"))
1883 .and(|req: &wiremock::Request| {
1884 req.url
1885 .query()
1886 .is_some_and(|q| q.contains("estimate_prioritized_gas_unit_price=true"))
1887 })
1888 .respond_with(ResponseTemplate::new(200).set_body_json(simulate_response_json()))
1889 .expect(1)
1890 .mount(&server)
1891 .await;
1892
1893 let client = create_mock_client(&server);
1894 let signed = create_minimal_signed_transaction();
1895 let opts = SimulateQueryOptions::new().estimate_prioritized_gas_unit_price(true);
1896 let result = client
1897 .simulate_transaction_with_options(&signed, opts)
1898 .await
1899 .unwrap();
1900 assert!(!result.data.is_empty());
1901 }
1902
1903 #[tokio::test]
1904 async fn test_simulate_transaction_with_all_options() {
1905 let server = MockServer::start().await;
1906
1907 Mock::given(method("POST"))
1908 .and(path("/v1/transactions/simulate"))
1909 .and(|req: &wiremock::Request| {
1910 req.url.query().is_some_and(|q| {
1911 q.contains("estimate_gas_unit_price=true")
1912 && q.contains("estimate_max_gas_amount=true")
1913 && q.contains("estimate_prioritized_gas_unit_price=true")
1914 })
1915 })
1916 .respond_with(ResponseTemplate::new(200).set_body_json(simulate_response_json()))
1917 .expect(1)
1918 .mount(&server)
1919 .await;
1920
1921 let client = create_mock_client(&server);
1922 let signed = create_minimal_signed_transaction();
1923 let opts = SimulateQueryOptions::new()
1924 .estimate_gas_unit_price(true)
1925 .estimate_max_gas_amount(true)
1926 .estimate_prioritized_gas_unit_price(true);
1927 let result = client
1928 .simulate_transaction_with_options(&signed, opts)
1929 .await
1930 .unwrap();
1931 assert!(!result.data.is_empty());
1932 }
1933
1934 #[tokio::test]
1935 async fn test_simulate_transaction_without_options() {
1936 let server = MockServer::start().await;
1937
1938 Mock::given(method("POST"))
1940 .and(path("/v1/transactions/simulate"))
1941 .and(|req: &wiremock::Request| {
1942 req.url.query().is_none_or(|q| {
1944 !q.contains("estimate_gas_unit_price=")
1945 && !q.contains("estimate_max_gas_amount=")
1946 && !q.contains("estimate_prioritized_gas_unit_price=")
1947 })
1948 })
1949 .respond_with(ResponseTemplate::new(200).set_body_json(simulate_response_json()))
1950 .expect(1)
1951 .mount(&server)
1952 .await;
1953
1954 let client = create_mock_client(&server);
1955 let signed = create_minimal_signed_transaction();
1956 let result = client.simulate_transaction(&signed).await.unwrap();
1957 assert!(!result.data.is_empty());
1958 }
1959
1960 #[tokio::test]
1961 async fn test_get_table_item() {
1962 let server = MockServer::start().await;
1963
1964 Mock::given(method("POST"))
1968 .and(path_regex(r"^/v1/tables/0x[0-9a-f]+/item$"))
1969 .and(wiremock::matchers::body_json(serde_json::json!({
1970 "key_type": "address",
1971 "value_type": "u64",
1972 "key": "0x1",
1973 })))
1974 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!("42")))
1975 .expect(1)
1976 .mount(&server)
1977 .await;
1978
1979 let client = create_mock_client(&server);
1980 let result = client
1981 .get_table_item(
1982 "0x0000000000000000000000000000000000000000000000000000000000000abc",
1983 "address",
1984 "u64",
1985 serde_json::json!("0x1"),
1986 )
1987 .await
1988 .unwrap();
1989
1990 assert_eq!(result.data, serde_json::json!("42"));
1991 }
1992
1993 #[tokio::test]
1994 async fn test_get_table_item_missing_key_is_404() {
1995 let server = MockServer::start().await;
1996
1997 Mock::given(method("POST"))
1998 .and(path_regex(r"^/v1/tables/0x[0-9a-f]+/item$"))
1999 .respond_with(ResponseTemplate::new(404).set_body_json(serde_json::json!({
2000 "message": "Table item not found",
2001 "error_code": "table_item_not_found"
2002 })))
2003 .expect(1)
2004 .mount(&server)
2005 .await;
2006
2007 let client = create_mock_client(&server);
2008 let err = client
2009 .get_table_item("0xabc", "address", "u64", serde_json::json!("0x2"))
2010 .await
2011 .unwrap_err();
2012
2013 assert!(matches!(
2014 err,
2015 AptosError::Api {
2016 status_code: 404,
2017 ..
2018 }
2019 ));
2020 }
2021
2022 #[tokio::test]
2023 async fn test_get_transaction_by_version() {
2024 let server = MockServer::start().await;
2025
2026 Mock::given(method("GET"))
2027 .and(path("/v1/transactions/by_version/100"))
2028 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2029 "version": "100",
2030 "hash": "0xdead",
2031 "success": true
2032 })))
2033 .expect(1)
2034 .mount(&server)
2035 .await;
2036
2037 let client = create_mock_client(&server);
2038 let result = client.get_transaction_by_version(100).await.unwrap();
2039
2040 assert_eq!(result.data.get("version").unwrap(), "100");
2041 }
2042
2043 #[tokio::test]
2044 async fn test_get_transactions_with_pagination() {
2045 let server = MockServer::start().await;
2046
2047 Mock::given(method("GET"))
2048 .and(path("/v1/transactions"))
2049 .and(query_param("start", "10"))
2050 .and(query_param("limit", "2"))
2051 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([
2052 {"version": "10"},
2053 {"version": "11"}
2054 ])))
2055 .expect(1)
2056 .mount(&server)
2057 .await;
2058
2059 let client = create_mock_client(&server);
2060 let result = client.get_transactions(Some(10), Some(2)).await.unwrap();
2061
2062 assert_eq!(result.data.len(), 2);
2063 }
2064
2065 #[tokio::test]
2066 async fn test_get_account_transactions() {
2067 let server = MockServer::start().await;
2068
2069 Mock::given(method("GET"))
2070 .and(path_regex(r"^/v1/accounts/0x[0-9a-f]+/transactions$"))
2071 .and(query_param("start", "0"))
2072 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([
2073 {"version": "5", "sender": "0x1"}
2074 ])))
2075 .expect(1)
2076 .mount(&server)
2077 .await;
2078
2079 let client = create_mock_client(&server);
2080 let result = client
2081 .get_account_transactions(AccountAddress::ONE, Some(0), None)
2082 .await
2083 .unwrap();
2084
2085 assert_eq!(result.data.len(), 1);
2086 }
2087
2088 #[tokio::test]
2089 async fn test_get_events_by_creation_number() {
2090 let server = MockServer::start().await;
2091
2092 Mock::given(method("GET"))
2093 .and(path_regex(r"^/v1/accounts/0x[0-9a-f]+/events/7$"))
2094 .and(query_param("limit", "25"))
2095 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([
2096 {"sequence_number": "0", "type": "0x1::coin::DepositEvent"}
2097 ])))
2098 .expect(1)
2099 .mount(&server)
2100 .await;
2101
2102 let client = create_mock_client(&server);
2103 let result = client
2104 .get_events_by_creation_number(AccountAddress::ONE, 7, None, Some(25))
2105 .await
2106 .unwrap();
2107
2108 assert_eq!(result.data.len(), 1);
2109 }
2110
2111 #[test]
2114 fn test_base_url_and_config_accessors() {
2115 let config = AptosConfig::testnet();
2116 let expected = config.fullnode_url().clone();
2117 let client = FullnodeClient::new(config).unwrap();
2118
2119 assert_eq!(client.base_url(), &expected);
2121 assert_eq!(client.config().fullnode_url(), &expected);
2123 }
2124
2125 #[test]
2126 fn test_retry_config_accessor() {
2127 let config = AptosConfig::testnet().with_max_retries(7);
2129 let client = FullnodeClient::new(config).unwrap();
2130 assert_eq!(client.retry_config().max_retries, 7);
2131
2132 let client = FullnodeClient::new(AptosConfig::testnet().without_retry()).unwrap();
2134 assert_eq!(client.retry_config().max_retries, 0);
2135 }
2136
2137 #[tokio::test]
2140 async fn test_get_sequence_number() {
2141 let server = MockServer::start().await;
2142
2143 Mock::given(method("GET"))
2144 .and(path_regex(r"^/v1/accounts/0x[0-9a-f]+$"))
2145 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2146 "sequence_number": "99",
2147 "authentication_key": "0x0000000000000000000000000000000000000000000000000000000000000001"
2148 })))
2149 .expect(1)
2150 .mount(&server)
2151 .await;
2152
2153 let client = create_mock_client(&server);
2154 let seq = client
2155 .get_sequence_number(AccountAddress::ONE)
2156 .await
2157 .unwrap();
2158 assert_eq!(seq, 99);
2159 }
2160
2161 #[tokio::test]
2162 async fn test_get_sequence_number_unparseable_is_internal_error() {
2163 let server = MockServer::start().await;
2164
2165 Mock::given(method("GET"))
2168 .and(path_regex(r"^/v1/accounts/0x[0-9a-f]+$"))
2169 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2170 "sequence_number": "not-a-number",
2171 "authentication_key": "0x0000000000000000000000000000000000000000000000000000000000000001"
2172 })))
2173 .expect(1)
2174 .mount(&server)
2175 .await;
2176
2177 let client = create_mock_client(&server);
2178 let err = client
2179 .get_sequence_number(AccountAddress::ONE)
2180 .await
2181 .unwrap_err();
2182 assert!(matches!(err, AptosError::Internal(_)));
2183 }
2184
2185 #[tokio::test]
2188 async fn test_get_account_module() {
2189 let server = MockServer::start().await;
2190
2191 Mock::given(method("GET"))
2192 .and(path_regex(r"^/v1/accounts/0x[0-9a-f]+/module/coin$"))
2193 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2194 "bytecode": "0xdeadbeef",
2195 "abi": {
2196 "address": "0x1",
2197 "name": "coin",
2198 "exposed_functions": [],
2199 "structs": []
2200 }
2201 })))
2202 .expect(1)
2203 .mount(&server)
2204 .await;
2205
2206 let client = create_mock_client(&server);
2207 let result = client
2208 .get_account_module(AccountAddress::ONE, "coin")
2209 .await
2210 .unwrap();
2211
2212 assert_eq!(result.data.bytecode, "0xdeadbeef");
2213 assert!(result.data.abi.is_some());
2214 }
2215
2216 #[tokio::test]
2217 async fn test_get_account_module_not_found() {
2218 let server = MockServer::start().await;
2219
2220 Mock::given(method("GET"))
2221 .and(path_regex(r"^/v1/accounts/0x[0-9a-f]+/module/.*$"))
2222 .respond_with(ResponseTemplate::new(404).set_body_json(serde_json::json!({
2223 "message": "Module not found",
2224 "error_code": "module_not_found"
2225 })))
2226 .expect(1)
2227 .mount(&server)
2228 .await;
2229
2230 let client = create_mock_client(&server);
2231 let err = client
2232 .get_account_module(AccountAddress::ONE, "missing")
2233 .await
2234 .unwrap_err();
2235 assert!(err.is_not_found());
2236 }
2237
2238 #[tokio::test]
2241 async fn test_get_account_balance() {
2242 let server = MockServer::start().await;
2243
2244 Mock::given(method("POST"))
2247 .and(path("/v1/view"))
2248 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!(["1234567"])))
2249 .expect(1)
2250 .mount(&server)
2251 .await;
2252
2253 let client = create_mock_client(&server);
2254 let balance = client
2255 .get_account_balance(AccountAddress::ONE)
2256 .await
2257 .unwrap();
2258 assert_eq!(balance, 1_234_567);
2259 }
2260
2261 #[tokio::test]
2262 async fn test_get_account_balance_unparseable_is_internal_error() {
2263 let server = MockServer::start().await;
2264
2265 Mock::given(method("POST"))
2267 .and(path("/v1/view"))
2268 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([])))
2269 .expect(1)
2270 .mount(&server)
2271 .await;
2272
2273 let client = create_mock_client(&server);
2274 let err = client
2275 .get_account_balance(AccountAddress::ONE)
2276 .await
2277 .unwrap_err();
2278 assert!(matches!(err, AptosError::Internal(_)));
2279 }
2280
2281 fn pending_transaction_json() -> serde_json::Value {
2284 serde_json::json!({
2285 "hash": "0x0000000000000000000000000000000000000000000000000000000000000001",
2286 "sender": "0x1",
2287 "sequence_number": "0",
2288 "max_gas_amount": "100000",
2289 "gas_unit_price": "100",
2290 "expiration_timestamp_secs": "1000000"
2291 })
2292 }
2293
2294 #[tokio::test]
2295 async fn test_submit_transaction_success() {
2296 let server = MockServer::start().await;
2297
2298 Mock::given(method("POST"))
2299 .and(path("/v1/transactions"))
2300 .and(wiremock::matchers::header(
2301 "content-type",
2302 "application/x.aptos.signed_transaction+bcs",
2303 ))
2304 .respond_with(ResponseTemplate::new(202).set_body_json(pending_transaction_json()))
2305 .expect(1)
2306 .mount(&server)
2307 .await;
2308
2309 let client = create_mock_client(&server);
2310 let signed = create_minimal_signed_transaction();
2311 let result = client.submit_transaction(&signed).await.unwrap();
2312
2313 assert_eq!(result.data.sequence_number, "0");
2314 assert_eq!(result.data.sender(), "0x1");
2315 }
2316
2317 #[tokio::test]
2318 async fn test_submit_transaction_rejected_is_api_error() {
2319 let server = MockServer::start().await;
2320
2321 Mock::given(method("POST"))
2324 .and(path("/v1/transactions"))
2325 .respond_with(ResponseTemplate::new(400).set_body_json(serde_json::json!({
2326 "message": "Invalid transaction",
2327 "error_code": "invalid_transaction_update"
2328 })))
2329 .expect(1)
2330 .mount(&server)
2331 .await;
2332
2333 let client = create_mock_client(&server);
2334 let signed = create_minimal_signed_transaction();
2335 let err = client.submit_transaction(&signed).await.unwrap_err();
2336
2337 match err {
2338 AptosError::Api {
2339 status_code,
2340 error_code,
2341 ..
2342 } => {
2343 assert_eq!(status_code, 400);
2344 assert_eq!(error_code.as_deref(), Some("invalid_transaction_update"));
2345 }
2346 other => panic!("expected Api error, got {other:?}"),
2347 }
2348 assert!(
2350 !AptosError::api(400, "x").is_retryable(),
2351 "sanity: 4xx not retryable"
2352 );
2353 }
2354
2355 #[tokio::test]
2358 async fn test_submit_and_wait_success() {
2359 let server = MockServer::start().await;
2360
2361 Mock::given(method("POST"))
2363 .and(path("/v1/transactions"))
2364 .respond_with(ResponseTemplate::new(202).set_body_json(pending_transaction_json()))
2365 .expect(1)
2366 .mount(&server)
2367 .await;
2368
2369 Mock::given(method("GET"))
2370 .and(path_regex(r"^/v1/transactions/by_hash/0x[0-9a-f]+$"))
2371 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2372 "type": "user_transaction",
2373 "version": "555",
2374 "hash": "0x0000000000000000000000000000000000000000000000000000000000000001",
2375 "success": true,
2376 "vm_status": "Executed successfully"
2377 })))
2378 .expect(1..)
2379 .mount(&server)
2380 .await;
2381
2382 let client = create_mock_client(&server);
2383 let signed = create_minimal_signed_transaction();
2384 let result = client
2385 .submit_and_wait(&signed, Some(Duration::from_secs(5)))
2386 .await
2387 .unwrap();
2388
2389 assert_eq!(result.data.get("version").unwrap(), "555");
2390 }
2391
2392 #[tokio::test]
2395 async fn test_wait_for_transaction_execution_failed() {
2396 let server = MockServer::start().await;
2397
2398 Mock::given(method("GET"))
2401 .and(path_regex(r"^/v1/transactions/by_hash/0x[0-9a-f]+$"))
2402 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2403 "type": "user_transaction",
2404 "version": "12345",
2405 "hash": "0x0000000000000000000000000000000000000000000000000000000000000001",
2406 "success": false,
2407 "vm_status": "Move abort: 0x1"
2408 })))
2409 .expect(1..)
2410 .mount(&server)
2411 .await;
2412
2413 let client = create_mock_client(&server);
2414 let hash = HashValue::from_hex(
2415 "0x0000000000000000000000000000000000000000000000000000000000000001",
2416 )
2417 .unwrap();
2418 let err = client
2419 .wait_for_transaction(&hash, Some(Duration::from_secs(5)))
2420 .await
2421 .unwrap_err();
2422
2423 match err {
2424 AptosError::ExecutionFailed { vm_status } => {
2425 assert_eq!(vm_status, "Move abort: 0x1");
2426 }
2427 other => panic!("expected ExecutionFailed, got {other:?}"),
2428 }
2429 }
2430
2431 #[tokio::test]
2432 async fn test_wait_for_transaction_times_out() {
2433 let server = MockServer::start().await;
2434
2435 Mock::given(method("GET"))
2438 .and(path_regex(r"^/v1/transactions/by_hash/0x[0-9a-f]+$"))
2439 .respond_with(ResponseTemplate::new(404).set_body_json(serde_json::json!({
2440 "message": "Transaction not found",
2441 "error_code": "transaction_not_found"
2442 })))
2443 .expect(1..)
2444 .mount(&server)
2445 .await;
2446
2447 let client = create_mock_client(&server);
2448 let hash = HashValue::from_hex(
2449 "0x0000000000000000000000000000000000000000000000000000000000000002",
2450 )
2451 .unwrap();
2452 let err = client
2453 .wait_for_transaction(&hash, Some(Duration::from_millis(1)))
2454 .await
2455 .unwrap_err();
2456
2457 assert!(err.is_timeout());
2458 match err {
2459 AptosError::TransactionTimeout { timeout_secs, .. } => {
2460 assert_eq!(timeout_secs, 0);
2461 }
2462 other => panic!("expected TransactionTimeout, got {other:?}"),
2463 }
2464 }
2465
2466 #[tokio::test]
2467 async fn test_wait_for_transaction_propagates_non_404_error() {
2468 let server = MockServer::start().await;
2469
2470 Mock::given(method("GET"))
2473 .and(path_regex(r"^/v1/transactions/by_hash/0x[0-9a-f]+$"))
2474 .respond_with(ResponseTemplate::new(500).set_body_json(serde_json::json!({
2475 "message": "boom"
2476 })))
2477 .expect(1)
2478 .mount(&server)
2479 .await;
2480
2481 let client = create_mock_client(&server);
2482 let hash = HashValue::from_hex(
2483 "0x0000000000000000000000000000000000000000000000000000000000000003",
2484 )
2485 .unwrap();
2486 let err = client
2487 .wait_for_transaction(&hash, Some(Duration::from_secs(5)))
2488 .await
2489 .unwrap_err();
2490
2491 assert!(matches!(
2492 err,
2493 AptosError::Api {
2494 status_code: 500,
2495 ..
2496 }
2497 ));
2498 }
2499
2500 #[tokio::test]
2503 async fn test_view_bcs_returns_raw_bytes() {
2504 let server = MockServer::start().await;
2505
2506 let raw = vec![1u8, 2, 3, 4, 5];
2509 Mock::given(method("POST"))
2510 .and(path("/v1/view"))
2511 .and(wiremock::matchers::header("accept", "application/x-bcs"))
2512 .respond_with(ResponseTemplate::new(200).set_body_bytes(raw.clone()))
2513 .expect(1)
2514 .mount(&server)
2515 .await;
2516
2517 let client = create_mock_client(&server);
2518 let result = client
2519 .view_bcs(
2520 "0x1::coin::balance",
2521 vec!["0x1::aptos_coin::AptosCoin".to_string()],
2522 vec![aptos_bcs::to_bytes(&AccountAddress::ONE).unwrap()],
2523 )
2524 .await
2525 .unwrap();
2526
2527 assert_eq!(result.data, raw);
2528 }
2529
2530 #[tokio::test]
2531 async fn test_view_bcs_error_status_is_api_error() {
2532 let server = MockServer::start().await;
2533
2534 Mock::given(method("POST"))
2537 .and(path("/v1/view"))
2538 .respond_with(ResponseTemplate::new(400).set_body_string("bad view request"))
2539 .expect(1)
2540 .mount(&server)
2541 .await;
2542
2543 let client = create_mock_client(&server);
2544 let err = client
2545 .view_bcs("0x1::coin::balance", vec![], vec![])
2546 .await
2547 .unwrap_err();
2548
2549 match err {
2550 AptosError::Api {
2551 status_code,
2552 message,
2553 ..
2554 } => {
2555 assert_eq!(status_code, 400);
2556 assert!(message.contains("bad view request"));
2557 }
2558 other => panic!("expected Api error, got {other:?}"),
2559 }
2560 }
2561
2562 #[tokio::test]
2565 async fn test_get_events_by_event_handle() {
2566 let server = MockServer::start().await;
2567
2568 Mock::given(method("GET"))
2569 .and(path_regex(
2570 r"^/v1/accounts/0x[0-9a-f]+/events/.+/withdraw_events$",
2571 ))
2572 .and(query_param("start", "0"))
2573 .and(query_param("limit", "10"))
2574 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([
2575 {"sequence_number": "0", "type": "0x1::coin::WithdrawEvent"}
2576 ])))
2577 .expect(1)
2578 .mount(&server)
2579 .await;
2580
2581 let client = create_mock_client(&server);
2582 let result = client
2583 .get_events_by_event_handle(
2584 AccountAddress::ONE,
2585 "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>",
2586 "withdraw_events",
2587 Some(0),
2588 Some(10),
2589 )
2590 .await
2591 .unwrap();
2592
2593 assert_eq!(result.data.len(), 1);
2594 }
2595
2596 #[tokio::test]
2599 async fn test_get_block_by_version() {
2600 let server = MockServer::start().await;
2601
2602 Mock::given(method("GET"))
2603 .and(path("/v1/blocks/by_version/200"))
2604 .and(query_param("with_transactions", "true"))
2605 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2606 "block_height": "50",
2607 "block_hash": "0xfeed",
2608 "block_timestamp": "1234567890",
2609 "first_version": "180",
2610 "last_version": "220"
2611 })))
2612 .expect(1)
2613 .mount(&server)
2614 .await;
2615
2616 let client = create_mock_client(&server);
2617 let result = client.get_block_by_version(200, true).await.unwrap();
2618
2619 assert_eq!(result.data.get("block_height").unwrap(), "50");
2620 }
2621
2622 #[tokio::test]
2625 async fn test_api_error_parses_error_code_and_vm_error_code() {
2626 let server = MockServer::start().await;
2627
2628 Mock::given(method("GET"))
2631 .and(path("/v1"))
2632 .respond_with(ResponseTemplate::new(400).set_body_json(serde_json::json!({
2633 "message": "VM execution error",
2634 "error_code": "vm_error",
2635 "vm_error_code": 4004
2636 })))
2637 .expect(1)
2638 .mount(&server)
2639 .await;
2640
2641 let client = create_mock_client(&server);
2642 let err = client.get_ledger_info().await.unwrap_err();
2643
2644 match err {
2645 AptosError::Api {
2646 status_code,
2647 message,
2648 error_code,
2649 vm_error_code,
2650 } => {
2651 assert_eq!(status_code, 400);
2652 assert_eq!(message, "VM execution error");
2653 assert_eq!(error_code.as_deref(), Some("vm_error"));
2654 assert_eq!(vm_error_code, Some(4004));
2655 }
2656 other => panic!("expected Api error, got {other:?}"),
2657 }
2658 }
2659
2660 #[tokio::test]
2661 async fn test_api_error_non_json_body_uses_default_message() {
2662 let server = MockServer::start().await;
2663
2664 Mock::given(method("GET"))
2667 .and(path("/v1"))
2668 .respond_with(ResponseTemplate::new(403).set_body_string("Forbidden (plain text)"))
2669 .expect(1)
2670 .mount(&server)
2671 .await;
2672
2673 let client = create_mock_client(&server);
2674 let err = client.get_ledger_info().await.unwrap_err();
2675
2676 match err {
2677 AptosError::Api {
2678 status_code,
2679 message,
2680 error_code,
2681 vm_error_code,
2682 } => {
2683 assert_eq!(status_code, 403);
2684 assert_eq!(message, "Unknown error");
2685 assert!(error_code.is_none());
2686 assert!(vm_error_code.is_none());
2687 }
2688 other => panic!("expected Api error, got {other:?}"),
2689 }
2690 }
2691
2692 #[tokio::test]
2693 async fn test_success_with_invalid_json_body_is_json_error() {
2694 let server = MockServer::start().await;
2695
2696 Mock::given(method("GET"))
2699 .and(path("/v1"))
2700 .respond_with(ResponseTemplate::new(200).set_body_string("this is not json"))
2701 .expect(1)
2702 .mount(&server)
2703 .await;
2704
2705 let client = create_mock_client(&server);
2706 let err = client.get_ledger_info().await.unwrap_err();
2707 assert!(matches!(err, AptosError::Json(_)));
2708 }
2709}