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 let resp = client
1493 .get_account_resources_paginated(AccountAddress::ONE, Some(opaque), None)
1494 .await
1495 .unwrap();
1496 assert!(
1497 resp.data.is_empty(),
1498 "mock returned an empty page, got {} resources",
1499 resp.data.len()
1500 );
1501 }
1502
1503 #[tokio::test]
1504 async fn test_get_account_resources_no_pagination_omits_query() {
1505 let server = MockServer::start().await;
1506
1507 Mock::given(method("GET"))
1510 .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/resources$"))
1511 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([])))
1512 .expect(1)
1513 .mount(&server)
1514 .await;
1515
1516 let client = create_mock_client(&server);
1517 let resp = client
1518 .get_account_resources(AccountAddress::ONE)
1519 .await
1520 .unwrap();
1521 assert!(
1522 resp.data.is_empty(),
1523 "mock returned an empty page, got {} resources",
1524 resp.data.len()
1525 );
1526 }
1527
1528 #[tokio::test]
1529 async fn test_get_account_resources_paginated_sends_start_only() {
1530 let server = MockServer::start().await;
1533 Mock::given(method("GET"))
1534 .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/resources"))
1535 .and(query_param("start", "1234"))
1536 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([])))
1537 .expect(1)
1538 .mount(&server)
1539 .await;
1540
1541 let client = create_mock_client(&server);
1542 let resp = client
1543 .get_account_resources_paginated(AccountAddress::ONE, Some("1234"), None)
1544 .await
1545 .unwrap();
1546 assert!(
1547 resp.data.is_empty(),
1548 "mock returned an empty page, got {} resources",
1549 resp.data.len()
1550 );
1551 }
1552
1553 #[tokio::test]
1554 async fn test_get_account_modules_paginated_sends_start_and_limit() {
1555 let server = MockServer::start().await;
1556 Mock::given(method("GET"))
1557 .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/modules"))
1558 .and(query_param("start", "7"))
1559 .and(query_param("limit", "100"))
1560 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([])))
1561 .expect(1)
1562 .mount(&server)
1563 .await;
1564
1565 let client = create_mock_client(&server);
1566 let resp = client
1567 .get_account_modules_paginated(AccountAddress::ONE, Some("7"), Some(100))
1568 .await
1569 .unwrap();
1570 assert!(
1571 resp.data.is_empty(),
1572 "mock returned an empty page, got {} modules",
1573 resp.data.len()
1574 );
1575 }
1576
1577 #[tokio::test]
1578 async fn test_get_account_modules_no_pagination_omits_query() {
1579 let server = MockServer::start().await;
1582 Mock::given(method("GET"))
1583 .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/modules$"))
1584 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([])))
1585 .expect(1)
1586 .mount(&server)
1587 .await;
1588
1589 let client = create_mock_client(&server);
1590 let resp = client
1591 .get_account_modules(AccountAddress::ONE)
1592 .await
1593 .unwrap();
1594 assert!(
1595 resp.data.is_empty(),
1596 "mock returned an empty page, got {} modules",
1597 resp.data.len()
1598 );
1599 }
1600
1601 #[tokio::test]
1602 async fn test_get_account_modules_paginated_sends_limit_only() {
1603 let server = MockServer::start().await;
1604
1605 Mock::given(method("GET"))
1608 .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/modules"))
1609 .and(query_param("limit", "25"))
1610 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([])))
1611 .expect(1)
1612 .mount(&server)
1613 .await;
1614
1615 let client = create_mock_client(&server);
1616 let resp = client
1617 .get_account_modules_paginated(AccountAddress::ONE, None, Some(25))
1618 .await
1619 .unwrap();
1620 assert!(
1621 resp.data.is_empty(),
1622 "mock returned an empty page, got {} modules",
1623 resp.data.len()
1624 );
1625 }
1626
1627 #[tokio::test]
1628 async fn test_estimate_gas_price() {
1629 let server = MockServer::start().await;
1630
1631 Mock::given(method("GET"))
1632 .and(path("/v1/estimate_gas_price"))
1633 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1634 "deprioritized_gas_estimate": 50,
1635 "gas_estimate": 100,
1636 "prioritized_gas_estimate": 150
1637 })))
1638 .expect(1)
1639 .mount(&server)
1640 .await;
1641
1642 let client = create_mock_client(&server);
1643 let result = client.estimate_gas_price().await.unwrap();
1644
1645 assert_eq!(result.data.gas_estimate, 100);
1646 assert_eq!(result.data.low(), 50);
1647 assert_eq!(result.data.high(), 150);
1648 }
1649
1650 #[tokio::test]
1651 async fn test_get_transaction_by_hash() {
1652 let server = MockServer::start().await;
1653
1654 Mock::given(method("GET"))
1655 .and(path_regex(r"/v1/transactions/by_hash/0x[0-9a-f]+"))
1656 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1657 "version": "12345",
1658 "hash": "0x0000000000000000000000000000000000000000000000000000000000000001",
1659 "success": true,
1660 "vm_status": "Executed successfully"
1661 })))
1662 .expect(1)
1663 .mount(&server)
1664 .await;
1665
1666 let client = create_mock_client(&server);
1667 let hash = HashValue::from_hex(
1668 "0x0000000000000000000000000000000000000000000000000000000000000001",
1669 )
1670 .unwrap();
1671 let result = client.get_transaction_by_hash(&hash).await.unwrap();
1672
1673 assert!(
1674 result
1675 .data
1676 .get("success")
1677 .and_then(serde_json::Value::as_bool)
1678 .unwrap()
1679 );
1680 }
1681
1682 #[tokio::test]
1683 async fn test_wait_for_transaction_success() {
1684 let server = MockServer::start().await;
1685
1686 Mock::given(method("GET"))
1687 .and(path_regex(r"/v1/transactions/by_hash/0x[0-9a-f]+"))
1688 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1689 "type": "user_transaction",
1690 "version": "12345",
1691 "hash": "0x0000000000000000000000000000000000000000000000000000000000000001",
1692 "success": true,
1693 "vm_status": "Executed successfully"
1694 })))
1695 .expect(1..)
1696 .mount(&server)
1697 .await;
1698
1699 let client = create_mock_client(&server);
1700 let hash = HashValue::from_hex(
1701 "0x0000000000000000000000000000000000000000000000000000000000000001",
1702 )
1703 .unwrap();
1704 let result = client
1705 .wait_for_transaction(&hash, Some(Duration::from_secs(5)))
1706 .await
1707 .unwrap();
1708
1709 assert!(
1710 result
1711 .data
1712 .get("success")
1713 .and_then(serde_json::Value::as_bool)
1714 .unwrap()
1715 );
1716 }
1717
1718 #[tokio::test]
1719 async fn test_server_error_retryable() {
1720 let server = MockServer::start().await;
1721
1722 Mock::given(method("GET"))
1723 .and(path("/v1"))
1724 .respond_with(ResponseTemplate::new(503).set_body_json(serde_json::json!({
1725 "message": "Service temporarily unavailable"
1726 })))
1727 .expect(1)
1728 .mount(&server)
1729 .await;
1730
1731 let url = format!("{}/v1", server.uri());
1732 let config = AptosConfig::custom(&url).unwrap().without_retry();
1733 let client = FullnodeClient::new(config).unwrap();
1734 let result = client.get_ledger_info().await;
1735
1736 assert!(result.is_err());
1737 assert!(result.unwrap_err().is_retryable());
1738 }
1739
1740 #[tokio::test]
1741 async fn test_rate_limited() {
1742 let server = MockServer::start().await;
1743
1744 Mock::given(method("GET"))
1745 .and(path("/v1"))
1746 .respond_with(
1747 ResponseTemplate::new(429)
1748 .set_body_json(serde_json::json!({
1749 "message": "Rate limited"
1750 }))
1751 .insert_header("retry-after", "30"),
1752 )
1753 .expect(1)
1754 .mount(&server)
1755 .await;
1756
1757 let url = format!("{}/v1", server.uri());
1758 let config = AptosConfig::custom(&url).unwrap().without_retry();
1759 let client = FullnodeClient::new(config).unwrap();
1760 let result = client.get_ledger_info().await;
1761
1762 assert!(result.is_err());
1763 assert!(result.unwrap_err().is_retryable());
1764 }
1765
1766 #[tokio::test]
1767 async fn test_get_block_by_height() {
1768 let server = MockServer::start().await;
1769
1770 Mock::given(method("GET"))
1771 .and(path_regex(r"/v1/blocks/by_height/\d+"))
1772 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1773 "block_height": "1000",
1774 "block_hash": "0xabc",
1775 "block_timestamp": "1234567890",
1776 "first_version": "100",
1777 "last_version": "200"
1778 })))
1779 .expect(1)
1780 .mount(&server)
1781 .await;
1782
1783 let client = create_mock_client(&server);
1784 let result = client.get_block_by_height(1000, false).await.unwrap();
1785
1786 assert!(result.data.get("block_height").is_some());
1787 }
1788
1789 #[tokio::test]
1790 async fn test_view() {
1791 let server = MockServer::start().await;
1792
1793 Mock::given(method("POST"))
1794 .and(path("/v1/view"))
1795 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!(["1000000"])))
1796 .expect(1)
1797 .mount(&server)
1798 .await;
1799
1800 let client = create_mock_client(&server);
1801 let result: AptosResponse<Vec<serde_json::Value>> = client
1802 .view(
1803 "0x1::coin::balance",
1804 vec!["0x1::aptos_coin::AptosCoin".to_string()],
1805 vec![serde_json::json!("0x1")],
1806 )
1807 .await
1808 .unwrap();
1809
1810 assert_eq!(result.data.len(), 1);
1811 }
1812
1813 #[tokio::test]
1814 async fn test_view_bcs_args_posts_bcs_request() {
1815 let server = MockServer::start().await;
1816
1817 let args = vec![aptos_bcs::to_bytes(&AccountAddress::ONE).unwrap()];
1822 let expected_body = aptos_bcs::to_bytes(
1823 &crate::transaction::EntryFunction::from_function_id(
1824 "0x1::coin::balance",
1825 vec![],
1826 args.clone(),
1827 )
1828 .unwrap(),
1829 )
1830 .unwrap();
1831
1832 Mock::given(method("POST"))
1833 .and(path("/v1/view"))
1834 .and(wiremock::matchers::header(
1835 "content-type",
1836 "application/x.aptos.view_function+bcs",
1837 ))
1838 .and(wiremock::matchers::body_bytes(expected_body))
1839 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!(["1000000"])))
1840 .expect(1)
1841 .mount(&server)
1842 .await;
1843
1844 let client = create_mock_client(&server);
1845 let result = client
1846 .view_bcs_args("0x1::coin::balance", vec![], args)
1847 .await
1848 .unwrap()
1849 .into_inner();
1850
1851 assert_eq!(result.len(), 1);
1852 assert_eq!(result[0].as_str().unwrap(), "1000000");
1853 }
1854
1855 #[tokio::test]
1856 async fn test_simulate_transaction_with_estimate_gas_unit_price() {
1857 let server = MockServer::start().await;
1858
1859 Mock::given(method("POST"))
1860 .and(path("/v1/transactions/simulate"))
1861 .and(|req: &wiremock::Request| {
1862 req.url
1863 .query()
1864 .is_some_and(|q| q.contains("estimate_gas_unit_price=true"))
1865 })
1866 .respond_with(ResponseTemplate::new(200).set_body_json(simulate_response_json()))
1867 .expect(1)
1868 .mount(&server)
1869 .await;
1870
1871 let client = create_mock_client(&server);
1872 let signed = create_minimal_signed_transaction();
1873 let opts = SimulateQueryOptions::new().estimate_gas_unit_price(true);
1874 let result = client
1875 .simulate_transaction_with_options(&signed, opts)
1876 .await
1877 .unwrap();
1878 assert!(!result.data.is_empty());
1879 }
1880
1881 #[tokio::test]
1882 async fn test_simulate_transaction_with_estimate_max_gas_amount() {
1883 let server = MockServer::start().await;
1884
1885 Mock::given(method("POST"))
1886 .and(path("/v1/transactions/simulate"))
1887 .and(|req: &wiremock::Request| {
1888 req.url
1889 .query()
1890 .is_some_and(|q| q.contains("estimate_max_gas_amount=true"))
1891 })
1892 .respond_with(ResponseTemplate::new(200).set_body_json(simulate_response_json()))
1893 .expect(1)
1894 .mount(&server)
1895 .await;
1896
1897 let client = create_mock_client(&server);
1898 let signed = create_minimal_signed_transaction();
1899 let opts = SimulateQueryOptions::new().estimate_max_gas_amount(true);
1900 let result = client
1901 .simulate_transaction_with_options(&signed, opts)
1902 .await
1903 .unwrap();
1904 assert!(!result.data.is_empty());
1905 }
1906
1907 #[tokio::test]
1908 async fn test_simulate_transaction_with_estimate_prioritized_gas_unit_price() {
1909 let server = MockServer::start().await;
1910
1911 Mock::given(method("POST"))
1912 .and(path("/v1/transactions/simulate"))
1913 .and(|req: &wiremock::Request| {
1914 req.url
1915 .query()
1916 .is_some_and(|q| q.contains("estimate_prioritized_gas_unit_price=true"))
1917 })
1918 .respond_with(ResponseTemplate::new(200).set_body_json(simulate_response_json()))
1919 .expect(1)
1920 .mount(&server)
1921 .await;
1922
1923 let client = create_mock_client(&server);
1924 let signed = create_minimal_signed_transaction();
1925 let opts = SimulateQueryOptions::new().estimate_prioritized_gas_unit_price(true);
1926 let result = client
1927 .simulate_transaction_with_options(&signed, opts)
1928 .await
1929 .unwrap();
1930 assert!(!result.data.is_empty());
1931 }
1932
1933 #[tokio::test]
1934 async fn test_simulate_transaction_with_all_options() {
1935 let server = MockServer::start().await;
1936
1937 Mock::given(method("POST"))
1938 .and(path("/v1/transactions/simulate"))
1939 .and(|req: &wiremock::Request| {
1940 req.url.query().is_some_and(|q| {
1941 q.contains("estimate_gas_unit_price=true")
1942 && q.contains("estimate_max_gas_amount=true")
1943 && q.contains("estimate_prioritized_gas_unit_price=true")
1944 })
1945 })
1946 .respond_with(ResponseTemplate::new(200).set_body_json(simulate_response_json()))
1947 .expect(1)
1948 .mount(&server)
1949 .await;
1950
1951 let client = create_mock_client(&server);
1952 let signed = create_minimal_signed_transaction();
1953 let opts = SimulateQueryOptions::new()
1954 .estimate_gas_unit_price(true)
1955 .estimate_max_gas_amount(true)
1956 .estimate_prioritized_gas_unit_price(true);
1957 let result = client
1958 .simulate_transaction_with_options(&signed, opts)
1959 .await
1960 .unwrap();
1961 assert!(!result.data.is_empty());
1962 }
1963
1964 #[tokio::test]
1965 async fn test_simulate_transaction_without_options() {
1966 let server = MockServer::start().await;
1967
1968 Mock::given(method("POST"))
1970 .and(path("/v1/transactions/simulate"))
1971 .and(|req: &wiremock::Request| {
1972 req.url.query().is_none_or(|q| {
1974 !q.contains("estimate_gas_unit_price=")
1975 && !q.contains("estimate_max_gas_amount=")
1976 && !q.contains("estimate_prioritized_gas_unit_price=")
1977 })
1978 })
1979 .respond_with(ResponseTemplate::new(200).set_body_json(simulate_response_json()))
1980 .expect(1)
1981 .mount(&server)
1982 .await;
1983
1984 let client = create_mock_client(&server);
1985 let signed = create_minimal_signed_transaction();
1986 let result = client.simulate_transaction(&signed).await.unwrap();
1987 assert!(!result.data.is_empty());
1988 }
1989
1990 #[tokio::test]
1991 async fn test_get_table_item() {
1992 let server = MockServer::start().await;
1993
1994 Mock::given(method("POST"))
1998 .and(path_regex(r"^/v1/tables/0x[0-9a-f]+/item$"))
1999 .and(wiremock::matchers::body_json(serde_json::json!({
2000 "key_type": "address",
2001 "value_type": "u64",
2002 "key": "0x1",
2003 })))
2004 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!("42")))
2005 .expect(1)
2006 .mount(&server)
2007 .await;
2008
2009 let client = create_mock_client(&server);
2010 let result = client
2011 .get_table_item(
2012 "0x0000000000000000000000000000000000000000000000000000000000000abc",
2013 "address",
2014 "u64",
2015 serde_json::json!("0x1"),
2016 )
2017 .await
2018 .unwrap();
2019
2020 assert_eq!(result.data, serde_json::json!("42"));
2021 }
2022
2023 #[tokio::test]
2024 async fn test_get_table_item_missing_key_is_404() {
2025 let server = MockServer::start().await;
2026
2027 Mock::given(method("POST"))
2028 .and(path_regex(r"^/v1/tables/0x[0-9a-f]+/item$"))
2029 .respond_with(ResponseTemplate::new(404).set_body_json(serde_json::json!({
2030 "message": "Table item not found",
2031 "error_code": "table_item_not_found"
2032 })))
2033 .expect(1)
2034 .mount(&server)
2035 .await;
2036
2037 let client = create_mock_client(&server);
2038 let err = client
2039 .get_table_item("0xabc", "address", "u64", serde_json::json!("0x2"))
2040 .await
2041 .unwrap_err();
2042
2043 assert!(matches!(
2044 err,
2045 AptosError::Api {
2046 status_code: 404,
2047 ..
2048 }
2049 ));
2050 }
2051
2052 #[tokio::test]
2053 async fn test_get_transaction_by_version() {
2054 let server = MockServer::start().await;
2055
2056 Mock::given(method("GET"))
2057 .and(path("/v1/transactions/by_version/100"))
2058 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2059 "version": "100",
2060 "hash": "0xdead",
2061 "success": true
2062 })))
2063 .expect(1)
2064 .mount(&server)
2065 .await;
2066
2067 let client = create_mock_client(&server);
2068 let result = client.get_transaction_by_version(100).await.unwrap();
2069
2070 assert_eq!(result.data.get("version").unwrap(), "100");
2071 }
2072
2073 #[tokio::test]
2074 async fn test_get_transactions_with_pagination() {
2075 let server = MockServer::start().await;
2076
2077 Mock::given(method("GET"))
2078 .and(path("/v1/transactions"))
2079 .and(query_param("start", "10"))
2080 .and(query_param("limit", "2"))
2081 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([
2082 {"version": "10"},
2083 {"version": "11"}
2084 ])))
2085 .expect(1)
2086 .mount(&server)
2087 .await;
2088
2089 let client = create_mock_client(&server);
2090 let result = client.get_transactions(Some(10), Some(2)).await.unwrap();
2091
2092 assert_eq!(result.data.len(), 2);
2093 }
2094
2095 #[tokio::test]
2096 async fn test_get_account_transactions() {
2097 let server = MockServer::start().await;
2098
2099 Mock::given(method("GET"))
2100 .and(path_regex(r"^/v1/accounts/0x[0-9a-f]+/transactions$"))
2101 .and(query_param("start", "0"))
2102 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([
2103 {"version": "5", "sender": "0x1"}
2104 ])))
2105 .expect(1)
2106 .mount(&server)
2107 .await;
2108
2109 let client = create_mock_client(&server);
2110 let result = client
2111 .get_account_transactions(AccountAddress::ONE, Some(0), None)
2112 .await
2113 .unwrap();
2114
2115 assert_eq!(result.data.len(), 1);
2116 }
2117
2118 #[tokio::test]
2119 async fn test_get_events_by_creation_number() {
2120 let server = MockServer::start().await;
2121
2122 Mock::given(method("GET"))
2123 .and(path_regex(r"^/v1/accounts/0x[0-9a-f]+/events/7$"))
2124 .and(query_param("limit", "25"))
2125 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([
2126 {"sequence_number": "0", "type": "0x1::coin::DepositEvent"}
2127 ])))
2128 .expect(1)
2129 .mount(&server)
2130 .await;
2131
2132 let client = create_mock_client(&server);
2133 let result = client
2134 .get_events_by_creation_number(AccountAddress::ONE, 7, None, Some(25))
2135 .await
2136 .unwrap();
2137
2138 assert_eq!(result.data.len(), 1);
2139 }
2140
2141 #[test]
2144 fn test_base_url_and_config_accessors() {
2145 let config = AptosConfig::testnet();
2146 let expected = config.fullnode_url().clone();
2147 let client = FullnodeClient::new(config).unwrap();
2148
2149 assert_eq!(client.base_url(), &expected);
2151 assert_eq!(client.config().fullnode_url(), &expected);
2153 }
2154
2155 #[test]
2156 fn test_retry_config_accessor() {
2157 let config = AptosConfig::testnet().with_max_retries(7);
2159 let client = FullnodeClient::new(config).unwrap();
2160 assert_eq!(client.retry_config().max_retries, 7);
2161
2162 let client = FullnodeClient::new(AptosConfig::testnet().without_retry()).unwrap();
2164 assert_eq!(client.retry_config().max_retries, 0);
2165 }
2166
2167 #[tokio::test]
2170 async fn test_get_sequence_number() {
2171 let server = MockServer::start().await;
2172
2173 Mock::given(method("GET"))
2174 .and(path_regex(r"^/v1/accounts/0x[0-9a-f]+$"))
2175 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2176 "sequence_number": "99",
2177 "authentication_key": "0x0000000000000000000000000000000000000000000000000000000000000001"
2178 })))
2179 .expect(1)
2180 .mount(&server)
2181 .await;
2182
2183 let client = create_mock_client(&server);
2184 let seq = client
2185 .get_sequence_number(AccountAddress::ONE)
2186 .await
2187 .unwrap();
2188 assert_eq!(seq, 99);
2189 }
2190
2191 #[tokio::test]
2192 async fn test_get_sequence_number_unparseable_is_internal_error() {
2193 let server = MockServer::start().await;
2194
2195 Mock::given(method("GET"))
2198 .and(path_regex(r"^/v1/accounts/0x[0-9a-f]+$"))
2199 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2200 "sequence_number": "not-a-number",
2201 "authentication_key": "0x0000000000000000000000000000000000000000000000000000000000000001"
2202 })))
2203 .expect(1)
2204 .mount(&server)
2205 .await;
2206
2207 let client = create_mock_client(&server);
2208 let err = client
2209 .get_sequence_number(AccountAddress::ONE)
2210 .await
2211 .unwrap_err();
2212 assert!(matches!(err, AptosError::Internal(_)));
2213 }
2214
2215 #[tokio::test]
2218 async fn test_get_account_module() {
2219 let server = MockServer::start().await;
2220
2221 Mock::given(method("GET"))
2222 .and(path_regex(r"^/v1/accounts/0x[0-9a-f]+/module/coin$"))
2223 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2224 "bytecode": "0xdeadbeef",
2225 "abi": {
2226 "address": "0x1",
2227 "name": "coin",
2228 "exposed_functions": [],
2229 "structs": []
2230 }
2231 })))
2232 .expect(1)
2233 .mount(&server)
2234 .await;
2235
2236 let client = create_mock_client(&server);
2237 let result = client
2238 .get_account_module(AccountAddress::ONE, "coin")
2239 .await
2240 .unwrap();
2241
2242 assert_eq!(result.data.bytecode, "0xdeadbeef");
2243 assert!(result.data.abi.is_some());
2244 }
2245
2246 #[tokio::test]
2247 async fn test_get_account_module_not_found() {
2248 let server = MockServer::start().await;
2249
2250 Mock::given(method("GET"))
2251 .and(path_regex(r"^/v1/accounts/0x[0-9a-f]+/module/.*$"))
2252 .respond_with(ResponseTemplate::new(404).set_body_json(serde_json::json!({
2253 "message": "Module not found",
2254 "error_code": "module_not_found"
2255 })))
2256 .expect(1)
2257 .mount(&server)
2258 .await;
2259
2260 let client = create_mock_client(&server);
2261 let err = client
2262 .get_account_module(AccountAddress::ONE, "missing")
2263 .await
2264 .unwrap_err();
2265 assert!(err.is_not_found());
2266 }
2267
2268 #[tokio::test]
2271 async fn test_get_account_balance() {
2272 let server = MockServer::start().await;
2273
2274 Mock::given(method("POST"))
2277 .and(path("/v1/view"))
2278 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!(["1234567"])))
2279 .expect(1)
2280 .mount(&server)
2281 .await;
2282
2283 let client = create_mock_client(&server);
2284 let balance = client
2285 .get_account_balance(AccountAddress::ONE)
2286 .await
2287 .unwrap();
2288 assert_eq!(balance, 1_234_567);
2289 }
2290
2291 #[tokio::test]
2292 async fn test_get_account_balance_unparseable_is_internal_error() {
2293 let server = MockServer::start().await;
2294
2295 Mock::given(method("POST"))
2297 .and(path("/v1/view"))
2298 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([])))
2299 .expect(1)
2300 .mount(&server)
2301 .await;
2302
2303 let client = create_mock_client(&server);
2304 let err = client
2305 .get_account_balance(AccountAddress::ONE)
2306 .await
2307 .unwrap_err();
2308 assert!(matches!(err, AptosError::Internal(_)));
2309 }
2310
2311 fn pending_transaction_json() -> serde_json::Value {
2314 serde_json::json!({
2315 "hash": "0x0000000000000000000000000000000000000000000000000000000000000001",
2316 "sender": "0x1",
2317 "sequence_number": "0",
2318 "max_gas_amount": "100000",
2319 "gas_unit_price": "100",
2320 "expiration_timestamp_secs": "1000000"
2321 })
2322 }
2323
2324 #[tokio::test]
2325 async fn test_submit_transaction_success() {
2326 let server = MockServer::start().await;
2327
2328 Mock::given(method("POST"))
2329 .and(path("/v1/transactions"))
2330 .and(wiremock::matchers::header(
2331 "content-type",
2332 "application/x.aptos.signed_transaction+bcs",
2333 ))
2334 .respond_with(ResponseTemplate::new(202).set_body_json(pending_transaction_json()))
2335 .expect(1)
2336 .mount(&server)
2337 .await;
2338
2339 let client = create_mock_client(&server);
2340 let signed = create_minimal_signed_transaction();
2341 let result = client.submit_transaction(&signed).await.unwrap();
2342
2343 assert_eq!(result.data.sequence_number, "0");
2344 assert_eq!(result.data.sender(), "0x1");
2345 }
2346
2347 #[tokio::test]
2348 async fn test_submit_transaction_rejected_is_api_error() {
2349 let server = MockServer::start().await;
2350
2351 Mock::given(method("POST"))
2354 .and(path("/v1/transactions"))
2355 .respond_with(ResponseTemplate::new(400).set_body_json(serde_json::json!({
2356 "message": "Invalid transaction",
2357 "error_code": "invalid_transaction_update"
2358 })))
2359 .expect(1)
2360 .mount(&server)
2361 .await;
2362
2363 let client = create_mock_client(&server);
2364 let signed = create_minimal_signed_transaction();
2365 let err = client.submit_transaction(&signed).await.unwrap_err();
2366
2367 match err {
2368 AptosError::Api {
2369 status_code,
2370 error_code,
2371 ..
2372 } => {
2373 assert_eq!(status_code, 400);
2374 assert_eq!(error_code.as_deref(), Some("invalid_transaction_update"));
2375 }
2376 other => panic!("expected Api error, got {other:?}"),
2377 }
2378 assert!(
2380 !AptosError::api(400, "x").is_retryable(),
2381 "sanity: 4xx not retryable"
2382 );
2383 }
2384
2385 #[tokio::test]
2388 async fn test_submit_and_wait_success() {
2389 let server = MockServer::start().await;
2390
2391 Mock::given(method("POST"))
2393 .and(path("/v1/transactions"))
2394 .respond_with(ResponseTemplate::new(202).set_body_json(pending_transaction_json()))
2395 .expect(1)
2396 .mount(&server)
2397 .await;
2398
2399 Mock::given(method("GET"))
2400 .and(path_regex(r"^/v1/transactions/by_hash/0x[0-9a-f]+$"))
2401 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2402 "type": "user_transaction",
2403 "version": "555",
2404 "hash": "0x0000000000000000000000000000000000000000000000000000000000000001",
2405 "success": true,
2406 "vm_status": "Executed successfully"
2407 })))
2408 .expect(1..)
2409 .mount(&server)
2410 .await;
2411
2412 let client = create_mock_client(&server);
2413 let signed = create_minimal_signed_transaction();
2414 let result = client
2415 .submit_and_wait(&signed, Some(Duration::from_secs(5)))
2416 .await
2417 .unwrap();
2418
2419 assert_eq!(result.data.get("version").unwrap(), "555");
2420 }
2421
2422 #[tokio::test]
2425 async fn test_wait_for_transaction_execution_failed() {
2426 let server = MockServer::start().await;
2427
2428 Mock::given(method("GET"))
2431 .and(path_regex(r"^/v1/transactions/by_hash/0x[0-9a-f]+$"))
2432 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2433 "type": "user_transaction",
2434 "version": "12345",
2435 "hash": "0x0000000000000000000000000000000000000000000000000000000000000001",
2436 "success": false,
2437 "vm_status": "Move abort: 0x1"
2438 })))
2439 .expect(1..)
2440 .mount(&server)
2441 .await;
2442
2443 let client = create_mock_client(&server);
2444 let hash = HashValue::from_hex(
2445 "0x0000000000000000000000000000000000000000000000000000000000000001",
2446 )
2447 .unwrap();
2448 let err = client
2449 .wait_for_transaction(&hash, Some(Duration::from_secs(5)))
2450 .await
2451 .unwrap_err();
2452
2453 match err {
2454 AptosError::ExecutionFailed { vm_status } => {
2455 assert_eq!(vm_status, "Move abort: 0x1");
2456 }
2457 other => panic!("expected ExecutionFailed, got {other:?}"),
2458 }
2459 }
2460
2461 #[tokio::test]
2462 async fn test_wait_for_transaction_times_out() {
2463 let server = MockServer::start().await;
2464
2465 Mock::given(method("GET"))
2468 .and(path_regex(r"^/v1/transactions/by_hash/0x[0-9a-f]+$"))
2469 .respond_with(ResponseTemplate::new(404).set_body_json(serde_json::json!({
2470 "message": "Transaction not found",
2471 "error_code": "transaction_not_found"
2472 })))
2473 .expect(1..)
2474 .mount(&server)
2475 .await;
2476
2477 let client = create_mock_client(&server);
2478 let hash = HashValue::from_hex(
2479 "0x0000000000000000000000000000000000000000000000000000000000000002",
2480 )
2481 .unwrap();
2482 let err = client
2483 .wait_for_transaction(&hash, Some(Duration::from_millis(1)))
2484 .await
2485 .unwrap_err();
2486
2487 assert!(err.is_timeout());
2488 match err {
2489 AptosError::TransactionTimeout { timeout_secs, .. } => {
2490 assert_eq!(timeout_secs, 0);
2491 }
2492 other => panic!("expected TransactionTimeout, got {other:?}"),
2493 }
2494 }
2495
2496 #[tokio::test]
2497 async fn test_wait_for_transaction_propagates_non_404_error() {
2498 let server = MockServer::start().await;
2499
2500 Mock::given(method("GET"))
2503 .and(path_regex(r"^/v1/transactions/by_hash/0x[0-9a-f]+$"))
2504 .respond_with(ResponseTemplate::new(500).set_body_json(serde_json::json!({
2505 "message": "boom"
2506 })))
2507 .expect(1)
2508 .mount(&server)
2509 .await;
2510
2511 let client = create_mock_client(&server);
2512 let hash = HashValue::from_hex(
2513 "0x0000000000000000000000000000000000000000000000000000000000000003",
2514 )
2515 .unwrap();
2516 let err = client
2517 .wait_for_transaction(&hash, Some(Duration::from_secs(5)))
2518 .await
2519 .unwrap_err();
2520
2521 assert!(matches!(
2522 err,
2523 AptosError::Api {
2524 status_code: 500,
2525 ..
2526 }
2527 ));
2528 }
2529
2530 #[tokio::test]
2533 async fn test_view_bcs_returns_raw_bytes() {
2534 let server = MockServer::start().await;
2535
2536 let raw = vec![1u8, 2, 3, 4, 5];
2539 Mock::given(method("POST"))
2540 .and(path("/v1/view"))
2541 .and(wiremock::matchers::header("accept", "application/x-bcs"))
2542 .respond_with(ResponseTemplate::new(200).set_body_bytes(raw.clone()))
2543 .expect(1)
2544 .mount(&server)
2545 .await;
2546
2547 let client = create_mock_client(&server);
2548 let result = client
2549 .view_bcs(
2550 "0x1::coin::balance",
2551 vec!["0x1::aptos_coin::AptosCoin".to_string()],
2552 vec![aptos_bcs::to_bytes(&AccountAddress::ONE).unwrap()],
2553 )
2554 .await
2555 .unwrap();
2556
2557 assert_eq!(result.data, raw);
2558 }
2559
2560 #[tokio::test]
2561 async fn test_view_bcs_error_status_is_api_error() {
2562 let server = MockServer::start().await;
2563
2564 Mock::given(method("POST"))
2567 .and(path("/v1/view"))
2568 .respond_with(ResponseTemplate::new(400).set_body_string("bad view request"))
2569 .expect(1)
2570 .mount(&server)
2571 .await;
2572
2573 let client = create_mock_client(&server);
2574 let err = client
2575 .view_bcs("0x1::coin::balance", vec![], vec![])
2576 .await
2577 .unwrap_err();
2578
2579 match err {
2580 AptosError::Api {
2581 status_code,
2582 message,
2583 ..
2584 } => {
2585 assert_eq!(status_code, 400);
2586 assert!(message.contains("bad view request"));
2587 }
2588 other => panic!("expected Api error, got {other:?}"),
2589 }
2590 }
2591
2592 #[tokio::test]
2595 async fn test_get_events_by_event_handle() {
2596 let server = MockServer::start().await;
2597
2598 Mock::given(method("GET"))
2599 .and(path_regex(
2600 r"^/v1/accounts/0x[0-9a-f]+/events/.+/withdraw_events$",
2601 ))
2602 .and(query_param("start", "0"))
2603 .and(query_param("limit", "10"))
2604 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([
2605 {"sequence_number": "0", "type": "0x1::coin::WithdrawEvent"}
2606 ])))
2607 .expect(1)
2608 .mount(&server)
2609 .await;
2610
2611 let client = create_mock_client(&server);
2612 let result = client
2613 .get_events_by_event_handle(
2614 AccountAddress::ONE,
2615 "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>",
2616 "withdraw_events",
2617 Some(0),
2618 Some(10),
2619 )
2620 .await
2621 .unwrap();
2622
2623 assert_eq!(result.data.len(), 1);
2624 }
2625
2626 #[tokio::test]
2629 async fn test_get_block_by_version() {
2630 let server = MockServer::start().await;
2631
2632 Mock::given(method("GET"))
2633 .and(path("/v1/blocks/by_version/200"))
2634 .and(query_param("with_transactions", "true"))
2635 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2636 "block_height": "50",
2637 "block_hash": "0xfeed",
2638 "block_timestamp": "1234567890",
2639 "first_version": "180",
2640 "last_version": "220"
2641 })))
2642 .expect(1)
2643 .mount(&server)
2644 .await;
2645
2646 let client = create_mock_client(&server);
2647 let result = client.get_block_by_version(200, true).await.unwrap();
2648
2649 assert_eq!(result.data.get("block_height").unwrap(), "50");
2650 }
2651
2652 #[tokio::test]
2655 async fn test_api_error_parses_error_code_and_vm_error_code() {
2656 let server = MockServer::start().await;
2657
2658 Mock::given(method("GET"))
2661 .and(path("/v1"))
2662 .respond_with(ResponseTemplate::new(400).set_body_json(serde_json::json!({
2663 "message": "VM execution error",
2664 "error_code": "vm_error",
2665 "vm_error_code": 4004
2666 })))
2667 .expect(1)
2668 .mount(&server)
2669 .await;
2670
2671 let client = create_mock_client(&server);
2672 let err = client.get_ledger_info().await.unwrap_err();
2673
2674 match err {
2675 AptosError::Api {
2676 status_code,
2677 message,
2678 error_code,
2679 vm_error_code,
2680 } => {
2681 assert_eq!(status_code, 400);
2682 assert_eq!(message, "VM execution error");
2683 assert_eq!(error_code.as_deref(), Some("vm_error"));
2684 assert_eq!(vm_error_code, Some(4004));
2685 }
2686 other => panic!("expected Api error, got {other:?}"),
2687 }
2688 }
2689
2690 #[tokio::test]
2691 async fn test_api_error_non_json_body_uses_default_message() {
2692 let server = MockServer::start().await;
2693
2694 Mock::given(method("GET"))
2697 .and(path("/v1"))
2698 .respond_with(ResponseTemplate::new(403).set_body_string("Forbidden (plain text)"))
2699 .expect(1)
2700 .mount(&server)
2701 .await;
2702
2703 let client = create_mock_client(&server);
2704 let err = client.get_ledger_info().await.unwrap_err();
2705
2706 match err {
2707 AptosError::Api {
2708 status_code,
2709 message,
2710 error_code,
2711 vm_error_code,
2712 } => {
2713 assert_eq!(status_code, 403);
2714 assert_eq!(message, "Unknown error");
2715 assert!(error_code.is_none());
2716 assert!(vm_error_code.is_none());
2717 }
2718 other => panic!("expected Api error, got {other:?}"),
2719 }
2720 }
2721
2722 #[tokio::test]
2723 async fn test_success_with_invalid_json_body_is_json_error() {
2724 let server = MockServer::start().await;
2725
2726 Mock::given(method("GET"))
2729 .and(path("/v1"))
2730 .respond_with(ResponseTemplate::new(200).set_body_string("this is not json"))
2731 .expect(1)
2732 .mount(&server)
2733 .await;
2734
2735 let client = create_mock_client(&server);
2736 let err = client.get_ledger_info().await.unwrap_err();
2737 assert!(matches!(err, AptosError::Json(_)));
2738 }
2739}