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::types::SignedTransaction;
10use crate::types::{AccountAddress, HashValue};
11use reqwest::Client;
12use reqwest::header::{ACCEPT, CONTENT_TYPE};
13use std::sync::Arc;
14use std::time::Duration;
15use url::Url;
16
17const BCS_CONTENT_TYPE: &str = "application/x.aptos.signed_transaction+bcs";
18const BCS_VIEW_CONTENT_TYPE: &str = "application/x-bcs";
19const JSON_CONTENT_TYPE: &str = "application/json";
20const DEFAULT_TRANSACTION_WAIT_TIMEOUT_SECS: u64 = 30;
22
23#[derive(Debug, Clone)]
56pub struct FullnodeClient {
57 config: AptosConfig,
58 client: Client,
59 retry_config: Arc<RetryConfig>,
60}
61
62impl FullnodeClient {
63 pub fn new(config: AptosConfig) -> AptosResult<Self> {
83 let pool = config.pool_config();
84
85 let mut builder = Client::builder()
89 .timeout(config.timeout)
90 .pool_max_idle_per_host(pool.max_idle_per_host.unwrap_or(usize::MAX))
91 .pool_idle_timeout(pool.idle_timeout)
92 .tcp_nodelay(pool.tcp_nodelay);
93
94 if let Some(keepalive) = pool.tcp_keepalive {
95 builder = builder.tcp_keepalive(keepalive);
96 }
97
98 let client = builder.build().map_err(AptosError::Http)?;
99
100 let retry_config = Arc::new(config.retry_config().clone());
101
102 Ok(Self {
103 config,
104 client,
105 retry_config,
106 })
107 }
108
109 pub fn base_url(&self) -> &Url {
111 self.config.fullnode_url()
112 }
113
114 pub fn retry_config(&self) -> &RetryConfig {
116 &self.retry_config
117 }
118
119 pub async fn get_ledger_info(&self) -> AptosResult<AptosResponse<LedgerInfo>> {
128 let url = self.build_url("");
129 self.get_json(url).await
130 }
131
132 pub async fn get_account(
141 &self,
142 address: AccountAddress,
143 ) -> AptosResult<AptosResponse<AccountData>> {
144 let url = self.build_url(&format!("accounts/{address}"));
145 self.get_json(url).await
146 }
147
148 pub async fn get_sequence_number(&self, address: AccountAddress) -> AptosResult<u64> {
155 let account = self.get_account(address).await?;
156 account
157 .data
158 .sequence_number()
159 .map_err(|e| AptosError::Internal(format!("failed to parse sequence number: {e}")))
160 }
161
162 pub async fn get_account_resources(
169 &self,
170 address: AccountAddress,
171 ) -> AptosResult<AptosResponse<Vec<Resource>>> {
172 let url = self.build_url(&format!("accounts/{address}/resources"));
173 self.get_json(url).await
174 }
175
176 pub async fn get_account_resource(
183 &self,
184 address: AccountAddress,
185 resource_type: &str,
186 ) -> AptosResult<AptosResponse<Resource>> {
187 let url = self.build_url(&format!(
188 "accounts/{}/resource/{}",
189 address,
190 urlencoding::encode(resource_type)
191 ));
192 self.get_json(url).await
193 }
194
195 pub async fn get_account_modules(
202 &self,
203 address: AccountAddress,
204 ) -> AptosResult<AptosResponse<Vec<MoveModule>>> {
205 let url = self.build_url(&format!("accounts/{address}/modules"));
206 self.get_json(url).await
207 }
208
209 pub async fn get_account_module(
216 &self,
217 address: AccountAddress,
218 module_name: &str,
219 ) -> AptosResult<AptosResponse<MoveModule>> {
220 let url = self.build_url(&format!("accounts/{address}/module/{module_name}"));
221 self.get_json(url).await
222 }
223
224 pub async fn get_account_balance(&self, address: AccountAddress) -> AptosResult<u64> {
233 let result = self
236 .view(
237 "0x1::coin::balance",
238 vec!["0x1::aptos_coin::AptosCoin".to_string()],
239 vec![serde_json::json!(address.to_string())],
240 )
241 .await?;
242
243 let balance_str = result
245 .data
246 .first()
247 .and_then(|v| v.as_str())
248 .ok_or_else(|| AptosError::Internal("failed to parse balance response".into()))?;
249
250 balance_str
251 .parse()
252 .map_err(|_| AptosError::Internal("failed to parse balance as u64".into()))
253 }
254
255 pub async fn submit_transaction(
267 &self,
268 signed_txn: &SignedTransaction,
269 ) -> AptosResult<AptosResponse<PendingTransaction>> {
270 let url = self.build_url("transactions");
271 let bcs_bytes = signed_txn.to_bcs()?;
272 let client = self.client.clone();
273 let retry_config = self.retry_config.clone();
274 let max_response_size = self.config.pool_config().max_response_size;
275
276 let executor = RetryExecutor::new((*retry_config).clone());
277 executor
278 .execute(|| {
279 let client = client.clone();
280 let url = url.clone();
281 let bcs_bytes = bcs_bytes.clone();
282 async move {
283 let response = client
284 .post(url)
285 .header(CONTENT_TYPE, BCS_CONTENT_TYPE)
286 .header(ACCEPT, JSON_CONTENT_TYPE)
287 .body(bcs_bytes)
288 .send()
289 .await?;
290
291 Self::handle_response_static(response, max_response_size).await
292 }
293 })
294 .await
295 }
296
297 pub async fn submit_and_wait(
304 &self,
305 signed_txn: &SignedTransaction,
306 timeout: Option<Duration>,
307 ) -> AptosResult<AptosResponse<serde_json::Value>> {
308 let pending = self.submit_transaction(signed_txn).await?;
309 self.wait_for_transaction(&pending.data.hash, timeout).await
310 }
311
312 pub async fn get_transaction_by_hash(
319 &self,
320 hash: &HashValue,
321 ) -> AptosResult<AptosResponse<serde_json::Value>> {
322 let url = self.build_url(&format!("transactions/by_hash/{hash}"));
323 self.get_json(url).await
324 }
325
326 pub async fn wait_for_transaction(
335 &self,
336 hash: &HashValue,
337 timeout: Option<Duration>,
338 ) -> AptosResult<AptosResponse<serde_json::Value>> {
339 let timeout = timeout.unwrap_or(Duration::from_secs(DEFAULT_TRANSACTION_WAIT_TIMEOUT_SECS));
340 let start = std::time::Instant::now();
341
342 let initial_interval = Duration::from_millis(200);
344 let max_interval = Duration::from_secs(2);
345 let mut current_interval = initial_interval;
346
347 loop {
348 match self.get_transaction_by_hash(hash).await {
349 Ok(response) => {
350 if response.data.get("version").is_some() {
352 let success = response
354 .data
355 .get("success")
356 .and_then(serde_json::Value::as_bool);
357 if success == Some(false) {
358 let vm_status = response
359 .data
360 .get("vm_status")
361 .and_then(|v| v.as_str())
362 .unwrap_or("unknown")
363 .to_string();
364 return Err(AptosError::ExecutionFailed { vm_status });
365 }
366 return Ok(response);
367 }
368 }
369 Err(AptosError::Api {
370 status_code: 404, ..
371 }) => {
372 }
374 Err(e) => return Err(e),
375 }
376
377 if start.elapsed() >= timeout {
378 return Err(AptosError::TransactionTimeout {
379 hash: hash.to_string(),
380 timeout_secs: timeout.as_secs(),
381 });
382 }
383
384 tokio::time::sleep(current_interval).await;
385
386 current_interval = std::cmp::min(current_interval * 2, max_interval);
388 }
389 }
390
391 pub async fn simulate_transaction(
398 &self,
399 signed_txn: &SignedTransaction,
400 ) -> AptosResult<AptosResponse<Vec<serde_json::Value>>> {
401 let url = self.build_url("transactions/simulate");
402 let bcs_bytes = signed_txn.to_bcs()?;
403 let client = self.client.clone();
404 let retry_config = self.retry_config.clone();
405 let max_response_size = self.config.pool_config().max_response_size;
406
407 let executor = RetryExecutor::new((*retry_config).clone());
408 executor
409 .execute(|| {
410 let client = client.clone();
411 let url = url.clone();
412 let bcs_bytes = bcs_bytes.clone();
413 async move {
414 let response = client
415 .post(url)
416 .header(CONTENT_TYPE, BCS_CONTENT_TYPE)
417 .header(ACCEPT, JSON_CONTENT_TYPE)
418 .body(bcs_bytes)
419 .send()
420 .await?;
421
422 Self::handle_response_static(response, max_response_size).await
423 }
424 })
425 .await
426 }
427
428 pub async fn estimate_gas_price(&self) -> AptosResult<AptosResponse<GasEstimation>> {
437 let url = self.build_url("estimate_gas_price");
438 self.get_json(url).await
439 }
440
441 pub async fn view(
450 &self,
451 function: &str,
452 type_args: Vec<String>,
453 args: Vec<serde_json::Value>,
454 ) -> AptosResult<AptosResponse<Vec<serde_json::Value>>> {
455 let url = self.build_url("view");
456
457 let body = serde_json::json!({
458 "function": function,
459 "type_arguments": type_args,
460 "arguments": args,
461 });
462
463 let client = self.client.clone();
464 let retry_config = self.retry_config.clone();
465 let max_response_size = self.config.pool_config().max_response_size;
466
467 let executor = RetryExecutor::new((*retry_config).clone());
468 executor
469 .execute(|| {
470 let client = client.clone();
471 let url = url.clone();
472 let body = body.clone();
473 async move {
474 let response = client
475 .post(url)
476 .header(CONTENT_TYPE, JSON_CONTENT_TYPE)
477 .header(ACCEPT, JSON_CONTENT_TYPE)
478 .json(&body)
479 .send()
480 .await?;
481
482 Self::handle_response_static(response, max_response_size).await
483 }
484 })
485 .await
486 }
487
488 pub async fn view_bcs(
510 &self,
511 function: &str,
512 type_args: Vec<String>,
513 args: Vec<Vec<u8>>,
514 ) -> AptosResult<AptosResponse<Vec<u8>>> {
515 let url = self.build_url("view");
516
517 let hex_args: Vec<serde_json::Value> = args
520 .iter()
521 .map(|bytes| serde_json::json!(format!("0x{}", hex::encode(bytes))))
522 .collect();
523
524 let body = serde_json::json!({
525 "function": function,
526 "type_arguments": type_args,
527 "arguments": hex_args,
528 });
529
530 let client = self.client.clone();
531 let retry_config = self.retry_config.clone();
532
533 let executor = RetryExecutor::new((*retry_config).clone());
534 executor
535 .execute(|| {
536 let client = client.clone();
537 let url = url.clone();
538 let body = body.clone();
539 async move {
540 let response = client
541 .post(url)
542 .header(CONTENT_TYPE, JSON_CONTENT_TYPE)
543 .header(ACCEPT, BCS_VIEW_CONTENT_TYPE)
544 .json(&body)
545 .send()
546 .await?;
547
548 let status = response.status();
550 if !status.is_success() {
551 let error_text = response.text().await.unwrap_or_default();
552 return Err(AptosError::Api {
553 status_code: status.as_u16(),
554 message: error_text,
555 error_code: None,
556 vm_error_code: None,
557 });
558 }
559
560 let bytes = response.bytes().await?;
562 Ok(AptosResponse::new(bytes.to_vec()))
563 }
564 })
565 .await
566 }
567
568 pub async fn get_events_by_event_handle(
577 &self,
578 address: AccountAddress,
579 event_handle_struct: &str,
580 field_name: &str,
581 start: Option<u64>,
582 limit: Option<u64>,
583 ) -> AptosResult<AptosResponse<Vec<serde_json::Value>>> {
584 let mut url = self.build_url(&format!(
585 "accounts/{}/events/{}/{}",
586 address,
587 urlencoding::encode(event_handle_struct),
588 field_name
589 ));
590
591 {
592 let mut query = url.query_pairs_mut();
593 if let Some(start) = start {
594 query.append_pair("start", &start.to_string());
595 }
596 if let Some(limit) = limit {
597 query.append_pair("limit", &limit.to_string());
598 }
599 }
600
601 self.get_json(url).await
602 }
603
604 pub async fn get_block_by_height(
613 &self,
614 height: u64,
615 with_transactions: bool,
616 ) -> AptosResult<AptosResponse<serde_json::Value>> {
617 let mut url = self.build_url(&format!("blocks/by_height/{height}"));
618 url.query_pairs_mut()
619 .append_pair("with_transactions", &with_transactions.to_string());
620 self.get_json(url).await
621 }
622
623 pub async fn get_block_by_version(
630 &self,
631 version: u64,
632 with_transactions: bool,
633 ) -> AptosResult<AptosResponse<serde_json::Value>> {
634 let mut url = self.build_url(&format!("blocks/by_version/{version}"));
635 url.query_pairs_mut()
636 .append_pair("with_transactions", &with_transactions.to_string());
637 self.get_json(url).await
638 }
639
640 fn build_url(&self, path: &str) -> Url {
643 let mut url = self.config.fullnode_url().clone();
644 if !path.is_empty() {
645 let base_path = url.path();
647 let needs_slash = !base_path.ends_with('/');
648 let new_len = base_path.len() + path.len() + usize::from(needs_slash);
649 let mut new_path = String::with_capacity(new_len);
650 new_path.push_str(base_path);
651 if needs_slash {
652 new_path.push('/');
653 }
654 new_path.push_str(path);
655 url.set_path(&new_path);
656 }
657 url
658 }
659
660 async fn get_json<T: for<'de> serde::Deserialize<'de>>(
661 &self,
662 url: Url,
663 ) -> AptosResult<AptosResponse<T>> {
664 let client = self.client.clone();
665 let url_clone = url.clone();
666 let retry_config = self.retry_config.clone();
667 let max_response_size = self.config.pool_config().max_response_size;
668
669 let executor = RetryExecutor::new((*retry_config).clone());
670 executor
671 .execute(|| {
672 let client = client.clone();
673 let url = url_clone.clone();
674 async move {
675 let response = client
676 .get(url)
677 .header(ACCEPT, JSON_CONTENT_TYPE)
678 .send()
679 .await?;
680
681 Self::handle_response_static(response, max_response_size).await
682 }
683 })
684 .await
685 }
686
687 async fn handle_response_static<T: for<'de> serde::Deserialize<'de>>(
694 response: reqwest::Response,
695 max_response_size: usize,
696 ) -> AptosResult<AptosResponse<T>> {
697 let status = response.status();
698
699 if let Some(content_length) = response.content_length()
702 && content_length > max_response_size as u64
703 {
704 return Err(AptosError::Api {
705 status_code: status.as_u16(),
706 message: format!(
707 "response body too large: {content_length} bytes exceeds limit of {max_response_size} bytes"
708 ),
709 error_code: Some("RESPONSE_TOO_LARGE".to_string()),
710 vm_error_code: None,
711 });
712 }
713
714 let ledger_version = response
716 .headers()
717 .get("x-aptos-ledger-version")
718 .and_then(|v| v.to_str().ok())
719 .and_then(|v| v.parse().ok());
720 let ledger_timestamp = response
721 .headers()
722 .get("x-aptos-ledger-timestamp")
723 .and_then(|v| v.to_str().ok())
724 .and_then(|v| v.parse().ok());
725 let epoch = response
726 .headers()
727 .get("x-aptos-epoch")
728 .and_then(|v| v.to_str().ok())
729 .and_then(|v| v.parse().ok());
730 let block_height = response
731 .headers()
732 .get("x-aptos-block-height")
733 .and_then(|v| v.to_str().ok())
734 .and_then(|v| v.parse().ok());
735 let oldest_ledger_version = response
736 .headers()
737 .get("x-aptos-oldest-ledger-version")
738 .and_then(|v| v.to_str().ok())
739 .and_then(|v| v.parse().ok());
740 let cursor = response
741 .headers()
742 .get("x-aptos-cursor")
743 .and_then(|v| v.to_str().ok())
744 .map(ToString::to_string);
745
746 let retry_after_secs = response
748 .headers()
749 .get("retry-after")
750 .and_then(|v| v.to_str().ok())
751 .and_then(|v| v.parse().ok());
752
753 if status.is_success() {
754 let data: T = response.json().await?;
755 Ok(AptosResponse {
756 data,
757 ledger_version,
758 ledger_timestamp,
759 epoch,
760 block_height,
761 oldest_ledger_version,
762 cursor,
763 })
764 } else if status.as_u16() == 429 {
765 Err(AptosError::RateLimited { retry_after_secs })
768 } else {
769 let body: serde_json::Value = response.json().await.unwrap_or_default();
770 let message = body
771 .get("message")
772 .and_then(|v| v.as_str())
773 .unwrap_or("Unknown error")
774 .to_string();
775 let error_code = body
776 .get("error_code")
777 .and_then(|v| v.as_str())
778 .map(ToString::to_string);
779 let vm_error_code = body
780 .get("vm_error_code")
781 .and_then(serde_json::Value::as_u64);
782
783 Err(AptosError::api_with_details(
784 status.as_u16(),
785 message,
786 error_code,
787 vm_error_code,
788 ))
789 }
790 }
791
792 #[allow(dead_code)]
794 async fn handle_response<T: for<'de> serde::Deserialize<'de>>(
795 &self,
796 response: reqwest::Response,
797 ) -> AptosResult<AptosResponse<T>> {
798 let max_response_size = self.config.pool_config().max_response_size;
799 Self::handle_response_static(response, max_response_size).await
800 }
801}
802
803#[cfg(test)]
804mod tests {
805 use super::*;
806 use wiremock::{
807 Mock, MockServer, ResponseTemplate,
808 matchers::{method, path, path_regex},
809 };
810
811 #[test]
812 fn test_build_url() {
813 let client = FullnodeClient::new(AptosConfig::testnet()).unwrap();
814 let url = client.build_url("accounts/0x1");
815 assert!(url.as_str().contains("accounts/0x1"));
816 }
817
818 fn create_mock_client(server: &MockServer) -> FullnodeClient {
819 let url = format!("{}/v1", server.uri());
821 let config = AptosConfig::custom(&url).unwrap().without_retry();
822 FullnodeClient::new(config).unwrap()
823 }
824
825 #[tokio::test]
826 async fn test_get_ledger_info() {
827 let server = MockServer::start().await;
828
829 Mock::given(method("GET"))
830 .and(path("/v1"))
831 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
832 "chain_id": 2,
833 "epoch": "100",
834 "ledger_version": "12345",
835 "oldest_ledger_version": "0",
836 "ledger_timestamp": "1000000",
837 "node_role": "full_node",
838 "oldest_block_height": "0",
839 "block_height": "5000"
840 })))
841 .expect(1)
842 .mount(&server)
843 .await;
844
845 let client = create_mock_client(&server);
846 let result = client.get_ledger_info().await.unwrap();
847
848 assert_eq!(result.data.chain_id, 2);
849 assert_eq!(result.data.version().unwrap(), 12345);
850 assert_eq!(result.data.height().unwrap(), 5000);
851 }
852
853 #[tokio::test]
854 async fn test_get_account() {
855 let server = MockServer::start().await;
856
857 Mock::given(method("GET"))
858 .and(path_regex(r"^/v1/accounts/0x[0-9a-f]+$"))
859 .respond_with(
860 ResponseTemplate::new(200)
861 .set_body_json(serde_json::json!({
862 "sequence_number": "42",
863 "authentication_key": "0x0000000000000000000000000000000000000000000000000000000000000001"
864 }))
865 .insert_header("x-aptos-ledger-version", "12345"),
866 )
867 .expect(1)
868 .mount(&server)
869 .await;
870
871 let client = create_mock_client(&server);
872 let result = client.get_account(AccountAddress::ONE).await.unwrap();
873
874 assert_eq!(result.data.sequence_number().unwrap(), 42);
875 assert_eq!(result.ledger_version, Some(12345));
876 }
877
878 #[tokio::test]
879 async fn test_get_account_not_found() {
880 let server = MockServer::start().await;
881
882 Mock::given(method("GET"))
883 .and(path_regex(r"/v1/accounts/0x[0-9a-f]+"))
884 .respond_with(ResponseTemplate::new(404).set_body_json(serde_json::json!({
885 "message": "Account not found",
886 "error_code": "account_not_found"
887 })))
888 .expect(1)
889 .mount(&server)
890 .await;
891
892 let client = create_mock_client(&server);
893 let result = client.get_account(AccountAddress::ONE).await;
894
895 assert!(result.is_err());
896 let err = result.unwrap_err();
897 assert!(err.is_not_found());
898 }
899
900 #[tokio::test]
901 async fn test_get_account_resources() {
902 let server = MockServer::start().await;
903
904 Mock::given(method("GET"))
905 .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/resources"))
906 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([
907 {
908 "type": "0x1::account::Account",
909 "data": {"sequence_number": "10"}
910 },
911 {
912 "type": "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>",
913 "data": {"coin": {"value": "1000000"}}
914 }
915 ])))
916 .expect(1)
917 .mount(&server)
918 .await;
919
920 let client = create_mock_client(&server);
921 let result = client
922 .get_account_resources(AccountAddress::ONE)
923 .await
924 .unwrap();
925
926 assert_eq!(result.data.len(), 2);
927 assert!(result.data[0].typ.contains("Account"));
928 }
929
930 #[tokio::test]
931 async fn test_get_account_resource() {
932 let server = MockServer::start().await;
933
934 Mock::given(method("GET"))
935 .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/resource/.*"))
936 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
937 "type": "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>",
938 "data": {"coin": {"value": "5000000"}}
939 })))
940 .expect(1)
941 .mount(&server)
942 .await;
943
944 let client = create_mock_client(&server);
945 let result = client
946 .get_account_resource(
947 AccountAddress::ONE,
948 "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>",
949 )
950 .await
951 .unwrap();
952
953 assert!(result.data.typ.contains("CoinStore"));
954 }
955
956 #[tokio::test]
957 async fn test_get_account_modules() {
958 let server = MockServer::start().await;
959
960 Mock::given(method("GET"))
961 .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/modules"))
962 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([
963 {
964 "bytecode": "0xabc123",
965 "abi": {
966 "address": "0x1",
967 "name": "coin",
968 "exposed_functions": [],
969 "structs": []
970 }
971 }
972 ])))
973 .expect(1)
974 .mount(&server)
975 .await;
976
977 let client = create_mock_client(&server);
978 let result = client
979 .get_account_modules(AccountAddress::ONE)
980 .await
981 .unwrap();
982
983 assert_eq!(result.data.len(), 1);
984 assert!(result.data[0].abi.is_some());
985 }
986
987 #[tokio::test]
988 async fn test_estimate_gas_price() {
989 let server = MockServer::start().await;
990
991 Mock::given(method("GET"))
992 .and(path("/v1/estimate_gas_price"))
993 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
994 "deprioritized_gas_estimate": 50,
995 "gas_estimate": 100,
996 "prioritized_gas_estimate": 150
997 })))
998 .expect(1)
999 .mount(&server)
1000 .await;
1001
1002 let client = create_mock_client(&server);
1003 let result = client.estimate_gas_price().await.unwrap();
1004
1005 assert_eq!(result.data.gas_estimate, 100);
1006 assert_eq!(result.data.low(), 50);
1007 assert_eq!(result.data.high(), 150);
1008 }
1009
1010 #[tokio::test]
1011 async fn test_get_transaction_by_hash() {
1012 let server = MockServer::start().await;
1013
1014 Mock::given(method("GET"))
1015 .and(path_regex(r"/v1/transactions/by_hash/0x[0-9a-f]+"))
1016 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1017 "version": "12345",
1018 "hash": "0x0000000000000000000000000000000000000000000000000000000000000001",
1019 "success": true,
1020 "vm_status": "Executed successfully"
1021 })))
1022 .expect(1)
1023 .mount(&server)
1024 .await;
1025
1026 let client = create_mock_client(&server);
1027 let hash = HashValue::from_hex(
1028 "0x0000000000000000000000000000000000000000000000000000000000000001",
1029 )
1030 .unwrap();
1031 let result = client.get_transaction_by_hash(&hash).await.unwrap();
1032
1033 assert!(
1034 result
1035 .data
1036 .get("success")
1037 .and_then(serde_json::Value::as_bool)
1038 .unwrap()
1039 );
1040 }
1041
1042 #[tokio::test]
1043 async fn test_wait_for_transaction_success() {
1044 let server = MockServer::start().await;
1045
1046 Mock::given(method("GET"))
1047 .and(path_regex(r"/v1/transactions/by_hash/0x[0-9a-f]+"))
1048 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1049 "type": "user_transaction",
1050 "version": "12345",
1051 "hash": "0x0000000000000000000000000000000000000000000000000000000000000001",
1052 "success": true,
1053 "vm_status": "Executed successfully"
1054 })))
1055 .expect(1..)
1056 .mount(&server)
1057 .await;
1058
1059 let client = create_mock_client(&server);
1060 let hash = HashValue::from_hex(
1061 "0x0000000000000000000000000000000000000000000000000000000000000001",
1062 )
1063 .unwrap();
1064 let result = client
1065 .wait_for_transaction(&hash, Some(Duration::from_secs(5)))
1066 .await
1067 .unwrap();
1068
1069 assert!(
1070 result
1071 .data
1072 .get("success")
1073 .and_then(serde_json::Value::as_bool)
1074 .unwrap()
1075 );
1076 }
1077
1078 #[tokio::test]
1079 async fn test_server_error_retryable() {
1080 let server = MockServer::start().await;
1081
1082 Mock::given(method("GET"))
1083 .and(path("/v1"))
1084 .respond_with(ResponseTemplate::new(503).set_body_json(serde_json::json!({
1085 "message": "Service temporarily unavailable"
1086 })))
1087 .expect(1)
1088 .mount(&server)
1089 .await;
1090
1091 let url = format!("{}/v1", server.uri());
1092 let config = AptosConfig::custom(&url).unwrap().without_retry();
1093 let client = FullnodeClient::new(config).unwrap();
1094 let result = client.get_ledger_info().await;
1095
1096 assert!(result.is_err());
1097 assert!(result.unwrap_err().is_retryable());
1098 }
1099
1100 #[tokio::test]
1101 async fn test_rate_limited() {
1102 let server = MockServer::start().await;
1103
1104 Mock::given(method("GET"))
1105 .and(path("/v1"))
1106 .respond_with(
1107 ResponseTemplate::new(429)
1108 .set_body_json(serde_json::json!({
1109 "message": "Rate limited"
1110 }))
1111 .insert_header("retry-after", "30"),
1112 )
1113 .expect(1)
1114 .mount(&server)
1115 .await;
1116
1117 let url = format!("{}/v1", server.uri());
1118 let config = AptosConfig::custom(&url).unwrap().without_retry();
1119 let client = FullnodeClient::new(config).unwrap();
1120 let result = client.get_ledger_info().await;
1121
1122 assert!(result.is_err());
1123 assert!(result.unwrap_err().is_retryable());
1124 }
1125
1126 #[tokio::test]
1127 async fn test_get_block_by_height() {
1128 let server = MockServer::start().await;
1129
1130 Mock::given(method("GET"))
1131 .and(path_regex(r"/v1/blocks/by_height/\d+"))
1132 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1133 "block_height": "1000",
1134 "block_hash": "0xabc",
1135 "block_timestamp": "1234567890",
1136 "first_version": "100",
1137 "last_version": "200"
1138 })))
1139 .expect(1)
1140 .mount(&server)
1141 .await;
1142
1143 let client = create_mock_client(&server);
1144 let result = client.get_block_by_height(1000, false).await.unwrap();
1145
1146 assert!(result.data.get("block_height").is_some());
1147 }
1148
1149 #[tokio::test]
1150 async fn test_view() {
1151 let server = MockServer::start().await;
1152
1153 Mock::given(method("POST"))
1154 .and(path("/v1/view"))
1155 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!(["1000000"])))
1156 .expect(1)
1157 .mount(&server)
1158 .await;
1159
1160 let client = create_mock_client(&server);
1161 let result: AptosResponse<Vec<serde_json::Value>> = client
1162 .view(
1163 "0x1::coin::balance",
1164 vec!["0x1::aptos_coin::AptosCoin".to_string()],
1165 vec![serde_json::json!("0x1")],
1166 )
1167 .await
1168 .unwrap();
1169
1170 assert_eq!(result.data.len(), 1);
1171 }
1172}