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;
22const MAX_ERROR_BODY_SIZE: usize = 8 * 1024;
29
30#[derive(Debug, Clone)]
63pub struct FullnodeClient {
64 config: AptosConfig,
65 client: Client,
66 retry_config: Arc<RetryConfig>,
67}
68
69impl FullnodeClient {
70 pub fn new(config: AptosConfig) -> AptosResult<Self> {
90 let pool = config.pool_config();
91
92 let mut builder = Client::builder()
96 .timeout(config.timeout)
97 .pool_max_idle_per_host(pool.max_idle_per_host.unwrap_or(usize::MAX))
98 .pool_idle_timeout(pool.idle_timeout)
99 .tcp_nodelay(pool.tcp_nodelay);
100
101 if let Some(keepalive) = pool.tcp_keepalive {
102 builder = builder.tcp_keepalive(keepalive);
103 }
104
105 let client = builder.build().map_err(AptosError::Http)?;
106
107 let retry_config = Arc::new(config.retry_config().clone());
108
109 Ok(Self {
110 config,
111 client,
112 retry_config,
113 })
114 }
115
116 pub fn base_url(&self) -> &Url {
118 self.config.fullnode_url()
119 }
120
121 pub fn retry_config(&self) -> &RetryConfig {
123 &self.retry_config
124 }
125
126 pub async fn get_ledger_info(&self) -> AptosResult<AptosResponse<LedgerInfo>> {
135 let url = self.build_url("");
136 self.get_json(url).await
137 }
138
139 pub async fn get_account(
148 &self,
149 address: AccountAddress,
150 ) -> AptosResult<AptosResponse<AccountData>> {
151 let url = self.build_url(&format!("accounts/{address}"));
152 self.get_json(url).await
153 }
154
155 pub async fn get_sequence_number(&self, address: AccountAddress) -> AptosResult<u64> {
162 let account = self.get_account(address).await?;
163 account
164 .data
165 .sequence_number()
166 .map_err(|e| AptosError::Internal(format!("failed to parse sequence number: {e}")))
167 }
168
169 pub async fn get_account_resources(
176 &self,
177 address: AccountAddress,
178 ) -> AptosResult<AptosResponse<Vec<Resource>>> {
179 let url = self.build_url(&format!("accounts/{address}/resources"));
180 self.get_json(url).await
181 }
182
183 pub async fn get_account_resource(
190 &self,
191 address: AccountAddress,
192 resource_type: &str,
193 ) -> AptosResult<AptosResponse<Resource>> {
194 let url = self.build_url(&format!(
195 "accounts/{}/resource/{}",
196 address,
197 urlencoding::encode(resource_type)
198 ));
199 self.get_json(url).await
200 }
201
202 pub async fn get_account_modules(
209 &self,
210 address: AccountAddress,
211 ) -> AptosResult<AptosResponse<Vec<MoveModule>>> {
212 let url = self.build_url(&format!("accounts/{address}/modules"));
213 self.get_json(url).await
214 }
215
216 pub async fn get_account_module(
223 &self,
224 address: AccountAddress,
225 module_name: &str,
226 ) -> AptosResult<AptosResponse<MoveModule>> {
227 let url = self.build_url(&format!("accounts/{address}/module/{module_name}"));
228 self.get_json(url).await
229 }
230
231 pub async fn get_account_balance(&self, address: AccountAddress) -> AptosResult<u64> {
240 let result = self
243 .view(
244 "0x1::coin::balance",
245 vec!["0x1::aptos_coin::AptosCoin".to_string()],
246 vec![serde_json::json!(address.to_string())],
247 )
248 .await?;
249
250 let balance_str = result
252 .data
253 .first()
254 .and_then(|v| v.as_str())
255 .ok_or_else(|| AptosError::Internal("failed to parse balance response".into()))?;
256
257 balance_str
258 .parse()
259 .map_err(|_| AptosError::Internal("failed to parse balance as u64".into()))
260 }
261
262 pub async fn submit_transaction(
274 &self,
275 signed_txn: &SignedTransaction,
276 ) -> AptosResult<AptosResponse<PendingTransaction>> {
277 let url = self.build_url("transactions");
278 let bcs_bytes = signed_txn.to_bcs()?;
279 let client = self.client.clone();
280 let retry_config = self.retry_config.clone();
281 let max_response_size = self.config.pool_config().max_response_size;
282
283 let executor = RetryExecutor::from_shared(retry_config);
284 executor
285 .execute(|| {
286 let client = client.clone();
287 let url = url.clone();
288 let bcs_bytes = bcs_bytes.clone();
289 async move {
290 let response = client
291 .post(url)
292 .header(CONTENT_TYPE, BCS_CONTENT_TYPE)
293 .header(ACCEPT, JSON_CONTENT_TYPE)
294 .body(bcs_bytes)
295 .send()
296 .await?;
297
298 Self::handle_response_static(response, max_response_size).await
299 }
300 })
301 .await
302 }
303
304 pub async fn submit_and_wait(
311 &self,
312 signed_txn: &SignedTransaction,
313 timeout: Option<Duration>,
314 ) -> AptosResult<AptosResponse<serde_json::Value>> {
315 let pending = self.submit_transaction(signed_txn).await?;
316 self.wait_for_transaction(&pending.data.hash, timeout).await
317 }
318
319 pub async fn get_transaction_by_hash(
326 &self,
327 hash: &HashValue,
328 ) -> AptosResult<AptosResponse<serde_json::Value>> {
329 let url = self.build_url(&format!("transactions/by_hash/{hash}"));
330 self.get_json(url).await
331 }
332
333 pub async fn wait_for_transaction(
342 &self,
343 hash: &HashValue,
344 timeout: Option<Duration>,
345 ) -> AptosResult<AptosResponse<serde_json::Value>> {
346 let timeout = timeout.unwrap_or(Duration::from_secs(DEFAULT_TRANSACTION_WAIT_TIMEOUT_SECS));
347 let start = std::time::Instant::now();
348
349 let initial_interval = Duration::from_millis(200);
351 let max_interval = Duration::from_secs(2);
352 let mut current_interval = initial_interval;
353
354 loop {
355 match self.get_transaction_by_hash(hash).await {
356 Ok(response) => {
357 if response.data.get("version").is_some() {
359 let success = response
361 .data
362 .get("success")
363 .and_then(serde_json::Value::as_bool);
364 if success == Some(false) {
365 let vm_status = response
366 .data
367 .get("vm_status")
368 .and_then(|v| v.as_str())
369 .unwrap_or("unknown")
370 .to_string();
371 return Err(AptosError::ExecutionFailed { vm_status });
372 }
373 return Ok(response);
374 }
375 }
376 Err(AptosError::Api {
377 status_code: 404, ..
378 }) => {
379 }
381 Err(e) => return Err(e),
382 }
383
384 if start.elapsed() >= timeout {
385 return Err(AptosError::TransactionTimeout {
386 hash: hash.to_string(),
387 timeout_secs: timeout.as_secs(),
388 });
389 }
390
391 tokio::time::sleep(current_interval).await;
392
393 current_interval = std::cmp::min(current_interval * 2, max_interval);
395 }
396 }
397
398 pub async fn simulate_transaction(
405 &self,
406 signed_txn: &SignedTransaction,
407 ) -> AptosResult<AptosResponse<Vec<serde_json::Value>>> {
408 let url = self.build_url("transactions/simulate");
409 let bcs_bytes = signed_txn.to_bcs()?;
410 let client = self.client.clone();
411 let retry_config = self.retry_config.clone();
412 let max_response_size = self.config.pool_config().max_response_size;
413
414 let executor = RetryExecutor::from_shared(retry_config);
415 executor
416 .execute(|| {
417 let client = client.clone();
418 let url = url.clone();
419 let bcs_bytes = bcs_bytes.clone();
420 async move {
421 let response = client
422 .post(url)
423 .header(CONTENT_TYPE, BCS_CONTENT_TYPE)
424 .header(ACCEPT, JSON_CONTENT_TYPE)
425 .body(bcs_bytes)
426 .send()
427 .await?;
428
429 Self::handle_response_static(response, max_response_size).await
430 }
431 })
432 .await
433 }
434
435 pub async fn estimate_gas_price(&self) -> AptosResult<AptosResponse<GasEstimation>> {
444 let url = self.build_url("estimate_gas_price");
445 self.get_json(url).await
446 }
447
448 pub async fn view(
457 &self,
458 function: &str,
459 type_args: Vec<String>,
460 args: Vec<serde_json::Value>,
461 ) -> AptosResult<AptosResponse<Vec<serde_json::Value>>> {
462 let url = self.build_url("view");
463
464 let body = serde_json::json!({
465 "function": function,
466 "type_arguments": type_args,
467 "arguments": args,
468 });
469
470 let client = self.client.clone();
471 let retry_config = self.retry_config.clone();
472 let max_response_size = self.config.pool_config().max_response_size;
473
474 let executor = RetryExecutor::from_shared(retry_config);
475 executor
476 .execute(|| {
477 let client = client.clone();
478 let url = url.clone();
479 let body = body.clone();
480 async move {
481 let response = client
482 .post(url)
483 .header(CONTENT_TYPE, JSON_CONTENT_TYPE)
484 .header(ACCEPT, JSON_CONTENT_TYPE)
485 .json(&body)
486 .send()
487 .await?;
488
489 Self::handle_response_static(response, max_response_size).await
490 }
491 })
492 .await
493 }
494
495 pub async fn view_bcs(
517 &self,
518 function: &str,
519 type_args: Vec<String>,
520 args: Vec<Vec<u8>>,
521 ) -> AptosResult<AptosResponse<Vec<u8>>> {
522 let url = self.build_url("view");
523
524 let hex_args: Vec<serde_json::Value> = args
527 .iter()
528 .map(|bytes| serde_json::json!(const_hex::encode_prefixed(bytes)))
529 .collect();
530
531 let body = serde_json::json!({
532 "function": function,
533 "type_arguments": type_args,
534 "arguments": hex_args,
535 });
536
537 let client = self.client.clone();
538 let retry_config = self.retry_config.clone();
539 let max_response_size = self.config.pool_config().max_response_size;
540
541 let executor = RetryExecutor::from_shared(retry_config);
542 executor
543 .execute(|| {
544 let client = client.clone();
545 let url = url.clone();
546 let body = body.clone();
547 async move {
548 let response = client
549 .post(url)
550 .header(CONTENT_TYPE, JSON_CONTENT_TYPE)
551 .header(ACCEPT, BCS_VIEW_CONTENT_TYPE)
552 .json(&body)
553 .send()
554 .await?;
555
556 let status = response.status();
558 if !status.is_success() {
559 let error_bytes =
562 crate::config::read_response_bounded(response, MAX_ERROR_BODY_SIZE)
563 .await
564 .ok();
565 let error_text = error_bytes
566 .and_then(|b| String::from_utf8(b).ok())
567 .unwrap_or_default();
568 return Err(AptosError::Api {
569 status_code: status.as_u16(),
570 message: Self::truncate_error_body(error_text),
571 error_code: None,
572 vm_error_code: None,
573 });
574 }
575
576 let bytes =
579 crate::config::read_response_bounded(response, max_response_size).await?;
580 Ok(AptosResponse::new(bytes))
581 }
582 })
583 .await
584 }
585
586 pub async fn get_events_by_event_handle(
595 &self,
596 address: AccountAddress,
597 event_handle_struct: &str,
598 field_name: &str,
599 start: Option<u64>,
600 limit: Option<u64>,
601 ) -> AptosResult<AptosResponse<Vec<serde_json::Value>>> {
602 let mut url = self.build_url(&format!(
603 "accounts/{}/events/{}/{}",
604 address,
605 urlencoding::encode(event_handle_struct),
606 field_name
607 ));
608
609 {
610 let mut query = url.query_pairs_mut();
611 if let Some(start) = start {
612 query.append_pair("start", &start.to_string());
613 }
614 if let Some(limit) = limit {
615 query.append_pair("limit", &limit.to_string());
616 }
617 }
618
619 self.get_json(url).await
620 }
621
622 pub async fn get_block_by_height(
631 &self,
632 height: u64,
633 with_transactions: bool,
634 ) -> AptosResult<AptosResponse<serde_json::Value>> {
635 let mut url = self.build_url(&format!("blocks/by_height/{height}"));
636 url.query_pairs_mut()
637 .append_pair("with_transactions", &with_transactions.to_string());
638 self.get_json(url).await
639 }
640
641 pub async fn get_block_by_version(
648 &self,
649 version: u64,
650 with_transactions: bool,
651 ) -> AptosResult<AptosResponse<serde_json::Value>> {
652 let mut url = self.build_url(&format!("blocks/by_version/{version}"));
653 url.query_pairs_mut()
654 .append_pair("with_transactions", &with_transactions.to_string());
655 self.get_json(url).await
656 }
657
658 fn build_url(&self, path: &str) -> Url {
661 let mut url = self.config.fullnode_url().clone();
662 if !path.is_empty() {
663 let base_path = url.path();
665 let needs_slash = !base_path.ends_with('/');
666 let new_len = base_path.len() + path.len() + usize::from(needs_slash);
667 let mut new_path = String::with_capacity(new_len);
668 new_path.push_str(base_path);
669 if needs_slash {
670 new_path.push('/');
671 }
672 new_path.push_str(path);
673 url.set_path(&new_path);
674 }
675 url
676 }
677
678 async fn get_json<T: for<'de> serde::Deserialize<'de>>(
679 &self,
680 url: Url,
681 ) -> AptosResult<AptosResponse<T>> {
682 let client = self.client.clone();
683 let url_clone = url.clone();
684 let retry_config = self.retry_config.clone();
685 let max_response_size = self.config.pool_config().max_response_size;
686
687 let executor = RetryExecutor::from_shared(retry_config);
688 executor
689 .execute(|| {
690 let client = client.clone();
691 let url = url_clone.clone();
692 async move {
693 let response = client
694 .get(url)
695 .header(ACCEPT, JSON_CONTENT_TYPE)
696 .send()
697 .await?;
698
699 Self::handle_response_static(response, max_response_size).await
700 }
701 })
702 .await
703 }
704
705 fn truncate_error_body(body: String) -> String {
711 if body.len() > MAX_ERROR_BODY_SIZE {
712 let mut end = MAX_ERROR_BODY_SIZE;
714 while end > 0 && !body.is_char_boundary(end) {
715 end -= 1;
716 }
717 format!(
718 "{}... [truncated, total: {} bytes]",
719 &body[..end],
720 body.len()
721 )
722 } else {
723 body
724 }
725 }
726
727 async fn handle_response_static<T: for<'de> serde::Deserialize<'de>>(
735 response: reqwest::Response,
736 max_response_size: usize,
737 ) -> AptosResult<AptosResponse<T>> {
738 let status = response.status();
739
740 let ledger_version = response
742 .headers()
743 .get("x-aptos-ledger-version")
744 .and_then(|v| v.to_str().ok())
745 .and_then(|v| v.parse().ok());
746 let ledger_timestamp = response
747 .headers()
748 .get("x-aptos-ledger-timestamp")
749 .and_then(|v| v.to_str().ok())
750 .and_then(|v| v.parse().ok());
751 let epoch = response
752 .headers()
753 .get("x-aptos-epoch")
754 .and_then(|v| v.to_str().ok())
755 .and_then(|v| v.parse().ok());
756 let block_height = response
757 .headers()
758 .get("x-aptos-block-height")
759 .and_then(|v| v.to_str().ok())
760 .and_then(|v| v.parse().ok());
761 let oldest_ledger_version = response
762 .headers()
763 .get("x-aptos-oldest-ledger-version")
764 .and_then(|v| v.to_str().ok())
765 .and_then(|v| v.parse().ok());
766 let cursor = response
767 .headers()
768 .get("x-aptos-cursor")
769 .and_then(|v| v.to_str().ok())
770 .map(ToString::to_string);
771
772 let retry_after_secs = response
774 .headers()
775 .get("retry-after")
776 .and_then(|v| v.to_str().ok())
777 .and_then(|v| v.parse().ok());
778
779 if status.is_success() {
780 let bytes = crate::config::read_response_bounded(response, max_response_size).await?;
783 let data: T = serde_json::from_slice(&bytes)?;
784 Ok(AptosResponse {
785 data,
786 ledger_version,
787 ledger_timestamp,
788 epoch,
789 block_height,
790 oldest_ledger_version,
791 cursor,
792 })
793 } else if status.as_u16() == 429 {
794 Err(AptosError::RateLimited { retry_after_secs })
797 } else {
798 let error_bytes = crate::config::read_response_bounded(response, MAX_ERROR_BODY_SIZE)
801 .await
802 .ok();
803 let error_text = error_bytes
804 .and_then(|b| String::from_utf8(b).ok())
805 .unwrap_or_default();
806 let error_text = Self::truncate_error_body(error_text);
807 let body: serde_json::Value = serde_json::from_str(&error_text).unwrap_or_default();
808 let message = body
809 .get("message")
810 .and_then(|v| v.as_str())
811 .unwrap_or("Unknown error")
812 .to_string();
813 let error_code = body
814 .get("error_code")
815 .and_then(|v| v.as_str())
816 .map(ToString::to_string);
817 let vm_error_code = body
818 .get("vm_error_code")
819 .and_then(serde_json::Value::as_u64);
820
821 Err(AptosError::api_with_details(
822 status.as_u16(),
823 message,
824 error_code,
825 vm_error_code,
826 ))
827 }
828 }
829
830 #[allow(dead_code)]
832 async fn handle_response<T: for<'de> serde::Deserialize<'de>>(
833 &self,
834 response: reqwest::Response,
835 ) -> AptosResult<AptosResponse<T>> {
836 let max_response_size = self.config.pool_config().max_response_size;
837 Self::handle_response_static(response, max_response_size).await
838 }
839}
840
841#[cfg(test)]
842mod tests {
843 use super::*;
844 use wiremock::{
845 Mock, MockServer, ResponseTemplate,
846 matchers::{method, path, path_regex},
847 };
848
849 #[test]
850 fn test_build_url() {
851 let client = FullnodeClient::new(AptosConfig::testnet()).unwrap();
852 let url = client.build_url("accounts/0x1");
853 assert!(url.as_str().contains("accounts/0x1"));
854 }
855
856 fn create_mock_client(server: &MockServer) -> FullnodeClient {
857 let url = format!("{}/v1", server.uri());
859 let config = AptosConfig::custom(&url).unwrap().without_retry();
860 FullnodeClient::new(config).unwrap()
861 }
862
863 #[tokio::test]
864 async fn test_get_ledger_info() {
865 let server = MockServer::start().await;
866
867 Mock::given(method("GET"))
868 .and(path("/v1"))
869 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
870 "chain_id": 2,
871 "epoch": "100",
872 "ledger_version": "12345",
873 "oldest_ledger_version": "0",
874 "ledger_timestamp": "1000000",
875 "node_role": "full_node",
876 "oldest_block_height": "0",
877 "block_height": "5000"
878 })))
879 .expect(1)
880 .mount(&server)
881 .await;
882
883 let client = create_mock_client(&server);
884 let result = client.get_ledger_info().await.unwrap();
885
886 assert_eq!(result.data.chain_id, 2);
887 assert_eq!(result.data.version().unwrap(), 12345);
888 assert_eq!(result.data.height().unwrap(), 5000);
889 }
890
891 #[tokio::test]
892 async fn test_get_account() {
893 let server = MockServer::start().await;
894
895 Mock::given(method("GET"))
896 .and(path_regex(r"^/v1/accounts/0x[0-9a-f]+$"))
897 .respond_with(
898 ResponseTemplate::new(200)
899 .set_body_json(serde_json::json!({
900 "sequence_number": "42",
901 "authentication_key": "0x0000000000000000000000000000000000000000000000000000000000000001"
902 }))
903 .insert_header("x-aptos-ledger-version", "12345"),
904 )
905 .expect(1)
906 .mount(&server)
907 .await;
908
909 let client = create_mock_client(&server);
910 let result = client.get_account(AccountAddress::ONE).await.unwrap();
911
912 assert_eq!(result.data.sequence_number().unwrap(), 42);
913 assert_eq!(result.ledger_version, Some(12345));
914 }
915
916 #[tokio::test]
917 async fn test_get_account_not_found() {
918 let server = MockServer::start().await;
919
920 Mock::given(method("GET"))
921 .and(path_regex(r"/v1/accounts/0x[0-9a-f]+"))
922 .respond_with(ResponseTemplate::new(404).set_body_json(serde_json::json!({
923 "message": "Account not found",
924 "error_code": "account_not_found"
925 })))
926 .expect(1)
927 .mount(&server)
928 .await;
929
930 let client = create_mock_client(&server);
931 let result = client.get_account(AccountAddress::ONE).await;
932
933 assert!(result.is_err());
934 let err = result.unwrap_err();
935 assert!(err.is_not_found());
936 }
937
938 #[tokio::test]
939 async fn test_get_account_resources() {
940 let server = MockServer::start().await;
941
942 Mock::given(method("GET"))
943 .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/resources"))
944 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([
945 {
946 "type": "0x1::account::Account",
947 "data": {"sequence_number": "10"}
948 },
949 {
950 "type": "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>",
951 "data": {"coin": {"value": "1000000"}}
952 }
953 ])))
954 .expect(1)
955 .mount(&server)
956 .await;
957
958 let client = create_mock_client(&server);
959 let result = client
960 .get_account_resources(AccountAddress::ONE)
961 .await
962 .unwrap();
963
964 assert_eq!(result.data.len(), 2);
965 assert!(result.data[0].typ.contains("Account"));
966 }
967
968 #[tokio::test]
969 async fn test_get_account_resource() {
970 let server = MockServer::start().await;
971
972 Mock::given(method("GET"))
973 .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/resource/.*"))
974 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
975 "type": "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>",
976 "data": {"coin": {"value": "5000000"}}
977 })))
978 .expect(1)
979 .mount(&server)
980 .await;
981
982 let client = create_mock_client(&server);
983 let result = client
984 .get_account_resource(
985 AccountAddress::ONE,
986 "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>",
987 )
988 .await
989 .unwrap();
990
991 assert!(result.data.typ.contains("CoinStore"));
992 }
993
994 #[tokio::test]
995 async fn test_get_account_modules() {
996 let server = MockServer::start().await;
997
998 Mock::given(method("GET"))
999 .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/modules"))
1000 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([
1001 {
1002 "bytecode": "0xabc123",
1003 "abi": {
1004 "address": "0x1",
1005 "name": "coin",
1006 "exposed_functions": [],
1007 "structs": []
1008 }
1009 }
1010 ])))
1011 .expect(1)
1012 .mount(&server)
1013 .await;
1014
1015 let client = create_mock_client(&server);
1016 let result = client
1017 .get_account_modules(AccountAddress::ONE)
1018 .await
1019 .unwrap();
1020
1021 assert_eq!(result.data.len(), 1);
1022 assert!(result.data[0].abi.is_some());
1023 }
1024
1025 #[tokio::test]
1026 async fn test_estimate_gas_price() {
1027 let server = MockServer::start().await;
1028
1029 Mock::given(method("GET"))
1030 .and(path("/v1/estimate_gas_price"))
1031 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1032 "deprioritized_gas_estimate": 50,
1033 "gas_estimate": 100,
1034 "prioritized_gas_estimate": 150
1035 })))
1036 .expect(1)
1037 .mount(&server)
1038 .await;
1039
1040 let client = create_mock_client(&server);
1041 let result = client.estimate_gas_price().await.unwrap();
1042
1043 assert_eq!(result.data.gas_estimate, 100);
1044 assert_eq!(result.data.low(), 50);
1045 assert_eq!(result.data.high(), 150);
1046 }
1047
1048 #[tokio::test]
1049 async fn test_get_transaction_by_hash() {
1050 let server = MockServer::start().await;
1051
1052 Mock::given(method("GET"))
1053 .and(path_regex(r"/v1/transactions/by_hash/0x[0-9a-f]+"))
1054 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1055 "version": "12345",
1056 "hash": "0x0000000000000000000000000000000000000000000000000000000000000001",
1057 "success": true,
1058 "vm_status": "Executed successfully"
1059 })))
1060 .expect(1)
1061 .mount(&server)
1062 .await;
1063
1064 let client = create_mock_client(&server);
1065 let hash = HashValue::from_hex(
1066 "0x0000000000000000000000000000000000000000000000000000000000000001",
1067 )
1068 .unwrap();
1069 let result = client.get_transaction_by_hash(&hash).await.unwrap();
1070
1071 assert!(
1072 result
1073 .data
1074 .get("success")
1075 .and_then(serde_json::Value::as_bool)
1076 .unwrap()
1077 );
1078 }
1079
1080 #[tokio::test]
1081 async fn test_wait_for_transaction_success() {
1082 let server = MockServer::start().await;
1083
1084 Mock::given(method("GET"))
1085 .and(path_regex(r"/v1/transactions/by_hash/0x[0-9a-f]+"))
1086 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1087 "type": "user_transaction",
1088 "version": "12345",
1089 "hash": "0x0000000000000000000000000000000000000000000000000000000000000001",
1090 "success": true,
1091 "vm_status": "Executed successfully"
1092 })))
1093 .expect(1..)
1094 .mount(&server)
1095 .await;
1096
1097 let client = create_mock_client(&server);
1098 let hash = HashValue::from_hex(
1099 "0x0000000000000000000000000000000000000000000000000000000000000001",
1100 )
1101 .unwrap();
1102 let result = client
1103 .wait_for_transaction(&hash, Some(Duration::from_secs(5)))
1104 .await
1105 .unwrap();
1106
1107 assert!(
1108 result
1109 .data
1110 .get("success")
1111 .and_then(serde_json::Value::as_bool)
1112 .unwrap()
1113 );
1114 }
1115
1116 #[tokio::test]
1117 async fn test_server_error_retryable() {
1118 let server = MockServer::start().await;
1119
1120 Mock::given(method("GET"))
1121 .and(path("/v1"))
1122 .respond_with(ResponseTemplate::new(503).set_body_json(serde_json::json!({
1123 "message": "Service temporarily unavailable"
1124 })))
1125 .expect(1)
1126 .mount(&server)
1127 .await;
1128
1129 let url = format!("{}/v1", server.uri());
1130 let config = AptosConfig::custom(&url).unwrap().without_retry();
1131 let client = FullnodeClient::new(config).unwrap();
1132 let result = client.get_ledger_info().await;
1133
1134 assert!(result.is_err());
1135 assert!(result.unwrap_err().is_retryable());
1136 }
1137
1138 #[tokio::test]
1139 async fn test_rate_limited() {
1140 let server = MockServer::start().await;
1141
1142 Mock::given(method("GET"))
1143 .and(path("/v1"))
1144 .respond_with(
1145 ResponseTemplate::new(429)
1146 .set_body_json(serde_json::json!({
1147 "message": "Rate limited"
1148 }))
1149 .insert_header("retry-after", "30"),
1150 )
1151 .expect(1)
1152 .mount(&server)
1153 .await;
1154
1155 let url = format!("{}/v1", server.uri());
1156 let config = AptosConfig::custom(&url).unwrap().without_retry();
1157 let client = FullnodeClient::new(config).unwrap();
1158 let result = client.get_ledger_info().await;
1159
1160 assert!(result.is_err());
1161 assert!(result.unwrap_err().is_retryable());
1162 }
1163
1164 #[tokio::test]
1165 async fn test_get_block_by_height() {
1166 let server = MockServer::start().await;
1167
1168 Mock::given(method("GET"))
1169 .and(path_regex(r"/v1/blocks/by_height/\d+"))
1170 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1171 "block_height": "1000",
1172 "block_hash": "0xabc",
1173 "block_timestamp": "1234567890",
1174 "first_version": "100",
1175 "last_version": "200"
1176 })))
1177 .expect(1)
1178 .mount(&server)
1179 .await;
1180
1181 let client = create_mock_client(&server);
1182 let result = client.get_block_by_height(1000, false).await.unwrap();
1183
1184 assert!(result.data.get("block_height").is_some());
1185 }
1186
1187 #[tokio::test]
1188 async fn test_view() {
1189 let server = MockServer::start().await;
1190
1191 Mock::given(method("POST"))
1192 .and(path("/v1/view"))
1193 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!(["1000000"])))
1194 .expect(1)
1195 .mount(&server)
1196 .await;
1197
1198 let client = create_mock_client(&server);
1199 let result: AptosResponse<Vec<serde_json::Value>> = client
1200 .view(
1201 "0x1::coin::balance",
1202 vec!["0x1::aptos_coin::AptosCoin".to_string()],
1203 vec![serde_json::json!("0x1")],
1204 )
1205 .await
1206 .unwrap();
1207
1208 assert_eq!(result.data.len(), 1);
1209 }
1210}