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(
498 &self,
499 start: Option<u64>,
500 limit: Option<u16>,
501 ) -> AptosResult<AptosResponse<Vec<serde_json::Value>>> {
502 let mut url = self.build_url("transactions");
503 {
504 let mut query = url.query_pairs_mut();
505 if let Some(start) = start {
506 query.append_pair("start", &start.to_string());
507 }
508 if let Some(limit) = limit {
509 query.append_pair("limit", &limit.to_string());
510 }
511 }
512 self.get_json(url).await
513 }
514
515 pub async fn get_account_transactions(
526 &self,
527 address: AccountAddress,
528 start: Option<u64>,
529 limit: Option<u16>,
530 ) -> AptosResult<AptosResponse<Vec<serde_json::Value>>> {
531 let mut url = self.build_url(&format!("accounts/{address}/transactions"));
532 {
533 let mut query = url.query_pairs_mut();
534 if let Some(start) = start {
535 query.append_pair("start", &start.to_string());
536 }
537 if let Some(limit) = limit {
538 query.append_pair("limit", &limit.to_string());
539 }
540 }
541 self.get_json(url).await
542 }
543
544 pub async fn wait_for_transaction(
553 &self,
554 hash: &HashValue,
555 timeout: Option<Duration>,
556 ) -> AptosResult<AptosResponse<serde_json::Value>> {
557 let timeout = timeout.unwrap_or(Duration::from_secs(DEFAULT_TRANSACTION_WAIT_TIMEOUT_SECS));
558 let start = std::time::Instant::now();
559
560 let initial_interval = Duration::from_millis(200);
562 let max_interval = Duration::from_secs(2);
563 let mut current_interval = initial_interval;
564
565 loop {
566 match self.get_transaction_by_hash(hash).await {
567 Ok(response) => {
568 if response.data.get("version").is_some() {
570 let success = response
572 .data
573 .get("success")
574 .and_then(serde_json::Value::as_bool);
575 if success == Some(false) {
576 let vm_status = response
577 .data
578 .get("vm_status")
579 .and_then(|v| v.as_str())
580 .unwrap_or("unknown")
581 .to_string();
582 return Err(AptosError::ExecutionFailed { vm_status });
583 }
584 return Ok(response);
585 }
586 }
587 Err(AptosError::Api {
588 status_code: 404, ..
589 }) => {
590 }
592 Err(e) => return Err(e),
593 }
594
595 if start.elapsed() >= timeout {
596 return Err(AptosError::TransactionTimeout {
597 hash: hash.to_string(),
598 timeout_secs: timeout.as_secs(),
599 });
600 }
601
602 tokio::time::sleep(current_interval).await;
603
604 current_interval = std::cmp::min(current_interval * 2, max_interval);
606 }
607 }
608
609 pub async fn simulate_transaction(
621 &self,
622 signed_txn: &SignedTransaction,
623 ) -> AptosResult<AptosResponse<Vec<serde_json::Value>>> {
624 self.simulate_transaction_with_options(signed_txn, None as Option<SimulateQueryOptions>)
625 .await
626 }
627
628 pub async fn simulate_transaction_with_options(
644 &self,
645 signed_txn: &SignedTransaction,
646 options: impl Into<Option<SimulateQueryOptions>>,
647 ) -> AptosResult<AptosResponse<Vec<serde_json::Value>>> {
648 let mut url = self.build_url("transactions/simulate");
649 if let Some(opts) = options.into() {
650 let mut pairs = url.query_pairs_mut();
651 if opts.estimate_gas_unit_price {
652 pairs.append_pair("estimate_gas_unit_price", "true");
653 }
654 if opts.estimate_max_gas_amount {
655 pairs.append_pair("estimate_max_gas_amount", "true");
656 }
657 if opts.estimate_prioritized_gas_unit_price {
658 pairs.append_pair("estimate_prioritized_gas_unit_price", "true");
659 }
660 }
661 let bcs_bytes = signed_txn.for_simulate_endpoint().to_bcs()?;
662 let client = self.client.clone();
663 let retry_config = self.retry_config.clone();
664 let max_response_size = self.config.pool_config().max_response_size;
665
666 let executor = RetryExecutor::from_shared(retry_config);
667 executor
668 .execute(|| {
669 let client = client.clone();
670 let url = url.clone();
671 let bcs_bytes = bcs_bytes.clone();
672 async move {
673 let response = client
674 .post(url)
675 .header(CONTENT_TYPE, BCS_CONTENT_TYPE)
676 .header(ACCEPT, JSON_CONTENT_TYPE)
677 .body(bcs_bytes)
678 .send()
679 .await?;
680
681 Self::handle_response_static(response, max_response_size).await
682 }
683 })
684 .await
685 }
686
687 pub async fn estimate_gas_price(&self) -> AptosResult<AptosResponse<GasEstimation>> {
696 let url = self.build_url("estimate_gas_price");
697 self.get_json(url).await
698 }
699
700 pub async fn view(
709 &self,
710 function: &str,
711 type_args: Vec<String>,
712 args: Vec<serde_json::Value>,
713 ) -> AptosResult<AptosResponse<Vec<serde_json::Value>>> {
714 let url = self.build_url("view");
715
716 let body = serde_json::json!({
717 "function": function,
718 "type_arguments": type_args,
719 "arguments": args,
720 });
721
722 let client = self.client.clone();
723 let retry_config = self.retry_config.clone();
724 let max_response_size = self.config.pool_config().max_response_size;
725
726 let executor = RetryExecutor::from_shared(retry_config);
727 executor
728 .execute(|| {
729 let client = client.clone();
730 let url = url.clone();
731 let body = body.clone();
732 async move {
733 let response = client
734 .post(url)
735 .header(CONTENT_TYPE, JSON_CONTENT_TYPE)
736 .header(ACCEPT, JSON_CONTENT_TYPE)
737 .json(&body)
738 .send()
739 .await?;
740
741 Self::handle_response_static(response, max_response_size).await
742 }
743 })
744 .await
745 }
746
747 pub async fn view_bcs(
769 &self,
770 function: &str,
771 type_args: Vec<String>,
772 args: Vec<Vec<u8>>,
773 ) -> AptosResult<AptosResponse<Vec<u8>>> {
774 let url = self.build_url("view");
775
776 let hex_args: Vec<serde_json::Value> = args
779 .iter()
780 .map(|bytes| serde_json::json!(const_hex::encode_prefixed(bytes)))
781 .collect();
782
783 let body = serde_json::json!({
784 "function": function,
785 "type_arguments": type_args,
786 "arguments": hex_args,
787 });
788
789 let client = self.client.clone();
790 let retry_config = self.retry_config.clone();
791 let max_response_size = self.config.pool_config().max_response_size;
792
793 let executor = RetryExecutor::from_shared(retry_config);
794 executor
795 .execute(|| {
796 let client = client.clone();
797 let url = url.clone();
798 let body = body.clone();
799 async move {
800 let response = client
801 .post(url)
802 .header(CONTENT_TYPE, JSON_CONTENT_TYPE)
803 .header(ACCEPT, BCS_VIEW_CONTENT_TYPE)
804 .json(&body)
805 .send()
806 .await?;
807
808 let status = response.status();
810 if !status.is_success() {
811 let error_bytes =
814 crate::config::read_response_bounded(response, MAX_ERROR_BODY_SIZE)
815 .await
816 .ok();
817 let error_text = error_bytes
818 .and_then(|b| String::from_utf8(b).ok())
819 .unwrap_or_default();
820 return Err(AptosError::Api {
821 status_code: status.as_u16(),
822 message: Self::truncate_error_body(error_text),
823 error_code: None,
824 vm_error_code: None,
825 });
826 }
827
828 let bytes =
831 crate::config::read_response_bounded(response, max_response_size).await?;
832 Ok(AptosResponse::new(bytes))
833 }
834 })
835 .await
836 }
837
838 pub async fn get_table_item(
864 &self,
865 handle: &str,
866 key_type: &str,
867 value_type: &str,
868 key: serde_json::Value,
869 ) -> AptosResult<AptosResponse<serde_json::Value>> {
870 let url = self.build_url(&format!("tables/{}/item", urlencoding::encode(handle)));
871 let body = serde_json::json!({
872 "key_type": key_type,
873 "value_type": value_type,
874 "key": key,
875 });
876 self.post_json(url, &body).await
877 }
878
879 pub async fn get_events_by_creation_number(
894 &self,
895 address: AccountAddress,
896 creation_number: u64,
897 start: Option<u64>,
898 limit: Option<u64>,
899 ) -> AptosResult<AptosResponse<Vec<serde_json::Value>>> {
900 let mut url = self.build_url(&format!("accounts/{address}/events/{creation_number}"));
901 {
902 let mut query = url.query_pairs_mut();
903 if let Some(start) = start {
904 query.append_pair("start", &start.to_string());
905 }
906 if let Some(limit) = limit {
907 query.append_pair("limit", &limit.to_string());
908 }
909 }
910 self.get_json(url).await
911 }
912
913 pub async fn get_events_by_event_handle(
920 &self,
921 address: AccountAddress,
922 event_handle_struct: &str,
923 field_name: &str,
924 start: Option<u64>,
925 limit: Option<u64>,
926 ) -> AptosResult<AptosResponse<Vec<serde_json::Value>>> {
927 let mut url = self.build_url(&format!(
928 "accounts/{}/events/{}/{}",
929 address,
930 urlencoding::encode(event_handle_struct),
931 field_name
932 ));
933
934 {
935 let mut query = url.query_pairs_mut();
936 if let Some(start) = start {
937 query.append_pair("start", &start.to_string());
938 }
939 if let Some(limit) = limit {
940 query.append_pair("limit", &limit.to_string());
941 }
942 }
943
944 self.get_json(url).await
945 }
946
947 pub async fn get_block_by_height(
956 &self,
957 height: u64,
958 with_transactions: bool,
959 ) -> AptosResult<AptosResponse<serde_json::Value>> {
960 let mut url = self.build_url(&format!("blocks/by_height/{height}"));
961 url.query_pairs_mut()
962 .append_pair("with_transactions", &with_transactions.to_string());
963 self.get_json(url).await
964 }
965
966 pub async fn get_block_by_version(
973 &self,
974 version: u64,
975 with_transactions: bool,
976 ) -> AptosResult<AptosResponse<serde_json::Value>> {
977 let mut url = self.build_url(&format!("blocks/by_version/{version}"));
978 url.query_pairs_mut()
979 .append_pair("with_transactions", &with_transactions.to_string());
980 self.get_json(url).await
981 }
982
983 fn build_url(&self, path: &str) -> Url {
986 let mut url = self.config.fullnode_url().clone();
987 if !path.is_empty() {
988 let base_path = url.path();
990 let needs_slash = !base_path.ends_with('/');
991 let new_len = base_path.len() + path.len() + usize::from(needs_slash);
992 let mut new_path = String::with_capacity(new_len);
993 new_path.push_str(base_path);
994 if needs_slash {
995 new_path.push('/');
996 }
997 new_path.push_str(path);
998 url.set_path(&new_path);
999 }
1000 url
1001 }
1002
1003 async fn get_json<T: for<'de> serde::Deserialize<'de>>(
1004 &self,
1005 url: Url,
1006 ) -> AptosResult<AptosResponse<T>> {
1007 let client = self.client.clone();
1008 let url_clone = url.clone();
1009 let retry_config = self.retry_config.clone();
1010 let max_response_size = self.config.pool_config().max_response_size;
1011
1012 let executor = RetryExecutor::from_shared(retry_config);
1013 executor
1014 .execute(|| {
1015 let client = client.clone();
1016 let url = url_clone.clone();
1017 async move {
1018 let response = client
1019 .get(url)
1020 .header(ACCEPT, JSON_CONTENT_TYPE)
1021 .send()
1022 .await?;
1023
1024 Self::handle_response_static(response, max_response_size).await
1025 }
1026 })
1027 .await
1028 }
1029
1030 async fn post_json<B: serde::Serialize, T: for<'de> serde::Deserialize<'de>>(
1037 &self,
1038 url: Url,
1039 body: &B,
1040 ) -> AptosResult<AptosResponse<T>> {
1041 let body = serde_json::to_vec(body)?;
1042 let client = self.client.clone();
1043 let retry_config = self.retry_config.clone();
1044 let max_response_size = self.config.pool_config().max_response_size;
1045
1046 let executor = RetryExecutor::from_shared(retry_config);
1047 executor
1048 .execute(|| {
1049 let client = client.clone();
1050 let url = url.clone();
1051 let body = body.clone();
1052 async move {
1053 let response = client
1054 .post(url)
1055 .header(CONTENT_TYPE, JSON_CONTENT_TYPE)
1056 .header(ACCEPT, JSON_CONTENT_TYPE)
1057 .body(body)
1058 .send()
1059 .await?;
1060
1061 Self::handle_response_static(response, max_response_size).await
1062 }
1063 })
1064 .await
1065 }
1066
1067 fn truncate_error_body(body: String) -> String {
1073 if body.len() > MAX_ERROR_BODY_SIZE {
1074 let mut end = MAX_ERROR_BODY_SIZE;
1076 while end > 0 && !body.is_char_boundary(end) {
1077 end -= 1;
1078 }
1079 format!(
1080 "{}... [truncated, total: {} bytes]",
1081 &body[..end],
1082 body.len()
1083 )
1084 } else {
1085 body
1086 }
1087 }
1088
1089 async fn handle_response_static<T: for<'de> serde::Deserialize<'de>>(
1097 response: reqwest::Response,
1098 max_response_size: usize,
1099 ) -> AptosResult<AptosResponse<T>> {
1100 let status = response.status();
1101
1102 let ledger_version = response
1104 .headers()
1105 .get("x-aptos-ledger-version")
1106 .and_then(|v| v.to_str().ok())
1107 .and_then(|v| v.parse().ok());
1108 let ledger_timestamp = response
1109 .headers()
1110 .get("x-aptos-ledger-timestamp")
1111 .and_then(|v| v.to_str().ok())
1112 .and_then(|v| v.parse().ok());
1113 let epoch = response
1114 .headers()
1115 .get("x-aptos-epoch")
1116 .and_then(|v| v.to_str().ok())
1117 .and_then(|v| v.parse().ok());
1118 let block_height = response
1119 .headers()
1120 .get("x-aptos-block-height")
1121 .and_then(|v| v.to_str().ok())
1122 .and_then(|v| v.parse().ok());
1123 let oldest_ledger_version = response
1124 .headers()
1125 .get("x-aptos-oldest-ledger-version")
1126 .and_then(|v| v.to_str().ok())
1127 .and_then(|v| v.parse().ok());
1128 let cursor = response
1129 .headers()
1130 .get("x-aptos-cursor")
1131 .and_then(|v| v.to_str().ok())
1132 .map(ToString::to_string);
1133
1134 let retry_after_secs = response
1136 .headers()
1137 .get("retry-after")
1138 .and_then(|v| v.to_str().ok())
1139 .and_then(|v| v.parse().ok());
1140
1141 if status.is_success() {
1142 let bytes = crate::config::read_response_bounded(response, max_response_size).await?;
1145 let data: T = serde_json::from_slice(&bytes)?;
1146 Ok(AptosResponse {
1147 data,
1148 ledger_version,
1149 ledger_timestamp,
1150 epoch,
1151 block_height,
1152 oldest_ledger_version,
1153 cursor,
1154 })
1155 } else if status.as_u16() == 429 {
1156 Err(AptosError::RateLimited { retry_after_secs })
1159 } else {
1160 let error_bytes = crate::config::read_response_bounded(response, MAX_ERROR_BODY_SIZE)
1163 .await
1164 .ok();
1165 let error_text = error_bytes
1166 .and_then(|b| String::from_utf8(b).ok())
1167 .unwrap_or_default();
1168 let error_text = Self::truncate_error_body(error_text);
1169 let body: serde_json::Value = serde_json::from_str(&error_text).unwrap_or_default();
1170 let message = body
1171 .get("message")
1172 .and_then(|v| v.as_str())
1173 .unwrap_or("Unknown error")
1174 .to_string();
1175 let error_code = body
1176 .get("error_code")
1177 .and_then(|v| v.as_str())
1178 .map(ToString::to_string);
1179 let vm_error_code = body
1180 .get("vm_error_code")
1181 .and_then(serde_json::Value::as_u64);
1182
1183 Err(AptosError::api_with_details(
1184 status.as_u16(),
1185 message,
1186 error_code,
1187 vm_error_code,
1188 ))
1189 }
1190 }
1191
1192 #[allow(dead_code)]
1194 async fn handle_response<T: for<'de> serde::Deserialize<'de>>(
1195 &self,
1196 response: reqwest::Response,
1197 ) -> AptosResult<AptosResponse<T>> {
1198 let max_response_size = self.config.pool_config().max_response_size;
1199 Self::handle_response_static(response, max_response_size).await
1200 }
1201}
1202
1203fn append_start_limit(url: &mut Url, start: Option<&str>, limit: Option<u16>) {
1211 if start.is_none() && limit.is_none() {
1212 return;
1213 }
1214 let mut query = url.query_pairs_mut();
1215 if let Some(start) = start {
1216 query.append_pair("start", start);
1217 }
1218 if let Some(limit) = limit {
1219 query.append_pair("limit", &limit.to_string());
1220 }
1221}
1222
1223#[cfg(test)]
1224mod tests {
1225 use super::*;
1226 use crate::transaction::authenticator::{
1227 Ed25519PublicKey, Ed25519Signature, TransactionAuthenticator,
1228 };
1229 use crate::transaction::simulation::SimulateQueryOptions;
1230 use crate::transaction::types::{RawTransaction, SignedTransaction};
1231 use crate::types::ChainId;
1232 use wiremock::{
1233 Mock, MockServer, ResponseTemplate,
1234 matchers::{method, path, path_regex, query_param},
1235 };
1236
1237 #[test]
1238 fn test_build_url() {
1239 let client = FullnodeClient::new(AptosConfig::testnet()).unwrap();
1240 let url = client.build_url("accounts/0x1");
1241 assert!(url.as_str().contains("accounts/0x1"));
1242 }
1243
1244 fn create_mock_client(server: &MockServer) -> FullnodeClient {
1245 let url = format!("{}/v1", server.uri());
1247 let config = AptosConfig::custom(&url).unwrap().without_retry();
1248 FullnodeClient::new(config).unwrap()
1249 }
1250
1251 fn create_minimal_signed_transaction() -> SignedTransaction {
1253 use crate::transaction::payload::{EntryFunction, TransactionPayload};
1254
1255 let raw = RawTransaction::new(
1256 AccountAddress::ONE,
1257 0,
1258 TransactionPayload::EntryFunction(
1259 EntryFunction::apt_transfer(AccountAddress::ONE, 0).unwrap(),
1260 ),
1261 100_000,
1262 100,
1263 std::time::SystemTime::now()
1264 .duration_since(std::time::UNIX_EPOCH)
1265 .unwrap()
1266 .as_secs()
1267 .saturating_add(600),
1268 ChainId::testnet(),
1269 );
1270 let auth = TransactionAuthenticator::Ed25519 {
1271 public_key: Ed25519PublicKey([0u8; 32]),
1272 signature: Ed25519Signature([0u8; 64]),
1273 };
1274 SignedTransaction::new(raw, auth)
1275 }
1276
1277 fn simulate_response_json() -> serde_json::Value {
1278 serde_json::json!([{
1279 "success": true,
1280 "vm_status": "Executed successfully",
1281 "gas_used": "100",
1282 "max_gas_amount": "200000",
1283 "gas_unit_price": "100",
1284 "hash": "0x1",
1285 "changes": [],
1286 "events": []
1287 }])
1288 }
1289
1290 #[tokio::test]
1291 async fn test_get_ledger_info() {
1292 let server = MockServer::start().await;
1293
1294 Mock::given(method("GET"))
1295 .and(path("/v1"))
1296 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1297 "chain_id": 2,
1298 "epoch": "100",
1299 "ledger_version": "12345",
1300 "oldest_ledger_version": "0",
1301 "ledger_timestamp": "1000000",
1302 "node_role": "full_node",
1303 "oldest_block_height": "0",
1304 "block_height": "5000"
1305 })))
1306 .expect(1)
1307 .mount(&server)
1308 .await;
1309
1310 let client = create_mock_client(&server);
1311 let result = client.get_ledger_info().await.unwrap();
1312
1313 assert_eq!(result.data.chain_id, 2);
1314 assert_eq!(result.data.version().unwrap(), 12345);
1315 assert_eq!(result.data.height().unwrap(), 5000);
1316 }
1317
1318 #[tokio::test]
1319 async fn test_get_account() {
1320 let server = MockServer::start().await;
1321
1322 Mock::given(method("GET"))
1323 .and(path_regex(r"^/v1/accounts/0x[0-9a-f]+$"))
1324 .respond_with(
1325 ResponseTemplate::new(200)
1326 .set_body_json(serde_json::json!({
1327 "sequence_number": "42",
1328 "authentication_key": "0x0000000000000000000000000000000000000000000000000000000000000001"
1329 }))
1330 .insert_header("x-aptos-ledger-version", "12345"),
1331 )
1332 .expect(1)
1333 .mount(&server)
1334 .await;
1335
1336 let client = create_mock_client(&server);
1337 let result = client.get_account(AccountAddress::ONE).await.unwrap();
1338
1339 assert_eq!(result.data.sequence_number().unwrap(), 42);
1340 assert_eq!(result.ledger_version, Some(12345));
1341 }
1342
1343 #[tokio::test]
1344 async fn test_get_account_not_found() {
1345 let server = MockServer::start().await;
1346
1347 Mock::given(method("GET"))
1348 .and(path_regex(r"/v1/accounts/0x[0-9a-f]+"))
1349 .respond_with(ResponseTemplate::new(404).set_body_json(serde_json::json!({
1350 "message": "Account not found",
1351 "error_code": "account_not_found"
1352 })))
1353 .expect(1)
1354 .mount(&server)
1355 .await;
1356
1357 let client = create_mock_client(&server);
1358 let result = client.get_account(AccountAddress::ONE).await;
1359
1360 assert!(result.is_err());
1361 let err = result.unwrap_err();
1362 assert!(err.is_not_found());
1363 }
1364
1365 #[tokio::test]
1366 async fn test_get_account_resources() {
1367 let server = MockServer::start().await;
1368
1369 Mock::given(method("GET"))
1370 .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/resources"))
1371 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([
1372 {
1373 "type": "0x1::account::Account",
1374 "data": {"sequence_number": "10"}
1375 },
1376 {
1377 "type": "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>",
1378 "data": {"coin": {"value": "1000000"}}
1379 }
1380 ])))
1381 .expect(1)
1382 .mount(&server)
1383 .await;
1384
1385 let client = create_mock_client(&server);
1386 let result = client
1387 .get_account_resources(AccountAddress::ONE)
1388 .await
1389 .unwrap();
1390
1391 assert_eq!(result.data.len(), 2);
1392 assert!(result.data[0].typ.contains("Account"));
1393 }
1394
1395 #[tokio::test]
1396 async fn test_get_account_resource() {
1397 let server = MockServer::start().await;
1398
1399 Mock::given(method("GET"))
1400 .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/resource/.*"))
1401 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1402 "type": "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>",
1403 "data": {"coin": {"value": "5000000"}}
1404 })))
1405 .expect(1)
1406 .mount(&server)
1407 .await;
1408
1409 let client = create_mock_client(&server);
1410 let result = client
1411 .get_account_resource(
1412 AccountAddress::ONE,
1413 "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>",
1414 )
1415 .await
1416 .unwrap();
1417
1418 assert!(result.data.typ.contains("CoinStore"));
1419 }
1420
1421 #[tokio::test]
1422 async fn test_get_account_modules() {
1423 let server = MockServer::start().await;
1424
1425 Mock::given(method("GET"))
1426 .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/modules"))
1427 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([
1428 {
1429 "bytecode": "0xabc123",
1430 "abi": {
1431 "address": "0x1",
1432 "name": "coin",
1433 "exposed_functions": [],
1434 "structs": []
1435 }
1436 }
1437 ])))
1438 .expect(1)
1439 .mount(&server)
1440 .await;
1441
1442 let client = create_mock_client(&server);
1443 let result = client
1444 .get_account_modules(AccountAddress::ONE)
1445 .await
1446 .unwrap();
1447
1448 assert_eq!(result.data.len(), 1);
1449 assert!(result.data[0].abi.is_some());
1450 }
1451
1452 #[tokio::test]
1453 async fn test_get_account_resources_paginated_sends_start_and_limit() {
1454 let server = MockServer::start().await;
1455
1456 Mock::given(method("GET"))
1458 .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/resources"))
1459 .and(query_param("start", "42"))
1460 .and(query_param("limit", "9"))
1461 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([])))
1462 .expect(1)
1463 .mount(&server)
1464 .await;
1465
1466 let client = create_mock_client(&server);
1467 let result = client
1468 .get_account_resources_paginated(AccountAddress::ONE, Some("42"), Some(9))
1469 .await
1470 .unwrap();
1471 assert_eq!(result.data.len(), 0);
1472 }
1473
1474 #[tokio::test]
1475 async fn test_get_account_resources_paginated_round_trips_opaque_cursor() {
1476 let server = MockServer::start().await;
1480 let opaque = "0x0a1b2c3d_state_key_token";
1481 Mock::given(method("GET"))
1482 .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/resources"))
1483 .and(query_param("start", opaque))
1484 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([])))
1485 .expect(1)
1486 .mount(&server)
1487 .await;
1488
1489 let client = create_mock_client(&server);
1490 client
1491 .get_account_resources_paginated(AccountAddress::ONE, Some(opaque), None)
1492 .await
1493 .unwrap();
1494 }
1495
1496 #[tokio::test]
1497 async fn test_get_account_resources_no_pagination_omits_query() {
1498 let server = MockServer::start().await;
1499
1500 Mock::given(method("GET"))
1503 .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/resources$"))
1504 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([])))
1505 .expect(1)
1506 .mount(&server)
1507 .await;
1508
1509 let client = create_mock_client(&server);
1510 client
1511 .get_account_resources(AccountAddress::ONE)
1512 .await
1513 .unwrap();
1514 }
1515
1516 #[tokio::test]
1517 async fn test_get_account_resources_paginated_sends_start_only() {
1518 let server = MockServer::start().await;
1521 Mock::given(method("GET"))
1522 .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/resources"))
1523 .and(query_param("start", "1234"))
1524 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([])))
1525 .expect(1)
1526 .mount(&server)
1527 .await;
1528
1529 let client = create_mock_client(&server);
1530 client
1531 .get_account_resources_paginated(AccountAddress::ONE, Some("1234"), None)
1532 .await
1533 .unwrap();
1534 }
1535
1536 #[tokio::test]
1537 async fn test_get_account_modules_paginated_sends_start_and_limit() {
1538 let server = MockServer::start().await;
1539 Mock::given(method("GET"))
1540 .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/modules"))
1541 .and(query_param("start", "7"))
1542 .and(query_param("limit", "100"))
1543 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([])))
1544 .expect(1)
1545 .mount(&server)
1546 .await;
1547
1548 let client = create_mock_client(&server);
1549 client
1550 .get_account_modules_paginated(AccountAddress::ONE, Some("7"), Some(100))
1551 .await
1552 .unwrap();
1553 }
1554
1555 #[tokio::test]
1556 async fn test_get_account_modules_no_pagination_omits_query() {
1557 let server = MockServer::start().await;
1560 Mock::given(method("GET"))
1561 .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/modules$"))
1562 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([])))
1563 .expect(1)
1564 .mount(&server)
1565 .await;
1566
1567 let client = create_mock_client(&server);
1568 client
1569 .get_account_modules(AccountAddress::ONE)
1570 .await
1571 .unwrap();
1572 }
1573
1574 #[tokio::test]
1575 async fn test_get_account_modules_paginated_sends_limit_only() {
1576 let server = MockServer::start().await;
1577
1578 Mock::given(method("GET"))
1581 .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/modules"))
1582 .and(query_param("limit", "25"))
1583 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([])))
1584 .expect(1)
1585 .mount(&server)
1586 .await;
1587
1588 let client = create_mock_client(&server);
1589 client
1590 .get_account_modules_paginated(AccountAddress::ONE, None, Some(25))
1591 .await
1592 .unwrap();
1593 }
1594
1595 #[tokio::test]
1596 async fn test_estimate_gas_price() {
1597 let server = MockServer::start().await;
1598
1599 Mock::given(method("GET"))
1600 .and(path("/v1/estimate_gas_price"))
1601 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1602 "deprioritized_gas_estimate": 50,
1603 "gas_estimate": 100,
1604 "prioritized_gas_estimate": 150
1605 })))
1606 .expect(1)
1607 .mount(&server)
1608 .await;
1609
1610 let client = create_mock_client(&server);
1611 let result = client.estimate_gas_price().await.unwrap();
1612
1613 assert_eq!(result.data.gas_estimate, 100);
1614 assert_eq!(result.data.low(), 50);
1615 assert_eq!(result.data.high(), 150);
1616 }
1617
1618 #[tokio::test]
1619 async fn test_get_transaction_by_hash() {
1620 let server = MockServer::start().await;
1621
1622 Mock::given(method("GET"))
1623 .and(path_regex(r"/v1/transactions/by_hash/0x[0-9a-f]+"))
1624 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1625 "version": "12345",
1626 "hash": "0x0000000000000000000000000000000000000000000000000000000000000001",
1627 "success": true,
1628 "vm_status": "Executed successfully"
1629 })))
1630 .expect(1)
1631 .mount(&server)
1632 .await;
1633
1634 let client = create_mock_client(&server);
1635 let hash = HashValue::from_hex(
1636 "0x0000000000000000000000000000000000000000000000000000000000000001",
1637 )
1638 .unwrap();
1639 let result = client.get_transaction_by_hash(&hash).await.unwrap();
1640
1641 assert!(
1642 result
1643 .data
1644 .get("success")
1645 .and_then(serde_json::Value::as_bool)
1646 .unwrap()
1647 );
1648 }
1649
1650 #[tokio::test]
1651 async fn test_wait_for_transaction_success() {
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 "type": "user_transaction",
1658 "version": "12345",
1659 "hash": "0x0000000000000000000000000000000000000000000000000000000000000001",
1660 "success": true,
1661 "vm_status": "Executed successfully"
1662 })))
1663 .expect(1..)
1664 .mount(&server)
1665 .await;
1666
1667 let client = create_mock_client(&server);
1668 let hash = HashValue::from_hex(
1669 "0x0000000000000000000000000000000000000000000000000000000000000001",
1670 )
1671 .unwrap();
1672 let result = client
1673 .wait_for_transaction(&hash, Some(Duration::from_secs(5)))
1674 .await
1675 .unwrap();
1676
1677 assert!(
1678 result
1679 .data
1680 .get("success")
1681 .and_then(serde_json::Value::as_bool)
1682 .unwrap()
1683 );
1684 }
1685
1686 #[tokio::test]
1687 async fn test_server_error_retryable() {
1688 let server = MockServer::start().await;
1689
1690 Mock::given(method("GET"))
1691 .and(path("/v1"))
1692 .respond_with(ResponseTemplate::new(503).set_body_json(serde_json::json!({
1693 "message": "Service temporarily unavailable"
1694 })))
1695 .expect(1)
1696 .mount(&server)
1697 .await;
1698
1699 let url = format!("{}/v1", server.uri());
1700 let config = AptosConfig::custom(&url).unwrap().without_retry();
1701 let client = FullnodeClient::new(config).unwrap();
1702 let result = client.get_ledger_info().await;
1703
1704 assert!(result.is_err());
1705 assert!(result.unwrap_err().is_retryable());
1706 }
1707
1708 #[tokio::test]
1709 async fn test_rate_limited() {
1710 let server = MockServer::start().await;
1711
1712 Mock::given(method("GET"))
1713 .and(path("/v1"))
1714 .respond_with(
1715 ResponseTemplate::new(429)
1716 .set_body_json(serde_json::json!({
1717 "message": "Rate limited"
1718 }))
1719 .insert_header("retry-after", "30"),
1720 )
1721 .expect(1)
1722 .mount(&server)
1723 .await;
1724
1725 let url = format!("{}/v1", server.uri());
1726 let config = AptosConfig::custom(&url).unwrap().without_retry();
1727 let client = FullnodeClient::new(config).unwrap();
1728 let result = client.get_ledger_info().await;
1729
1730 assert!(result.is_err());
1731 assert!(result.unwrap_err().is_retryable());
1732 }
1733
1734 #[tokio::test]
1735 async fn test_get_block_by_height() {
1736 let server = MockServer::start().await;
1737
1738 Mock::given(method("GET"))
1739 .and(path_regex(r"/v1/blocks/by_height/\d+"))
1740 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1741 "block_height": "1000",
1742 "block_hash": "0xabc",
1743 "block_timestamp": "1234567890",
1744 "first_version": "100",
1745 "last_version": "200"
1746 })))
1747 .expect(1)
1748 .mount(&server)
1749 .await;
1750
1751 let client = create_mock_client(&server);
1752 let result = client.get_block_by_height(1000, false).await.unwrap();
1753
1754 assert!(result.data.get("block_height").is_some());
1755 }
1756
1757 #[tokio::test]
1758 async fn test_view() {
1759 let server = MockServer::start().await;
1760
1761 Mock::given(method("POST"))
1762 .and(path("/v1/view"))
1763 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!(["1000000"])))
1764 .expect(1)
1765 .mount(&server)
1766 .await;
1767
1768 let client = create_mock_client(&server);
1769 let result: AptosResponse<Vec<serde_json::Value>> = client
1770 .view(
1771 "0x1::coin::balance",
1772 vec!["0x1::aptos_coin::AptosCoin".to_string()],
1773 vec![serde_json::json!("0x1")],
1774 )
1775 .await
1776 .unwrap();
1777
1778 assert_eq!(result.data.len(), 1);
1779 }
1780
1781 #[tokio::test]
1782 async fn test_view_bcs_args_posts_bcs_request() {
1783 let server = MockServer::start().await;
1784
1785 let args = vec![aptos_bcs::to_bytes(&AccountAddress::ONE).unwrap()];
1790 let expected_body = aptos_bcs::to_bytes(
1791 &crate::transaction::EntryFunction::from_function_id(
1792 "0x1::coin::balance",
1793 vec![],
1794 args.clone(),
1795 )
1796 .unwrap(),
1797 )
1798 .unwrap();
1799
1800 Mock::given(method("POST"))
1801 .and(path("/v1/view"))
1802 .and(wiremock::matchers::header(
1803 "content-type",
1804 "application/x.aptos.view_function+bcs",
1805 ))
1806 .and(wiremock::matchers::body_bytes(expected_body))
1807 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!(["1000000"])))
1808 .expect(1)
1809 .mount(&server)
1810 .await;
1811
1812 let client = create_mock_client(&server);
1813 let result = client
1814 .view_bcs_args("0x1::coin::balance", vec![], args)
1815 .await
1816 .unwrap()
1817 .into_inner();
1818
1819 assert_eq!(result.len(), 1);
1820 assert_eq!(result[0].as_str().unwrap(), "1000000");
1821 }
1822
1823 #[tokio::test]
1824 async fn test_simulate_transaction_with_estimate_gas_unit_price() {
1825 let server = MockServer::start().await;
1826
1827 Mock::given(method("POST"))
1828 .and(path("/v1/transactions/simulate"))
1829 .and(|req: &wiremock::Request| {
1830 req.url
1831 .query()
1832 .is_some_and(|q| q.contains("estimate_gas_unit_price=true"))
1833 })
1834 .respond_with(ResponseTemplate::new(200).set_body_json(simulate_response_json()))
1835 .expect(1)
1836 .mount(&server)
1837 .await;
1838
1839 let client = create_mock_client(&server);
1840 let signed = create_minimal_signed_transaction();
1841 let opts = SimulateQueryOptions::new().estimate_gas_unit_price(true);
1842 let result = client
1843 .simulate_transaction_with_options(&signed, opts)
1844 .await
1845 .unwrap();
1846 assert!(!result.data.is_empty());
1847 }
1848
1849 #[tokio::test]
1850 async fn test_simulate_transaction_with_estimate_max_gas_amount() {
1851 let server = MockServer::start().await;
1852
1853 Mock::given(method("POST"))
1854 .and(path("/v1/transactions/simulate"))
1855 .and(|req: &wiremock::Request| {
1856 req.url
1857 .query()
1858 .is_some_and(|q| q.contains("estimate_max_gas_amount=true"))
1859 })
1860 .respond_with(ResponseTemplate::new(200).set_body_json(simulate_response_json()))
1861 .expect(1)
1862 .mount(&server)
1863 .await;
1864
1865 let client = create_mock_client(&server);
1866 let signed = create_minimal_signed_transaction();
1867 let opts = SimulateQueryOptions::new().estimate_max_gas_amount(true);
1868 let result = client
1869 .simulate_transaction_with_options(&signed, opts)
1870 .await
1871 .unwrap();
1872 assert!(!result.data.is_empty());
1873 }
1874
1875 #[tokio::test]
1876 async fn test_simulate_transaction_with_estimate_prioritized_gas_unit_price() {
1877 let server = MockServer::start().await;
1878
1879 Mock::given(method("POST"))
1880 .and(path("/v1/transactions/simulate"))
1881 .and(|req: &wiremock::Request| {
1882 req.url
1883 .query()
1884 .is_some_and(|q| q.contains("estimate_prioritized_gas_unit_price=true"))
1885 })
1886 .respond_with(ResponseTemplate::new(200).set_body_json(simulate_response_json()))
1887 .expect(1)
1888 .mount(&server)
1889 .await;
1890
1891 let client = create_mock_client(&server);
1892 let signed = create_minimal_signed_transaction();
1893 let opts = SimulateQueryOptions::new().estimate_prioritized_gas_unit_price(true);
1894 let result = client
1895 .simulate_transaction_with_options(&signed, opts)
1896 .await
1897 .unwrap();
1898 assert!(!result.data.is_empty());
1899 }
1900
1901 #[tokio::test]
1902 async fn test_simulate_transaction_with_all_options() {
1903 let server = MockServer::start().await;
1904
1905 Mock::given(method("POST"))
1906 .and(path("/v1/transactions/simulate"))
1907 .and(|req: &wiremock::Request| {
1908 req.url.query().is_some_and(|q| {
1909 q.contains("estimate_gas_unit_price=true")
1910 && q.contains("estimate_max_gas_amount=true")
1911 && q.contains("estimate_prioritized_gas_unit_price=true")
1912 })
1913 })
1914 .respond_with(ResponseTemplate::new(200).set_body_json(simulate_response_json()))
1915 .expect(1)
1916 .mount(&server)
1917 .await;
1918
1919 let client = create_mock_client(&server);
1920 let signed = create_minimal_signed_transaction();
1921 let opts = SimulateQueryOptions::new()
1922 .estimate_gas_unit_price(true)
1923 .estimate_max_gas_amount(true)
1924 .estimate_prioritized_gas_unit_price(true);
1925 let result = client
1926 .simulate_transaction_with_options(&signed, opts)
1927 .await
1928 .unwrap();
1929 assert!(!result.data.is_empty());
1930 }
1931
1932 #[tokio::test]
1933 async fn test_simulate_transaction_without_options() {
1934 let server = MockServer::start().await;
1935
1936 Mock::given(method("POST"))
1938 .and(path("/v1/transactions/simulate"))
1939 .and(|req: &wiremock::Request| {
1940 req.url.query().is_none_or(|q| {
1942 !q.contains("estimate_gas_unit_price=")
1943 && !q.contains("estimate_max_gas_amount=")
1944 && !q.contains("estimate_prioritized_gas_unit_price=")
1945 })
1946 })
1947 .respond_with(ResponseTemplate::new(200).set_body_json(simulate_response_json()))
1948 .expect(1)
1949 .mount(&server)
1950 .await;
1951
1952 let client = create_mock_client(&server);
1953 let signed = create_minimal_signed_transaction();
1954 let result = client.simulate_transaction(&signed).await.unwrap();
1955 assert!(!result.data.is_empty());
1956 }
1957
1958 #[tokio::test]
1959 async fn test_get_table_item() {
1960 let server = MockServer::start().await;
1961
1962 Mock::given(method("POST"))
1966 .and(path_regex(r"^/v1/tables/0x[0-9a-f]+/item$"))
1967 .and(wiremock::matchers::body_json(serde_json::json!({
1968 "key_type": "address",
1969 "value_type": "u64",
1970 "key": "0x1",
1971 })))
1972 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!("42")))
1973 .expect(1)
1974 .mount(&server)
1975 .await;
1976
1977 let client = create_mock_client(&server);
1978 let result = client
1979 .get_table_item(
1980 "0x0000000000000000000000000000000000000000000000000000000000000abc",
1981 "address",
1982 "u64",
1983 serde_json::json!("0x1"),
1984 )
1985 .await
1986 .unwrap();
1987
1988 assert_eq!(result.data, serde_json::json!("42"));
1989 }
1990
1991 #[tokio::test]
1992 async fn test_get_table_item_missing_key_is_404() {
1993 let server = MockServer::start().await;
1994
1995 Mock::given(method("POST"))
1996 .and(path_regex(r"^/v1/tables/0x[0-9a-f]+/item$"))
1997 .respond_with(ResponseTemplate::new(404).set_body_json(serde_json::json!({
1998 "message": "Table item not found",
1999 "error_code": "table_item_not_found"
2000 })))
2001 .expect(1)
2002 .mount(&server)
2003 .await;
2004
2005 let client = create_mock_client(&server);
2006 let err = client
2007 .get_table_item("0xabc", "address", "u64", serde_json::json!("0x2"))
2008 .await
2009 .unwrap_err();
2010
2011 assert!(matches!(
2012 err,
2013 AptosError::Api {
2014 status_code: 404,
2015 ..
2016 }
2017 ));
2018 }
2019
2020 #[tokio::test]
2021 async fn test_get_transaction_by_version() {
2022 let server = MockServer::start().await;
2023
2024 Mock::given(method("GET"))
2025 .and(path("/v1/transactions/by_version/100"))
2026 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2027 "version": "100",
2028 "hash": "0xdead",
2029 "success": true
2030 })))
2031 .expect(1)
2032 .mount(&server)
2033 .await;
2034
2035 let client = create_mock_client(&server);
2036 let result = client.get_transaction_by_version(100).await.unwrap();
2037
2038 assert_eq!(result.data.get("version").unwrap(), "100");
2039 }
2040
2041 #[tokio::test]
2042 async fn test_get_transactions_with_pagination() {
2043 let server = MockServer::start().await;
2044
2045 Mock::given(method("GET"))
2046 .and(path("/v1/transactions"))
2047 .and(query_param("start", "10"))
2048 .and(query_param("limit", "2"))
2049 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([
2050 {"version": "10"},
2051 {"version": "11"}
2052 ])))
2053 .expect(1)
2054 .mount(&server)
2055 .await;
2056
2057 let client = create_mock_client(&server);
2058 let result = client.get_transactions(Some(10), Some(2)).await.unwrap();
2059
2060 assert_eq!(result.data.len(), 2);
2061 }
2062
2063 #[tokio::test]
2064 async fn test_get_account_transactions() {
2065 let server = MockServer::start().await;
2066
2067 Mock::given(method("GET"))
2068 .and(path_regex(r"^/v1/accounts/0x[0-9a-f]+/transactions$"))
2069 .and(query_param("start", "0"))
2070 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([
2071 {"version": "5", "sender": "0x1"}
2072 ])))
2073 .expect(1)
2074 .mount(&server)
2075 .await;
2076
2077 let client = create_mock_client(&server);
2078 let result = client
2079 .get_account_transactions(AccountAddress::ONE, Some(0), None)
2080 .await
2081 .unwrap();
2082
2083 assert_eq!(result.data.len(), 1);
2084 }
2085
2086 #[tokio::test]
2087 async fn test_get_events_by_creation_number() {
2088 let server = MockServer::start().await;
2089
2090 Mock::given(method("GET"))
2091 .and(path_regex(r"^/v1/accounts/0x[0-9a-f]+/events/7$"))
2092 .and(query_param("limit", "25"))
2093 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([
2094 {"sequence_number": "0", "type": "0x1::coin::DepositEvent"}
2095 ])))
2096 .expect(1)
2097 .mount(&server)
2098 .await;
2099
2100 let client = create_mock_client(&server);
2101 let result = client
2102 .get_events_by_creation_number(AccountAddress::ONE, 7, None, Some(25))
2103 .await
2104 .unwrap();
2105
2106 assert_eq!(result.data.len(), 1);
2107 }
2108}