Skip to main content

aptos_sdk/api/
fullnode.rs

1//! Fullnode REST API client.
2
3use 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";
21/// Default timeout for waiting for a transaction to be committed.
22const DEFAULT_TRANSACTION_WAIT_TIMEOUT_SECS: u64 = 30;
23/// Maximum size for error response bodies (8 KB).
24///
25/// # Security
26///
27/// This prevents memory exhaustion from malicious servers sending extremely
28/// large error response bodies.
29const MAX_ERROR_BODY_SIZE: usize = 8 * 1024;
30
31/// Client for the Aptos fullnode REST API.
32///
33/// The client supports automatic retry with exponential backoff for transient
34/// failures. Configure retry behavior via [`AptosConfig::with_retry`].
35///
36/// # Example
37///
38/// ```rust,no_run
39/// use aptos_sdk::api::FullnodeClient;
40/// use aptos_sdk::config::AptosConfig;
41/// use aptos_sdk::retry::RetryConfig;
42///
43/// #[tokio::main]
44/// async fn main() -> anyhow::Result<()> {
45///     // Default retry configuration
46///     let client = FullnodeClient::new(AptosConfig::testnet())?;
47///     
48///     // Aggressive retry for unstable networks
49///     let client = FullnodeClient::new(
50///         AptosConfig::testnet().with_retry(RetryConfig::aggressive())
51///     )?;
52///     
53///     // Disable retry for debugging
54///     let client = FullnodeClient::new(
55///         AptosConfig::testnet().without_retry()
56///     )?;
57///     
58///     let ledger_info = client.get_ledger_info().await?;
59///     println!("Ledger version: {:?}", ledger_info.data.version());
60///     Ok(())
61/// }
62/// ```
63#[derive(Debug, Clone)]
64pub struct FullnodeClient {
65    config: AptosConfig,
66    client: Client,
67    retry_config: Arc<RetryConfig>,
68}
69
70impl FullnodeClient {
71    /// Creates a new fullnode client.
72    ///
73    /// # TLS Security
74    ///
75    /// This client uses `reqwest` with its default TLS configuration, which:
76    /// - Validates server certificates against the system's certificate store
77    /// - Requires valid TLS certificates for HTTPS connections
78    /// - Uses secure TLS versions (TLS 1.2+)
79    ///
80    /// All Aptos network endpoints (mainnet, testnet, devnet) use HTTPS with
81    /// valid certificates. The local configuration uses HTTP for development.
82    ///
83    /// For custom deployments requiring custom CA certificates, use the
84    /// `REQUESTS_CA_BUNDLE` or `SSL_CERT_FILE` environment variables, or
85    /// configure a custom `reqwest::Client` and use `from_client()`.
86    ///
87    /// # Errors
88    ///
89    /// Returns an error if the HTTP client fails to build (e.g., invalid TLS configuration).
90    pub fn new(config: AptosConfig) -> AptosResult<Self> {
91        let pool = config.pool_config();
92
93        // SECURITY: TLS certificate validation is enabled by default via reqwest.
94        // The client will reject connections to servers with invalid certificates.
95        // All production Aptos endpoints use HTTPS with valid certificates.
96        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    /// Returns the base URL for the fullnode.
118    pub fn base_url(&self) -> &Url {
119        self.config.fullnode_url()
120    }
121
122    /// Returns the configuration backing this client.
123    pub fn config(&self) -> &AptosConfig {
124        &self.config
125    }
126
127    /// Calls a view function whose arguments are supplied as **BCS-encoded
128    /// bytes**, returning the JSON-decoded result values.
129    ///
130    /// This differs from [`view`](Self::view) (which takes JSON arguments) and
131    /// from [`view_bcs`](Self::view_bcs) (which hex-encodes BCS bytes into the
132    /// JSON body, and therefore only round-trips correctly for argument types
133    /// whose hex happens to coincide with their JSON form, e.g. addresses).
134    ///
135    /// Here the entire request is serialized as an on-wire `ViewRequest`
136    /// (identical BCS layout to [`crate::transaction::EntryFunction`]) and
137    /// posted with the `application/x.aptos.view_function+bcs` content type,
138    /// exactly like the TypeScript SDK's `view`. This is the only reliable way
139    /// to pass typed arguments such as `Option<String>`, `vector<u8>`, or
140    /// `String` to a view function. The response is requested as JSON and
141    /// returned as an array of [`serde_json::Value`], one entry per declared
142    /// return value.
143    ///
144    /// # Arguments
145    ///
146    /// * `function` - Fully qualified function id (e.g. `0x1::coin::balance`).
147    /// * `type_args` - Type arguments for generic functions.
148    /// * `args` - Each argument already BCS-encoded (e.g. via
149    ///   `aptos_bcs::to_bytes`).
150    ///
151    /// # Errors
152    ///
153    /// Returns an error if the function id is malformed, the request cannot be
154    /// BCS-serialized, the HTTP request fails, or the API returns an error
155    /// status code.
156    pub async fn view_bcs_args(
157        &self,
158        function: &str,
159        type_args: Vec<crate::types::TypeTag>,
160        args: Vec<Vec<u8>>,
161    ) -> AptosResult<AptosResponse<Vec<serde_json::Value>>> {
162        // `Content-Type` for a request whose body is a BCS-serialized
163        // `ViewRequest` (as opposed to `application/x-bcs`, which is an `Accept`
164        // value asking the node to BCS-encode the *response*).
165        const BCS_VIEW_REQUEST_CONTENT_TYPE: &str = "application/x.aptos.view_function+bcs";
166
167        // A `ViewRequest`/`ViewFunction` is BCS-identical to an `EntryFunction`:
168        // `module: ModuleId, function: Identifier, ty_args: Vec<TypeTag>,
169        // args: Vec<Vec<u8>>`. Reusing `EntryFunction` keeps the wire format in
170        // one place and gives us the function-id parsing for free.
171        let view_fn =
172            crate::transaction::EntryFunction::from_function_id(function, type_args, args)?;
173        let body = aptos_bcs::to_bytes(&view_fn).map_err(AptosError::bcs)?;
174        let url = self.build_url("view");
175
176        let client = self.client.clone();
177        let retry_config = self.retry_config.clone();
178        let max_response_size = self.config.pool_config().max_response_size;
179
180        let executor = RetryExecutor::from_shared(retry_config);
181        executor
182            .execute(|| {
183                let client = client.clone();
184                let url = url.clone();
185                let body = body.clone();
186                async move {
187                    let response = client
188                        .post(url)
189                        .header(CONTENT_TYPE, BCS_VIEW_REQUEST_CONTENT_TYPE)
190                        .header(ACCEPT, JSON_CONTENT_TYPE)
191                        .body(body)
192                        .send()
193                        .await?;
194
195                    Self::handle_response_static(response, max_response_size).await
196                }
197            })
198            .await
199    }
200
201    /// Returns the retry configuration.
202    pub fn retry_config(&self) -> &RetryConfig {
203        &self.retry_config
204    }
205
206    // === Ledger Info ===
207
208    /// Gets the current ledger information.
209    ///
210    /// # Errors
211    ///
212    /// Returns an error if the HTTP request fails, the API returns an error status code,
213    /// or the response cannot be parsed as JSON.
214    pub async fn get_ledger_info(&self) -> AptosResult<AptosResponse<LedgerInfo>> {
215        let url = self.build_url("");
216        self.get_json(url).await
217    }
218
219    // === Account ===
220
221    /// Gets account information.
222    ///
223    /// # Errors
224    ///
225    /// Returns an error if the HTTP request fails, the API returns an error status code,
226    /// the response cannot be parsed as JSON, or the account is not found (404).
227    pub async fn get_account(
228        &self,
229        address: AccountAddress,
230    ) -> AptosResult<AptosResponse<AccountData>> {
231        let url = self.build_url(&format!("accounts/{address}"));
232        self.get_json(url).await
233    }
234
235    /// Gets the sequence number for an account.
236    ///
237    /// # Errors
238    ///
239    /// Returns an error if fetching the account fails, the account is not found (404),
240    /// or the sequence number cannot be parsed from the account data.
241    pub async fn get_sequence_number(&self, address: AccountAddress) -> AptosResult<u64> {
242        let account = self.get_account(address).await?;
243        account
244            .data
245            .sequence_number()
246            .map_err(|e| AptosError::Internal(format!("failed to parse sequence number: {e}")))
247    }
248
249    /// Gets all resources for an account in a single page (uses the
250    /// fullnode's default page size; large accounts may be truncated).
251    ///
252    /// For paginated access on accounts that hold many resources, use
253    /// [`get_account_resources_paginated`](Self::get_account_resources_paginated).
254    ///
255    /// # Errors
256    ///
257    /// Returns an error if the HTTP request fails, the API returns an error status code,
258    /// or the response cannot be parsed as JSON.
259    pub async fn get_account_resources(
260        &self,
261        address: AccountAddress,
262    ) -> AptosResult<AptosResponse<Vec<Resource>>> {
263        self.get_account_resources_paginated(address, None, None)
264            .await
265    }
266
267    /// Gets resources for an account with explicit pagination cursors.
268    ///
269    /// * `start` -- opaque cursor token returned by the previous page in
270    ///   the `x-aptos-cursor` header (and surfaced as
271    ///   [`AptosResponse::cursor`](super::response::AptosResponse#field.cursor)).
272    ///   The type is `Option<&str>` rather than `Option<u64>` so opaque
273    ///   non-numeric cursors round-trip losslessly. Pass `None` for the
274    ///   first page; for subsequent pages forward
275    ///   `previous_response.cursor.as_deref()`.
276    /// * `limit` -- maximum number of resources to return on this page.
277    ///   The fullnode caps this server-side; callers should not assume
278    ///   their requested limit is honored verbatim.
279    ///
280    /// Matches the TypeScript SDK's `getAccountResources({ start, limit })`.
281    ///
282    /// # Errors
283    ///
284    /// Returns an error if the HTTP request fails, the API returns an error status code,
285    /// or the response cannot be parsed as JSON.
286    pub async fn get_account_resources_paginated(
287        &self,
288        address: AccountAddress,
289        start: Option<&str>,
290        limit: Option<u16>,
291    ) -> AptosResult<AptosResponse<Vec<Resource>>> {
292        let mut url = self.build_url(&format!("accounts/{address}/resources"));
293        append_start_limit(&mut url, start, limit);
294        self.get_json(url).await
295    }
296
297    /// Gets a specific resource for an account.
298    ///
299    /// # Errors
300    ///
301    /// Returns an error if the HTTP request fails, the API returns an error status code,
302    /// the response cannot be parsed as JSON, or the resource is not found (404).
303    pub async fn get_account_resource(
304        &self,
305        address: AccountAddress,
306        resource_type: &str,
307    ) -> AptosResult<AptosResponse<Resource>> {
308        let url = self.build_url(&format!(
309            "accounts/{}/resource/{}",
310            address,
311            urlencoding::encode(resource_type)
312        ));
313        self.get_json(url).await
314    }
315
316    /// Gets all modules for an account in a single page (uses the
317    /// fullnode's default page size; accounts that publish many modules
318    /// may be truncated).
319    ///
320    /// For paginated access, use
321    /// [`get_account_modules_paginated`](Self::get_account_modules_paginated).
322    ///
323    /// # Errors
324    ///
325    /// Returns an error if the HTTP request fails, the API returns an error status code,
326    /// or the response cannot be parsed as JSON.
327    pub async fn get_account_modules(
328        &self,
329        address: AccountAddress,
330    ) -> AptosResult<AptosResponse<Vec<MoveModule>>> {
331        self.get_account_modules_paginated(address, None, None)
332            .await
333    }
334
335    /// Gets modules for an account with explicit pagination cursors.
336    ///
337    /// See [`get_account_resources_paginated`](Self::get_account_resources_paginated)
338    /// for `start` / `limit` semantics; they are interpreted the same way
339    /// by the fullnode.
340    ///
341    /// # Errors
342    ///
343    /// Returns an error if the HTTP request fails, the API returns an error status code,
344    /// or the response cannot be parsed as JSON.
345    pub async fn get_account_modules_paginated(
346        &self,
347        address: AccountAddress,
348        start: Option<&str>,
349        limit: Option<u16>,
350    ) -> AptosResult<AptosResponse<Vec<MoveModule>>> {
351        let mut url = self.build_url(&format!("accounts/{address}/modules"));
352        append_start_limit(&mut url, start, limit);
353        self.get_json(url).await
354    }
355
356    /// Gets a specific module for an account.
357    ///
358    /// # Errors
359    ///
360    /// Returns an error if the HTTP request fails, the API returns an error status code,
361    /// the response cannot be parsed as JSON, or the module is not found (404).
362    pub async fn get_account_module(
363        &self,
364        address: AccountAddress,
365        module_name: &str,
366    ) -> AptosResult<AptosResponse<MoveModule>> {
367        let url = self.build_url(&format!("accounts/{address}/module/{module_name}"));
368        self.get_json(url).await
369    }
370
371    // === Balance ===
372
373    /// Gets the APT balance for an account in octas.
374    ///
375    /// # Errors
376    ///
377    /// Returns an error if the view function call fails, the response cannot be parsed,
378    /// or the balance value cannot be converted to u64.
379    pub async fn get_account_balance(&self, address: AccountAddress) -> AptosResult<u64> {
380        // Use the coin::balance view function which works with both legacy CoinStore
381        // and the newer Fungible Asset standard
382        let result = self
383            .view(
384                "0x1::coin::balance",
385                vec!["0x1::aptos_coin::AptosCoin".to_string()],
386                vec![serde_json::json!(address.to_string())],
387            )
388            .await?;
389
390        // The view function returns an array with a single string value
391        let balance_str = result
392            .data
393            .first()
394            .and_then(|v| v.as_str())
395            .ok_or_else(|| AptosError::Internal("failed to parse balance response".into()))?;
396
397        balance_str
398            .parse()
399            .map_err(|_| AptosError::Internal("failed to parse balance as u64".into()))
400    }
401
402    // === Transactions ===
403
404    /// Submits a signed transaction.
405    ///
406    /// Note: Transaction submission is automatically retried for transient errors.
407    /// Duplicate transaction submissions (same hash) are safe and idempotent.
408    ///
409    /// # Errors
410    ///
411    /// Returns an error if the transaction cannot be serialized to BCS, the HTTP request fails,
412    /// the API returns an error status code, or the response cannot be parsed as JSON.
413    pub async fn submit_transaction(
414        &self,
415        signed_txn: &SignedTransaction,
416    ) -> AptosResult<AptosResponse<PendingTransaction>> {
417        let url = self.build_url("transactions");
418        let bcs_bytes = signed_txn.to_bcs()?;
419        let client = self.client.clone();
420        let retry_config = self.retry_config.clone();
421        let max_response_size = self.config.pool_config().max_response_size;
422
423        let executor = RetryExecutor::from_shared(retry_config);
424        executor
425            .execute(|| {
426                let client = client.clone();
427                let url = url.clone();
428                let bcs_bytes = bcs_bytes.clone();
429                async move {
430                    let response = client
431                        .post(url)
432                        .header(CONTENT_TYPE, BCS_CONTENT_TYPE)
433                        .header(ACCEPT, JSON_CONTENT_TYPE)
434                        .body(bcs_bytes)
435                        .send()
436                        .await?;
437
438                    Self::handle_response_static(response, max_response_size).await
439                }
440            })
441            .await
442    }
443
444    /// Submits a transaction and waits for it to be committed.
445    ///
446    /// # Errors
447    ///
448    /// Returns an error if transaction submission fails, the transaction times out waiting
449    /// for commitment, the transaction execution fails, or any HTTP/API errors occur.
450    pub async fn submit_and_wait(
451        &self,
452        signed_txn: &SignedTransaction,
453        timeout: Option<Duration>,
454    ) -> AptosResult<AptosResponse<serde_json::Value>> {
455        let pending = self.submit_transaction(signed_txn).await?;
456        self.wait_for_transaction(&pending.data.hash, timeout).await
457    }
458
459    /// Gets a transaction by hash.
460    ///
461    /// # Errors
462    ///
463    /// Returns an error if the HTTP request fails, the API returns an error status code,
464    /// the response cannot be parsed as JSON, or the transaction is not found (404).
465    pub async fn get_transaction_by_hash(
466        &self,
467        hash: &HashValue,
468    ) -> AptosResult<AptosResponse<serde_json::Value>> {
469        let url = self.build_url(&format!("transactions/by_hash/{hash}"));
470        self.get_json(url).await
471    }
472
473    /// Gets a committed transaction by its ledger version.
474    ///
475    /// # Errors
476    ///
477    /// Returns an error if the HTTP request fails, the API returns an error status code,
478    /// the response cannot be parsed as JSON, or the transaction is not found (404).
479    pub async fn get_transaction_by_version(
480        &self,
481        version: u64,
482    ) -> AptosResult<AptosResponse<serde_json::Value>> {
483        let url = self.build_url(&format!("transactions/by_version/{version}"));
484        self.get_json(url).await
485    }
486
487    /// Lists committed transactions, most-recent first.
488    ///
489    /// `start` is the ledger version to begin at (defaults to the most recent
490    /// transactions when `None`); `limit` bounds the page size (the fullnode
491    /// caps this regardless of the requested value).
492    ///
493    /// # Errors
494    ///
495    /// Returns an error if the HTTP request fails, the API returns an error status code,
496    /// or the response cannot be parsed as JSON.
497    pub async fn get_transactions(
498        &self,
499        start: Option<u64>,
500        limit: Option<u16>,
501    ) -> AptosResult<AptosResponse<Vec<serde_json::Value>>> {
502        let mut url = self.build_url("transactions");
503        {
504            let mut query = url.query_pairs_mut();
505            if let Some(start) = start {
506                query.append_pair("start", &start.to_string());
507            }
508            if let Some(limit) = limit {
509                query.append_pair("limit", &limit.to_string());
510            }
511        }
512        self.get_json(url).await
513    }
514
515    /// Lists transactions sent by a specific account, ordered by the account's
516    /// sequence number.
517    ///
518    /// `start` is the sequence number to begin at (defaults to `0` when `None`);
519    /// `limit` bounds the page size.
520    ///
521    /// # Errors
522    ///
523    /// Returns an error if the HTTP request fails, the API returns an error status code,
524    /// or the response cannot be parsed as JSON.
525    pub async fn get_account_transactions(
526        &self,
527        address: AccountAddress,
528        start: Option<u64>,
529        limit: Option<u16>,
530    ) -> AptosResult<AptosResponse<Vec<serde_json::Value>>> {
531        let mut url = self.build_url(&format!("accounts/{address}/transactions"));
532        {
533            let mut query = url.query_pairs_mut();
534            if let Some(start) = start {
535                query.append_pair("start", &start.to_string());
536            }
537            if let Some(limit) = limit {
538                query.append_pair("limit", &limit.to_string());
539            }
540        }
541        self.get_json(url).await
542    }
543
544    /// Waits for a transaction to be committed.
545    ///
546    /// Uses exponential backoff for polling, starting at 200ms and doubling up to 2s.
547    ///
548    /// # Errors
549    ///
550    /// Returns an error if the transaction times out waiting for commitment, the transaction
551    /// execution fails (`vm_status` indicates failure), or HTTP/API errors occur while polling.
552    pub async fn wait_for_transaction(
553        &self,
554        hash: &HashValue,
555        timeout: Option<Duration>,
556    ) -> AptosResult<AptosResponse<serde_json::Value>> {
557        let timeout = timeout.unwrap_or(Duration::from_secs(DEFAULT_TRANSACTION_WAIT_TIMEOUT_SECS));
558        let start = std::time::Instant::now();
559
560        // Exponential backoff: start at 200ms, double each time, max 2s
561        let initial_interval = Duration::from_millis(200);
562        let max_interval = Duration::from_secs(2);
563        let mut current_interval = initial_interval;
564
565        loop {
566            match self.get_transaction_by_hash(hash).await {
567                Ok(response) => {
568                    // Check if transaction is committed (has version)
569                    if response.data.get("version").is_some() {
570                        // Check success
571                        let success = response
572                            .data
573                            .get("success")
574                            .and_then(serde_json::Value::as_bool);
575                        if success == Some(false) {
576                            let vm_status = response
577                                .data
578                                .get("vm_status")
579                                .and_then(|v| v.as_str())
580                                .unwrap_or("unknown")
581                                .to_string();
582                            return Err(AptosError::ExecutionFailed { vm_status });
583                        }
584                        return Ok(response);
585                    }
586                }
587                Err(AptosError::Api {
588                    status_code: 404, ..
589                }) => {
590                    // Transaction not found yet, continue waiting
591                }
592                Err(e) => return Err(e),
593            }
594
595            if start.elapsed() >= timeout {
596                return Err(AptosError::TransactionTimeout {
597                    hash: hash.to_string(),
598                    timeout_secs: timeout.as_secs(),
599                });
600            }
601
602            tokio::time::sleep(current_interval).await;
603
604            // Exponential backoff with cap
605            current_interval = std::cmp::min(current_interval * 2, max_interval);
606        }
607    }
608
609    /// Simulates a transaction.
610    ///
611    /// Delegates to [`simulate_transaction_with_options`](Self::simulate_transaction_with_options) with `None` for options.
612    ///
613    /// The request body is derived from `signed_txn` after rewriting authenticators for the
614    /// simulate endpoint (see [`SignedTransaction::for_simulate_endpoint`]).
615    ///
616    /// # Errors
617    ///
618    /// Returns an error if the transaction cannot be serialized to BCS, the HTTP request fails,
619    /// the API returns an error status code, or the response cannot be parsed as JSON.
620    pub async fn simulate_transaction(
621        &self,
622        signed_txn: &SignedTransaction,
623    ) -> AptosResult<AptosResponse<Vec<serde_json::Value>>> {
624        self.simulate_transaction_with_options(signed_txn, None as Option<SimulateQueryOptions>)
625            .await
626    }
627
628    /// Simulates a transaction with optional query parameters.
629    ///
630    /// Pass [`SimulateQueryOptions`] to request gas estimation behavior
631    /// (e.g. `estimate_gas_unit_price`, `estimate_max_gas_amount`) as query
632    /// parameters to the `/transactions/simulate` endpoint.
633    ///
634    /// Authenticators on `signed_txn` are rewritten client-side (via
635    /// [`SignedTransaction::for_simulate_endpoint`]) before BCS serialization so the
636    /// fullnode never receives a cryptographically valid signature (which it rejects with
637    /// HTTP 400).
638    ///
639    /// # Errors
640    ///
641    /// Returns an error if the transaction cannot be serialized to BCS, the HTTP request fails,
642    /// the API returns an error status code, or the response cannot be parsed as JSON.
643    pub async fn simulate_transaction_with_options(
644        &self,
645        signed_txn: &SignedTransaction,
646        options: impl Into<Option<SimulateQueryOptions>>,
647    ) -> AptosResult<AptosResponse<Vec<serde_json::Value>>> {
648        let mut url = self.build_url("transactions/simulate");
649        if let Some(opts) = options.into() {
650            let mut pairs = url.query_pairs_mut();
651            if opts.estimate_gas_unit_price {
652                pairs.append_pair("estimate_gas_unit_price", "true");
653            }
654            if opts.estimate_max_gas_amount {
655                pairs.append_pair("estimate_max_gas_amount", "true");
656            }
657            if opts.estimate_prioritized_gas_unit_price {
658                pairs.append_pair("estimate_prioritized_gas_unit_price", "true");
659            }
660        }
661        let bcs_bytes = signed_txn.for_simulate_endpoint().to_bcs()?;
662        let client = self.client.clone();
663        let retry_config = self.retry_config.clone();
664        let max_response_size = self.config.pool_config().max_response_size;
665
666        let executor = RetryExecutor::from_shared(retry_config);
667        executor
668            .execute(|| {
669                let client = client.clone();
670                let url = url.clone();
671                let bcs_bytes = bcs_bytes.clone();
672                async move {
673                    let response = client
674                        .post(url)
675                        .header(CONTENT_TYPE, BCS_CONTENT_TYPE)
676                        .header(ACCEPT, JSON_CONTENT_TYPE)
677                        .body(bcs_bytes)
678                        .send()
679                        .await?;
680
681                    Self::handle_response_static(response, max_response_size).await
682                }
683            })
684            .await
685    }
686
687    // === Gas ===
688
689    /// Gets the current gas estimation.
690    ///
691    /// # Errors
692    ///
693    /// Returns an error if the HTTP request fails, the API returns an error status code,
694    /// or the response cannot be parsed as JSON.
695    pub async fn estimate_gas_price(&self) -> AptosResult<AptosResponse<GasEstimation>> {
696        let url = self.build_url("estimate_gas_price");
697        self.get_json(url).await
698    }
699
700    // === View Functions ===
701
702    /// Calls a view function.
703    ///
704    /// # Errors
705    ///
706    /// Returns an error if the HTTP request fails, the API returns an error status code,
707    /// or the response cannot be parsed as JSON.
708    pub async fn view(
709        &self,
710        function: &str,
711        type_args: Vec<String>,
712        args: Vec<serde_json::Value>,
713    ) -> AptosResult<AptosResponse<Vec<serde_json::Value>>> {
714        let url = self.build_url("view");
715
716        let body = serde_json::json!({
717            "function": function,
718            "type_arguments": type_args,
719            "arguments": args,
720        });
721
722        let client = self.client.clone();
723        let retry_config = self.retry_config.clone();
724        let max_response_size = self.config.pool_config().max_response_size;
725
726        let executor = RetryExecutor::from_shared(retry_config);
727        executor
728            .execute(|| {
729                let client = client.clone();
730                let url = url.clone();
731                let body = body.clone();
732                async move {
733                    let response = client
734                        .post(url)
735                        .header(CONTENT_TYPE, JSON_CONTENT_TYPE)
736                        .header(ACCEPT, JSON_CONTENT_TYPE)
737                        .json(&body)
738                        .send()
739                        .await?;
740
741                    Self::handle_response_static(response, max_response_size).await
742                }
743            })
744            .await
745    }
746
747    /// Calls a view function using BCS encoding for both inputs and outputs.
748    ///
749    /// This method provides lossless serialization by using BCS (Binary Canonical Serialization)
750    /// instead of JSON, which is important for large integers (u128, u256) and other types
751    /// where JSON can lose precision.
752    ///
753    /// # Arguments
754    ///
755    /// * `function` - The fully qualified function name (e.g., `0x1::coin::balance`)
756    /// * `type_args` - Type arguments as strings (e.g., `0x1::aptos_coin::AptosCoin`)
757    /// * `args` - Pre-serialized BCS arguments as byte vectors
758    ///
759    /// # Returns
760    ///
761    /// Returns the raw BCS-encoded response bytes, which can be deserialized
762    /// into the expected return type using `aptos_bcs::from_bytes`.
763    ///
764    /// # Errors
765    ///
766    /// Returns an error if the HTTP request fails, the API returns an error status code,
767    /// or the BCS serialization fails.
768    pub async fn view_bcs(
769        &self,
770        function: &str,
771        type_args: Vec<String>,
772        args: Vec<Vec<u8>>,
773    ) -> AptosResult<AptosResponse<Vec<u8>>> {
774        let url = self.build_url("view");
775
776        // Convert BCS args to hex strings for the JSON request body.
777        // The Aptos API accepts hex-encoded BCS bytes in the arguments array.
778        let hex_args: Vec<serde_json::Value> = args
779            .iter()
780            .map(|bytes| serde_json::json!(const_hex::encode_prefixed(bytes)))
781            .collect();
782
783        let body = serde_json::json!({
784            "function": function,
785            "type_arguments": type_args,
786            "arguments": hex_args,
787        });
788
789        let client = self.client.clone();
790        let retry_config = self.retry_config.clone();
791        let max_response_size = self.config.pool_config().max_response_size;
792
793        let executor = RetryExecutor::from_shared(retry_config);
794        executor
795            .execute(|| {
796                let client = client.clone();
797                let url = url.clone();
798                let body = body.clone();
799                async move {
800                    let response = client
801                        .post(url)
802                        .header(CONTENT_TYPE, JSON_CONTENT_TYPE)
803                        .header(ACCEPT, BCS_VIEW_CONTENT_TYPE)
804                        .json(&body)
805                        .send()
806                        .await?;
807
808                    // Check for errors before reading body
809                    let status = response.status();
810                    if !status.is_success() {
811                        // SECURITY: Bound error body reads to prevent OOM from
812                        // malicious servers sending huge error responses.
813                        let error_bytes =
814                            crate::config::read_response_bounded(response, MAX_ERROR_BODY_SIZE)
815                                .await
816                                .ok();
817                        let error_text = error_bytes
818                            .and_then(|b| String::from_utf8(b).ok())
819                            .unwrap_or_default();
820                        return Err(AptosError::Api {
821                            status_code: status.as_u16(),
822                            message: Self::truncate_error_body(error_text),
823                            error_code: None,
824                            vm_error_code: None,
825                        });
826                    }
827
828                    // SECURITY: Stream body with size limit to prevent OOM
829                    // from malicious responses (including chunked encoding).
830                    let bytes =
831                        crate::config::read_response_bounded(response, max_response_size).await?;
832                    Ok(AptosResponse::new(bytes))
833                }
834            })
835            .await
836    }
837
838    // === Tables ===
839
840    /// Reads an item from a Move table by its key.
841    ///
842    /// Table state is not addressable as a normal account resource, so the
843    /// fullnode exposes a dedicated `POST /tables/{handle}/item` endpoint that
844    /// takes the table's key/value Move types and the (JSON-encoded) key, and
845    /// returns the stored value. This mirrors the TypeScript SDK's
846    /// `getTableItem`.
847    ///
848    /// # Arguments
849    ///
850    /// * `handle` - The table handle (the address-like `0x…` identifier of the
851    ///   `Table` on chain, e.g. read from a resource field).
852    /// * `key_type` - The Move type of the key (e.g. `address`, `u64`,
853    ///   `0x1::string::String`).
854    /// * `value_type` - The Move type of the stored value.
855    /// * `key` - The key to look up, encoded as the fullnode expects it in JSON
856    ///   (e.g. `serde_json::json!("0x1")` for an `address` key).
857    ///
858    /// # Errors
859    ///
860    /// Returns an error if the request cannot be serialized, the HTTP request
861    /// fails, the API returns an error status code (including 404 when the key
862    /// is absent from the table), or the response cannot be parsed as JSON.
863    pub async fn get_table_item(
864        &self,
865        handle: &str,
866        key_type: &str,
867        value_type: &str,
868        key: serde_json::Value,
869    ) -> AptosResult<AptosResponse<serde_json::Value>> {
870        let url = self.build_url(&format!("tables/{}/item", urlencoding::encode(handle)));
871        let body = serde_json::json!({
872            "key_type": key_type,
873            "value_type": value_type,
874            "key": key,
875        });
876        self.post_json(url, &body).await
877    }
878
879    // === Events ===
880
881    /// Gets events emitted from an account by their creation number.
882    ///
883    /// Each `EventHandle` an account owns has a unique creation number; this
884    /// endpoint returns the events for one such handle without needing to know
885    /// the handle's Move struct type (unlike
886    /// [`get_events_by_event_handle`](Self::get_events_by_event_handle)). Mirrors
887    /// the TypeScript SDK's `getAccountEventsByCreationNumber`.
888    ///
889    /// # Errors
890    ///
891    /// Returns an error if the HTTP request fails, the API returns an error status code,
892    /// or the response cannot be parsed as JSON.
893    pub async fn get_events_by_creation_number(
894        &self,
895        address: AccountAddress,
896        creation_number: u64,
897        start: Option<u64>,
898        limit: Option<u64>,
899    ) -> AptosResult<AptosResponse<Vec<serde_json::Value>>> {
900        let mut url = self.build_url(&format!("accounts/{address}/events/{creation_number}"));
901        {
902            let mut query = url.query_pairs_mut();
903            if let Some(start) = start {
904                query.append_pair("start", &start.to_string());
905            }
906            if let Some(limit) = limit {
907                query.append_pair("limit", &limit.to_string());
908            }
909        }
910        self.get_json(url).await
911    }
912
913    /// Gets events by event handle.
914    ///
915    /// # Errors
916    ///
917    /// Returns an error if the HTTP request fails, the API returns an error status code,
918    /// or the response cannot be parsed as JSON.
919    pub async fn get_events_by_event_handle(
920        &self,
921        address: AccountAddress,
922        event_handle_struct: &str,
923        field_name: &str,
924        start: Option<u64>,
925        limit: Option<u64>,
926    ) -> AptosResult<AptosResponse<Vec<serde_json::Value>>> {
927        let mut url = self.build_url(&format!(
928            "accounts/{}/events/{}/{}",
929            address,
930            urlencoding::encode(event_handle_struct),
931            field_name
932        ));
933
934        {
935            let mut query = url.query_pairs_mut();
936            if let Some(start) = start {
937                query.append_pair("start", &start.to_string());
938            }
939            if let Some(limit) = limit {
940                query.append_pair("limit", &limit.to_string());
941            }
942        }
943
944        self.get_json(url).await
945    }
946
947    // === Blocks ===
948
949    /// Gets block by height.
950    ///
951    /// # Errors
952    ///
953    /// Returns an error if the HTTP request fails, the API returns an error status code,
954    /// the response cannot be parsed as JSON, or the block is not found (404).
955    pub async fn get_block_by_height(
956        &self,
957        height: u64,
958        with_transactions: bool,
959    ) -> AptosResult<AptosResponse<serde_json::Value>> {
960        let mut url = self.build_url(&format!("blocks/by_height/{height}"));
961        url.query_pairs_mut()
962            .append_pair("with_transactions", &with_transactions.to_string());
963        self.get_json(url).await
964    }
965
966    /// Gets block by version.
967    ///
968    /// # Errors
969    ///
970    /// Returns an error if the HTTP request fails, the API returns an error status code,
971    /// the response cannot be parsed as JSON, or the block is not found (404).
972    pub async fn get_block_by_version(
973        &self,
974        version: u64,
975        with_transactions: bool,
976    ) -> AptosResult<AptosResponse<serde_json::Value>> {
977        let mut url = self.build_url(&format!("blocks/by_version/{version}"));
978        url.query_pairs_mut()
979            .append_pair("with_transactions", &with_transactions.to_string());
980        self.get_json(url).await
981    }
982
983    // === Helper Methods ===
984
985    fn build_url(&self, path: &str) -> Url {
986        let mut url = self.config.fullnode_url().clone();
987        if !path.is_empty() {
988            // Avoid format! allocations by building the path string manually
989            let base_path = url.path();
990            let needs_slash = !base_path.ends_with('/');
991            let new_len = base_path.len() + path.len() + usize::from(needs_slash);
992            let mut new_path = String::with_capacity(new_len);
993            new_path.push_str(base_path);
994            if needs_slash {
995                new_path.push('/');
996            }
997            new_path.push_str(path);
998            url.set_path(&new_path);
999        }
1000        url
1001    }
1002
1003    async fn get_json<T: for<'de> serde::Deserialize<'de>>(
1004        &self,
1005        url: Url,
1006    ) -> AptosResult<AptosResponse<T>> {
1007        let client = self.client.clone();
1008        let url_clone = url.clone();
1009        let retry_config = self.retry_config.clone();
1010        let max_response_size = self.config.pool_config().max_response_size;
1011
1012        let executor = RetryExecutor::from_shared(retry_config);
1013        executor
1014            .execute(|| {
1015                let client = client.clone();
1016                let url = url_clone.clone();
1017                async move {
1018                    let response = client
1019                        .get(url)
1020                        .header(ACCEPT, JSON_CONTENT_TYPE)
1021                        .send()
1022                        .await?;
1023
1024                    Self::handle_response_static(response, max_response_size).await
1025                }
1026            })
1027            .await
1028    }
1029
1030    /// Posts a JSON body and deserializes the JSON response.
1031    ///
1032    /// Shares the retry/backoff and bounded-response handling with
1033    /// [`get_json`](Self::get_json); used by read endpoints that require a
1034    /// request body (e.g. table-item lookups). Retries are safe because these
1035    /// endpoints are read-only and idempotent.
1036    async fn post_json<B: serde::Serialize, T: for<'de> serde::Deserialize<'de>>(
1037        &self,
1038        url: Url,
1039        body: &B,
1040    ) -> AptosResult<AptosResponse<T>> {
1041        let body = serde_json::to_vec(body)?;
1042        let client = self.client.clone();
1043        let retry_config = self.retry_config.clone();
1044        let max_response_size = self.config.pool_config().max_response_size;
1045
1046        let executor = RetryExecutor::from_shared(retry_config);
1047        executor
1048            .execute(|| {
1049                let client = client.clone();
1050                let url = url.clone();
1051                let body = body.clone();
1052                async move {
1053                    let response = client
1054                        .post(url)
1055                        .header(CONTENT_TYPE, JSON_CONTENT_TYPE)
1056                        .header(ACCEPT, JSON_CONTENT_TYPE)
1057                        .body(body)
1058                        .send()
1059                        .await?;
1060
1061                    Self::handle_response_static(response, max_response_size).await
1062                }
1063            })
1064            .await
1065    }
1066
1067    /// Truncates a string to the maximum error body size.
1068    ///
1069    /// # Security
1070    ///
1071    /// Prevents storing extremely large error messages from malicious servers.
1072    fn truncate_error_body(body: String) -> String {
1073        if body.len() > MAX_ERROR_BODY_SIZE {
1074            // Find the last valid UTF-8 char boundary at or before the limit
1075            let mut end = MAX_ERROR_BODY_SIZE;
1076            while end > 0 && !body.is_char_boundary(end) {
1077                end -= 1;
1078            }
1079            format!(
1080                "{}... [truncated, total: {} bytes]",
1081                &body[..end],
1082                body.len()
1083            )
1084        } else {
1085            body
1086        }
1087    }
1088
1089    /// Handles an HTTP response without retry (for internal use).
1090    ///
1091    /// # Security
1092    ///
1093    /// This method enforces `max_response_size` on the actual response body,
1094    /// not just the Content-Length header, to prevent memory exhaustion even
1095    /// when the server uses chunked transfer encoding.
1096    async fn handle_response_static<T: for<'de> serde::Deserialize<'de>>(
1097        response: reqwest::Response,
1098        max_response_size: usize,
1099    ) -> AptosResult<AptosResponse<T>> {
1100        let status = response.status();
1101
1102        // Extract headers before consuming response body
1103        let ledger_version = response
1104            .headers()
1105            .get("x-aptos-ledger-version")
1106            .and_then(|v| v.to_str().ok())
1107            .and_then(|v| v.parse().ok());
1108        let ledger_timestamp = response
1109            .headers()
1110            .get("x-aptos-ledger-timestamp")
1111            .and_then(|v| v.to_str().ok())
1112            .and_then(|v| v.parse().ok());
1113        let epoch = response
1114            .headers()
1115            .get("x-aptos-epoch")
1116            .and_then(|v| v.to_str().ok())
1117            .and_then(|v| v.parse().ok());
1118        let block_height = response
1119            .headers()
1120            .get("x-aptos-block-height")
1121            .and_then(|v| v.to_str().ok())
1122            .and_then(|v| v.parse().ok());
1123        let oldest_ledger_version = response
1124            .headers()
1125            .get("x-aptos-oldest-ledger-version")
1126            .and_then(|v| v.to_str().ok())
1127            .and_then(|v| v.parse().ok());
1128        let cursor = response
1129            .headers()
1130            .get("x-aptos-cursor")
1131            .and_then(|v| v.to_str().ok())
1132            .map(ToString::to_string);
1133
1134        // Extract Retry-After header for rate limiting (before consuming body)
1135        let retry_after_secs = response
1136            .headers()
1137            .get("retry-after")
1138            .and_then(|v| v.to_str().ok())
1139            .and_then(|v| v.parse().ok());
1140
1141        if status.is_success() {
1142            // SECURITY: Stream body with size limit to prevent OOM
1143            // from malicious responses (including chunked encoding).
1144            let bytes = crate::config::read_response_bounded(response, max_response_size).await?;
1145            let data: T = serde_json::from_slice(&bytes)?;
1146            Ok(AptosResponse {
1147                data,
1148                ledger_version,
1149                ledger_timestamp,
1150                epoch,
1151                block_height,
1152                oldest_ledger_version,
1153                cursor,
1154            })
1155        } else if status.as_u16() == 429 {
1156            // SECURITY: Return specific RateLimited error with Retry-After info
1157            // This allows callers to respect the server's rate limiting
1158            Err(AptosError::RateLimited { retry_after_secs })
1159        } else {
1160            // SECURITY: Bound error body reads to prevent OOM from malicious
1161            // servers sending huge error responses (including chunked encoding).
1162            let error_bytes = crate::config::read_response_bounded(response, MAX_ERROR_BODY_SIZE)
1163                .await
1164                .ok();
1165            let error_text = error_bytes
1166                .and_then(|b| String::from_utf8(b).ok())
1167                .unwrap_or_default();
1168            let error_text = Self::truncate_error_body(error_text);
1169            let body: serde_json::Value = serde_json::from_str(&error_text).unwrap_or_default();
1170            let message = body
1171                .get("message")
1172                .and_then(|v| v.as_str())
1173                .unwrap_or("Unknown error")
1174                .to_string();
1175            let error_code = body
1176                .get("error_code")
1177                .and_then(|v| v.as_str())
1178                .map(ToString::to_string);
1179            let vm_error_code = body
1180                .get("vm_error_code")
1181                .and_then(serde_json::Value::as_u64);
1182
1183            Err(AptosError::api_with_details(
1184                status.as_u16(),
1185                message,
1186                error_code,
1187                vm_error_code,
1188            ))
1189        }
1190    }
1191
1192    /// Legacy `handle_response` - delegates to static version.
1193    #[allow(dead_code)]
1194    async fn handle_response<T: for<'de> serde::Deserialize<'de>>(
1195        &self,
1196        response: reqwest::Response,
1197    ) -> AptosResult<AptosResponse<T>> {
1198        let max_response_size = self.config.pool_config().max_response_size;
1199        Self::handle_response_static(response, max_response_size).await
1200    }
1201}
1202
1203/// Appends `start` and `limit` query parameters to `url` when present.
1204///
1205/// Shared by paginated REST endpoints (`/accounts/{addr}/resources`,
1206/// `/accounts/{addr}/modules`, ...) so the formatting stays consistent.
1207/// `start` is forwarded verbatim as a string so opaque pagination cursors
1208/// returned in the `x-aptos-cursor` header round-trip losslessly (the
1209/// fullnode does not promise numeric cursors).
1210fn append_start_limit(url: &mut Url, start: Option<&str>, limit: Option<u16>) {
1211    if start.is_none() && limit.is_none() {
1212        return;
1213    }
1214    let mut query = url.query_pairs_mut();
1215    if let Some(start) = start {
1216        query.append_pair("start", start);
1217    }
1218    if let Some(limit) = limit {
1219        query.append_pair("limit", &limit.to_string());
1220    }
1221}
1222
1223#[cfg(test)]
1224mod tests {
1225    use super::*;
1226    use crate::transaction::authenticator::{
1227        Ed25519PublicKey, Ed25519Signature, TransactionAuthenticator,
1228    };
1229    use crate::transaction::simulation::SimulateQueryOptions;
1230    use crate::transaction::types::{RawTransaction, SignedTransaction};
1231    use crate::types::ChainId;
1232    use wiremock::{
1233        Mock, MockServer, ResponseTemplate,
1234        matchers::{method, path, path_regex, query_param},
1235    };
1236
1237    #[test]
1238    fn test_build_url() {
1239        let client = FullnodeClient::new(AptosConfig::testnet()).unwrap();
1240        let url = client.build_url("accounts/0x1");
1241        assert!(url.as_str().contains("accounts/0x1"));
1242    }
1243
1244    fn create_mock_client(server: &MockServer) -> FullnodeClient {
1245        // The mock server URL needs to include /v1 since that's part of the base URL
1246        let url = format!("{}/v1", server.uri());
1247        let config = AptosConfig::custom(&url).unwrap().without_retry();
1248        FullnodeClient::new(config).unwrap()
1249    }
1250
1251    /// Creates a minimal `SignedTransaction` for use in `simulate_transaction` tests.
1252    fn create_minimal_signed_transaction() -> SignedTransaction {
1253        use crate::transaction::payload::{EntryFunction, TransactionPayload};
1254
1255        let raw = RawTransaction::new(
1256            AccountAddress::ONE,
1257            0,
1258            TransactionPayload::EntryFunction(
1259                EntryFunction::apt_transfer(AccountAddress::ONE, 0).unwrap(),
1260            ),
1261            100_000,
1262            100,
1263            std::time::SystemTime::now()
1264                .duration_since(std::time::UNIX_EPOCH)
1265                .unwrap()
1266                .as_secs()
1267                .saturating_add(600),
1268            ChainId::testnet(),
1269        );
1270        let auth = TransactionAuthenticator::Ed25519 {
1271            public_key: Ed25519PublicKey([0u8; 32]),
1272            signature: Ed25519Signature([0u8; 64]),
1273        };
1274        SignedTransaction::new(raw, auth)
1275    }
1276
1277    fn simulate_response_json() -> serde_json::Value {
1278        serde_json::json!([{
1279            "success": true,
1280            "vm_status": "Executed successfully",
1281            "gas_used": "100",
1282            "max_gas_amount": "200000",
1283            "gas_unit_price": "100",
1284            "hash": "0x1",
1285            "changes": [],
1286            "events": []
1287        }])
1288    }
1289
1290    #[tokio::test]
1291    async fn test_get_ledger_info() {
1292        let server = MockServer::start().await;
1293
1294        Mock::given(method("GET"))
1295            .and(path("/v1"))
1296            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1297                "chain_id": 2,
1298                "epoch": "100",
1299                "ledger_version": "12345",
1300                "oldest_ledger_version": "0",
1301                "ledger_timestamp": "1000000",
1302                "node_role": "full_node",
1303                "oldest_block_height": "0",
1304                "block_height": "5000"
1305            })))
1306            .expect(1)
1307            .mount(&server)
1308            .await;
1309
1310        let client = create_mock_client(&server);
1311        let result = client.get_ledger_info().await.unwrap();
1312
1313        assert_eq!(result.data.chain_id, 2);
1314        assert_eq!(result.data.version().unwrap(), 12345);
1315        assert_eq!(result.data.height().unwrap(), 5000);
1316    }
1317
1318    #[tokio::test]
1319    async fn test_get_account() {
1320        let server = MockServer::start().await;
1321
1322        Mock::given(method("GET"))
1323            .and(path_regex(r"^/v1/accounts/0x[0-9a-f]+$"))
1324            .respond_with(
1325                ResponseTemplate::new(200)
1326                    .set_body_json(serde_json::json!({
1327                        "sequence_number": "42",
1328                        "authentication_key": "0x0000000000000000000000000000000000000000000000000000000000000001"
1329                    }))
1330                    .insert_header("x-aptos-ledger-version", "12345"),
1331            )
1332            .expect(1)
1333            .mount(&server)
1334            .await;
1335
1336        let client = create_mock_client(&server);
1337        let result = client.get_account(AccountAddress::ONE).await.unwrap();
1338
1339        assert_eq!(result.data.sequence_number().unwrap(), 42);
1340        assert_eq!(result.ledger_version, Some(12345));
1341    }
1342
1343    #[tokio::test]
1344    async fn test_get_account_not_found() {
1345        let server = MockServer::start().await;
1346
1347        Mock::given(method("GET"))
1348            .and(path_regex(r"/v1/accounts/0x[0-9a-f]+"))
1349            .respond_with(ResponseTemplate::new(404).set_body_json(serde_json::json!({
1350                "message": "Account not found",
1351                "error_code": "account_not_found"
1352            })))
1353            .expect(1)
1354            .mount(&server)
1355            .await;
1356
1357        let client = create_mock_client(&server);
1358        let result = client.get_account(AccountAddress::ONE).await;
1359
1360        assert!(result.is_err());
1361        let err = result.unwrap_err();
1362        assert!(err.is_not_found());
1363    }
1364
1365    #[tokio::test]
1366    async fn test_get_account_resources() {
1367        let server = MockServer::start().await;
1368
1369        Mock::given(method("GET"))
1370            .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/resources"))
1371            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([
1372                {
1373                    "type": "0x1::account::Account",
1374                    "data": {"sequence_number": "10"}
1375                },
1376                {
1377                    "type": "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>",
1378                    "data": {"coin": {"value": "1000000"}}
1379                }
1380            ])))
1381            .expect(1)
1382            .mount(&server)
1383            .await;
1384
1385        let client = create_mock_client(&server);
1386        let result = client
1387            .get_account_resources(AccountAddress::ONE)
1388            .await
1389            .unwrap();
1390
1391        assert_eq!(result.data.len(), 2);
1392        assert!(result.data[0].typ.contains("Account"));
1393    }
1394
1395    #[tokio::test]
1396    async fn test_get_account_resource() {
1397        let server = MockServer::start().await;
1398
1399        Mock::given(method("GET"))
1400            .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/resource/.*"))
1401            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1402                "type": "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>",
1403                "data": {"coin": {"value": "5000000"}}
1404            })))
1405            .expect(1)
1406            .mount(&server)
1407            .await;
1408
1409        let client = create_mock_client(&server);
1410        let result = client
1411            .get_account_resource(
1412                AccountAddress::ONE,
1413                "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>",
1414            )
1415            .await
1416            .unwrap();
1417
1418        assert!(result.data.typ.contains("CoinStore"));
1419    }
1420
1421    #[tokio::test]
1422    async fn test_get_account_modules() {
1423        let server = MockServer::start().await;
1424
1425        Mock::given(method("GET"))
1426            .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/modules"))
1427            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([
1428                {
1429                    "bytecode": "0xabc123",
1430                    "abi": {
1431                        "address": "0x1",
1432                        "name": "coin",
1433                        "exposed_functions": [],
1434                        "structs": []
1435                    }
1436                }
1437            ])))
1438            .expect(1)
1439            .mount(&server)
1440            .await;
1441
1442        let client = create_mock_client(&server);
1443        let result = client
1444            .get_account_modules(AccountAddress::ONE)
1445            .await
1446            .unwrap();
1447
1448        assert_eq!(result.data.len(), 1);
1449        assert!(result.data[0].abi.is_some());
1450    }
1451
1452    #[tokio::test]
1453    async fn test_get_account_resources_paginated_sends_start_and_limit() {
1454        let server = MockServer::start().await;
1455
1456        // Verify the SDK forwards both query params verbatim.
1457        Mock::given(method("GET"))
1458            .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/resources"))
1459            .and(query_param("start", "42"))
1460            .and(query_param("limit", "9"))
1461            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([])))
1462            .expect(1)
1463            .mount(&server)
1464            .await;
1465
1466        let client = create_mock_client(&server);
1467        let result = client
1468            .get_account_resources_paginated(AccountAddress::ONE, Some("42"), Some(9))
1469            .await
1470            .unwrap();
1471        assert_eq!(result.data.len(), 0);
1472    }
1473
1474    #[tokio::test]
1475    async fn test_get_account_resources_paginated_round_trips_opaque_cursor() {
1476        // The `x-aptos-cursor` header is opaque (`Option<String>` on
1477        // `AptosResponse`). A caller pulling page N+1 must be able to pass
1478        // page N's cursor verbatim, even when it's not a decimal integer.
1479        let server = MockServer::start().await;
1480        let opaque = "0x0a1b2c3d_state_key_token";
1481        Mock::given(method("GET"))
1482            .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/resources"))
1483            .and(query_param("start", opaque))
1484            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([])))
1485            .expect(1)
1486            .mount(&server)
1487            .await;
1488
1489        let client = create_mock_client(&server);
1490        client
1491            .get_account_resources_paginated(AccountAddress::ONE, Some(opaque), None)
1492            .await
1493            .unwrap();
1494    }
1495
1496    #[tokio::test]
1497    async fn test_get_account_resources_no_pagination_omits_query() {
1498        let server = MockServer::start().await;
1499
1500        // When both args are None, no `start`/`limit` query params should
1501        // be appended -- the fullnode default page applies.
1502        Mock::given(method("GET"))
1503            .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/resources$"))
1504            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([])))
1505            .expect(1)
1506            .mount(&server)
1507            .await;
1508
1509        let client = create_mock_client(&server);
1510        client
1511            .get_account_resources(AccountAddress::ONE)
1512            .await
1513            .unwrap();
1514    }
1515
1516    #[tokio::test]
1517    async fn test_get_account_resources_paginated_sends_start_only() {
1518        // Start without limit: caller is paging from a saved cursor and is
1519        // happy with the fullnode default page size.
1520        let server = MockServer::start().await;
1521        Mock::given(method("GET"))
1522            .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/resources"))
1523            .and(query_param("start", "1234"))
1524            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([])))
1525            .expect(1)
1526            .mount(&server)
1527            .await;
1528
1529        let client = create_mock_client(&server);
1530        client
1531            .get_account_resources_paginated(AccountAddress::ONE, Some("1234"), None)
1532            .await
1533            .unwrap();
1534    }
1535
1536    #[tokio::test]
1537    async fn test_get_account_modules_paginated_sends_start_and_limit() {
1538        let server = MockServer::start().await;
1539        Mock::given(method("GET"))
1540            .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/modules"))
1541            .and(query_param("start", "7"))
1542            .and(query_param("limit", "100"))
1543            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([])))
1544            .expect(1)
1545            .mount(&server)
1546            .await;
1547
1548        let client = create_mock_client(&server);
1549        client
1550            .get_account_modules_paginated(AccountAddress::ONE, Some("7"), Some(100))
1551            .await
1552            .unwrap();
1553    }
1554
1555    #[tokio::test]
1556    async fn test_get_account_modules_no_pagination_omits_query() {
1557        // Symmetric with the resources variant: no `start` / `limit` query
1558        // params should be appended when both are None.
1559        let server = MockServer::start().await;
1560        Mock::given(method("GET"))
1561            .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/modules$"))
1562            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([])))
1563            .expect(1)
1564            .mount(&server)
1565            .await;
1566
1567        let client = create_mock_client(&server);
1568        client
1569            .get_account_modules(AccountAddress::ONE)
1570            .await
1571            .unwrap();
1572    }
1573
1574    #[tokio::test]
1575    async fn test_get_account_modules_paginated_sends_limit_only() {
1576        let server = MockServer::start().await;
1577
1578        // Only `limit` is sent when `start` is omitted -- caller is fetching
1579        // the first page with a custom page size.
1580        Mock::given(method("GET"))
1581            .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/modules"))
1582            .and(query_param("limit", "25"))
1583            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([])))
1584            .expect(1)
1585            .mount(&server)
1586            .await;
1587
1588        let client = create_mock_client(&server);
1589        client
1590            .get_account_modules_paginated(AccountAddress::ONE, None, Some(25))
1591            .await
1592            .unwrap();
1593    }
1594
1595    #[tokio::test]
1596    async fn test_estimate_gas_price() {
1597        let server = MockServer::start().await;
1598
1599        Mock::given(method("GET"))
1600            .and(path("/v1/estimate_gas_price"))
1601            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1602                "deprioritized_gas_estimate": 50,
1603                "gas_estimate": 100,
1604                "prioritized_gas_estimate": 150
1605            })))
1606            .expect(1)
1607            .mount(&server)
1608            .await;
1609
1610        let client = create_mock_client(&server);
1611        let result = client.estimate_gas_price().await.unwrap();
1612
1613        assert_eq!(result.data.gas_estimate, 100);
1614        assert_eq!(result.data.low(), 50);
1615        assert_eq!(result.data.high(), 150);
1616    }
1617
1618    #[tokio::test]
1619    async fn test_get_transaction_by_hash() {
1620        let server = MockServer::start().await;
1621
1622        Mock::given(method("GET"))
1623            .and(path_regex(r"/v1/transactions/by_hash/0x[0-9a-f]+"))
1624            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1625                "version": "12345",
1626                "hash": "0x0000000000000000000000000000000000000000000000000000000000000001",
1627                "success": true,
1628                "vm_status": "Executed successfully"
1629            })))
1630            .expect(1)
1631            .mount(&server)
1632            .await;
1633
1634        let client = create_mock_client(&server);
1635        let hash = HashValue::from_hex(
1636            "0x0000000000000000000000000000000000000000000000000000000000000001",
1637        )
1638        .unwrap();
1639        let result = client.get_transaction_by_hash(&hash).await.unwrap();
1640
1641        assert!(
1642            result
1643                .data
1644                .get("success")
1645                .and_then(serde_json::Value::as_bool)
1646                .unwrap()
1647        );
1648    }
1649
1650    #[tokio::test]
1651    async fn test_wait_for_transaction_success() {
1652        let server = MockServer::start().await;
1653
1654        Mock::given(method("GET"))
1655            .and(path_regex(r"/v1/transactions/by_hash/0x[0-9a-f]+"))
1656            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1657                "type": "user_transaction",
1658                "version": "12345",
1659                "hash": "0x0000000000000000000000000000000000000000000000000000000000000001",
1660                "success": true,
1661                "vm_status": "Executed successfully"
1662            })))
1663            .expect(1..)
1664            .mount(&server)
1665            .await;
1666
1667        let client = create_mock_client(&server);
1668        let hash = HashValue::from_hex(
1669            "0x0000000000000000000000000000000000000000000000000000000000000001",
1670        )
1671        .unwrap();
1672        let result = client
1673            .wait_for_transaction(&hash, Some(Duration::from_secs(5)))
1674            .await
1675            .unwrap();
1676
1677        assert!(
1678            result
1679                .data
1680                .get("success")
1681                .and_then(serde_json::Value::as_bool)
1682                .unwrap()
1683        );
1684    }
1685
1686    #[tokio::test]
1687    async fn test_server_error_retryable() {
1688        let server = MockServer::start().await;
1689
1690        Mock::given(method("GET"))
1691            .and(path("/v1"))
1692            .respond_with(ResponseTemplate::new(503).set_body_json(serde_json::json!({
1693                "message": "Service temporarily unavailable"
1694            })))
1695            .expect(1)
1696            .mount(&server)
1697            .await;
1698
1699        let url = format!("{}/v1", server.uri());
1700        let config = AptosConfig::custom(&url).unwrap().without_retry();
1701        let client = FullnodeClient::new(config).unwrap();
1702        let result = client.get_ledger_info().await;
1703
1704        assert!(result.is_err());
1705        assert!(result.unwrap_err().is_retryable());
1706    }
1707
1708    #[tokio::test]
1709    async fn test_rate_limited() {
1710        let server = MockServer::start().await;
1711
1712        Mock::given(method("GET"))
1713            .and(path("/v1"))
1714            .respond_with(
1715                ResponseTemplate::new(429)
1716                    .set_body_json(serde_json::json!({
1717                        "message": "Rate limited"
1718                    }))
1719                    .insert_header("retry-after", "30"),
1720            )
1721            .expect(1)
1722            .mount(&server)
1723            .await;
1724
1725        let url = format!("{}/v1", server.uri());
1726        let config = AptosConfig::custom(&url).unwrap().without_retry();
1727        let client = FullnodeClient::new(config).unwrap();
1728        let result = client.get_ledger_info().await;
1729
1730        assert!(result.is_err());
1731        assert!(result.unwrap_err().is_retryable());
1732    }
1733
1734    #[tokio::test]
1735    async fn test_get_block_by_height() {
1736        let server = MockServer::start().await;
1737
1738        Mock::given(method("GET"))
1739            .and(path_regex(r"/v1/blocks/by_height/\d+"))
1740            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1741                "block_height": "1000",
1742                "block_hash": "0xabc",
1743                "block_timestamp": "1234567890",
1744                "first_version": "100",
1745                "last_version": "200"
1746            })))
1747            .expect(1)
1748            .mount(&server)
1749            .await;
1750
1751        let client = create_mock_client(&server);
1752        let result = client.get_block_by_height(1000, false).await.unwrap();
1753
1754        assert!(result.data.get("block_height").is_some());
1755    }
1756
1757    #[tokio::test]
1758    async fn test_view() {
1759        let server = MockServer::start().await;
1760
1761        Mock::given(method("POST"))
1762            .and(path("/v1/view"))
1763            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!(["1000000"])))
1764            .expect(1)
1765            .mount(&server)
1766            .await;
1767
1768        let client = create_mock_client(&server);
1769        let result: AptosResponse<Vec<serde_json::Value>> = client
1770            .view(
1771                "0x1::coin::balance",
1772                vec!["0x1::aptos_coin::AptosCoin".to_string()],
1773                vec![serde_json::json!("0x1")],
1774            )
1775            .await
1776            .unwrap();
1777
1778        assert_eq!(result.data.len(), 1);
1779    }
1780
1781    #[tokio::test]
1782    async fn test_view_bcs_args_posts_bcs_request() {
1783        let server = MockServer::start().await;
1784
1785        // Arguments are real BCS bytes; the request must be sent as a BCS
1786        // `ViewRequest` body (Content-Type application/x.aptos.view_function+bcs),
1787        // not JSON. Pin the exact body bytes to guard the wire format: it must
1788        // equal the BCS of an `EntryFunction`-shaped `ViewRequest`.
1789        let args = vec![aptos_bcs::to_bytes(&AccountAddress::ONE).unwrap()];
1790        let expected_body = aptos_bcs::to_bytes(
1791            &crate::transaction::EntryFunction::from_function_id(
1792                "0x1::coin::balance",
1793                vec![],
1794                args.clone(),
1795            )
1796            .unwrap(),
1797        )
1798        .unwrap();
1799
1800        Mock::given(method("POST"))
1801            .and(path("/v1/view"))
1802            .and(wiremock::matchers::header(
1803                "content-type",
1804                "application/x.aptos.view_function+bcs",
1805            ))
1806            .and(wiremock::matchers::body_bytes(expected_body))
1807            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!(["1000000"])))
1808            .expect(1)
1809            .mount(&server)
1810            .await;
1811
1812        let client = create_mock_client(&server);
1813        let result = client
1814            .view_bcs_args("0x1::coin::balance", vec![], args)
1815            .await
1816            .unwrap()
1817            .into_inner();
1818
1819        assert_eq!(result.len(), 1);
1820        assert_eq!(result[0].as_str().unwrap(), "1000000");
1821    }
1822
1823    #[tokio::test]
1824    async fn test_simulate_transaction_with_estimate_gas_unit_price() {
1825        let server = MockServer::start().await;
1826
1827        Mock::given(method("POST"))
1828            .and(path("/v1/transactions/simulate"))
1829            .and(|req: &wiremock::Request| {
1830                req.url
1831                    .query()
1832                    .is_some_and(|q| q.contains("estimate_gas_unit_price=true"))
1833            })
1834            .respond_with(ResponseTemplate::new(200).set_body_json(simulate_response_json()))
1835            .expect(1)
1836            .mount(&server)
1837            .await;
1838
1839        let client = create_mock_client(&server);
1840        let signed = create_minimal_signed_transaction();
1841        let opts = SimulateQueryOptions::new().estimate_gas_unit_price(true);
1842        let result = client
1843            .simulate_transaction_with_options(&signed, opts)
1844            .await
1845            .unwrap();
1846        assert!(!result.data.is_empty());
1847    }
1848
1849    #[tokio::test]
1850    async fn test_simulate_transaction_with_estimate_max_gas_amount() {
1851        let server = MockServer::start().await;
1852
1853        Mock::given(method("POST"))
1854            .and(path("/v1/transactions/simulate"))
1855            .and(|req: &wiremock::Request| {
1856                req.url
1857                    .query()
1858                    .is_some_and(|q| q.contains("estimate_max_gas_amount=true"))
1859            })
1860            .respond_with(ResponseTemplate::new(200).set_body_json(simulate_response_json()))
1861            .expect(1)
1862            .mount(&server)
1863            .await;
1864
1865        let client = create_mock_client(&server);
1866        let signed = create_minimal_signed_transaction();
1867        let opts = SimulateQueryOptions::new().estimate_max_gas_amount(true);
1868        let result = client
1869            .simulate_transaction_with_options(&signed, opts)
1870            .await
1871            .unwrap();
1872        assert!(!result.data.is_empty());
1873    }
1874
1875    #[tokio::test]
1876    async fn test_simulate_transaction_with_estimate_prioritized_gas_unit_price() {
1877        let server = MockServer::start().await;
1878
1879        Mock::given(method("POST"))
1880            .and(path("/v1/transactions/simulate"))
1881            .and(|req: &wiremock::Request| {
1882                req.url
1883                    .query()
1884                    .is_some_and(|q| q.contains("estimate_prioritized_gas_unit_price=true"))
1885            })
1886            .respond_with(ResponseTemplate::new(200).set_body_json(simulate_response_json()))
1887            .expect(1)
1888            .mount(&server)
1889            .await;
1890
1891        let client = create_mock_client(&server);
1892        let signed = create_minimal_signed_transaction();
1893        let opts = SimulateQueryOptions::new().estimate_prioritized_gas_unit_price(true);
1894        let result = client
1895            .simulate_transaction_with_options(&signed, opts)
1896            .await
1897            .unwrap();
1898        assert!(!result.data.is_empty());
1899    }
1900
1901    #[tokio::test]
1902    async fn test_simulate_transaction_with_all_options() {
1903        let server = MockServer::start().await;
1904
1905        Mock::given(method("POST"))
1906            .and(path("/v1/transactions/simulate"))
1907            .and(|req: &wiremock::Request| {
1908                req.url.query().is_some_and(|q| {
1909                    q.contains("estimate_gas_unit_price=true")
1910                        && q.contains("estimate_max_gas_amount=true")
1911                        && q.contains("estimate_prioritized_gas_unit_price=true")
1912                })
1913            })
1914            .respond_with(ResponseTemplate::new(200).set_body_json(simulate_response_json()))
1915            .expect(1)
1916            .mount(&server)
1917            .await;
1918
1919        let client = create_mock_client(&server);
1920        let signed = create_minimal_signed_transaction();
1921        let opts = SimulateQueryOptions::new()
1922            .estimate_gas_unit_price(true)
1923            .estimate_max_gas_amount(true)
1924            .estimate_prioritized_gas_unit_price(true);
1925        let result = client
1926            .simulate_transaction_with_options(&signed, opts)
1927            .await
1928            .unwrap();
1929        assert!(!result.data.is_empty());
1930    }
1931
1932    #[tokio::test]
1933    async fn test_simulate_transaction_without_options() {
1934        let server = MockServer::start().await;
1935
1936        // Mock must NOT match if query contains any of the simulate options (so we use path only and expect no query param)
1937        Mock::given(method("POST"))
1938            .and(path("/v1/transactions/simulate"))
1939            .and(|req: &wiremock::Request| {
1940                // URL must not contain the simulate query params when options is None
1941                req.url.query().is_none_or(|q| {
1942                    !q.contains("estimate_gas_unit_price=")
1943                        && !q.contains("estimate_max_gas_amount=")
1944                        && !q.contains("estimate_prioritized_gas_unit_price=")
1945                })
1946            })
1947            .respond_with(ResponseTemplate::new(200).set_body_json(simulate_response_json()))
1948            .expect(1)
1949            .mount(&server)
1950            .await;
1951
1952        let client = create_mock_client(&server);
1953        let signed = create_minimal_signed_transaction();
1954        let result = client.simulate_transaction(&signed).await.unwrap();
1955        assert!(!result.data.is_empty());
1956    }
1957
1958    #[tokio::test]
1959    async fn test_get_table_item() {
1960        let server = MockServer::start().await;
1961
1962        // The endpoint is POST /tables/{handle}/item with a JSON body carrying
1963        // the key/value Move types and the key. Pin the body so the wire format
1964        // is guarded.
1965        Mock::given(method("POST"))
1966            .and(path_regex(r"^/v1/tables/0x[0-9a-f]+/item$"))
1967            .and(wiremock::matchers::body_json(serde_json::json!({
1968                "key_type": "address",
1969                "value_type": "u64",
1970                "key": "0x1",
1971            })))
1972            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!("42")))
1973            .expect(1)
1974            .mount(&server)
1975            .await;
1976
1977        let client = create_mock_client(&server);
1978        let result = client
1979            .get_table_item(
1980                "0x0000000000000000000000000000000000000000000000000000000000000abc",
1981                "address",
1982                "u64",
1983                serde_json::json!("0x1"),
1984            )
1985            .await
1986            .unwrap();
1987
1988        assert_eq!(result.data, serde_json::json!("42"));
1989    }
1990
1991    #[tokio::test]
1992    async fn test_get_table_item_missing_key_is_404() {
1993        let server = MockServer::start().await;
1994
1995        Mock::given(method("POST"))
1996            .and(path_regex(r"^/v1/tables/0x[0-9a-f]+/item$"))
1997            .respond_with(ResponseTemplate::new(404).set_body_json(serde_json::json!({
1998                "message": "Table item not found",
1999                "error_code": "table_item_not_found"
2000            })))
2001            .expect(1)
2002            .mount(&server)
2003            .await;
2004
2005        let client = create_mock_client(&server);
2006        let err = client
2007            .get_table_item("0xabc", "address", "u64", serde_json::json!("0x2"))
2008            .await
2009            .unwrap_err();
2010
2011        assert!(matches!(
2012            err,
2013            AptosError::Api {
2014                status_code: 404,
2015                ..
2016            }
2017        ));
2018    }
2019
2020    #[tokio::test]
2021    async fn test_get_transaction_by_version() {
2022        let server = MockServer::start().await;
2023
2024        Mock::given(method("GET"))
2025            .and(path("/v1/transactions/by_version/100"))
2026            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2027                "version": "100",
2028                "hash": "0xdead",
2029                "success": true
2030            })))
2031            .expect(1)
2032            .mount(&server)
2033            .await;
2034
2035        let client = create_mock_client(&server);
2036        let result = client.get_transaction_by_version(100).await.unwrap();
2037
2038        assert_eq!(result.data.get("version").unwrap(), "100");
2039    }
2040
2041    #[tokio::test]
2042    async fn test_get_transactions_with_pagination() {
2043        let server = MockServer::start().await;
2044
2045        Mock::given(method("GET"))
2046            .and(path("/v1/transactions"))
2047            .and(query_param("start", "10"))
2048            .and(query_param("limit", "2"))
2049            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([
2050                {"version": "10"},
2051                {"version": "11"}
2052            ])))
2053            .expect(1)
2054            .mount(&server)
2055            .await;
2056
2057        let client = create_mock_client(&server);
2058        let result = client.get_transactions(Some(10), Some(2)).await.unwrap();
2059
2060        assert_eq!(result.data.len(), 2);
2061    }
2062
2063    #[tokio::test]
2064    async fn test_get_account_transactions() {
2065        let server = MockServer::start().await;
2066
2067        Mock::given(method("GET"))
2068            .and(path_regex(r"^/v1/accounts/0x[0-9a-f]+/transactions$"))
2069            .and(query_param("start", "0"))
2070            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([
2071                {"version": "5", "sender": "0x1"}
2072            ])))
2073            .expect(1)
2074            .mount(&server)
2075            .await;
2076
2077        let client = create_mock_client(&server);
2078        let result = client
2079            .get_account_transactions(AccountAddress::ONE, Some(0), None)
2080            .await
2081            .unwrap();
2082
2083        assert_eq!(result.data.len(), 1);
2084    }
2085
2086    #[tokio::test]
2087    async fn test_get_events_by_creation_number() {
2088        let server = MockServer::start().await;
2089
2090        Mock::given(method("GET"))
2091            .and(path_regex(r"^/v1/accounts/0x[0-9a-f]+/events/7$"))
2092            .and(query_param("limit", "25"))
2093            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([
2094                {"sequence_number": "0", "type": "0x1::coin::DepositEvent"}
2095            ])))
2096            .expect(1)
2097            .mount(&server)
2098            .await;
2099
2100        let client = create_mock_client(&server);
2101        let result = client
2102            .get_events_by_creation_number(AccountAddress::ONE, 7, None, Some(25))
2103            .await
2104            .unwrap();
2105
2106        assert_eq!(result.data.len(), 1);
2107    }
2108}