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