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 retry_config(&self) -> &RetryConfig {
124 &self.retry_config
125 }
126
127 pub async fn get_ledger_info(&self) -> AptosResult<AptosResponse<LedgerInfo>> {
136 let url = self.build_url("");
137 self.get_json(url).await
138 }
139
140 pub async fn get_account(
149 &self,
150 address: AccountAddress,
151 ) -> AptosResult<AptosResponse<AccountData>> {
152 let url = self.build_url(&format!("accounts/{address}"));
153 self.get_json(url).await
154 }
155
156 pub async fn get_sequence_number(&self, address: AccountAddress) -> AptosResult<u64> {
163 let account = self.get_account(address).await?;
164 account
165 .data
166 .sequence_number()
167 .map_err(|e| AptosError::Internal(format!("failed to parse sequence number: {e}")))
168 }
169
170 pub async fn get_account_resources(
181 &self,
182 address: AccountAddress,
183 ) -> AptosResult<AptosResponse<Vec<Resource>>> {
184 self.get_account_resources_paginated(address, None, None)
185 .await
186 }
187
188 pub async fn get_account_resources_paginated(
208 &self,
209 address: AccountAddress,
210 start: Option<&str>,
211 limit: Option<u16>,
212 ) -> AptosResult<AptosResponse<Vec<Resource>>> {
213 let mut url = self.build_url(&format!("accounts/{address}/resources"));
214 append_start_limit(&mut url, start, limit);
215 self.get_json(url).await
216 }
217
218 pub async fn get_account_resource(
225 &self,
226 address: AccountAddress,
227 resource_type: &str,
228 ) -> AptosResult<AptosResponse<Resource>> {
229 let url = self.build_url(&format!(
230 "accounts/{}/resource/{}",
231 address,
232 urlencoding::encode(resource_type)
233 ));
234 self.get_json(url).await
235 }
236
237 pub async fn get_account_modules(
249 &self,
250 address: AccountAddress,
251 ) -> AptosResult<AptosResponse<Vec<MoveModule>>> {
252 self.get_account_modules_paginated(address, None, None)
253 .await
254 }
255
256 pub async fn get_account_modules_paginated(
267 &self,
268 address: AccountAddress,
269 start: Option<&str>,
270 limit: Option<u16>,
271 ) -> AptosResult<AptosResponse<Vec<MoveModule>>> {
272 let mut url = self.build_url(&format!("accounts/{address}/modules"));
273 append_start_limit(&mut url, start, limit);
274 self.get_json(url).await
275 }
276
277 pub async fn get_account_module(
284 &self,
285 address: AccountAddress,
286 module_name: &str,
287 ) -> AptosResult<AptosResponse<MoveModule>> {
288 let url = self.build_url(&format!("accounts/{address}/module/{module_name}"));
289 self.get_json(url).await
290 }
291
292 pub async fn get_account_balance(&self, address: AccountAddress) -> AptosResult<u64> {
301 let result = self
304 .view(
305 "0x1::coin::balance",
306 vec!["0x1::aptos_coin::AptosCoin".to_string()],
307 vec![serde_json::json!(address.to_string())],
308 )
309 .await?;
310
311 let balance_str = result
313 .data
314 .first()
315 .and_then(|v| v.as_str())
316 .ok_or_else(|| AptosError::Internal("failed to parse balance response".into()))?;
317
318 balance_str
319 .parse()
320 .map_err(|_| AptosError::Internal("failed to parse balance as u64".into()))
321 }
322
323 pub async fn submit_transaction(
335 &self,
336 signed_txn: &SignedTransaction,
337 ) -> AptosResult<AptosResponse<PendingTransaction>> {
338 let url = self.build_url("transactions");
339 let bcs_bytes = signed_txn.to_bcs()?;
340 let client = self.client.clone();
341 let retry_config = self.retry_config.clone();
342 let max_response_size = self.config.pool_config().max_response_size;
343
344 let executor = RetryExecutor::from_shared(retry_config);
345 executor
346 .execute(|| {
347 let client = client.clone();
348 let url = url.clone();
349 let bcs_bytes = bcs_bytes.clone();
350 async move {
351 let response = client
352 .post(url)
353 .header(CONTENT_TYPE, BCS_CONTENT_TYPE)
354 .header(ACCEPT, JSON_CONTENT_TYPE)
355 .body(bcs_bytes)
356 .send()
357 .await?;
358
359 Self::handle_response_static(response, max_response_size).await
360 }
361 })
362 .await
363 }
364
365 pub async fn submit_and_wait(
372 &self,
373 signed_txn: &SignedTransaction,
374 timeout: Option<Duration>,
375 ) -> AptosResult<AptosResponse<serde_json::Value>> {
376 let pending = self.submit_transaction(signed_txn).await?;
377 self.wait_for_transaction(&pending.data.hash, timeout).await
378 }
379
380 pub async fn get_transaction_by_hash(
387 &self,
388 hash: &HashValue,
389 ) -> AptosResult<AptosResponse<serde_json::Value>> {
390 let url = self.build_url(&format!("transactions/by_hash/{hash}"));
391 self.get_json(url).await
392 }
393
394 pub async fn wait_for_transaction(
403 &self,
404 hash: &HashValue,
405 timeout: Option<Duration>,
406 ) -> AptosResult<AptosResponse<serde_json::Value>> {
407 let timeout = timeout.unwrap_or(Duration::from_secs(DEFAULT_TRANSACTION_WAIT_TIMEOUT_SECS));
408 let start = std::time::Instant::now();
409
410 let initial_interval = Duration::from_millis(200);
412 let max_interval = Duration::from_secs(2);
413 let mut current_interval = initial_interval;
414
415 loop {
416 match self.get_transaction_by_hash(hash).await {
417 Ok(response) => {
418 if response.data.get("version").is_some() {
420 let success = response
422 .data
423 .get("success")
424 .and_then(serde_json::Value::as_bool);
425 if success == Some(false) {
426 let vm_status = response
427 .data
428 .get("vm_status")
429 .and_then(|v| v.as_str())
430 .unwrap_or("unknown")
431 .to_string();
432 return Err(AptosError::ExecutionFailed { vm_status });
433 }
434 return Ok(response);
435 }
436 }
437 Err(AptosError::Api {
438 status_code: 404, ..
439 }) => {
440 }
442 Err(e) => return Err(e),
443 }
444
445 if start.elapsed() >= timeout {
446 return Err(AptosError::TransactionTimeout {
447 hash: hash.to_string(),
448 timeout_secs: timeout.as_secs(),
449 });
450 }
451
452 tokio::time::sleep(current_interval).await;
453
454 current_interval = std::cmp::min(current_interval * 2, max_interval);
456 }
457 }
458
459 pub async fn simulate_transaction(
471 &self,
472 signed_txn: &SignedTransaction,
473 ) -> AptosResult<AptosResponse<Vec<serde_json::Value>>> {
474 self.simulate_transaction_with_options(signed_txn, None as Option<SimulateQueryOptions>)
475 .await
476 }
477
478 pub async fn simulate_transaction_with_options(
494 &self,
495 signed_txn: &SignedTransaction,
496 options: impl Into<Option<SimulateQueryOptions>>,
497 ) -> AptosResult<AptosResponse<Vec<serde_json::Value>>> {
498 let mut url = self.build_url("transactions/simulate");
499 if let Some(opts) = options.into() {
500 let mut pairs = url.query_pairs_mut();
501 if opts.estimate_gas_unit_price {
502 pairs.append_pair("estimate_gas_unit_price", "true");
503 }
504 if opts.estimate_max_gas_amount {
505 pairs.append_pair("estimate_max_gas_amount", "true");
506 }
507 if opts.estimate_prioritized_gas_unit_price {
508 pairs.append_pair("estimate_prioritized_gas_unit_price", "true");
509 }
510 }
511 let bcs_bytes = signed_txn.for_simulate_endpoint().to_bcs()?;
512 let client = self.client.clone();
513 let retry_config = self.retry_config.clone();
514 let max_response_size = self.config.pool_config().max_response_size;
515
516 let executor = RetryExecutor::from_shared(retry_config);
517 executor
518 .execute(|| {
519 let client = client.clone();
520 let url = url.clone();
521 let bcs_bytes = bcs_bytes.clone();
522 async move {
523 let response = client
524 .post(url)
525 .header(CONTENT_TYPE, BCS_CONTENT_TYPE)
526 .header(ACCEPT, JSON_CONTENT_TYPE)
527 .body(bcs_bytes)
528 .send()
529 .await?;
530
531 Self::handle_response_static(response, max_response_size).await
532 }
533 })
534 .await
535 }
536
537 pub async fn estimate_gas_price(&self) -> AptosResult<AptosResponse<GasEstimation>> {
546 let url = self.build_url("estimate_gas_price");
547 self.get_json(url).await
548 }
549
550 pub async fn view(
559 &self,
560 function: &str,
561 type_args: Vec<String>,
562 args: Vec<serde_json::Value>,
563 ) -> AptosResult<AptosResponse<Vec<serde_json::Value>>> {
564 let url = self.build_url("view");
565
566 let body = serde_json::json!({
567 "function": function,
568 "type_arguments": type_args,
569 "arguments": args,
570 });
571
572 let client = self.client.clone();
573 let retry_config = self.retry_config.clone();
574 let max_response_size = self.config.pool_config().max_response_size;
575
576 let executor = RetryExecutor::from_shared(retry_config);
577 executor
578 .execute(|| {
579 let client = client.clone();
580 let url = url.clone();
581 let body = body.clone();
582 async move {
583 let response = client
584 .post(url)
585 .header(CONTENT_TYPE, JSON_CONTENT_TYPE)
586 .header(ACCEPT, JSON_CONTENT_TYPE)
587 .json(&body)
588 .send()
589 .await?;
590
591 Self::handle_response_static(response, max_response_size).await
592 }
593 })
594 .await
595 }
596
597 pub async fn view_bcs(
619 &self,
620 function: &str,
621 type_args: Vec<String>,
622 args: Vec<Vec<u8>>,
623 ) -> AptosResult<AptosResponse<Vec<u8>>> {
624 let url = self.build_url("view");
625
626 let hex_args: Vec<serde_json::Value> = args
629 .iter()
630 .map(|bytes| serde_json::json!(const_hex::encode_prefixed(bytes)))
631 .collect();
632
633 let body = serde_json::json!({
634 "function": function,
635 "type_arguments": type_args,
636 "arguments": hex_args,
637 });
638
639 let client = self.client.clone();
640 let retry_config = self.retry_config.clone();
641 let max_response_size = self.config.pool_config().max_response_size;
642
643 let executor = RetryExecutor::from_shared(retry_config);
644 executor
645 .execute(|| {
646 let client = client.clone();
647 let url = url.clone();
648 let body = body.clone();
649 async move {
650 let response = client
651 .post(url)
652 .header(CONTENT_TYPE, JSON_CONTENT_TYPE)
653 .header(ACCEPT, BCS_VIEW_CONTENT_TYPE)
654 .json(&body)
655 .send()
656 .await?;
657
658 let status = response.status();
660 if !status.is_success() {
661 let error_bytes =
664 crate::config::read_response_bounded(response, MAX_ERROR_BODY_SIZE)
665 .await
666 .ok();
667 let error_text = error_bytes
668 .and_then(|b| String::from_utf8(b).ok())
669 .unwrap_or_default();
670 return Err(AptosError::Api {
671 status_code: status.as_u16(),
672 message: Self::truncate_error_body(error_text),
673 error_code: None,
674 vm_error_code: None,
675 });
676 }
677
678 let bytes =
681 crate::config::read_response_bounded(response, max_response_size).await?;
682 Ok(AptosResponse::new(bytes))
683 }
684 })
685 .await
686 }
687
688 pub async fn get_events_by_event_handle(
697 &self,
698 address: AccountAddress,
699 event_handle_struct: &str,
700 field_name: &str,
701 start: Option<u64>,
702 limit: Option<u64>,
703 ) -> AptosResult<AptosResponse<Vec<serde_json::Value>>> {
704 let mut url = self.build_url(&format!(
705 "accounts/{}/events/{}/{}",
706 address,
707 urlencoding::encode(event_handle_struct),
708 field_name
709 ));
710
711 {
712 let mut query = url.query_pairs_mut();
713 if let Some(start) = start {
714 query.append_pair("start", &start.to_string());
715 }
716 if let Some(limit) = limit {
717 query.append_pair("limit", &limit.to_string());
718 }
719 }
720
721 self.get_json(url).await
722 }
723
724 pub async fn get_block_by_height(
733 &self,
734 height: u64,
735 with_transactions: bool,
736 ) -> AptosResult<AptosResponse<serde_json::Value>> {
737 let mut url = self.build_url(&format!("blocks/by_height/{height}"));
738 url.query_pairs_mut()
739 .append_pair("with_transactions", &with_transactions.to_string());
740 self.get_json(url).await
741 }
742
743 pub async fn get_block_by_version(
750 &self,
751 version: u64,
752 with_transactions: bool,
753 ) -> AptosResult<AptosResponse<serde_json::Value>> {
754 let mut url = self.build_url(&format!("blocks/by_version/{version}"));
755 url.query_pairs_mut()
756 .append_pair("with_transactions", &with_transactions.to_string());
757 self.get_json(url).await
758 }
759
760 fn build_url(&self, path: &str) -> Url {
763 let mut url = self.config.fullnode_url().clone();
764 if !path.is_empty() {
765 let base_path = url.path();
767 let needs_slash = !base_path.ends_with('/');
768 let new_len = base_path.len() + path.len() + usize::from(needs_slash);
769 let mut new_path = String::with_capacity(new_len);
770 new_path.push_str(base_path);
771 if needs_slash {
772 new_path.push('/');
773 }
774 new_path.push_str(path);
775 url.set_path(&new_path);
776 }
777 url
778 }
779
780 async fn get_json<T: for<'de> serde::Deserialize<'de>>(
781 &self,
782 url: Url,
783 ) -> AptosResult<AptosResponse<T>> {
784 let client = self.client.clone();
785 let url_clone = url.clone();
786 let retry_config = self.retry_config.clone();
787 let max_response_size = self.config.pool_config().max_response_size;
788
789 let executor = RetryExecutor::from_shared(retry_config);
790 executor
791 .execute(|| {
792 let client = client.clone();
793 let url = url_clone.clone();
794 async move {
795 let response = client
796 .get(url)
797 .header(ACCEPT, JSON_CONTENT_TYPE)
798 .send()
799 .await?;
800
801 Self::handle_response_static(response, max_response_size).await
802 }
803 })
804 .await
805 }
806
807 fn truncate_error_body(body: String) -> String {
813 if body.len() > MAX_ERROR_BODY_SIZE {
814 let mut end = MAX_ERROR_BODY_SIZE;
816 while end > 0 && !body.is_char_boundary(end) {
817 end -= 1;
818 }
819 format!(
820 "{}... [truncated, total: {} bytes]",
821 &body[..end],
822 body.len()
823 )
824 } else {
825 body
826 }
827 }
828
829 async fn handle_response_static<T: for<'de> serde::Deserialize<'de>>(
837 response: reqwest::Response,
838 max_response_size: usize,
839 ) -> AptosResult<AptosResponse<T>> {
840 let status = response.status();
841
842 let ledger_version = response
844 .headers()
845 .get("x-aptos-ledger-version")
846 .and_then(|v| v.to_str().ok())
847 .and_then(|v| v.parse().ok());
848 let ledger_timestamp = response
849 .headers()
850 .get("x-aptos-ledger-timestamp")
851 .and_then(|v| v.to_str().ok())
852 .and_then(|v| v.parse().ok());
853 let epoch = response
854 .headers()
855 .get("x-aptos-epoch")
856 .and_then(|v| v.to_str().ok())
857 .and_then(|v| v.parse().ok());
858 let block_height = response
859 .headers()
860 .get("x-aptos-block-height")
861 .and_then(|v| v.to_str().ok())
862 .and_then(|v| v.parse().ok());
863 let oldest_ledger_version = response
864 .headers()
865 .get("x-aptos-oldest-ledger-version")
866 .and_then(|v| v.to_str().ok())
867 .and_then(|v| v.parse().ok());
868 let cursor = response
869 .headers()
870 .get("x-aptos-cursor")
871 .and_then(|v| v.to_str().ok())
872 .map(ToString::to_string);
873
874 let retry_after_secs = response
876 .headers()
877 .get("retry-after")
878 .and_then(|v| v.to_str().ok())
879 .and_then(|v| v.parse().ok());
880
881 if status.is_success() {
882 let bytes = crate::config::read_response_bounded(response, max_response_size).await?;
885 let data: T = serde_json::from_slice(&bytes)?;
886 Ok(AptosResponse {
887 data,
888 ledger_version,
889 ledger_timestamp,
890 epoch,
891 block_height,
892 oldest_ledger_version,
893 cursor,
894 })
895 } else if status.as_u16() == 429 {
896 Err(AptosError::RateLimited { retry_after_secs })
899 } else {
900 let error_bytes = crate::config::read_response_bounded(response, MAX_ERROR_BODY_SIZE)
903 .await
904 .ok();
905 let error_text = error_bytes
906 .and_then(|b| String::from_utf8(b).ok())
907 .unwrap_or_default();
908 let error_text = Self::truncate_error_body(error_text);
909 let body: serde_json::Value = serde_json::from_str(&error_text).unwrap_or_default();
910 let message = body
911 .get("message")
912 .and_then(|v| v.as_str())
913 .unwrap_or("Unknown error")
914 .to_string();
915 let error_code = body
916 .get("error_code")
917 .and_then(|v| v.as_str())
918 .map(ToString::to_string);
919 let vm_error_code = body
920 .get("vm_error_code")
921 .and_then(serde_json::Value::as_u64);
922
923 Err(AptosError::api_with_details(
924 status.as_u16(),
925 message,
926 error_code,
927 vm_error_code,
928 ))
929 }
930 }
931
932 #[allow(dead_code)]
934 async fn handle_response<T: for<'de> serde::Deserialize<'de>>(
935 &self,
936 response: reqwest::Response,
937 ) -> AptosResult<AptosResponse<T>> {
938 let max_response_size = self.config.pool_config().max_response_size;
939 Self::handle_response_static(response, max_response_size).await
940 }
941}
942
943fn append_start_limit(url: &mut Url, start: Option<&str>, limit: Option<u16>) {
951 if start.is_none() && limit.is_none() {
952 return;
953 }
954 let mut query = url.query_pairs_mut();
955 if let Some(start) = start {
956 query.append_pair("start", start);
957 }
958 if let Some(limit) = limit {
959 query.append_pair("limit", &limit.to_string());
960 }
961}
962
963#[cfg(test)]
964mod tests {
965 use super::*;
966 use crate::transaction::authenticator::{
967 Ed25519PublicKey, Ed25519Signature, TransactionAuthenticator,
968 };
969 use crate::transaction::simulation::SimulateQueryOptions;
970 use crate::transaction::types::{RawTransaction, SignedTransaction};
971 use crate::types::ChainId;
972 use wiremock::{
973 Mock, MockServer, ResponseTemplate,
974 matchers::{method, path, path_regex, query_param},
975 };
976
977 #[test]
978 fn test_build_url() {
979 let client = FullnodeClient::new(AptosConfig::testnet()).unwrap();
980 let url = client.build_url("accounts/0x1");
981 assert!(url.as_str().contains("accounts/0x1"));
982 }
983
984 fn create_mock_client(server: &MockServer) -> FullnodeClient {
985 let url = format!("{}/v1", server.uri());
987 let config = AptosConfig::custom(&url).unwrap().without_retry();
988 FullnodeClient::new(config).unwrap()
989 }
990
991 fn create_minimal_signed_transaction() -> SignedTransaction {
993 use crate::transaction::payload::{EntryFunction, TransactionPayload};
994
995 let raw = RawTransaction::new(
996 AccountAddress::ONE,
997 0,
998 TransactionPayload::EntryFunction(
999 EntryFunction::apt_transfer(AccountAddress::ONE, 0).unwrap(),
1000 ),
1001 100_000,
1002 100,
1003 std::time::SystemTime::now()
1004 .duration_since(std::time::UNIX_EPOCH)
1005 .unwrap()
1006 .as_secs()
1007 .saturating_add(600),
1008 ChainId::testnet(),
1009 );
1010 let auth = TransactionAuthenticator::Ed25519 {
1011 public_key: Ed25519PublicKey([0u8; 32]),
1012 signature: Ed25519Signature([0u8; 64]),
1013 };
1014 SignedTransaction::new(raw, auth)
1015 }
1016
1017 fn simulate_response_json() -> serde_json::Value {
1018 serde_json::json!([{
1019 "success": true,
1020 "vm_status": "Executed successfully",
1021 "gas_used": "100",
1022 "max_gas_amount": "200000",
1023 "gas_unit_price": "100",
1024 "hash": "0x1",
1025 "changes": [],
1026 "events": []
1027 }])
1028 }
1029
1030 #[tokio::test]
1031 async fn test_get_ledger_info() {
1032 let server = MockServer::start().await;
1033
1034 Mock::given(method("GET"))
1035 .and(path("/v1"))
1036 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1037 "chain_id": 2,
1038 "epoch": "100",
1039 "ledger_version": "12345",
1040 "oldest_ledger_version": "0",
1041 "ledger_timestamp": "1000000",
1042 "node_role": "full_node",
1043 "oldest_block_height": "0",
1044 "block_height": "5000"
1045 })))
1046 .expect(1)
1047 .mount(&server)
1048 .await;
1049
1050 let client = create_mock_client(&server);
1051 let result = client.get_ledger_info().await.unwrap();
1052
1053 assert_eq!(result.data.chain_id, 2);
1054 assert_eq!(result.data.version().unwrap(), 12345);
1055 assert_eq!(result.data.height().unwrap(), 5000);
1056 }
1057
1058 #[tokio::test]
1059 async fn test_get_account() {
1060 let server = MockServer::start().await;
1061
1062 Mock::given(method("GET"))
1063 .and(path_regex(r"^/v1/accounts/0x[0-9a-f]+$"))
1064 .respond_with(
1065 ResponseTemplate::new(200)
1066 .set_body_json(serde_json::json!({
1067 "sequence_number": "42",
1068 "authentication_key": "0x0000000000000000000000000000000000000000000000000000000000000001"
1069 }))
1070 .insert_header("x-aptos-ledger-version", "12345"),
1071 )
1072 .expect(1)
1073 .mount(&server)
1074 .await;
1075
1076 let client = create_mock_client(&server);
1077 let result = client.get_account(AccountAddress::ONE).await.unwrap();
1078
1079 assert_eq!(result.data.sequence_number().unwrap(), 42);
1080 assert_eq!(result.ledger_version, Some(12345));
1081 }
1082
1083 #[tokio::test]
1084 async fn test_get_account_not_found() {
1085 let server = MockServer::start().await;
1086
1087 Mock::given(method("GET"))
1088 .and(path_regex(r"/v1/accounts/0x[0-9a-f]+"))
1089 .respond_with(ResponseTemplate::new(404).set_body_json(serde_json::json!({
1090 "message": "Account not found",
1091 "error_code": "account_not_found"
1092 })))
1093 .expect(1)
1094 .mount(&server)
1095 .await;
1096
1097 let client = create_mock_client(&server);
1098 let result = client.get_account(AccountAddress::ONE).await;
1099
1100 assert!(result.is_err());
1101 let err = result.unwrap_err();
1102 assert!(err.is_not_found());
1103 }
1104
1105 #[tokio::test]
1106 async fn test_get_account_resources() {
1107 let server = MockServer::start().await;
1108
1109 Mock::given(method("GET"))
1110 .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/resources"))
1111 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([
1112 {
1113 "type": "0x1::account::Account",
1114 "data": {"sequence_number": "10"}
1115 },
1116 {
1117 "type": "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>",
1118 "data": {"coin": {"value": "1000000"}}
1119 }
1120 ])))
1121 .expect(1)
1122 .mount(&server)
1123 .await;
1124
1125 let client = create_mock_client(&server);
1126 let result = client
1127 .get_account_resources(AccountAddress::ONE)
1128 .await
1129 .unwrap();
1130
1131 assert_eq!(result.data.len(), 2);
1132 assert!(result.data[0].typ.contains("Account"));
1133 }
1134
1135 #[tokio::test]
1136 async fn test_get_account_resource() {
1137 let server = MockServer::start().await;
1138
1139 Mock::given(method("GET"))
1140 .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/resource/.*"))
1141 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1142 "type": "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>",
1143 "data": {"coin": {"value": "5000000"}}
1144 })))
1145 .expect(1)
1146 .mount(&server)
1147 .await;
1148
1149 let client = create_mock_client(&server);
1150 let result = client
1151 .get_account_resource(
1152 AccountAddress::ONE,
1153 "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>",
1154 )
1155 .await
1156 .unwrap();
1157
1158 assert!(result.data.typ.contains("CoinStore"));
1159 }
1160
1161 #[tokio::test]
1162 async fn test_get_account_modules() {
1163 let server = MockServer::start().await;
1164
1165 Mock::given(method("GET"))
1166 .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/modules"))
1167 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([
1168 {
1169 "bytecode": "0xabc123",
1170 "abi": {
1171 "address": "0x1",
1172 "name": "coin",
1173 "exposed_functions": [],
1174 "structs": []
1175 }
1176 }
1177 ])))
1178 .expect(1)
1179 .mount(&server)
1180 .await;
1181
1182 let client = create_mock_client(&server);
1183 let result = client
1184 .get_account_modules(AccountAddress::ONE)
1185 .await
1186 .unwrap();
1187
1188 assert_eq!(result.data.len(), 1);
1189 assert!(result.data[0].abi.is_some());
1190 }
1191
1192 #[tokio::test]
1193 async fn test_get_account_resources_paginated_sends_start_and_limit() {
1194 let server = MockServer::start().await;
1195
1196 Mock::given(method("GET"))
1198 .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/resources"))
1199 .and(query_param("start", "42"))
1200 .and(query_param("limit", "9"))
1201 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([])))
1202 .expect(1)
1203 .mount(&server)
1204 .await;
1205
1206 let client = create_mock_client(&server);
1207 let result = client
1208 .get_account_resources_paginated(AccountAddress::ONE, Some("42"), Some(9))
1209 .await
1210 .unwrap();
1211 assert_eq!(result.data.len(), 0);
1212 }
1213
1214 #[tokio::test]
1215 async fn test_get_account_resources_paginated_round_trips_opaque_cursor() {
1216 let server = MockServer::start().await;
1220 let opaque = "0x0a1b2c3d_state_key_token";
1221 Mock::given(method("GET"))
1222 .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/resources"))
1223 .and(query_param("start", opaque))
1224 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([])))
1225 .expect(1)
1226 .mount(&server)
1227 .await;
1228
1229 let client = create_mock_client(&server);
1230 client
1231 .get_account_resources_paginated(AccountAddress::ONE, Some(opaque), None)
1232 .await
1233 .unwrap();
1234 }
1235
1236 #[tokio::test]
1237 async fn test_get_account_resources_no_pagination_omits_query() {
1238 let server = MockServer::start().await;
1239
1240 Mock::given(method("GET"))
1243 .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/resources$"))
1244 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([])))
1245 .expect(1)
1246 .mount(&server)
1247 .await;
1248
1249 let client = create_mock_client(&server);
1250 client
1251 .get_account_resources(AccountAddress::ONE)
1252 .await
1253 .unwrap();
1254 }
1255
1256 #[tokio::test]
1257 async fn test_get_account_resources_paginated_sends_start_only() {
1258 let server = MockServer::start().await;
1261 Mock::given(method("GET"))
1262 .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/resources"))
1263 .and(query_param("start", "1234"))
1264 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([])))
1265 .expect(1)
1266 .mount(&server)
1267 .await;
1268
1269 let client = create_mock_client(&server);
1270 client
1271 .get_account_resources_paginated(AccountAddress::ONE, Some("1234"), None)
1272 .await
1273 .unwrap();
1274 }
1275
1276 #[tokio::test]
1277 async fn test_get_account_modules_paginated_sends_start_and_limit() {
1278 let server = MockServer::start().await;
1279 Mock::given(method("GET"))
1280 .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/modules"))
1281 .and(query_param("start", "7"))
1282 .and(query_param("limit", "100"))
1283 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([])))
1284 .expect(1)
1285 .mount(&server)
1286 .await;
1287
1288 let client = create_mock_client(&server);
1289 client
1290 .get_account_modules_paginated(AccountAddress::ONE, Some("7"), Some(100))
1291 .await
1292 .unwrap();
1293 }
1294
1295 #[tokio::test]
1296 async fn test_get_account_modules_no_pagination_omits_query() {
1297 let server = MockServer::start().await;
1300 Mock::given(method("GET"))
1301 .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/modules$"))
1302 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([])))
1303 .expect(1)
1304 .mount(&server)
1305 .await;
1306
1307 let client = create_mock_client(&server);
1308 client
1309 .get_account_modules(AccountAddress::ONE)
1310 .await
1311 .unwrap();
1312 }
1313
1314 #[tokio::test]
1315 async fn test_get_account_modules_paginated_sends_limit_only() {
1316 let server = MockServer::start().await;
1317
1318 Mock::given(method("GET"))
1321 .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/modules"))
1322 .and(query_param("limit", "25"))
1323 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([])))
1324 .expect(1)
1325 .mount(&server)
1326 .await;
1327
1328 let client = create_mock_client(&server);
1329 client
1330 .get_account_modules_paginated(AccountAddress::ONE, None, Some(25))
1331 .await
1332 .unwrap();
1333 }
1334
1335 #[tokio::test]
1336 async fn test_estimate_gas_price() {
1337 let server = MockServer::start().await;
1338
1339 Mock::given(method("GET"))
1340 .and(path("/v1/estimate_gas_price"))
1341 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1342 "deprioritized_gas_estimate": 50,
1343 "gas_estimate": 100,
1344 "prioritized_gas_estimate": 150
1345 })))
1346 .expect(1)
1347 .mount(&server)
1348 .await;
1349
1350 let client = create_mock_client(&server);
1351 let result = client.estimate_gas_price().await.unwrap();
1352
1353 assert_eq!(result.data.gas_estimate, 100);
1354 assert_eq!(result.data.low(), 50);
1355 assert_eq!(result.data.high(), 150);
1356 }
1357
1358 #[tokio::test]
1359 async fn test_get_transaction_by_hash() {
1360 let server = MockServer::start().await;
1361
1362 Mock::given(method("GET"))
1363 .and(path_regex(r"/v1/transactions/by_hash/0x[0-9a-f]+"))
1364 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1365 "version": "12345",
1366 "hash": "0x0000000000000000000000000000000000000000000000000000000000000001",
1367 "success": true,
1368 "vm_status": "Executed successfully"
1369 })))
1370 .expect(1)
1371 .mount(&server)
1372 .await;
1373
1374 let client = create_mock_client(&server);
1375 let hash = HashValue::from_hex(
1376 "0x0000000000000000000000000000000000000000000000000000000000000001",
1377 )
1378 .unwrap();
1379 let result = client.get_transaction_by_hash(&hash).await.unwrap();
1380
1381 assert!(
1382 result
1383 .data
1384 .get("success")
1385 .and_then(serde_json::Value::as_bool)
1386 .unwrap()
1387 );
1388 }
1389
1390 #[tokio::test]
1391 async fn test_wait_for_transaction_success() {
1392 let server = MockServer::start().await;
1393
1394 Mock::given(method("GET"))
1395 .and(path_regex(r"/v1/transactions/by_hash/0x[0-9a-f]+"))
1396 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1397 "type": "user_transaction",
1398 "version": "12345",
1399 "hash": "0x0000000000000000000000000000000000000000000000000000000000000001",
1400 "success": true,
1401 "vm_status": "Executed successfully"
1402 })))
1403 .expect(1..)
1404 .mount(&server)
1405 .await;
1406
1407 let client = create_mock_client(&server);
1408 let hash = HashValue::from_hex(
1409 "0x0000000000000000000000000000000000000000000000000000000000000001",
1410 )
1411 .unwrap();
1412 let result = client
1413 .wait_for_transaction(&hash, Some(Duration::from_secs(5)))
1414 .await
1415 .unwrap();
1416
1417 assert!(
1418 result
1419 .data
1420 .get("success")
1421 .and_then(serde_json::Value::as_bool)
1422 .unwrap()
1423 );
1424 }
1425
1426 #[tokio::test]
1427 async fn test_server_error_retryable() {
1428 let server = MockServer::start().await;
1429
1430 Mock::given(method("GET"))
1431 .and(path("/v1"))
1432 .respond_with(ResponseTemplate::new(503).set_body_json(serde_json::json!({
1433 "message": "Service temporarily unavailable"
1434 })))
1435 .expect(1)
1436 .mount(&server)
1437 .await;
1438
1439 let url = format!("{}/v1", server.uri());
1440 let config = AptosConfig::custom(&url).unwrap().without_retry();
1441 let client = FullnodeClient::new(config).unwrap();
1442 let result = client.get_ledger_info().await;
1443
1444 assert!(result.is_err());
1445 assert!(result.unwrap_err().is_retryable());
1446 }
1447
1448 #[tokio::test]
1449 async fn test_rate_limited() {
1450 let server = MockServer::start().await;
1451
1452 Mock::given(method("GET"))
1453 .and(path("/v1"))
1454 .respond_with(
1455 ResponseTemplate::new(429)
1456 .set_body_json(serde_json::json!({
1457 "message": "Rate limited"
1458 }))
1459 .insert_header("retry-after", "30"),
1460 )
1461 .expect(1)
1462 .mount(&server)
1463 .await;
1464
1465 let url = format!("{}/v1", server.uri());
1466 let config = AptosConfig::custom(&url).unwrap().without_retry();
1467 let client = FullnodeClient::new(config).unwrap();
1468 let result = client.get_ledger_info().await;
1469
1470 assert!(result.is_err());
1471 assert!(result.unwrap_err().is_retryable());
1472 }
1473
1474 #[tokio::test]
1475 async fn test_get_block_by_height() {
1476 let server = MockServer::start().await;
1477
1478 Mock::given(method("GET"))
1479 .and(path_regex(r"/v1/blocks/by_height/\d+"))
1480 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1481 "block_height": "1000",
1482 "block_hash": "0xabc",
1483 "block_timestamp": "1234567890",
1484 "first_version": "100",
1485 "last_version": "200"
1486 })))
1487 .expect(1)
1488 .mount(&server)
1489 .await;
1490
1491 let client = create_mock_client(&server);
1492 let result = client.get_block_by_height(1000, false).await.unwrap();
1493
1494 assert!(result.data.get("block_height").is_some());
1495 }
1496
1497 #[tokio::test]
1498 async fn test_view() {
1499 let server = MockServer::start().await;
1500
1501 Mock::given(method("POST"))
1502 .and(path("/v1/view"))
1503 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!(["1000000"])))
1504 .expect(1)
1505 .mount(&server)
1506 .await;
1507
1508 let client = create_mock_client(&server);
1509 let result: AptosResponse<Vec<serde_json::Value>> = client
1510 .view(
1511 "0x1::coin::balance",
1512 vec!["0x1::aptos_coin::AptosCoin".to_string()],
1513 vec![serde_json::json!("0x1")],
1514 )
1515 .await
1516 .unwrap();
1517
1518 assert_eq!(result.data.len(), 1);
1519 }
1520
1521 #[tokio::test]
1522 async fn test_simulate_transaction_with_estimate_gas_unit_price() {
1523 let server = MockServer::start().await;
1524
1525 Mock::given(method("POST"))
1526 .and(path("/v1/transactions/simulate"))
1527 .and(|req: &wiremock::Request| {
1528 req.url
1529 .query()
1530 .is_some_and(|q| q.contains("estimate_gas_unit_price=true"))
1531 })
1532 .respond_with(ResponseTemplate::new(200).set_body_json(simulate_response_json()))
1533 .expect(1)
1534 .mount(&server)
1535 .await;
1536
1537 let client = create_mock_client(&server);
1538 let signed = create_minimal_signed_transaction();
1539 let opts = SimulateQueryOptions::new().estimate_gas_unit_price(true);
1540 let result = client
1541 .simulate_transaction_with_options(&signed, opts)
1542 .await
1543 .unwrap();
1544 assert!(!result.data.is_empty());
1545 }
1546
1547 #[tokio::test]
1548 async fn test_simulate_transaction_with_estimate_max_gas_amount() {
1549 let server = MockServer::start().await;
1550
1551 Mock::given(method("POST"))
1552 .and(path("/v1/transactions/simulate"))
1553 .and(|req: &wiremock::Request| {
1554 req.url
1555 .query()
1556 .is_some_and(|q| q.contains("estimate_max_gas_amount=true"))
1557 })
1558 .respond_with(ResponseTemplate::new(200).set_body_json(simulate_response_json()))
1559 .expect(1)
1560 .mount(&server)
1561 .await;
1562
1563 let client = create_mock_client(&server);
1564 let signed = create_minimal_signed_transaction();
1565 let opts = SimulateQueryOptions::new().estimate_max_gas_amount(true);
1566 let result = client
1567 .simulate_transaction_with_options(&signed, opts)
1568 .await
1569 .unwrap();
1570 assert!(!result.data.is_empty());
1571 }
1572
1573 #[tokio::test]
1574 async fn test_simulate_transaction_with_estimate_prioritized_gas_unit_price() {
1575 let server = MockServer::start().await;
1576
1577 Mock::given(method("POST"))
1578 .and(path("/v1/transactions/simulate"))
1579 .and(|req: &wiremock::Request| {
1580 req.url
1581 .query()
1582 .is_some_and(|q| q.contains("estimate_prioritized_gas_unit_price=true"))
1583 })
1584 .respond_with(ResponseTemplate::new(200).set_body_json(simulate_response_json()))
1585 .expect(1)
1586 .mount(&server)
1587 .await;
1588
1589 let client = create_mock_client(&server);
1590 let signed = create_minimal_signed_transaction();
1591 let opts = SimulateQueryOptions::new().estimate_prioritized_gas_unit_price(true);
1592 let result = client
1593 .simulate_transaction_with_options(&signed, opts)
1594 .await
1595 .unwrap();
1596 assert!(!result.data.is_empty());
1597 }
1598
1599 #[tokio::test]
1600 async fn test_simulate_transaction_with_all_options() {
1601 let server = MockServer::start().await;
1602
1603 Mock::given(method("POST"))
1604 .and(path("/v1/transactions/simulate"))
1605 .and(|req: &wiremock::Request| {
1606 req.url.query().is_some_and(|q| {
1607 q.contains("estimate_gas_unit_price=true")
1608 && q.contains("estimate_max_gas_amount=true")
1609 && q.contains("estimate_prioritized_gas_unit_price=true")
1610 })
1611 })
1612 .respond_with(ResponseTemplate::new(200).set_body_json(simulate_response_json()))
1613 .expect(1)
1614 .mount(&server)
1615 .await;
1616
1617 let client = create_mock_client(&server);
1618 let signed = create_minimal_signed_transaction();
1619 let opts = SimulateQueryOptions::new()
1620 .estimate_gas_unit_price(true)
1621 .estimate_max_gas_amount(true)
1622 .estimate_prioritized_gas_unit_price(true);
1623 let result = client
1624 .simulate_transaction_with_options(&signed, opts)
1625 .await
1626 .unwrap();
1627 assert!(!result.data.is_empty());
1628 }
1629
1630 #[tokio::test]
1631 async fn test_simulate_transaction_without_options() {
1632 let server = MockServer::start().await;
1633
1634 Mock::given(method("POST"))
1636 .and(path("/v1/transactions/simulate"))
1637 .and(|req: &wiremock::Request| {
1638 req.url.query().is_none_or(|q| {
1640 !q.contains("estimate_gas_unit_price=")
1641 && !q.contains("estimate_max_gas_amount=")
1642 && !q.contains("estimate_prioritized_gas_unit_price=")
1643 })
1644 })
1645 .respond_with(ResponseTemplate::new(200).set_body_json(simulate_response_json()))
1646 .expect(1)
1647 .mount(&server)
1648 .await;
1649
1650 let client = create_mock_client(&server);
1651 let signed = create_minimal_signed_transaction();
1652 let result = client.simulate_transaction(&signed).await.unwrap();
1653 assert!(!result.data.is_empty());
1654 }
1655}