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 #[cfg(not(target_arch = "wasm32"))]
92 crate::rustls_provider::install_default();
93
94 #[cfg(not(target_arch = "wasm32"))]
98 let builder = {
99 let pool = config.pool_config();
100 let mut builder = Client::builder()
101 .timeout(config.timeout)
102 .pool_max_idle_per_host(pool.max_idle_per_host.unwrap_or(usize::MAX))
103 .pool_idle_timeout(pool.idle_timeout)
104 .tcp_nodelay(pool.tcp_nodelay);
105
106 if let Some(keepalive) = pool.tcp_keepalive {
107 builder = builder.tcp_keepalive(keepalive);
108 }
109 builder
110 };
111
112 #[cfg(target_arch = "wasm32")]
114 let builder = Client::builder();
115
116 let client = builder.build().map_err(AptosError::Http)?;
117
118 let retry_config = Arc::new(config.retry_config().clone());
119
120 Ok(Self {
121 config,
122 client,
123 retry_config,
124 })
125 }
126
127 pub fn base_url(&self) -> &Url {
129 self.config.fullnode_url()
130 }
131
132 pub fn config(&self) -> &AptosConfig {
134 &self.config
135 }
136
137 pub async fn view_bcs_args(
167 &self,
168 function: &str,
169 type_args: Vec<crate::types::TypeTag>,
170 args: Vec<Vec<u8>>,
171 ) -> AptosResult<AptosResponse<Vec<serde_json::Value>>> {
172 const BCS_VIEW_REQUEST_CONTENT_TYPE: &str = "application/x.aptos.view_function+bcs";
176
177 let view_fn =
182 crate::transaction::EntryFunction::from_function_id(function, type_args, args)?;
183 let body = aptos_bcs::to_bytes(&view_fn).map_err(AptosError::bcs)?;
184 let url = self.build_url("view");
185
186 let client = self.client.clone();
187 let retry_config = self.retry_config.clone();
188 let max_response_size = self.config.pool_config().max_response_size;
189
190 let executor = RetryExecutor::from_shared(retry_config);
191 executor
192 .execute(|| {
193 let client = client.clone();
194 let url = url.clone();
195 let body = body.clone();
196 async move {
197 let response = client
198 .post(url)
199 .header(CONTENT_TYPE, BCS_VIEW_REQUEST_CONTENT_TYPE)
200 .header(ACCEPT, JSON_CONTENT_TYPE)
201 .body(body)
202 .send()
203 .await?;
204
205 Self::handle_response_static(response, max_response_size).await
206 }
207 })
208 .await
209 }
210
211 pub fn retry_config(&self) -> &RetryConfig {
213 &self.retry_config
214 }
215
216 pub async fn get_ledger_info(&self) -> AptosResult<AptosResponse<LedgerInfo>> {
225 let url = self.build_url("");
226 self.get_json(url).await
227 }
228
229 pub async fn get_account(
238 &self,
239 address: AccountAddress,
240 ) -> AptosResult<AptosResponse<AccountData>> {
241 let url = self.build_url(&format!("accounts/{address}"));
242 self.get_json(url).await
243 }
244
245 pub async fn get_sequence_number(&self, address: AccountAddress) -> AptosResult<u64> {
252 let account = self.get_account(address).await?;
253 account
254 .data
255 .sequence_number()
256 .map_err(|e| AptosError::Internal(format!("failed to parse sequence number: {e}")))
257 }
258
259 pub async fn get_account_resources(
270 &self,
271 address: AccountAddress,
272 ) -> AptosResult<AptosResponse<Vec<Resource>>> {
273 self.get_account_resources_paginated(address, None, None)
274 .await
275 }
276
277 pub async fn get_account_resources_paginated(
297 &self,
298 address: AccountAddress,
299 start: Option<&str>,
300 limit: Option<u16>,
301 ) -> AptosResult<AptosResponse<Vec<Resource>>> {
302 let mut url = self.build_url(&format!("accounts/{address}/resources"));
303 append_start_limit(&mut url, start, limit);
304 self.get_json(url).await
305 }
306
307 pub async fn get_account_resource(
314 &self,
315 address: AccountAddress,
316 resource_type: &str,
317 ) -> AptosResult<AptosResponse<Resource>> {
318 let url = self.build_url(&format!(
319 "accounts/{}/resource/{}",
320 address,
321 urlencoding::encode(resource_type)
322 ));
323 self.get_json(url).await
324 }
325
326 pub async fn get_account_modules(
338 &self,
339 address: AccountAddress,
340 ) -> AptosResult<AptosResponse<Vec<MoveModule>>> {
341 self.get_account_modules_paginated(address, None, None)
342 .await
343 }
344
345 pub async fn get_account_modules_paginated(
356 &self,
357 address: AccountAddress,
358 start: Option<&str>,
359 limit: Option<u16>,
360 ) -> AptosResult<AptosResponse<Vec<MoveModule>>> {
361 let mut url = self.build_url(&format!("accounts/{address}/modules"));
362 append_start_limit(&mut url, start, limit);
363 self.get_json(url).await
364 }
365
366 pub async fn get_account_module(
373 &self,
374 address: AccountAddress,
375 module_name: &str,
376 ) -> AptosResult<AptosResponse<MoveModule>> {
377 let url = self.build_url(&format!("accounts/{address}/module/{module_name}"));
378 self.get_json(url).await
379 }
380
381 pub async fn get_account_balance(&self, address: AccountAddress) -> AptosResult<u64> {
390 let result = self
393 .view(
394 "0x1::coin::balance",
395 vec!["0x1::aptos_coin::AptosCoin".to_string()],
396 vec![serde_json::json!(address.to_string())],
397 )
398 .await?;
399
400 let balance_str = result
402 .data
403 .first()
404 .and_then(|v| v.as_str())
405 .ok_or_else(|| AptosError::Internal("failed to parse balance response".into()))?;
406
407 balance_str
408 .parse()
409 .map_err(|_| AptosError::Internal("failed to parse balance as u64".into()))
410 }
411
412 pub async fn submit_transaction(
424 &self,
425 signed_txn: &SignedTransaction,
426 ) -> AptosResult<AptosResponse<PendingTransaction>> {
427 let url = self.build_url("transactions");
428 let bcs_bytes = signed_txn.to_bcs()?;
429 let client = self.client.clone();
430 let retry_config = self.retry_config.clone();
431 let max_response_size = self.config.pool_config().max_response_size;
432
433 let executor = RetryExecutor::from_shared(retry_config);
434 executor
435 .execute(|| {
436 let client = client.clone();
437 let url = url.clone();
438 let bcs_bytes = bcs_bytes.clone();
439 async move {
440 let response = client
441 .post(url)
442 .header(CONTENT_TYPE, BCS_CONTENT_TYPE)
443 .header(ACCEPT, JSON_CONTENT_TYPE)
444 .body(bcs_bytes)
445 .send()
446 .await?;
447
448 Self::handle_response_static(response, max_response_size).await
449 }
450 })
451 .await
452 }
453
454 pub async fn submit_and_wait(
461 &self,
462 signed_txn: &SignedTransaction,
463 timeout: Option<Duration>,
464 ) -> AptosResult<AptosResponse<serde_json::Value>> {
465 let pending = self.submit_transaction(signed_txn).await?;
466 self.wait_for_transaction(&pending.data.hash, timeout).await
467 }
468
469 pub async fn get_transaction_by_hash(
476 &self,
477 hash: &HashValue,
478 ) -> AptosResult<AptosResponse<serde_json::Value>> {
479 let url = self.build_url(&format!("transactions/by_hash/{hash}"));
480 self.get_json(url).await
481 }
482
483 pub async fn get_transaction_by_version(
490 &self,
491 version: u64,
492 ) -> AptosResult<AptosResponse<serde_json::Value>> {
493 let url = self.build_url(&format!("transactions/by_version/{version}"));
494 self.get_json(url).await
495 }
496
497 pub async fn get_transactions(
510 &self,
511 start: Option<u64>,
512 limit: Option<u16>,
513 ) -> AptosResult<AptosResponse<Vec<serde_json::Value>>> {
514 let mut url = self.build_url("transactions");
515 {
516 let mut query = url.query_pairs_mut();
517 if let Some(start) = start {
518 query.append_pair("start", &start.to_string());
519 }
520 if let Some(limit) = limit {
521 query.append_pair("limit", &limit.to_string());
522 }
523 }
524 self.get_json(url).await
525 }
526
527 pub async fn get_account_transactions(
538 &self,
539 address: AccountAddress,
540 start: Option<u64>,
541 limit: Option<u16>,
542 ) -> AptosResult<AptosResponse<Vec<serde_json::Value>>> {
543 let mut url = self.build_url(&format!("accounts/{address}/transactions"));
544 {
545 let mut query = url.query_pairs_mut();
546 if let Some(start) = start {
547 query.append_pair("start", &start.to_string());
548 }
549 if let Some(limit) = limit {
550 query.append_pair("limit", &limit.to_string());
551 }
552 }
553 self.get_json(url).await
554 }
555
556 pub async fn wait_for_transaction(
565 &self,
566 hash: &HashValue,
567 timeout: Option<Duration>,
568 ) -> AptosResult<AptosResponse<serde_json::Value>> {
569 let timeout = timeout.unwrap_or(Duration::from_secs(DEFAULT_TRANSACTION_WAIT_TIMEOUT_SECS));
570 let start = std::time::Instant::now();
571
572 let initial_interval = Duration::from_millis(200);
574 let max_interval = Duration::from_secs(2);
575 let mut current_interval = initial_interval;
576
577 loop {
578 match self.get_transaction_by_hash(hash).await {
579 Ok(response) => {
580 if response.data.get("version").is_some() {
582 let success = response
584 .data
585 .get("success")
586 .and_then(serde_json::Value::as_bool);
587 if success == Some(false) {
588 let vm_status = response
589 .data
590 .get("vm_status")
591 .and_then(|v| v.as_str())
592 .unwrap_or("unknown")
593 .to_string();
594 return Err(AptosError::ExecutionFailed { vm_status });
595 }
596 return Ok(response);
597 }
598 }
599 Err(AptosError::Api {
600 status_code: 404, ..
601 }) => {
602 }
604 Err(e) => return Err(e),
605 }
606
607 if start.elapsed() >= timeout {
608 return Err(AptosError::TransactionTimeout {
609 hash: hash.to_string(),
610 timeout_secs: timeout.as_secs(),
611 });
612 }
613
614 tokio::time::sleep(current_interval).await;
615
616 current_interval = std::cmp::min(current_interval * 2, max_interval);
618 }
619 }
620
621 pub async fn simulate_transaction(
633 &self,
634 signed_txn: &SignedTransaction,
635 ) -> AptosResult<AptosResponse<Vec<serde_json::Value>>> {
636 self.simulate_transaction_with_options(signed_txn, None as Option<SimulateQueryOptions>)
637 .await
638 }
639
640 pub async fn simulate_transaction_with_options(
656 &self,
657 signed_txn: &SignedTransaction,
658 options: impl Into<Option<SimulateQueryOptions>>,
659 ) -> AptosResult<AptosResponse<Vec<serde_json::Value>>> {
660 let mut url = self.build_url("transactions/simulate");
661 if let Some(opts) = options.into() {
662 let mut pairs = url.query_pairs_mut();
663 if opts.estimate_gas_unit_price {
664 pairs.append_pair("estimate_gas_unit_price", "true");
665 }
666 if opts.estimate_max_gas_amount {
667 pairs.append_pair("estimate_max_gas_amount", "true");
668 }
669 if opts.estimate_prioritized_gas_unit_price {
670 pairs.append_pair("estimate_prioritized_gas_unit_price", "true");
671 }
672 }
673 let bcs_bytes = signed_txn.for_simulate_endpoint().to_bcs()?;
674 let client = self.client.clone();
675 let retry_config = self.retry_config.clone();
676 let max_response_size = self.config.pool_config().max_response_size;
677
678 let executor = RetryExecutor::from_shared(retry_config);
679 executor
680 .execute(|| {
681 let client = client.clone();
682 let url = url.clone();
683 let bcs_bytes = bcs_bytes.clone();
684 async move {
685 let response = client
686 .post(url)
687 .header(CONTENT_TYPE, BCS_CONTENT_TYPE)
688 .header(ACCEPT, JSON_CONTENT_TYPE)
689 .body(bcs_bytes)
690 .send()
691 .await?;
692
693 Self::handle_response_static(response, max_response_size).await
694 }
695 })
696 .await
697 }
698
699 pub async fn estimate_gas_price(&self) -> AptosResult<AptosResponse<GasEstimation>> {
708 let url = self.build_url("estimate_gas_price");
709 self.get_json(url).await
710 }
711
712 pub async fn view(
721 &self,
722 function: &str,
723 type_args: Vec<String>,
724 args: Vec<serde_json::Value>,
725 ) -> AptosResult<AptosResponse<Vec<serde_json::Value>>> {
726 let url = self.build_url("view");
727
728 let body = serde_json::json!({
729 "function": function,
730 "type_arguments": type_args,
731 "arguments": args,
732 });
733
734 let client = self.client.clone();
735 let retry_config = self.retry_config.clone();
736 let max_response_size = self.config.pool_config().max_response_size;
737
738 let executor = RetryExecutor::from_shared(retry_config);
739 executor
740 .execute(|| {
741 let client = client.clone();
742 let url = url.clone();
743 let body = body.clone();
744 async move {
745 let response = client
746 .post(url)
747 .header(CONTENT_TYPE, JSON_CONTENT_TYPE)
748 .header(ACCEPT, JSON_CONTENT_TYPE)
749 .json(&body)
750 .send()
751 .await?;
752
753 Self::handle_response_static(response, max_response_size).await
754 }
755 })
756 .await
757 }
758
759 pub async fn view_bcs(
781 &self,
782 function: &str,
783 type_args: Vec<String>,
784 args: Vec<Vec<u8>>,
785 ) -> AptosResult<AptosResponse<Vec<u8>>> {
786 let url = self.build_url("view");
787
788 let hex_args: Vec<serde_json::Value> = args
791 .iter()
792 .map(|bytes| serde_json::json!(const_hex::encode_prefixed(bytes)))
793 .collect();
794
795 let body = serde_json::json!({
796 "function": function,
797 "type_arguments": type_args,
798 "arguments": hex_args,
799 });
800
801 let client = self.client.clone();
802 let retry_config = self.retry_config.clone();
803 let max_response_size = self.config.pool_config().max_response_size;
804
805 let executor = RetryExecutor::from_shared(retry_config);
806 executor
807 .execute(|| {
808 let client = client.clone();
809 let url = url.clone();
810 let body = body.clone();
811 async move {
812 let response = client
813 .post(url)
814 .header(CONTENT_TYPE, JSON_CONTENT_TYPE)
815 .header(ACCEPT, BCS_VIEW_CONTENT_TYPE)
816 .json(&body)
817 .send()
818 .await?;
819
820 let status = response.status();
822 if !status.is_success() {
823 let error_bytes =
826 crate::config::read_response_bounded(response, MAX_ERROR_BODY_SIZE)
827 .await
828 .ok();
829 let error_text = error_bytes
830 .and_then(|b| String::from_utf8(b).ok())
831 .unwrap_or_default();
832 return Err(AptosError::Api {
833 status_code: status.as_u16(),
834 message: Self::truncate_error_body(error_text),
835 error_code: None,
836 vm_error_code: None,
837 });
838 }
839
840 let bytes =
843 crate::config::read_response_bounded(response, max_response_size).await?;
844 Ok(AptosResponse::new(bytes))
845 }
846 })
847 .await
848 }
849
850 pub async fn get_table_item(
876 &self,
877 handle: &str,
878 key_type: &str,
879 value_type: &str,
880 key: serde_json::Value,
881 ) -> AptosResult<AptosResponse<serde_json::Value>> {
882 let url = self.build_url(&format!("tables/{}/item", urlencoding::encode(handle)));
883 let body = serde_json::json!({
884 "key_type": key_type,
885 "value_type": value_type,
886 "key": key,
887 });
888 self.post_json(url, &body).await
889 }
890
891 pub async fn get_events_by_creation_number(
906 &self,
907 address: AccountAddress,
908 creation_number: u64,
909 start: Option<u64>,
910 limit: Option<u64>,
911 ) -> AptosResult<AptosResponse<Vec<serde_json::Value>>> {
912 let mut url = self.build_url(&format!("accounts/{address}/events/{creation_number}"));
913 {
914 let mut query = url.query_pairs_mut();
915 if let Some(start) = start {
916 query.append_pair("start", &start.to_string());
917 }
918 if let Some(limit) = limit {
919 query.append_pair("limit", &limit.to_string());
920 }
921 }
922 self.get_json(url).await
923 }
924
925 pub async fn get_events_by_event_handle(
932 &self,
933 address: AccountAddress,
934 event_handle_struct: &str,
935 field_name: &str,
936 start: Option<u64>,
937 limit: Option<u64>,
938 ) -> AptosResult<AptosResponse<Vec<serde_json::Value>>> {
939 let mut url = self.build_url(&format!(
940 "accounts/{}/events/{}/{}",
941 address,
942 urlencoding::encode(event_handle_struct),
943 field_name
944 ));
945
946 {
947 let mut query = url.query_pairs_mut();
948 if let Some(start) = start {
949 query.append_pair("start", &start.to_string());
950 }
951 if let Some(limit) = limit {
952 query.append_pair("limit", &limit.to_string());
953 }
954 }
955
956 self.get_json(url).await
957 }
958
959 pub async fn get_block_by_height(
968 &self,
969 height: u64,
970 with_transactions: bool,
971 ) -> AptosResult<AptosResponse<serde_json::Value>> {
972 let mut url = self.build_url(&format!("blocks/by_height/{height}"));
973 url.query_pairs_mut()
974 .append_pair("with_transactions", &with_transactions.to_string());
975 self.get_json(url).await
976 }
977
978 pub async fn get_block_by_version(
985 &self,
986 version: u64,
987 with_transactions: bool,
988 ) -> AptosResult<AptosResponse<serde_json::Value>> {
989 let mut url = self.build_url(&format!("blocks/by_version/{version}"));
990 url.query_pairs_mut()
991 .append_pair("with_transactions", &with_transactions.to_string());
992 self.get_json(url).await
993 }
994
995 fn build_url(&self, path: &str) -> Url {
998 let mut url = self.config.fullnode_url().clone();
999 if !path.is_empty() {
1000 let base_path = url.path();
1002 let needs_slash = !base_path.ends_with('/');
1003 let new_len = base_path.len() + path.len() + usize::from(needs_slash);
1004 let mut new_path = String::with_capacity(new_len);
1005 new_path.push_str(base_path);
1006 if needs_slash {
1007 new_path.push('/');
1008 }
1009 new_path.push_str(path);
1010 url.set_path(&new_path);
1011 }
1012 url
1013 }
1014
1015 async fn get_json<T: for<'de> serde::Deserialize<'de>>(
1016 &self,
1017 url: Url,
1018 ) -> AptosResult<AptosResponse<T>> {
1019 let client = self.client.clone();
1020 let url_clone = url.clone();
1021 let retry_config = self.retry_config.clone();
1022 let max_response_size = self.config.pool_config().max_response_size;
1023
1024 let executor = RetryExecutor::from_shared(retry_config);
1025 executor
1026 .execute(|| {
1027 let client = client.clone();
1028 let url = url_clone.clone();
1029 async move {
1030 let response = client
1031 .get(url)
1032 .header(ACCEPT, JSON_CONTENT_TYPE)
1033 .send()
1034 .await?;
1035
1036 Self::handle_response_static(response, max_response_size).await
1037 }
1038 })
1039 .await
1040 }
1041
1042 async fn post_json<B: serde::Serialize, T: for<'de> serde::Deserialize<'de>>(
1049 &self,
1050 url: Url,
1051 body: &B,
1052 ) -> AptosResult<AptosResponse<T>> {
1053 let body = serde_json::to_vec(body)?;
1054 let client = self.client.clone();
1055 let retry_config = self.retry_config.clone();
1056 let max_response_size = self.config.pool_config().max_response_size;
1057
1058 let executor = RetryExecutor::from_shared(retry_config);
1059 executor
1060 .execute(|| {
1061 let client = client.clone();
1062 let url = url.clone();
1063 let body = body.clone();
1064 async move {
1065 let response = client
1066 .post(url)
1067 .header(CONTENT_TYPE, JSON_CONTENT_TYPE)
1068 .header(ACCEPT, JSON_CONTENT_TYPE)
1069 .body(body)
1070 .send()
1071 .await?;
1072
1073 Self::handle_response_static(response, max_response_size).await
1074 }
1075 })
1076 .await
1077 }
1078
1079 fn truncate_error_body(body: String) -> String {
1085 if body.len() > MAX_ERROR_BODY_SIZE {
1086 let mut end = MAX_ERROR_BODY_SIZE;
1088 while end > 0 && !body.is_char_boundary(end) {
1089 end -= 1;
1090 }
1091 format!(
1092 "{}... [truncated, total: {} bytes]",
1093 &body[..end],
1094 body.len()
1095 )
1096 } else {
1097 body
1098 }
1099 }
1100
1101 async fn handle_response_static<T: for<'de> serde::Deserialize<'de>>(
1109 response: reqwest::Response,
1110 max_response_size: usize,
1111 ) -> AptosResult<AptosResponse<T>> {
1112 let status = response.status();
1113
1114 let ledger_version = response
1116 .headers()
1117 .get("x-aptos-ledger-version")
1118 .and_then(|v| v.to_str().ok())
1119 .and_then(|v| v.parse().ok());
1120 let ledger_timestamp = response
1121 .headers()
1122 .get("x-aptos-ledger-timestamp")
1123 .and_then(|v| v.to_str().ok())
1124 .and_then(|v| v.parse().ok());
1125 let epoch = response
1126 .headers()
1127 .get("x-aptos-epoch")
1128 .and_then(|v| v.to_str().ok())
1129 .and_then(|v| v.parse().ok());
1130 let block_height = response
1131 .headers()
1132 .get("x-aptos-block-height")
1133 .and_then(|v| v.to_str().ok())
1134 .and_then(|v| v.parse().ok());
1135 let oldest_ledger_version = response
1136 .headers()
1137 .get("x-aptos-oldest-ledger-version")
1138 .and_then(|v| v.to_str().ok())
1139 .and_then(|v| v.parse().ok());
1140 let cursor = response
1141 .headers()
1142 .get("x-aptos-cursor")
1143 .and_then(|v| v.to_str().ok())
1144 .map(ToString::to_string);
1145
1146 let retry_after_secs = response
1148 .headers()
1149 .get("retry-after")
1150 .and_then(|v| v.to_str().ok())
1151 .and_then(|v| v.parse().ok());
1152
1153 if status.is_success() {
1154 let bytes = crate::config::read_response_bounded(response, max_response_size).await?;
1157 let data: T = serde_json::from_slice(&bytes)?;
1158 Ok(AptosResponse {
1159 data,
1160 ledger_version,
1161 ledger_timestamp,
1162 epoch,
1163 block_height,
1164 oldest_ledger_version,
1165 cursor,
1166 })
1167 } else if status.as_u16() == 429 {
1168 Err(AptosError::RateLimited { retry_after_secs })
1171 } else {
1172 let error_bytes = crate::config::read_response_bounded(response, MAX_ERROR_BODY_SIZE)
1175 .await
1176 .ok();
1177 let error_text = error_bytes
1178 .and_then(|b| String::from_utf8(b).ok())
1179 .unwrap_or_default();
1180 let error_text = Self::truncate_error_body(error_text);
1181 let body: serde_json::Value = serde_json::from_str(&error_text).unwrap_or_default();
1182 let message = body
1183 .get("message")
1184 .and_then(|v| v.as_str())
1185 .unwrap_or("Unknown error")
1186 .to_string();
1187 let error_code = body
1188 .get("error_code")
1189 .and_then(|v| v.as_str())
1190 .map(ToString::to_string);
1191 let vm_error_code = body
1192 .get("vm_error_code")
1193 .and_then(serde_json::Value::as_u64);
1194
1195 Err(AptosError::api_with_details(
1196 status.as_u16(),
1197 message,
1198 error_code,
1199 vm_error_code,
1200 ))
1201 }
1202 }
1203
1204 #[allow(dead_code)]
1206 async fn handle_response<T: for<'de> serde::Deserialize<'de>>(
1207 &self,
1208 response: reqwest::Response,
1209 ) -> AptosResult<AptosResponse<T>> {
1210 let max_response_size = self.config.pool_config().max_response_size;
1211 Self::handle_response_static(response, max_response_size).await
1212 }
1213}
1214
1215fn append_start_limit(url: &mut Url, start: Option<&str>, limit: Option<u16>) {
1223 if start.is_none() && limit.is_none() {
1224 return;
1225 }
1226 let mut query = url.query_pairs_mut();
1227 if let Some(start) = start {
1228 query.append_pair("start", start);
1229 }
1230 if let Some(limit) = limit {
1231 query.append_pair("limit", &limit.to_string());
1232 }
1233}
1234
1235#[cfg(test)]
1236mod tests {
1237 use super::*;
1238 use crate::transaction::authenticator::{
1239 Ed25519PublicKey, Ed25519Signature, TransactionAuthenticator,
1240 };
1241 use crate::transaction::simulation::SimulateQueryOptions;
1242 use crate::transaction::types::{RawTransaction, SignedTransaction};
1243 use crate::types::ChainId;
1244 use wiremock::{
1245 Mock, MockServer, ResponseTemplate,
1246 matchers::{method, path, path_regex, query_param},
1247 };
1248
1249 #[test]
1250 fn test_build_url() {
1251 let client = FullnodeClient::new(AptosConfig::testnet()).unwrap();
1252 let url = client.build_url("accounts/0x1");
1253 assert!(url.as_str().contains("accounts/0x1"));
1254 }
1255
1256 fn create_mock_client(server: &MockServer) -> FullnodeClient {
1257 let url = format!("{}/v1", server.uri());
1259 let config = AptosConfig::custom(&url).unwrap().without_retry();
1260 FullnodeClient::new(config).unwrap()
1261 }
1262
1263 fn create_minimal_signed_transaction() -> SignedTransaction {
1265 use crate::transaction::payload::{EntryFunction, TransactionPayload};
1266
1267 let raw = RawTransaction::new(
1268 AccountAddress::ONE,
1269 0,
1270 TransactionPayload::EntryFunction(
1271 EntryFunction::apt_transfer(AccountAddress::ONE, 0).unwrap(),
1272 ),
1273 100_000,
1274 100,
1275 std::time::SystemTime::now()
1276 .duration_since(std::time::UNIX_EPOCH)
1277 .unwrap()
1278 .as_secs()
1279 .saturating_add(600),
1280 ChainId::testnet(),
1281 );
1282 let auth = TransactionAuthenticator::Ed25519 {
1283 public_key: Ed25519PublicKey([0u8; 32]),
1284 signature: Ed25519Signature([0u8; 64]),
1285 };
1286 SignedTransaction::new(raw, auth)
1287 }
1288
1289 fn simulate_response_json() -> serde_json::Value {
1290 serde_json::json!([{
1291 "success": true,
1292 "vm_status": "Executed successfully",
1293 "gas_used": "100",
1294 "max_gas_amount": "200000",
1295 "gas_unit_price": "100",
1296 "hash": "0x1",
1297 "changes": [],
1298 "events": []
1299 }])
1300 }
1301
1302 #[tokio::test]
1303 async fn test_get_ledger_info() {
1304 let server = MockServer::start().await;
1305
1306 Mock::given(method("GET"))
1307 .and(path("/v1"))
1308 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1309 "chain_id": 2,
1310 "epoch": "100",
1311 "ledger_version": "12345",
1312 "oldest_ledger_version": "0",
1313 "ledger_timestamp": "1000000",
1314 "node_role": "full_node",
1315 "oldest_block_height": "0",
1316 "block_height": "5000"
1317 })))
1318 .expect(1)
1319 .mount(&server)
1320 .await;
1321
1322 let client = create_mock_client(&server);
1323 let result = client.get_ledger_info().await.unwrap();
1324
1325 assert_eq!(result.data.chain_id, 2);
1326 assert_eq!(result.data.version().unwrap(), 12345);
1327 assert_eq!(result.data.height().unwrap(), 5000);
1328 }
1329
1330 #[tokio::test]
1331 async fn test_get_account() {
1332 let server = MockServer::start().await;
1333
1334 Mock::given(method("GET"))
1335 .and(path_regex(r"^/v1/accounts/0x[0-9a-f]+$"))
1336 .respond_with(
1337 ResponseTemplate::new(200)
1338 .set_body_json(serde_json::json!({
1339 "sequence_number": "42",
1340 "authentication_key": "0x0000000000000000000000000000000000000000000000000000000000000001"
1341 }))
1342 .insert_header("x-aptos-ledger-version", "12345"),
1343 )
1344 .expect(1)
1345 .mount(&server)
1346 .await;
1347
1348 let client = create_mock_client(&server);
1349 let result = client.get_account(AccountAddress::ONE).await.unwrap();
1350
1351 assert_eq!(result.data.sequence_number().unwrap(), 42);
1352 assert_eq!(result.ledger_version, Some(12345));
1353 }
1354
1355 #[tokio::test]
1356 async fn test_get_account_not_found() {
1357 let server = MockServer::start().await;
1358
1359 Mock::given(method("GET"))
1360 .and(path_regex(r"/v1/accounts/0x[0-9a-f]+"))
1361 .respond_with(ResponseTemplate::new(404).set_body_json(serde_json::json!({
1362 "message": "Account not found",
1363 "error_code": "account_not_found"
1364 })))
1365 .expect(1)
1366 .mount(&server)
1367 .await;
1368
1369 let client = create_mock_client(&server);
1370 let result = client.get_account(AccountAddress::ONE).await;
1371
1372 assert!(result.is_err());
1373 let err = result.unwrap_err();
1374 assert!(err.is_not_found());
1375 }
1376
1377 #[tokio::test]
1378 async fn test_get_account_resources() {
1379 let server = MockServer::start().await;
1380
1381 Mock::given(method("GET"))
1382 .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/resources"))
1383 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([
1384 {
1385 "type": "0x1::account::Account",
1386 "data": {"sequence_number": "10"}
1387 },
1388 {
1389 "type": "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>",
1390 "data": {"coin": {"value": "1000000"}}
1391 }
1392 ])))
1393 .expect(1)
1394 .mount(&server)
1395 .await;
1396
1397 let client = create_mock_client(&server);
1398 let result = client
1399 .get_account_resources(AccountAddress::ONE)
1400 .await
1401 .unwrap();
1402
1403 assert_eq!(result.data.len(), 2);
1404 assert!(result.data[0].typ.contains("Account"));
1405 }
1406
1407 #[tokio::test]
1408 async fn test_get_account_resource() {
1409 let server = MockServer::start().await;
1410
1411 Mock::given(method("GET"))
1412 .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/resource/.*"))
1413 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1414 "type": "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>",
1415 "data": {"coin": {"value": "5000000"}}
1416 })))
1417 .expect(1)
1418 .mount(&server)
1419 .await;
1420
1421 let client = create_mock_client(&server);
1422 let result = client
1423 .get_account_resource(
1424 AccountAddress::ONE,
1425 "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>",
1426 )
1427 .await
1428 .unwrap();
1429
1430 assert!(result.data.typ.contains("CoinStore"));
1431 }
1432
1433 #[tokio::test]
1434 async fn test_get_account_modules() {
1435 let server = MockServer::start().await;
1436
1437 Mock::given(method("GET"))
1438 .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/modules"))
1439 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([
1440 {
1441 "bytecode": "0xabc123",
1442 "abi": {
1443 "address": "0x1",
1444 "name": "coin",
1445 "exposed_functions": [],
1446 "structs": []
1447 }
1448 }
1449 ])))
1450 .expect(1)
1451 .mount(&server)
1452 .await;
1453
1454 let client = create_mock_client(&server);
1455 let result = client
1456 .get_account_modules(AccountAddress::ONE)
1457 .await
1458 .unwrap();
1459
1460 assert_eq!(result.data.len(), 1);
1461 assert!(result.data[0].abi.is_some());
1462 }
1463
1464 #[tokio::test]
1465 async fn test_get_account_resources_paginated_sends_start_and_limit() {
1466 let server = MockServer::start().await;
1467
1468 Mock::given(method("GET"))
1470 .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/resources"))
1471 .and(query_param("start", "42"))
1472 .and(query_param("limit", "9"))
1473 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([])))
1474 .expect(1)
1475 .mount(&server)
1476 .await;
1477
1478 let client = create_mock_client(&server);
1479 let result = client
1480 .get_account_resources_paginated(AccountAddress::ONE, Some("42"), Some(9))
1481 .await
1482 .unwrap();
1483 assert_eq!(result.data.len(), 0);
1484 }
1485
1486 #[tokio::test]
1487 async fn test_get_account_resources_paginated_round_trips_opaque_cursor() {
1488 let server = MockServer::start().await;
1492 let opaque = "0x0a1b2c3d_state_key_token";
1493 Mock::given(method("GET"))
1494 .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/resources"))
1495 .and(query_param("start", opaque))
1496 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([])))
1497 .expect(1)
1498 .mount(&server)
1499 .await;
1500
1501 let client = create_mock_client(&server);
1502 let resp = client
1503 .get_account_resources_paginated(AccountAddress::ONE, Some(opaque), None)
1504 .await
1505 .unwrap();
1506 assert!(
1507 resp.data.is_empty(),
1508 "mock returned an empty page, got {} resources",
1509 resp.data.len()
1510 );
1511 }
1512
1513 #[tokio::test]
1514 async fn test_get_account_resources_no_pagination_omits_query() {
1515 let server = MockServer::start().await;
1516
1517 Mock::given(method("GET"))
1520 .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/resources$"))
1521 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([])))
1522 .expect(1)
1523 .mount(&server)
1524 .await;
1525
1526 let client = create_mock_client(&server);
1527 let resp = client
1528 .get_account_resources(AccountAddress::ONE)
1529 .await
1530 .unwrap();
1531 assert!(
1532 resp.data.is_empty(),
1533 "mock returned an empty page, got {} resources",
1534 resp.data.len()
1535 );
1536 }
1537
1538 #[tokio::test]
1539 async fn test_get_account_resources_paginated_sends_start_only() {
1540 let server = MockServer::start().await;
1543 Mock::given(method("GET"))
1544 .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/resources"))
1545 .and(query_param("start", "1234"))
1546 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([])))
1547 .expect(1)
1548 .mount(&server)
1549 .await;
1550
1551 let client = create_mock_client(&server);
1552 let resp = client
1553 .get_account_resources_paginated(AccountAddress::ONE, Some("1234"), None)
1554 .await
1555 .unwrap();
1556 assert!(
1557 resp.data.is_empty(),
1558 "mock returned an empty page, got {} resources",
1559 resp.data.len()
1560 );
1561 }
1562
1563 #[tokio::test]
1564 async fn test_get_account_modules_paginated_sends_start_and_limit() {
1565 let server = MockServer::start().await;
1566 Mock::given(method("GET"))
1567 .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/modules"))
1568 .and(query_param("start", "7"))
1569 .and(query_param("limit", "100"))
1570 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([])))
1571 .expect(1)
1572 .mount(&server)
1573 .await;
1574
1575 let client = create_mock_client(&server);
1576 let resp = client
1577 .get_account_modules_paginated(AccountAddress::ONE, Some("7"), Some(100))
1578 .await
1579 .unwrap();
1580 assert!(
1581 resp.data.is_empty(),
1582 "mock returned an empty page, got {} modules",
1583 resp.data.len()
1584 );
1585 }
1586
1587 #[tokio::test]
1588 async fn test_get_account_modules_no_pagination_omits_query() {
1589 let server = MockServer::start().await;
1592 Mock::given(method("GET"))
1593 .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/modules$"))
1594 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([])))
1595 .expect(1)
1596 .mount(&server)
1597 .await;
1598
1599 let client = create_mock_client(&server);
1600 let resp = client
1601 .get_account_modules(AccountAddress::ONE)
1602 .await
1603 .unwrap();
1604 assert!(
1605 resp.data.is_empty(),
1606 "mock returned an empty page, got {} modules",
1607 resp.data.len()
1608 );
1609 }
1610
1611 #[tokio::test]
1612 async fn test_get_account_modules_paginated_sends_limit_only() {
1613 let server = MockServer::start().await;
1614
1615 Mock::given(method("GET"))
1618 .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/modules"))
1619 .and(query_param("limit", "25"))
1620 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([])))
1621 .expect(1)
1622 .mount(&server)
1623 .await;
1624
1625 let client = create_mock_client(&server);
1626 let resp = client
1627 .get_account_modules_paginated(AccountAddress::ONE, None, Some(25))
1628 .await
1629 .unwrap();
1630 assert!(
1631 resp.data.is_empty(),
1632 "mock returned an empty page, got {} modules",
1633 resp.data.len()
1634 );
1635 }
1636
1637 #[tokio::test]
1638 async fn test_estimate_gas_price() {
1639 let server = MockServer::start().await;
1640
1641 Mock::given(method("GET"))
1642 .and(path("/v1/estimate_gas_price"))
1643 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1644 "deprioritized_gas_estimate": 50,
1645 "gas_estimate": 100,
1646 "prioritized_gas_estimate": 150
1647 })))
1648 .expect(1)
1649 .mount(&server)
1650 .await;
1651
1652 let client = create_mock_client(&server);
1653 let result = client.estimate_gas_price().await.unwrap();
1654
1655 assert_eq!(result.data.gas_estimate, 100);
1656 assert_eq!(result.data.low(), 50);
1657 assert_eq!(result.data.high(), 150);
1658 }
1659
1660 #[tokio::test]
1661 async fn test_get_transaction_by_hash() {
1662 let server = MockServer::start().await;
1663
1664 Mock::given(method("GET"))
1665 .and(path_regex(r"/v1/transactions/by_hash/0x[0-9a-f]+"))
1666 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1667 "version": "12345",
1668 "hash": "0x0000000000000000000000000000000000000000000000000000000000000001",
1669 "success": true,
1670 "vm_status": "Executed successfully"
1671 })))
1672 .expect(1)
1673 .mount(&server)
1674 .await;
1675
1676 let client = create_mock_client(&server);
1677 let hash = HashValue::from_hex(
1678 "0x0000000000000000000000000000000000000000000000000000000000000001",
1679 )
1680 .unwrap();
1681 let result = client.get_transaction_by_hash(&hash).await.unwrap();
1682
1683 assert!(
1684 result
1685 .data
1686 .get("success")
1687 .and_then(serde_json::Value::as_bool)
1688 .unwrap()
1689 );
1690 }
1691
1692 #[tokio::test]
1693 async fn test_wait_for_transaction_success() {
1694 let server = MockServer::start().await;
1695
1696 Mock::given(method("GET"))
1697 .and(path_regex(r"/v1/transactions/by_hash/0x[0-9a-f]+"))
1698 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1699 "type": "user_transaction",
1700 "version": "12345",
1701 "hash": "0x0000000000000000000000000000000000000000000000000000000000000001",
1702 "success": true,
1703 "vm_status": "Executed successfully"
1704 })))
1705 .expect(1..)
1706 .mount(&server)
1707 .await;
1708
1709 let client = create_mock_client(&server);
1710 let hash = HashValue::from_hex(
1711 "0x0000000000000000000000000000000000000000000000000000000000000001",
1712 )
1713 .unwrap();
1714 let result = client
1715 .wait_for_transaction(&hash, Some(Duration::from_secs(5)))
1716 .await
1717 .unwrap();
1718
1719 assert!(
1720 result
1721 .data
1722 .get("success")
1723 .and_then(serde_json::Value::as_bool)
1724 .unwrap()
1725 );
1726 }
1727
1728 #[tokio::test]
1729 async fn test_server_error_retryable() {
1730 let server = MockServer::start().await;
1731
1732 Mock::given(method("GET"))
1733 .and(path("/v1"))
1734 .respond_with(ResponseTemplate::new(503).set_body_json(serde_json::json!({
1735 "message": "Service temporarily unavailable"
1736 })))
1737 .expect(1)
1738 .mount(&server)
1739 .await;
1740
1741 let url = format!("{}/v1", server.uri());
1742 let config = AptosConfig::custom(&url).unwrap().without_retry();
1743 let client = FullnodeClient::new(config).unwrap();
1744 let result = client.get_ledger_info().await;
1745
1746 assert!(result.is_err());
1747 assert!(result.unwrap_err().is_retryable());
1748 }
1749
1750 #[tokio::test]
1751 async fn test_rate_limited() {
1752 let server = MockServer::start().await;
1753
1754 Mock::given(method("GET"))
1755 .and(path("/v1"))
1756 .respond_with(
1757 ResponseTemplate::new(429)
1758 .set_body_json(serde_json::json!({
1759 "message": "Rate limited"
1760 }))
1761 .insert_header("retry-after", "30"),
1762 )
1763 .expect(1)
1764 .mount(&server)
1765 .await;
1766
1767 let url = format!("{}/v1", server.uri());
1768 let config = AptosConfig::custom(&url).unwrap().without_retry();
1769 let client = FullnodeClient::new(config).unwrap();
1770 let result = client.get_ledger_info().await;
1771
1772 assert!(result.is_err());
1773 assert!(result.unwrap_err().is_retryable());
1774 }
1775
1776 #[tokio::test]
1777 async fn test_get_block_by_height() {
1778 let server = MockServer::start().await;
1779
1780 Mock::given(method("GET"))
1781 .and(path_regex(r"/v1/blocks/by_height/\d+"))
1782 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1783 "block_height": "1000",
1784 "block_hash": "0xabc",
1785 "block_timestamp": "1234567890",
1786 "first_version": "100",
1787 "last_version": "200"
1788 })))
1789 .expect(1)
1790 .mount(&server)
1791 .await;
1792
1793 let client = create_mock_client(&server);
1794 let result = client.get_block_by_height(1000, false).await.unwrap();
1795
1796 assert!(result.data.get("block_height").is_some());
1797 }
1798
1799 #[tokio::test]
1800 async fn test_view() {
1801 let server = MockServer::start().await;
1802
1803 Mock::given(method("POST"))
1804 .and(path("/v1/view"))
1805 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!(["1000000"])))
1806 .expect(1)
1807 .mount(&server)
1808 .await;
1809
1810 let client = create_mock_client(&server);
1811 let result: AptosResponse<Vec<serde_json::Value>> = client
1812 .view(
1813 "0x1::coin::balance",
1814 vec!["0x1::aptos_coin::AptosCoin".to_string()],
1815 vec![serde_json::json!("0x1")],
1816 )
1817 .await
1818 .unwrap();
1819
1820 assert_eq!(result.data.len(), 1);
1821 }
1822
1823 #[tokio::test]
1824 async fn test_view_bcs_args_posts_bcs_request() {
1825 let server = MockServer::start().await;
1826
1827 let args = vec![aptos_bcs::to_bytes(&AccountAddress::ONE).unwrap()];
1832 let expected_body = aptos_bcs::to_bytes(
1833 &crate::transaction::EntryFunction::from_function_id(
1834 "0x1::coin::balance",
1835 vec![],
1836 args.clone(),
1837 )
1838 .unwrap(),
1839 )
1840 .unwrap();
1841
1842 Mock::given(method("POST"))
1843 .and(path("/v1/view"))
1844 .and(wiremock::matchers::header(
1845 "content-type",
1846 "application/x.aptos.view_function+bcs",
1847 ))
1848 .and(wiremock::matchers::body_bytes(expected_body))
1849 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!(["1000000"])))
1850 .expect(1)
1851 .mount(&server)
1852 .await;
1853
1854 let client = create_mock_client(&server);
1855 let result = client
1856 .view_bcs_args("0x1::coin::balance", vec![], args)
1857 .await
1858 .unwrap()
1859 .into_inner();
1860
1861 assert_eq!(result.len(), 1);
1862 assert_eq!(result[0].as_str().unwrap(), "1000000");
1863 }
1864
1865 #[tokio::test]
1866 async fn test_simulate_transaction_with_estimate_gas_unit_price() {
1867 let server = MockServer::start().await;
1868
1869 Mock::given(method("POST"))
1870 .and(path("/v1/transactions/simulate"))
1871 .and(|req: &wiremock::Request| {
1872 req.url
1873 .query()
1874 .is_some_and(|q| q.contains("estimate_gas_unit_price=true"))
1875 })
1876 .respond_with(ResponseTemplate::new(200).set_body_json(simulate_response_json()))
1877 .expect(1)
1878 .mount(&server)
1879 .await;
1880
1881 let client = create_mock_client(&server);
1882 let signed = create_minimal_signed_transaction();
1883 let opts = SimulateQueryOptions::new().estimate_gas_unit_price(true);
1884 let result = client
1885 .simulate_transaction_with_options(&signed, opts)
1886 .await
1887 .unwrap();
1888 assert!(!result.data.is_empty());
1889 }
1890
1891 #[tokio::test]
1892 async fn test_simulate_transaction_with_estimate_max_gas_amount() {
1893 let server = MockServer::start().await;
1894
1895 Mock::given(method("POST"))
1896 .and(path("/v1/transactions/simulate"))
1897 .and(|req: &wiremock::Request| {
1898 req.url
1899 .query()
1900 .is_some_and(|q| q.contains("estimate_max_gas_amount=true"))
1901 })
1902 .respond_with(ResponseTemplate::new(200).set_body_json(simulate_response_json()))
1903 .expect(1)
1904 .mount(&server)
1905 .await;
1906
1907 let client = create_mock_client(&server);
1908 let signed = create_minimal_signed_transaction();
1909 let opts = SimulateQueryOptions::new().estimate_max_gas_amount(true);
1910 let result = client
1911 .simulate_transaction_with_options(&signed, opts)
1912 .await
1913 .unwrap();
1914 assert!(!result.data.is_empty());
1915 }
1916
1917 #[tokio::test]
1918 async fn test_simulate_transaction_with_estimate_prioritized_gas_unit_price() {
1919 let server = MockServer::start().await;
1920
1921 Mock::given(method("POST"))
1922 .and(path("/v1/transactions/simulate"))
1923 .and(|req: &wiremock::Request| {
1924 req.url
1925 .query()
1926 .is_some_and(|q| q.contains("estimate_prioritized_gas_unit_price=true"))
1927 })
1928 .respond_with(ResponseTemplate::new(200).set_body_json(simulate_response_json()))
1929 .expect(1)
1930 .mount(&server)
1931 .await;
1932
1933 let client = create_mock_client(&server);
1934 let signed = create_minimal_signed_transaction();
1935 let opts = SimulateQueryOptions::new().estimate_prioritized_gas_unit_price(true);
1936 let result = client
1937 .simulate_transaction_with_options(&signed, opts)
1938 .await
1939 .unwrap();
1940 assert!(!result.data.is_empty());
1941 }
1942
1943 #[tokio::test]
1944 async fn test_simulate_transaction_with_all_options() {
1945 let server = MockServer::start().await;
1946
1947 Mock::given(method("POST"))
1948 .and(path("/v1/transactions/simulate"))
1949 .and(|req: &wiremock::Request| {
1950 req.url.query().is_some_and(|q| {
1951 q.contains("estimate_gas_unit_price=true")
1952 && q.contains("estimate_max_gas_amount=true")
1953 && q.contains("estimate_prioritized_gas_unit_price=true")
1954 })
1955 })
1956 .respond_with(ResponseTemplate::new(200).set_body_json(simulate_response_json()))
1957 .expect(1)
1958 .mount(&server)
1959 .await;
1960
1961 let client = create_mock_client(&server);
1962 let signed = create_minimal_signed_transaction();
1963 let opts = SimulateQueryOptions::new()
1964 .estimate_gas_unit_price(true)
1965 .estimate_max_gas_amount(true)
1966 .estimate_prioritized_gas_unit_price(true);
1967 let result = client
1968 .simulate_transaction_with_options(&signed, opts)
1969 .await
1970 .unwrap();
1971 assert!(!result.data.is_empty());
1972 }
1973
1974 #[tokio::test]
1975 async fn test_simulate_transaction_without_options() {
1976 let server = MockServer::start().await;
1977
1978 Mock::given(method("POST"))
1980 .and(path("/v1/transactions/simulate"))
1981 .and(|req: &wiremock::Request| {
1982 req.url.query().is_none_or(|q| {
1984 !q.contains("estimate_gas_unit_price=")
1985 && !q.contains("estimate_max_gas_amount=")
1986 && !q.contains("estimate_prioritized_gas_unit_price=")
1987 })
1988 })
1989 .respond_with(ResponseTemplate::new(200).set_body_json(simulate_response_json()))
1990 .expect(1)
1991 .mount(&server)
1992 .await;
1993
1994 let client = create_mock_client(&server);
1995 let signed = create_minimal_signed_transaction();
1996 let result = client.simulate_transaction(&signed).await.unwrap();
1997 assert!(!result.data.is_empty());
1998 }
1999
2000 #[tokio::test]
2001 async fn test_get_table_item() {
2002 let server = MockServer::start().await;
2003
2004 Mock::given(method("POST"))
2008 .and(path_regex(r"^/v1/tables/0x[0-9a-f]+/item$"))
2009 .and(wiremock::matchers::body_json(serde_json::json!({
2010 "key_type": "address",
2011 "value_type": "u64",
2012 "key": "0x1",
2013 })))
2014 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!("42")))
2015 .expect(1)
2016 .mount(&server)
2017 .await;
2018
2019 let client = create_mock_client(&server);
2020 let result = client
2021 .get_table_item(
2022 "0x0000000000000000000000000000000000000000000000000000000000000abc",
2023 "address",
2024 "u64",
2025 serde_json::json!("0x1"),
2026 )
2027 .await
2028 .unwrap();
2029
2030 assert_eq!(result.data, serde_json::json!("42"));
2031 }
2032
2033 #[tokio::test]
2034 async fn test_get_table_item_missing_key_is_404() {
2035 let server = MockServer::start().await;
2036
2037 Mock::given(method("POST"))
2038 .and(path_regex(r"^/v1/tables/0x[0-9a-f]+/item$"))
2039 .respond_with(ResponseTemplate::new(404).set_body_json(serde_json::json!({
2040 "message": "Table item not found",
2041 "error_code": "table_item_not_found"
2042 })))
2043 .expect(1)
2044 .mount(&server)
2045 .await;
2046
2047 let client = create_mock_client(&server);
2048 let err = client
2049 .get_table_item("0xabc", "address", "u64", serde_json::json!("0x2"))
2050 .await
2051 .unwrap_err();
2052
2053 assert!(matches!(
2054 err,
2055 AptosError::Api {
2056 status_code: 404,
2057 ..
2058 }
2059 ));
2060 }
2061
2062 #[tokio::test]
2063 async fn test_get_transaction_by_version() {
2064 let server = MockServer::start().await;
2065
2066 Mock::given(method("GET"))
2067 .and(path("/v1/transactions/by_version/100"))
2068 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2069 "version": "100",
2070 "hash": "0xdead",
2071 "success": true
2072 })))
2073 .expect(1)
2074 .mount(&server)
2075 .await;
2076
2077 let client = create_mock_client(&server);
2078 let result = client.get_transaction_by_version(100).await.unwrap();
2079
2080 assert_eq!(result.data.get("version").unwrap(), "100");
2081 }
2082
2083 #[tokio::test]
2084 async fn test_get_transactions_with_pagination() {
2085 let server = MockServer::start().await;
2086
2087 Mock::given(method("GET"))
2088 .and(path("/v1/transactions"))
2089 .and(query_param("start", "10"))
2090 .and(query_param("limit", "2"))
2091 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([
2092 {"version": "10"},
2093 {"version": "11"}
2094 ])))
2095 .expect(1)
2096 .mount(&server)
2097 .await;
2098
2099 let client = create_mock_client(&server);
2100 let result = client.get_transactions(Some(10), Some(2)).await.unwrap();
2101
2102 assert_eq!(result.data.len(), 2);
2103 }
2104
2105 #[tokio::test]
2106 async fn test_get_account_transactions() {
2107 let server = MockServer::start().await;
2108
2109 Mock::given(method("GET"))
2110 .and(path_regex(r"^/v1/accounts/0x[0-9a-f]+/transactions$"))
2111 .and(query_param("start", "0"))
2112 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([
2113 {"version": "5", "sender": "0x1"}
2114 ])))
2115 .expect(1)
2116 .mount(&server)
2117 .await;
2118
2119 let client = create_mock_client(&server);
2120 let result = client
2121 .get_account_transactions(AccountAddress::ONE, Some(0), None)
2122 .await
2123 .unwrap();
2124
2125 assert_eq!(result.data.len(), 1);
2126 }
2127
2128 #[tokio::test]
2129 async fn test_get_events_by_creation_number() {
2130 let server = MockServer::start().await;
2131
2132 Mock::given(method("GET"))
2133 .and(path_regex(r"^/v1/accounts/0x[0-9a-f]+/events/7$"))
2134 .and(query_param("limit", "25"))
2135 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([
2136 {"sequence_number": "0", "type": "0x1::coin::DepositEvent"}
2137 ])))
2138 .expect(1)
2139 .mount(&server)
2140 .await;
2141
2142 let client = create_mock_client(&server);
2143 let result = client
2144 .get_events_by_creation_number(AccountAddress::ONE, 7, None, Some(25))
2145 .await
2146 .unwrap();
2147
2148 assert_eq!(result.data.len(), 1);
2149 }
2150
2151 #[test]
2154 fn test_base_url_and_config_accessors() {
2155 let config = AptosConfig::testnet();
2156 let expected = config.fullnode_url().clone();
2157 let client = FullnodeClient::new(config).unwrap();
2158
2159 assert_eq!(client.base_url(), &expected);
2161 assert_eq!(client.config().fullnode_url(), &expected);
2163 }
2164
2165 #[test]
2166 fn test_retry_config_accessor() {
2167 let config = AptosConfig::testnet().with_max_retries(7);
2169 let client = FullnodeClient::new(config).unwrap();
2170 assert_eq!(client.retry_config().max_retries, 7);
2171
2172 let client = FullnodeClient::new(AptosConfig::testnet().without_retry()).unwrap();
2174 assert_eq!(client.retry_config().max_retries, 0);
2175 }
2176
2177 #[tokio::test]
2180 async fn test_get_sequence_number() {
2181 let server = MockServer::start().await;
2182
2183 Mock::given(method("GET"))
2184 .and(path_regex(r"^/v1/accounts/0x[0-9a-f]+$"))
2185 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2186 "sequence_number": "99",
2187 "authentication_key": "0x0000000000000000000000000000000000000000000000000000000000000001"
2188 })))
2189 .expect(1)
2190 .mount(&server)
2191 .await;
2192
2193 let client = create_mock_client(&server);
2194 let seq = client
2195 .get_sequence_number(AccountAddress::ONE)
2196 .await
2197 .unwrap();
2198 assert_eq!(seq, 99);
2199 }
2200
2201 #[tokio::test]
2202 async fn test_get_sequence_number_unparseable_is_internal_error() {
2203 let server = MockServer::start().await;
2204
2205 Mock::given(method("GET"))
2208 .and(path_regex(r"^/v1/accounts/0x[0-9a-f]+$"))
2209 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2210 "sequence_number": "not-a-number",
2211 "authentication_key": "0x0000000000000000000000000000000000000000000000000000000000000001"
2212 })))
2213 .expect(1)
2214 .mount(&server)
2215 .await;
2216
2217 let client = create_mock_client(&server);
2218 let err = client
2219 .get_sequence_number(AccountAddress::ONE)
2220 .await
2221 .unwrap_err();
2222 assert!(matches!(err, AptosError::Internal(_)));
2223 }
2224
2225 #[tokio::test]
2228 async fn test_get_account_module() {
2229 let server = MockServer::start().await;
2230
2231 Mock::given(method("GET"))
2232 .and(path_regex(r"^/v1/accounts/0x[0-9a-f]+/module/coin$"))
2233 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2234 "bytecode": "0xdeadbeef",
2235 "abi": {
2236 "address": "0x1",
2237 "name": "coin",
2238 "exposed_functions": [],
2239 "structs": []
2240 }
2241 })))
2242 .expect(1)
2243 .mount(&server)
2244 .await;
2245
2246 let client = create_mock_client(&server);
2247 let result = client
2248 .get_account_module(AccountAddress::ONE, "coin")
2249 .await
2250 .unwrap();
2251
2252 assert_eq!(result.data.bytecode, "0xdeadbeef");
2253 assert!(result.data.abi.is_some());
2254 }
2255
2256 #[tokio::test]
2257 async fn test_get_account_module_not_found() {
2258 let server = MockServer::start().await;
2259
2260 Mock::given(method("GET"))
2261 .and(path_regex(r"^/v1/accounts/0x[0-9a-f]+/module/.*$"))
2262 .respond_with(ResponseTemplate::new(404).set_body_json(serde_json::json!({
2263 "message": "Module not found",
2264 "error_code": "module_not_found"
2265 })))
2266 .expect(1)
2267 .mount(&server)
2268 .await;
2269
2270 let client = create_mock_client(&server);
2271 let err = client
2272 .get_account_module(AccountAddress::ONE, "missing")
2273 .await
2274 .unwrap_err();
2275 assert!(err.is_not_found());
2276 }
2277
2278 #[tokio::test]
2281 async fn test_get_account_balance() {
2282 let server = MockServer::start().await;
2283
2284 Mock::given(method("POST"))
2287 .and(path("/v1/view"))
2288 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!(["1234567"])))
2289 .expect(1)
2290 .mount(&server)
2291 .await;
2292
2293 let client = create_mock_client(&server);
2294 let balance = client
2295 .get_account_balance(AccountAddress::ONE)
2296 .await
2297 .unwrap();
2298 assert_eq!(balance, 1_234_567);
2299 }
2300
2301 #[tokio::test]
2302 async fn test_get_account_balance_unparseable_is_internal_error() {
2303 let server = MockServer::start().await;
2304
2305 Mock::given(method("POST"))
2307 .and(path("/v1/view"))
2308 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([])))
2309 .expect(1)
2310 .mount(&server)
2311 .await;
2312
2313 let client = create_mock_client(&server);
2314 let err = client
2315 .get_account_balance(AccountAddress::ONE)
2316 .await
2317 .unwrap_err();
2318 assert!(matches!(err, AptosError::Internal(_)));
2319 }
2320
2321 fn pending_transaction_json() -> serde_json::Value {
2324 serde_json::json!({
2325 "hash": "0x0000000000000000000000000000000000000000000000000000000000000001",
2326 "sender": "0x1",
2327 "sequence_number": "0",
2328 "max_gas_amount": "100000",
2329 "gas_unit_price": "100",
2330 "expiration_timestamp_secs": "1000000"
2331 })
2332 }
2333
2334 #[tokio::test]
2335 async fn test_submit_transaction_success() {
2336 let server = MockServer::start().await;
2337
2338 Mock::given(method("POST"))
2339 .and(path("/v1/transactions"))
2340 .and(wiremock::matchers::header(
2341 "content-type",
2342 "application/x.aptos.signed_transaction+bcs",
2343 ))
2344 .respond_with(ResponseTemplate::new(202).set_body_json(pending_transaction_json()))
2345 .expect(1)
2346 .mount(&server)
2347 .await;
2348
2349 let client = create_mock_client(&server);
2350 let signed = create_minimal_signed_transaction();
2351 let result = client.submit_transaction(&signed).await.unwrap();
2352
2353 assert_eq!(result.data.sequence_number, "0");
2354 assert_eq!(result.data.sender(), "0x1");
2355 }
2356
2357 #[tokio::test]
2358 async fn test_submit_transaction_rejected_is_api_error() {
2359 let server = MockServer::start().await;
2360
2361 Mock::given(method("POST"))
2364 .and(path("/v1/transactions"))
2365 .respond_with(ResponseTemplate::new(400).set_body_json(serde_json::json!({
2366 "message": "Invalid transaction",
2367 "error_code": "invalid_transaction_update"
2368 })))
2369 .expect(1)
2370 .mount(&server)
2371 .await;
2372
2373 let client = create_mock_client(&server);
2374 let signed = create_minimal_signed_transaction();
2375 let err = client.submit_transaction(&signed).await.unwrap_err();
2376
2377 match err {
2378 AptosError::Api {
2379 status_code,
2380 error_code,
2381 ..
2382 } => {
2383 assert_eq!(status_code, 400);
2384 assert_eq!(error_code.as_deref(), Some("invalid_transaction_update"));
2385 }
2386 other => panic!("expected Api error, got {other:?}"),
2387 }
2388 assert!(
2390 !AptosError::api(400, "x").is_retryable(),
2391 "sanity: 4xx not retryable"
2392 );
2393 }
2394
2395 #[tokio::test]
2398 async fn test_submit_and_wait_success() {
2399 let server = MockServer::start().await;
2400
2401 Mock::given(method("POST"))
2403 .and(path("/v1/transactions"))
2404 .respond_with(ResponseTemplate::new(202).set_body_json(pending_transaction_json()))
2405 .expect(1)
2406 .mount(&server)
2407 .await;
2408
2409 Mock::given(method("GET"))
2410 .and(path_regex(r"^/v1/transactions/by_hash/0x[0-9a-f]+$"))
2411 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2412 "type": "user_transaction",
2413 "version": "555",
2414 "hash": "0x0000000000000000000000000000000000000000000000000000000000000001",
2415 "success": true,
2416 "vm_status": "Executed successfully"
2417 })))
2418 .expect(1..)
2419 .mount(&server)
2420 .await;
2421
2422 let client = create_mock_client(&server);
2423 let signed = create_minimal_signed_transaction();
2424 let result = client
2425 .submit_and_wait(&signed, Some(Duration::from_secs(5)))
2426 .await
2427 .unwrap();
2428
2429 assert_eq!(result.data.get("version").unwrap(), "555");
2430 }
2431
2432 #[tokio::test]
2435 async fn test_wait_for_transaction_execution_failed() {
2436 let server = MockServer::start().await;
2437
2438 Mock::given(method("GET"))
2441 .and(path_regex(r"^/v1/transactions/by_hash/0x[0-9a-f]+$"))
2442 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2443 "type": "user_transaction",
2444 "version": "12345",
2445 "hash": "0x0000000000000000000000000000000000000000000000000000000000000001",
2446 "success": false,
2447 "vm_status": "Move abort: 0x1"
2448 })))
2449 .expect(1..)
2450 .mount(&server)
2451 .await;
2452
2453 let client = create_mock_client(&server);
2454 let hash = HashValue::from_hex(
2455 "0x0000000000000000000000000000000000000000000000000000000000000001",
2456 )
2457 .unwrap();
2458 let err = client
2459 .wait_for_transaction(&hash, Some(Duration::from_secs(5)))
2460 .await
2461 .unwrap_err();
2462
2463 match err {
2464 AptosError::ExecutionFailed { vm_status } => {
2465 assert_eq!(vm_status, "Move abort: 0x1");
2466 }
2467 other => panic!("expected ExecutionFailed, got {other:?}"),
2468 }
2469 }
2470
2471 #[tokio::test]
2472 async fn test_wait_for_transaction_times_out() {
2473 let server = MockServer::start().await;
2474
2475 Mock::given(method("GET"))
2478 .and(path_regex(r"^/v1/transactions/by_hash/0x[0-9a-f]+$"))
2479 .respond_with(ResponseTemplate::new(404).set_body_json(serde_json::json!({
2480 "message": "Transaction not found",
2481 "error_code": "transaction_not_found"
2482 })))
2483 .expect(1..)
2484 .mount(&server)
2485 .await;
2486
2487 let client = create_mock_client(&server);
2488 let hash = HashValue::from_hex(
2489 "0x0000000000000000000000000000000000000000000000000000000000000002",
2490 )
2491 .unwrap();
2492 let err = client
2493 .wait_for_transaction(&hash, Some(Duration::from_millis(1)))
2494 .await
2495 .unwrap_err();
2496
2497 assert!(err.is_timeout());
2498 match err {
2499 AptosError::TransactionTimeout { timeout_secs, .. } => {
2500 assert_eq!(timeout_secs, 0);
2501 }
2502 other => panic!("expected TransactionTimeout, got {other:?}"),
2503 }
2504 }
2505
2506 #[tokio::test]
2507 async fn test_wait_for_transaction_propagates_non_404_error() {
2508 let server = MockServer::start().await;
2509
2510 Mock::given(method("GET"))
2513 .and(path_regex(r"^/v1/transactions/by_hash/0x[0-9a-f]+$"))
2514 .respond_with(ResponseTemplate::new(500).set_body_json(serde_json::json!({
2515 "message": "boom"
2516 })))
2517 .expect(1)
2518 .mount(&server)
2519 .await;
2520
2521 let client = create_mock_client(&server);
2522 let hash = HashValue::from_hex(
2523 "0x0000000000000000000000000000000000000000000000000000000000000003",
2524 )
2525 .unwrap();
2526 let err = client
2527 .wait_for_transaction(&hash, Some(Duration::from_secs(5)))
2528 .await
2529 .unwrap_err();
2530
2531 assert!(matches!(
2532 err,
2533 AptosError::Api {
2534 status_code: 500,
2535 ..
2536 }
2537 ));
2538 }
2539
2540 #[tokio::test]
2543 async fn test_view_bcs_returns_raw_bytes() {
2544 let server = MockServer::start().await;
2545
2546 let raw = vec![1u8, 2, 3, 4, 5];
2549 Mock::given(method("POST"))
2550 .and(path("/v1/view"))
2551 .and(wiremock::matchers::header("accept", "application/x-bcs"))
2552 .respond_with(ResponseTemplate::new(200).set_body_bytes(raw.clone()))
2553 .expect(1)
2554 .mount(&server)
2555 .await;
2556
2557 let client = create_mock_client(&server);
2558 let result = client
2559 .view_bcs(
2560 "0x1::coin::balance",
2561 vec!["0x1::aptos_coin::AptosCoin".to_string()],
2562 vec![aptos_bcs::to_bytes(&AccountAddress::ONE).unwrap()],
2563 )
2564 .await
2565 .unwrap();
2566
2567 assert_eq!(result.data, raw);
2568 }
2569
2570 #[tokio::test]
2571 async fn test_view_bcs_error_status_is_api_error() {
2572 let server = MockServer::start().await;
2573
2574 Mock::given(method("POST"))
2577 .and(path("/v1/view"))
2578 .respond_with(ResponseTemplate::new(400).set_body_string("bad view request"))
2579 .expect(1)
2580 .mount(&server)
2581 .await;
2582
2583 let client = create_mock_client(&server);
2584 let err = client
2585 .view_bcs("0x1::coin::balance", vec![], vec![])
2586 .await
2587 .unwrap_err();
2588
2589 match err {
2590 AptosError::Api {
2591 status_code,
2592 message,
2593 ..
2594 } => {
2595 assert_eq!(status_code, 400);
2596 assert!(message.contains("bad view request"));
2597 }
2598 other => panic!("expected Api error, got {other:?}"),
2599 }
2600 }
2601
2602 #[tokio::test]
2605 async fn test_get_events_by_event_handle() {
2606 let server = MockServer::start().await;
2607
2608 Mock::given(method("GET"))
2609 .and(path_regex(
2610 r"^/v1/accounts/0x[0-9a-f]+/events/.+/withdraw_events$",
2611 ))
2612 .and(query_param("start", "0"))
2613 .and(query_param("limit", "10"))
2614 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([
2615 {"sequence_number": "0", "type": "0x1::coin::WithdrawEvent"}
2616 ])))
2617 .expect(1)
2618 .mount(&server)
2619 .await;
2620
2621 let client = create_mock_client(&server);
2622 let result = client
2623 .get_events_by_event_handle(
2624 AccountAddress::ONE,
2625 "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>",
2626 "withdraw_events",
2627 Some(0),
2628 Some(10),
2629 )
2630 .await
2631 .unwrap();
2632
2633 assert_eq!(result.data.len(), 1);
2634 }
2635
2636 #[tokio::test]
2639 async fn test_get_block_by_version() {
2640 let server = MockServer::start().await;
2641
2642 Mock::given(method("GET"))
2643 .and(path("/v1/blocks/by_version/200"))
2644 .and(query_param("with_transactions", "true"))
2645 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2646 "block_height": "50",
2647 "block_hash": "0xfeed",
2648 "block_timestamp": "1234567890",
2649 "first_version": "180",
2650 "last_version": "220"
2651 })))
2652 .expect(1)
2653 .mount(&server)
2654 .await;
2655
2656 let client = create_mock_client(&server);
2657 let result = client.get_block_by_version(200, true).await.unwrap();
2658
2659 assert_eq!(result.data.get("block_height").unwrap(), "50");
2660 }
2661
2662 #[tokio::test]
2665 async fn test_api_error_parses_error_code_and_vm_error_code() {
2666 let server = MockServer::start().await;
2667
2668 Mock::given(method("GET"))
2671 .and(path("/v1"))
2672 .respond_with(ResponseTemplate::new(400).set_body_json(serde_json::json!({
2673 "message": "VM execution error",
2674 "error_code": "vm_error",
2675 "vm_error_code": 4004
2676 })))
2677 .expect(1)
2678 .mount(&server)
2679 .await;
2680
2681 let client = create_mock_client(&server);
2682 let err = client.get_ledger_info().await.unwrap_err();
2683
2684 match err {
2685 AptosError::Api {
2686 status_code,
2687 message,
2688 error_code,
2689 vm_error_code,
2690 } => {
2691 assert_eq!(status_code, 400);
2692 assert_eq!(message, "VM execution error");
2693 assert_eq!(error_code.as_deref(), Some("vm_error"));
2694 assert_eq!(vm_error_code, Some(4004));
2695 }
2696 other => panic!("expected Api error, got {other:?}"),
2697 }
2698 }
2699
2700 #[tokio::test]
2701 async fn test_api_error_non_json_body_uses_default_message() {
2702 let server = MockServer::start().await;
2703
2704 Mock::given(method("GET"))
2707 .and(path("/v1"))
2708 .respond_with(ResponseTemplate::new(403).set_body_string("Forbidden (plain text)"))
2709 .expect(1)
2710 .mount(&server)
2711 .await;
2712
2713 let client = create_mock_client(&server);
2714 let err = client.get_ledger_info().await.unwrap_err();
2715
2716 match err {
2717 AptosError::Api {
2718 status_code,
2719 message,
2720 error_code,
2721 vm_error_code,
2722 } => {
2723 assert_eq!(status_code, 403);
2724 assert_eq!(message, "Unknown error");
2725 assert!(error_code.is_none());
2726 assert!(vm_error_code.is_none());
2727 }
2728 other => panic!("expected Api error, got {other:?}"),
2729 }
2730 }
2731
2732 #[tokio::test]
2733 async fn test_success_with_invalid_json_body_is_json_error() {
2734 let server = MockServer::start().await;
2735
2736 Mock::given(method("GET"))
2739 .and(path("/v1"))
2740 .respond_with(ResponseTemplate::new(200).set_body_string("this is not json"))
2741 .expect(1)
2742 .mount(&server)
2743 .await;
2744
2745 let client = create_mock_client(&server);
2746 let err = client.get_ledger_info().await.unwrap_err();
2747 assert!(matches!(err, AptosError::Json(_)));
2748 }
2749}