Skip to main content

aptos_sdk/
aptos.rs

1//! Main Aptos client entry point.
2//!
3//! The [`Aptos`] struct provides a unified interface for all SDK functionality.
4
5use crate::account::Account;
6use crate::api::{AptosResponse, FullnodeClient, PendingTransaction};
7use crate::config::AptosConfig;
8use crate::error::{AptosError, AptosResult};
9use crate::transaction::{
10    FeePayerRawTransaction, MultiAgentRawTransaction, RawTransaction, SignedTransaction,
11    SimulateQueryOptions, SimulationResult, TransactionBuilder, TransactionPayload,
12    build_simulation_signed_fee_payer, build_simulation_signed_multi_agent,
13};
14use crate::types::{AccountAddress, ChainId};
15use std::sync::Arc;
16use std::sync::atomic::{AtomicU8, Ordering};
17use std::time::Duration;
18
19#[cfg(feature = "ed25519")]
20use crate::transaction::EntryFunction;
21#[cfg(feature = "ed25519")]
22use crate::types::TypeTag;
23
24#[cfg(feature = "faucet")]
25use crate::api::FaucetClient;
26#[cfg(feature = "faucet")]
27use crate::types::HashValue;
28
29#[cfg(feature = "indexer")]
30use crate::api::IndexerClient;
31
32/// The main entry point for the Aptos SDK.
33///
34/// This struct provides a unified interface for interacting with the Aptos blockchain,
35/// including account management, transaction building and submission, and queries.
36///
37/// # Example
38///
39/// ```rust,no_run
40/// use aptos_sdk::{Aptos, AptosConfig};
41///
42/// #[tokio::main]
43/// async fn main() -> anyhow::Result<()> {
44///     // Create client for testnet
45///     let aptos = Aptos::new(AptosConfig::testnet())?;
46///
47///     // Get ledger info
48///     let ledger = aptos.ledger_info().await?;
49///     println!("Ledger version: {:?}", ledger.version());
50///
51///     Ok(())
52/// }
53/// ```
54#[derive(Debug)]
55pub struct Aptos {
56    config: AptosConfig,
57    fullnode: Arc<FullnodeClient>,
58    /// Resolved chain ID. Initialized from config; lazily fetched from node
59    /// for custom networks where the chain ID is unknown (0).
60    /// Stored as `AtomicU8` to avoid lock overhead for this single-byte value.
61    chain_id: AtomicU8,
62    #[cfg(feature = "faucet")]
63    faucet: Option<FaucetClient>,
64    #[cfg(feature = "indexer")]
65    indexer: Option<IndexerClient>,
66}
67
68impl Aptos {
69    /// Creates a new Aptos client with the given configuration.
70    ///
71    /// # Errors
72    ///
73    /// Returns an error if the HTTP client fails to build (e.g., invalid TLS configuration).
74    pub fn new(config: AptosConfig) -> AptosResult<Self> {
75        let fullnode = Arc::new(FullnodeClient::new(config.clone())?);
76
77        #[cfg(feature = "faucet")]
78        let faucet = FaucetClient::new(&config).ok();
79
80        #[cfg(feature = "indexer")]
81        let indexer = IndexerClient::new(&config).ok();
82
83        let chain_id = AtomicU8::new(config.chain_id().id());
84
85        Ok(Self {
86            config,
87            fullnode,
88            chain_id,
89            #[cfg(feature = "faucet")]
90            faucet,
91            #[cfg(feature = "indexer")]
92            indexer,
93        })
94    }
95
96    /// Creates a client for testnet with default settings.
97    ///
98    /// # Errors
99    ///
100    /// Returns an error if the HTTP client fails to build (e.g., invalid TLS configuration).
101    pub fn testnet() -> AptosResult<Self> {
102        Self::new(AptosConfig::testnet())
103    }
104
105    /// Creates a client for devnet with default settings.
106    ///
107    /// # Errors
108    ///
109    /// Returns an error if the HTTP client fails to build (e.g., invalid TLS configuration).
110    pub fn devnet() -> AptosResult<Self> {
111        Self::new(AptosConfig::devnet())
112    }
113
114    /// Creates a client for mainnet with default settings.
115    ///
116    /// # Errors
117    ///
118    /// Returns an error if the HTTP client fails to build (e.g., invalid TLS configuration).
119    pub fn mainnet() -> AptosResult<Self> {
120        Self::new(AptosConfig::mainnet())
121    }
122
123    /// Creates a client for local development network.
124    ///
125    /// # Errors
126    ///
127    /// Returns an error if the HTTP client fails to build (e.g., invalid TLS configuration).
128    pub fn local() -> AptosResult<Self> {
129        Self::new(AptosConfig::local())
130    }
131
132    /// Returns the configuration.
133    pub fn config(&self) -> &AptosConfig {
134        &self.config
135    }
136
137    /// Returns the fullnode client.
138    pub fn fullnode(&self) -> &FullnodeClient {
139        &self.fullnode
140    }
141
142    /// Returns the faucet client, if available.
143    #[cfg(feature = "faucet")]
144    pub fn faucet(&self) -> Option<&FaucetClient> {
145        self.faucet.as_ref()
146    }
147
148    /// Returns the indexer client, if available.
149    #[cfg(feature = "indexer")]
150    pub fn indexer(&self) -> Option<&IndexerClient> {
151        self.indexer.as_ref()
152    }
153
154    // === Ledger Info ===
155
156    /// Gets the current ledger information.
157    ///
158    /// As a side effect, this also resolves the chain ID if it was unknown
159    /// (e.g., for custom network configurations).
160    ///
161    /// # Errors
162    ///
163    /// Returns an error if the HTTP request fails, the API returns an error status code,
164    /// or the response cannot be parsed.
165    pub async fn ledger_info(&self) -> AptosResult<crate::api::response::LedgerInfo> {
166        let response = self.fullnode.get_ledger_info().await?;
167        let info = response.into_inner();
168
169        // Update chain_id if it was unknown (custom network).
170        // NOTE: The load-then-store pattern has a benign TOCTOU race: multiple
171        // threads may concurrently see chain_id == 0 and all store the same
172        // value from the ledger info response. This is safe because they always
173        // store the identical chain_id value returned by the node.
174        if self.chain_id.load(Ordering::Relaxed) == 0 && info.chain_id > 0 {
175            self.chain_id.store(info.chain_id, Ordering::Relaxed);
176        }
177
178        Ok(info)
179    }
180
181    /// Returns the current chain ID.
182    ///
183    /// For known networks (mainnet, testnet, devnet, local), this returns the
184    /// well-known chain ID immediately. For custom networks, this returns
185    /// `ChainId(0)` until the chain ID is resolved via [`ensure_chain_id`](Self::ensure_chain_id)
186    /// or any method that makes a request to the node (e.g., [`build_transaction`](Self::build_transaction),
187    /// [`ledger_info`](Self::ledger_info)).
188    ///
189    pub fn chain_id(&self) -> ChainId {
190        ChainId::new(self.chain_id.load(Ordering::Relaxed))
191    }
192
193    /// Resolves the chain ID from the node if it is unknown.
194    ///
195    /// For known networks, this returns the chain ID immediately without
196    /// making a network request. For custom networks (chain ID 0), this
197    /// fetches the ledger info from the node to discover the actual chain ID
198    /// and caches it for future use.
199    ///
200    /// This is called automatically by [`build_transaction`](Self::build_transaction)
201    /// and other transaction methods, so you typically don't need to call it
202    /// directly unless you need the chain ID before building a transaction.
203    ///
204    /// # Errors
205    ///
206    /// Returns an error if the HTTP request to fetch ledger info fails.
207    ///
208    pub async fn ensure_chain_id(&self) -> AptosResult<ChainId> {
209        let id = self.chain_id.load(Ordering::Relaxed);
210        if id > 0 {
211            return Ok(ChainId::new(id));
212        }
213        // Chain ID is unknown; fetch from node
214        let response = self.fullnode.get_ledger_info().await?;
215        let info = response.into_inner();
216        self.chain_id.store(info.chain_id, Ordering::Relaxed);
217        Ok(ChainId::new(info.chain_id))
218    }
219
220    // === Account ===
221
222    /// Gets the sequence number for an account.
223    ///
224    /// # Errors
225    ///
226    /// Returns an error if the HTTP request fails, the API returns an error status code
227    /// (e.g., account not found 404), or the response cannot be parsed.
228    pub async fn get_sequence_number(&self, address: AccountAddress) -> AptosResult<u64> {
229        self.fullnode.get_sequence_number(address).await
230    }
231
232    /// Gets the APT balance for an account.
233    ///
234    /// # Errors
235    ///
236    /// Returns an error if the HTTP request fails, the API returns an error status code,
237    /// or the response cannot be parsed.
238    pub async fn get_balance(&self, address: AccountAddress) -> AptosResult<u64> {
239        self.fullnode.get_account_balance(address).await
240    }
241
242    /// Checks if an account exists.
243    ///
244    /// # Errors
245    ///
246    /// Returns an error if the HTTP request fails or the API returns an error status code
247    /// other than 404 (not found). A 404 error is handled gracefully and returns `Ok(false)`.
248    pub async fn account_exists(&self, address: AccountAddress) -> AptosResult<bool> {
249        match self.fullnode.get_account(address).await {
250            Ok(_) => Ok(true),
251            Err(AptosError::Api {
252                status_code: 404, ..
253            }) => Ok(false),
254            Err(e) => Err(e),
255        }
256    }
257
258    // === Transactions ===
259
260    /// Builds a transaction for the given account.
261    ///
262    /// This automatically fetches the sequence number and gas price.
263    ///
264    /// # Errors
265    ///
266    /// Returns an error if fetching the sequence number fails, fetching the gas price fails,
267    /// or if the transaction builder fails to construct a valid transaction (e.g., missing
268    /// required fields).
269    pub async fn build_transaction<A: Account>(
270        &self,
271        sender: &A,
272        payload: TransactionPayload,
273    ) -> AptosResult<RawTransaction> {
274        // Fetch sequence number, gas price, and chain ID in parallel
275        let (sequence_number, gas_estimation, chain_id) = tokio::join!(
276            self.get_sequence_number(sender.address()),
277            self.fullnode.estimate_gas_price(),
278            self.ensure_chain_id()
279        );
280        let sequence_number = sequence_number?;
281        let gas_estimation = gas_estimation?;
282        let chain_id = chain_id?;
283
284        TransactionBuilder::new()
285            .sender(sender.address())
286            .sequence_number(sequence_number)
287            .payload(payload)
288            .gas_unit_price(gas_estimation.data.recommended())
289            .chain_id(chain_id)
290            .expiration_from_now(600)
291            .build()
292    }
293
294    /// Signs and submits a transaction.
295    ///
296    /// # Errors
297    ///
298    /// Returns an error if building the transaction fails, signing fails (e.g., invalid key),
299    /// the transaction cannot be serialized to BCS, the HTTP request fails, or the API returns
300    /// an error status code.
301    #[cfg(feature = "ed25519")]
302    pub async fn sign_and_submit<A: Account>(
303        &self,
304        account: &A,
305        payload: TransactionPayload,
306    ) -> AptosResult<AptosResponse<PendingTransaction>> {
307        let raw_txn = self.build_transaction(account, payload).await?;
308        let signed = crate::transaction::builder::sign_transaction(&raw_txn, account)?;
309        self.fullnode.submit_transaction(&signed).await
310    }
311
312    /// Signs, submits, and waits for a transaction to complete.
313    ///
314    /// # Errors
315    ///
316    /// Returns an error if building the transaction fails, signing fails, submission fails,
317    /// the transaction times out waiting for commitment, the transaction execution fails,
318    /// or any HTTP/API errors occur.
319    #[cfg(feature = "ed25519")]
320    pub async fn sign_submit_and_wait<A: Account>(
321        &self,
322        account: &A,
323        payload: TransactionPayload,
324        timeout: Option<Duration>,
325    ) -> AptosResult<AptosResponse<serde_json::Value>> {
326        let raw_txn = self.build_transaction(account, payload).await?;
327        let signed = crate::transaction::builder::sign_transaction(&raw_txn, account)?;
328        self.fullnode.submit_and_wait(&signed, timeout).await
329    }
330
331    /// Submits a pre-signed transaction.
332    ///
333    /// # Errors
334    ///
335    /// Returns an error if the transaction cannot be serialized to BCS, the HTTP request fails,
336    /// or the API returns an error status code.
337    pub async fn submit_transaction(
338        &self,
339        signed_txn: &SignedTransaction,
340    ) -> AptosResult<AptosResponse<PendingTransaction>> {
341        self.fullnode.submit_transaction(signed_txn).await
342    }
343
344    /// Submits and waits for a pre-signed transaction.
345    ///
346    /// # Errors
347    ///
348    /// Returns an error if transaction submission fails, the transaction times out waiting
349    /// for commitment, the transaction execution fails (`vm_status` indicates failure),
350    /// or any HTTP/API errors occur.
351    pub async fn submit_and_wait(
352        &self,
353        signed_txn: &SignedTransaction,
354        timeout: Option<Duration>,
355    ) -> AptosResult<AptosResponse<serde_json::Value>> {
356        self.fullnode.submit_and_wait(signed_txn, timeout).await
357    }
358
359    /// Simulates a transaction.
360    ///
361    /// # Errors
362    ///
363    /// Returns an error if the transaction cannot be serialized to BCS, the HTTP request fails,
364    /// the API returns an error status code, or the response cannot be parsed as JSON.
365    ///
366    /// Note: [`FullnodeClient::simulate_transaction`] rewrites authenticators for the simulate
367    /// endpoint before sending; callers may pass a normally signed transaction.
368    pub async fn simulate_transaction(
369        &self,
370        signed_txn: &SignedTransaction,
371    ) -> AptosResult<AptosResponse<Vec<serde_json::Value>>> {
372        self.fullnode.simulate_transaction(signed_txn).await
373    }
374
375    /// Simulates a transaction and returns a parsed result.
376    ///
377    /// This method provides a more ergonomic way to simulate transactions
378    /// with detailed result parsing.
379    ///
380    /// # Example
381    ///
382    /// ```rust,ignore
383    /// let result = aptos.simulate(&account, payload).await?;
384    /// if result.success() {
385    ///     println!("Gas estimate: {}", result.gas_used());
386    /// } else {
387    ///     println!("Would fail: {}", result.error_message().unwrap_or_default());
388    /// }
389    /// ```
390    ///
391    /// # Errors
392    ///
393    /// Returns an error if building the transaction fails, signing fails, simulation fails,
394    /// or the simulation response cannot be parsed.
395    #[cfg(feature = "ed25519")]
396    pub async fn simulate<A: Account>(
397        &self,
398        account: &A,
399        payload: TransactionPayload,
400    ) -> AptosResult<crate::transaction::SimulationResult> {
401        use crate::transaction::SignedTransaction;
402
403        let raw_txn = self.build_transaction(account, payload).await?;
404
405        // The simulation endpoint *rejects* valid signatures
406        // (it returns 400 "Simulated transactions must not have a valid
407        // signature") because its job is gas estimation, not actual
408        // execution. We attach the account's real public key (the simulator
409        // still uses it to walk the signing-message hash) and a zeroed
410        // signature of the appropriate shape for the account's
411        // signature scheme.
412        let auth = build_zero_signed_authenticator(account)?;
413        let signed = SignedTransaction::new(raw_txn, auth);
414
415        let response = self.fullnode.simulate_transaction(&signed).await?;
416        crate::transaction::SimulationResult::from_response(response.into_inner())
417    }
418
419    /// Simulates a transaction with a pre-built signed transaction.
420    ///
421    /// For gas estimation options (e.g. `estimate_gas_unit_price`), use
422    /// [`simulate_signed_with_options`](Self::simulate_signed_with_options).
423    ///
424    /// Authenticators are rewritten client-side before the HTTP request (see
425    /// [`SignedTransaction::for_simulate_endpoint`]); you do not need to swap in
426    /// [`AccountAuthenticator::NoAccountAuthenticator`](crate::transaction::authenticator::AccountAuthenticator::no_account_authenticator)
427    /// manually to avoid the fullnode's "must not have a valid signature" error.
428    ///
429    /// # Errors
430    ///
431    /// Returns an error if simulation fails or the simulation response cannot be parsed.
432    pub async fn simulate_signed(
433        &self,
434        signed_txn: &SignedTransaction,
435    ) -> AptosResult<SimulationResult> {
436        let response = self.fullnode.simulate_transaction(signed_txn).await?;
437        SimulationResult::from_response(response.into_inner())
438    }
439
440    /// Simulates a signed transaction with query options for the node.
441    ///
442    /// Use this when you need [`SimulateQueryOptions`] (e.g. `estimate_gas_unit_price`,
443    /// `estimate_max_gas_amount`). For the common case without options, use
444    /// [`simulate_signed`](Self::simulate_signed) instead.
445    ///
446    /// Like [`simulate_signed`](Self::simulate_signed), this applies
447    /// [`SignedTransaction::for_simulate_endpoint`] before calling the fullnode.
448    ///
449    /// # Errors
450    ///
451    /// Returns an error if simulation fails or the simulation response cannot be parsed.
452    pub async fn simulate_signed_with_options(
453        &self,
454        signed_txn: &SignedTransaction,
455        options: SimulateQueryOptions,
456    ) -> AptosResult<SimulationResult> {
457        let response = self
458            .fullnode
459            .simulate_transaction_with_options(signed_txn, Some(options))
460            .await?;
461        SimulationResult::from_response(response.into_inner())
462    }
463
464    /// Simulates a multi-agent transaction without requiring real signatures.
465    ///
466    /// Builds a simulation-only signed transaction (using
467    /// [`crate::transaction::authenticator::AccountAuthenticator::NoAccountAuthenticator`]) and sends it to the
468    /// simulate endpoint. Use this to check outcome and gas before collecting
469    /// signatures from sender and secondary signers.
470    ///
471    /// # Example
472    ///
473    /// ```rust,ignore
474    /// let multi_agent = MultiAgentRawTransaction::new(raw_txn, secondary_addresses);
475    /// let result = aptos.simulate_multi_agent(&multi_agent, None).await?;
476    /// if result.success() {
477    ///     println!("Gas: {}", result.gas_used());
478    /// }
479    /// ```
480    ///
481    /// # Errors
482    ///
483    /// Returns an error if the simulate request fails or the response cannot be parsed.
484    pub async fn simulate_multi_agent(
485        &self,
486        multi_agent: &MultiAgentRawTransaction,
487        options: impl Into<Option<SimulateQueryOptions>>,
488    ) -> AptosResult<SimulationResult> {
489        let signed = build_simulation_signed_multi_agent(multi_agent);
490        match options.into() {
491            None => self.simulate_signed(&signed).await,
492            Some(opts) => self.simulate_signed_with_options(&signed, opts).await,
493        }
494    }
495
496    /// Simulates a fee-payer (sponsored) transaction without requiring real signatures.
497    ///
498    /// Builds a simulation-only signed transaction (using
499    /// [`crate::transaction::authenticator::AccountAuthenticator::NoAccountAuthenticator`]) and sends it to the
500    /// simulate endpoint. Use this to check outcome and gas before collecting
501    /// signatures from sender, secondary signers, and fee payer.
502    ///
503    /// # Example
504    ///
505    /// ```rust,ignore
506    /// let fee_payer_txn = FeePayerRawTransaction::new_simple(raw_txn, fee_payer_address);
507    /// let result = aptos.simulate_fee_payer(&fee_payer_txn, None).await?;
508    /// if result.success() {
509    ///     println!("Gas: {}", result.gas_used());
510    /// }
511    /// ```
512    ///
513    /// # Errors
514    ///
515    /// Returns an error if the simulate request fails or the response cannot be parsed.
516    pub async fn simulate_fee_payer(
517        &self,
518        fee_payer_txn: &FeePayerRawTransaction,
519        options: impl Into<Option<SimulateQueryOptions>>,
520    ) -> AptosResult<SimulationResult> {
521        let signed = build_simulation_signed_fee_payer(fee_payer_txn);
522        match options.into() {
523            None => self.simulate_signed(&signed).await,
524            Some(opts) => self.simulate_signed_with_options(&signed, opts).await,
525        }
526    }
527
528    /// Estimates gas for a transaction by simulating it.
529    ///
530    /// Returns the estimated gas usage with a 20% safety margin.
531    ///
532    /// # Example
533    ///
534    /// ```rust,ignore
535    /// let gas = aptos.estimate_gas(&account, payload).await?;
536    /// println!("Estimated gas: {}", gas);
537    /// ```
538    ///
539    /// # Errors
540    ///
541    /// Returns an error if simulation fails or if the simulation indicates the transaction
542    /// would fail (returns [`AptosError::SimulationFailed`]).
543    #[cfg(feature = "ed25519")]
544    pub async fn estimate_gas<A: Account>(
545        &self,
546        account: &A,
547        payload: TransactionPayload,
548    ) -> AptosResult<u64> {
549        let result = self.simulate(account, payload).await?;
550        if result.success() {
551            Ok(result.safe_gas_estimate())
552        } else {
553            Err(AptosError::SimulationFailed(
554                result
555                    .error_message()
556                    .unwrap_or_else(|| result.vm_status().to_string()),
557            ))
558        }
559    }
560
561    /// Simulates and submits a transaction if successful.
562    ///
563    /// This is a "dry run" approach that first simulates the transaction
564    /// to verify it will succeed before actually submitting it.
565    ///
566    /// # Example
567    ///
568    /// ```rust,ignore
569    /// let result = aptos.simulate_and_submit(&account, payload).await?;
570    /// println!("Transaction submitted: {}", result.hash);
571    /// ```
572    ///
573    /// # Errors
574    ///
575    /// Returns an error if building the transaction fails, signing fails, simulation fails,
576    /// the simulation indicates the transaction would fail (returns [`AptosError::SimulationFailed`]),
577    /// or transaction submission fails.
578    #[cfg(feature = "ed25519")]
579    pub async fn simulate_and_submit<A: Account>(
580        &self,
581        account: &A,
582        payload: TransactionPayload,
583    ) -> AptosResult<AptosResponse<PendingTransaction>> {
584        // First simulate with an intentionally invalid (zeroed) signature;
585        // the node rejects real signatures on the simulate endpoint.
586        let raw_txn = self.build_transaction(account, payload).await?;
587        let sim_auth = build_zero_signed_authenticator(account)?;
588        let sim_signed = SignedTransaction::new(raw_txn.clone(), sim_auth);
589        let sim_response = self.fullnode.simulate_transaction(&sim_signed).await?;
590        let sim_result =
591            crate::transaction::SimulationResult::from_response(sim_response.into_inner())?;
592
593        if sim_result.failed() {
594            return Err(AptosError::SimulationFailed(
595                sim_result
596                    .error_message()
597                    .unwrap_or_else(|| sim_result.vm_status().to_string()),
598            ));
599        }
600
601        let signed = crate::transaction::builder::sign_transaction(&raw_txn, account)?;
602        self.fullnode.submit_transaction(&signed).await
603    }
604
605    /// Simulates, submits, and waits for a transaction.
606    ///
607    /// Like `simulate_and_submit` but also waits for the transaction to complete.
608    ///
609    /// # Errors
610    ///
611    /// Returns an error if building the transaction fails, signing fails, simulation fails,
612    /// the simulation indicates the transaction would fail (returns [`AptosError::SimulationFailed`]),
613    /// submission fails, the transaction times out waiting for commitment, or the transaction
614    /// execution fails.
615    #[cfg(feature = "ed25519")]
616    pub async fn simulate_submit_and_wait<A: Account>(
617        &self,
618        account: &A,
619        payload: TransactionPayload,
620        timeout: Option<Duration>,
621    ) -> AptosResult<AptosResponse<serde_json::Value>> {
622        // First simulate with an intentionally invalid (zeroed) signature;
623        // the node rejects real signatures on the simulate endpoint.
624        let raw_txn = self.build_transaction(account, payload).await?;
625        let sim_auth = build_zero_signed_authenticator(account)?;
626        let sim_signed = SignedTransaction::new(raw_txn.clone(), sim_auth);
627        let sim_response = self.fullnode.simulate_transaction(&sim_signed).await?;
628        let sim_result =
629            crate::transaction::SimulationResult::from_response(sim_response.into_inner())?;
630
631        if sim_result.failed() {
632            return Err(AptosError::SimulationFailed(
633                sim_result
634                    .error_message()
635                    .unwrap_or_else(|| sim_result.vm_status().to_string()),
636            ));
637        }
638
639        let signed = crate::transaction::builder::sign_transaction(&raw_txn, account)?;
640        self.fullnode.submit_and_wait(&signed, timeout).await
641    }
642
643    // === Transfers ===
644
645    /// Transfers APT from one account to another.
646    ///
647    /// # Errors
648    ///
649    /// Returns an error if building the transfer payload fails (e.g., invalid address),
650    /// signing fails, submission fails, the transaction times out, or the transaction
651    /// execution fails.
652    #[cfg(feature = "ed25519")]
653    pub async fn transfer_apt<A: Account>(
654        &self,
655        sender: &A,
656        recipient: AccountAddress,
657        amount: u64,
658    ) -> AptosResult<AptosResponse<serde_json::Value>> {
659        let payload = EntryFunction::apt_transfer(recipient, amount)?;
660        self.sign_submit_and_wait(sender, payload.into(), None)
661            .await
662    }
663
664    /// Transfers a coin from one account to another.
665    ///
666    /// # Errors
667    ///
668    /// Returns an error if building the transfer payload fails (e.g., invalid type tag or address),
669    /// signing fails, submission fails, the transaction times out, or the transaction
670    /// execution fails.
671    #[cfg(feature = "ed25519")]
672    pub async fn transfer_coin<A: Account>(
673        &self,
674        sender: &A,
675        recipient: AccountAddress,
676        coin_type: TypeTag,
677        amount: u64,
678    ) -> AptosResult<AptosResponse<serde_json::Value>> {
679        let payload = EntryFunction::coin_transfer(coin_type, recipient, amount)?;
680        self.sign_submit_and_wait(sender, payload.into(), None)
681            .await
682    }
683
684    // === View Functions ===
685
686    /// Calls a view function using JSON encoding.
687    ///
688    /// For lossless serialization of large integers, use [`view_bcs`](Self::view_bcs) instead.
689    ///
690    /// # Errors
691    ///
692    /// Returns an error if the HTTP request fails, the API returns an error status code,
693    /// or the response cannot be parsed as JSON.
694    pub async fn view(
695        &self,
696        function: &str,
697        type_args: Vec<String>,
698        args: Vec<serde_json::Value>,
699    ) -> AptosResult<Vec<serde_json::Value>> {
700        let response = self.fullnode.view(function, type_args, args).await?;
701        Ok(response.into_inner())
702    }
703
704    /// Calls a view function using BCS encoding for both inputs and outputs.
705    ///
706    /// This method provides lossless serialization by using BCS (Binary Canonical Serialization)
707    /// instead of JSON, which is important for large integers (u128, u256) and other types
708    /// where JSON can lose precision.
709    ///
710    /// # Type Parameter
711    ///
712    /// * `T` - The expected return type. Must implement `serde::de::DeserializeOwned`.
713    ///
714    /// # Arguments
715    ///
716    /// * `function` - The fully qualified function name (e.g., `0x1::coin::balance`)
717    /// * `type_args` - Type arguments as strings (e.g., `0x1::aptos_coin::AptosCoin`)
718    /// * `args` - Pre-serialized BCS arguments as byte vectors
719    ///
720    /// # Example
721    ///
722    /// ```rust,ignore
723    /// use aptos_sdk::{Aptos, AptosConfig, AccountAddress};
724    ///
725    /// let aptos = Aptos::new(AptosConfig::testnet())?;
726    /// let owner = AccountAddress::from_hex("0x1")?;
727    ///
728    /// // BCS-encode the argument
729    /// let args = vec![aptos_bcs::to_bytes(&owner)?];
730    ///
731    /// // Call view function with typed return
732    /// let balance: u64 = aptos.view_bcs(
733    ///     "0x1::coin::balance",
734    ///     vec!["0x1::aptos_coin::AptosCoin".to_string()],
735    ///     args,
736    /// ).await?;
737    /// ```
738    ///
739    /// # Errors
740    ///
741    /// Returns an error if the HTTP request fails, the API returns an error status code,
742    /// or the BCS deserialization fails.
743    pub async fn view_bcs<T: serde::de::DeserializeOwned>(
744        &self,
745        function: &str,
746        type_args: Vec<String>,
747        args: Vec<Vec<u8>>,
748    ) -> AptosResult<T> {
749        let response = self.fullnode.view_bcs(function, type_args, args).await?;
750        let bytes = response.into_inner();
751        aptos_bcs::from_bytes(&bytes).map_err(|e| AptosError::Bcs(e.to_string()))
752    }
753
754    /// Calls a view function with BCS inputs and returns raw BCS bytes.
755    ///
756    /// Use this when you need to manually deserialize the response or when
757    /// the return type is complex or dynamic.
758    ///
759    /// # Errors
760    ///
761    /// Returns an error if the HTTP request fails or the API returns an error status code.
762    pub async fn view_bcs_raw(
763        &self,
764        function: &str,
765        type_args: Vec<String>,
766        args: Vec<Vec<u8>>,
767    ) -> AptosResult<Vec<u8>> {
768        let response = self.fullnode.view_bcs(function, type_args, args).await?;
769        Ok(response.into_inner())
770    }
771
772    // === Faucet ===
773
774    /// Funds an account using the faucet.
775    ///
776    /// This method waits for the faucet transactions to be confirmed before returning.
777    ///
778    /// Some Aptos faucets (notably devnet) cap the amount delivered per request to a
779    /// fixed value (typically 1 APT / 100,000,000 octas) regardless of the requested
780    /// amount. This method automatically issues additional faucet requests, up to a
781    /// reasonable limit, until the account's balance has been topped up by at least
782    /// `amount` octas. The returned vector contains the transaction hashes from every
783    /// underlying faucet call.
784    ///
785    /// # Errors
786    ///
787    /// Returns an error if the faucet feature is not enabled, the faucet request fails
788    /// (e.g., rate limiting 429, server error 500), waiting for transaction confirmation
789    /// times out, any HTTP/API errors occur, or if the requested amount cannot be
790    /// delivered after several attempts.
791    #[cfg(feature = "faucet")]
792    pub async fn fund_account(
793        &self,
794        address: AccountAddress,
795        amount: u64,
796    ) -> AptosResult<Vec<String>> {
797        // Hard-cap on how many faucet calls we'll make to satisfy a single
798        // `fund_account` request. This prevents unbounded faucet usage if the
799        // faucet is silently dropping requests.
800        const MAX_FAUCET_ATTEMPTS: u32 = 16;
801
802        let faucet = self
803            .faucet
804            .as_ref()
805            .ok_or_else(|| AptosError::FeatureNotEnabled("faucet".into()))?;
806
807        // Snapshot the starting balance (0 if the account doesn't yet exist).
808        let starting_balance = self.get_balance(address).await.unwrap_or(0);
809        let target_balance = starting_balance.saturating_add(amount);
810
811        let mut all_hashes: Vec<String> = Vec::new();
812        let mut current_balance = starting_balance;
813        let mut attempts = 0u32;
814
815        while current_balance < target_balance && attempts < MAX_FAUCET_ATTEMPTS {
816            attempts += 1;
817            let still_needed = target_balance.saturating_sub(current_balance);
818            let txn_hashes = faucet.fund(address, still_needed).await?;
819
820            // Parse hashes for waiting on confirmation.
821            let hashes: Vec<HashValue> = txn_hashes
822                .iter()
823                .filter_map(|hash_str| {
824                    let hash_str_clean = hash_str.strip_prefix("0x").unwrap_or(hash_str);
825                    HashValue::from_hex(hash_str_clean).ok()
826                })
827                .collect();
828
829            // Wait for all faucet transactions in this batch to be confirmed in parallel.
830            let wait_futures: Vec<_> = hashes
831                .iter()
832                .map(|hash| {
833                    self.fullnode
834                        .wait_for_transaction(hash, Some(Duration::from_mins(1)))
835                })
836                .collect();
837            let results = futures::future::join_all(wait_futures).await;
838            for result in results {
839                result?;
840            }
841
842            all_hashes.extend(txn_hashes);
843
844            // Re-read balance; if it didn't move, the faucet isn't going to help.
845            let new_balance = self.get_balance(address).await.unwrap_or(current_balance);
846            if new_balance <= current_balance {
847                return Err(AptosError::api(
848                    400,
849                    format!(
850                        "faucet returned successful response but balance did not increase (\
851                         attempts={attempts}, balance={new_balance}, requested top-up={amount})"
852                    ),
853                ));
854            }
855            current_balance = new_balance;
856        }
857
858        if current_balance < target_balance {
859            return Err(AptosError::api(
860                429,
861                format!(
862                    "faucet could not deliver {amount} octas in {attempts} attempts \
863                     (starting balance={starting_balance}, current balance={current_balance})"
864                ),
865            ));
866        }
867
868        Ok(all_hashes)
869    }
870
871    #[cfg(all(feature = "faucet", feature = "ed25519"))]
872    /// Creates a funded account.
873    ///
874    /// # Errors
875    ///
876    /// Returns an error if funding the account fails (see [`Self::fund_account`] for details).
877    pub async fn create_funded_account(
878        &self,
879        amount: u64,
880    ) -> AptosResult<crate::account::Ed25519Account> {
881        let account = crate::account::Ed25519Account::generate();
882        self.fund_account(account.address(), amount).await?;
883        Ok(account)
884    }
885
886    // === Transaction Batching ===
887
888    /// Returns a batch operations helper for submitting multiple transactions.
889    ///
890    /// # Example
891    ///
892    /// ```rust,ignore
893    /// let aptos = Aptos::testnet()?;
894    ///
895    /// // Build and submit batch of transfers
896    /// let payloads = vec![
897    ///     EntryFunction::apt_transfer(addr1, 1000)?.into(),
898    ///     EntryFunction::apt_transfer(addr2, 2000)?.into(),
899    ///     EntryFunction::apt_transfer(addr3, 3000)?.into(),
900    /// ];
901    ///
902    /// let results = aptos.batch().submit_and_wait(&sender, payloads, None).await?;
903    /// ```
904    pub fn batch(&self) -> crate::transaction::BatchOperations<'_> {
905        crate::transaction::BatchOperations::new(&self.fullnode, &self.chain_id)
906    }
907
908    /// Submits multiple transactions in parallel.
909    ///
910    /// This is a convenience method that builds, signs, and submits
911    /// multiple transactions at once.
912    ///
913    /// # Arguments
914    ///
915    /// * `account` - The account to sign with
916    /// * `payloads` - The transaction payloads to submit
917    ///
918    /// # Returns
919    ///
920    /// Results for each transaction in the batch.
921    ///
922    /// # Errors
923    ///
924    /// Returns an error if building any transaction fails, signing fails, or submission fails
925    /// for any transaction in the batch.
926    #[cfg(feature = "ed25519")]
927    pub async fn submit_batch<A: Account>(
928        &self,
929        account: &A,
930        payloads: Vec<TransactionPayload>,
931    ) -> AptosResult<Vec<crate::transaction::BatchTransactionResult>> {
932        self.batch().submit(account, payloads).await
933    }
934
935    /// Submits multiple transactions and waits for all to complete.
936    ///
937    /// # Arguments
938    ///
939    /// * `account` - The account to sign with
940    /// * `payloads` - The transaction payloads to submit
941    /// * `timeout` - Optional timeout for waiting
942    ///
943    /// # Returns
944    ///
945    /// Results for each transaction in the batch.
946    ///
947    /// # Errors
948    ///
949    /// Returns an error if building any transaction fails, signing fails, submission fails,
950    /// any transaction times out waiting for commitment, or any transaction execution fails.
951    #[cfg(feature = "ed25519")]
952    pub async fn submit_batch_and_wait<A: Account>(
953        &self,
954        account: &A,
955        payloads: Vec<TransactionPayload>,
956        timeout: Option<Duration>,
957    ) -> AptosResult<Vec<crate::transaction::BatchTransactionResult>> {
958        self.batch()
959            .submit_and_wait(account, payloads, timeout)
960            .await
961    }
962
963    /// Transfers APT to multiple recipients in a batch.
964    ///
965    /// # Arguments
966    ///
967    /// * `sender` - The sending account
968    /// * `transfers` - List of (recipient, amount) pairs
969    ///
970    /// # Example
971    ///
972    /// ```rust,ignore
973    /// let results = aptos.batch_transfer_apt(&sender, vec![
974    ///     (addr1, 1_000_000),  // 0.01 APT
975    ///     (addr2, 2_000_000),  // 0.02 APT
976    ///     (addr3, 3_000_000),  // 0.03 APT
977    /// ]).await?;
978    /// ```
979    ///
980    /// # Errors
981    ///
982    /// Returns an error if building any transfer payload fails, signing fails, submission fails,
983    /// any transaction times out, or any transaction execution fails.
984    #[cfg(feature = "ed25519")]
985    pub async fn batch_transfer_apt<A: Account>(
986        &self,
987        sender: &A,
988        transfers: Vec<(AccountAddress, u64)>,
989    ) -> AptosResult<Vec<crate::transaction::BatchTransactionResult>> {
990        self.batch().transfer_apt(sender, transfers).await
991    }
992}
993
994// The simulation helpers below are only reachable from `Aptos::simulate`,
995// which is `#[cfg(feature = "ed25519")]`. Mirror that gate on the helpers so
996// `cargo clippy -p aptos-sdk --no-default-features` does not flag them as
997// dead code.
998
999// Backticks on every identifier in this module-internal doc comment keep
1000// `clippy::doc_markdown` happy and make the rendering clearer too.
1001
1002#[cfg(feature = "ed25519")]
1003/// Builds a [`TransactionAuthenticator`] containing the account's real
1004/// public key paired with an **all-zero** signature of the correct shape
1005/// for the account's signature scheme.
1006///
1007/// Used by [`Aptos::simulate`] / [`Aptos::estimate_gas`]: the on-chain
1008/// simulation endpoint rejects transactions carrying a valid signature
1009/// (it is a gas-estimation tool, not an execution tool) but it does need
1010/// to walk the signing-message hash to estimate gas correctly. A
1011/// well-shaped zero-signed authenticator is exactly what it expects.
1012///
1013/// Supported schemes (everything the SDK can already produce signatures for):
1014/// * `ED25519_SCHEME` -- `Ed25519` 32-byte pubkey + 64-byte zero signature.
1015/// * `MULTI_ED25519_SCHEME` -- `MultiEd25519` pubkey/signature wrapped as
1016///   `TransactionAuthenticator::MultiEd25519`; signature is `64 * t` zero
1017///   bytes plus a 4-byte bitmap with bits `0..t` set, where `t` is the
1018///   account's threshold (recovered from the pubkey's last byte).
1019/// * `SINGLE_KEY_SCHEME` -- Wraps any single-key account (`Ed25519SingleKey`,
1020///   `Secp256k1`, `Secp256r1`, `WebAuthn`) by emitting a zeroed `AnySignature`
1021///   whose variant tag matches the account's pubkey variant.
1022/// * `MULTI_KEY_SCHEME` -- Wraps a `MultiKey` account by emitting a zeroed
1023///   `MultiKeySignature` whose `AnySignature` variants match the pubkeys.
1024fn build_zero_signed_authenticator<A: Account>(
1025    account: &A,
1026) -> AptosResult<crate::transaction::TransactionAuthenticator> {
1027    use crate::crypto::{
1028        ED25519_SCHEME, MULTI_ED25519_SCHEME, MULTI_KEY_SCHEME, SINGLE_KEY_SCHEME,
1029    };
1030    use crate::transaction::TransactionAuthenticator;
1031    use crate::transaction::authenticator::{
1032        AccountAuthenticator, Ed25519PublicKey, Ed25519Signature,
1033    };
1034
1035    let pubkey_bytes = account.public_key_bytes();
1036    let scheme = account.signature_scheme();
1037
1038    match scheme {
1039        // Single Ed25519: 32-byte pubkey, 64-byte zero signature, top-level
1040        // TransactionAuthenticator::Ed25519 variant.
1041        s if s == ED25519_SCHEME => {
1042            let pubkey_arr: [u8; 32] = pubkey_bytes.as_slice().try_into().map_err(|_| {
1043                crate::error::AptosError::transaction(
1044                    "simulate(): Ed25519 account exposed a non-32-byte public key",
1045                )
1046            })?;
1047            Ok(TransactionAuthenticator::Ed25519 {
1048                public_key: Ed25519PublicKey(pubkey_arr),
1049                signature: Ed25519Signature([0u8; 64]),
1050            })
1051        }
1052
1053        // MultiEd25519: the pubkey blob is `pk_0 || pk_1 || ... || pk_{n-1} || threshold`
1054        // where each `pk_i` is 32 bytes. We can recover `n` and `t`, then emit
1055        // `t` zero signatures plus a bitmap with bits 0..t set (MSB-first
1056        // ordering, matching `MultiEd25519Signature::new`).
1057        s if s == MULTI_ED25519_SCHEME => {
1058            const PK_LEN: usize = 32;
1059            const SIG_LEN: usize = 64;
1060            if pubkey_bytes.is_empty() || !(pubkey_bytes.len() - 1).is_multiple_of(PK_LEN) {
1061                return Err(crate::error::AptosError::transaction(
1062                    "simulate(): MultiEd25519 public_key_bytes has invalid length",
1063                ));
1064            }
1065            let threshold = *pubkey_bytes.last().unwrap() as usize;
1066            if threshold == 0 {
1067                return Err(crate::error::AptosError::transaction(
1068                    "simulate(): MultiEd25519 threshold cannot be zero",
1069                ));
1070            }
1071            // MSB-first bitmap, threshold bits set starting at index 0.
1072            let mut bitmap = [0u8; 4];
1073            for i in 0..threshold {
1074                let byte = i / 8;
1075                let bit = i % 8;
1076                bitmap[byte] |= 0b1000_0000_u8 >> bit;
1077            }
1078            let mut signature = Vec::with_capacity(threshold * SIG_LEN + 4);
1079            signature.extend(std::iter::repeat_n(0u8, threshold * SIG_LEN));
1080            signature.extend_from_slice(&bitmap);
1081            Ok(TransactionAuthenticator::MultiEd25519 {
1082                public_key: pubkey_bytes,
1083                signature,
1084            })
1085        }
1086
1087        // SingleKey: pubkey is already `BCS(AnyPublicKey)` (variant + ULEB128(len) + bytes).
1088        // We mirror the variant tag in a matching zero AnySignature and wrap
1089        // in a SingleSender top-level TransactionAuthenticator.
1090        s if s == SINGLE_KEY_SCHEME => {
1091            let zero_sig = zero_any_signature_for_pubkey(&pubkey_bytes).ok_or_else(|| {
1092                crate::error::AptosError::transaction(
1093                    "simulate(): unsupported AnyPublicKey variant in SingleKey account",
1094                )
1095            })?;
1096            Ok(TransactionAuthenticator::single_sender(
1097                AccountAuthenticator::single_key(pubkey_bytes, zero_sig),
1098            ))
1099        }
1100
1101        // MultiKey: pubkey is `num_keys || (variant || ULEB128(len) || bytes) * n || threshold`.
1102        // Emit a zeroed MultiKeySignature: one zero AnySignature per pubkey
1103        // for the first `threshold` keys, plus the BCS BitVec length prefix
1104        // and a bitmap with bits 0..threshold set (MSB-first).
1105        s if s == MULTI_KEY_SCHEME => {
1106            let (variants, threshold) = parse_multi_key_pubkey(&pubkey_bytes)?;
1107            if threshold == 0 || (threshold as usize) > variants.len() {
1108                return Err(crate::error::AptosError::transaction(
1109                    "simulate(): invalid MultiKey threshold",
1110                ));
1111            }
1112            let mut sig_bytes = Vec::with_capacity(1 + threshold as usize * 66 + 1 + 4);
1113            sig_bytes.push(threshold); // ULEB128(num_sigs); fits in 1 byte for n <= 32.
1114            for variant in variants.iter().take(threshold as usize) {
1115                let zero_sig = zero_any_signature_for_variant(*variant).ok_or_else(|| {
1116                    crate::error::AptosError::transaction(
1117                        "simulate(): unsupported AnyPublicKey variant in MultiKey account",
1118                    )
1119                })?;
1120                sig_bytes.extend_from_slice(&zero_sig);
1121            }
1122            // BitVec length prefix + 4-byte bitmap (MSB-first).
1123            sig_bytes.push(4);
1124            let mut bitmap = [0u8; 4];
1125            for i in 0..threshold as usize {
1126                let byte = i / 8;
1127                let bit = i % 8;
1128                bitmap[byte] |= 0b1000_0000_u8 >> bit;
1129            }
1130            sig_bytes.extend_from_slice(&bitmap);
1131            Ok(TransactionAuthenticator::single_sender(
1132                AccountAuthenticator::multi_key(pubkey_bytes, sig_bytes),
1133            ))
1134        }
1135
1136        _ => Err(crate::error::AptosError::transaction(format!(
1137            "simulate(): unsupported signature scheme {scheme}; \
1138             use simulate_signed() with a hand-built zero-signed transaction"
1139        ))),
1140    }
1141}
1142
1143/// Builds a BCS-encoded zero `AnySignature` whose variant tag matches the
1144/// `AnyPublicKey` carried by the given `SingleKey` pubkey blob. Returns
1145/// `None` for unknown variants.
1146#[cfg(feature = "ed25519")]
1147fn zero_any_signature_for_pubkey(any_public_key_bcs: &[u8]) -> Option<Vec<u8>> {
1148    let variant = *any_public_key_bcs.first()?;
1149    zero_any_signature_for_variant(variant)
1150}
1151
1152/// Builds a BCS-encoded zero `AnySignature` for the given variant tag.
1153#[cfg(feature = "ed25519")]
1154fn zero_any_signature_for_variant(variant: u8) -> Option<Vec<u8>> {
1155    // For all SDK-supported variants, the inner signature payload is 64
1156    // bytes (Ed25519, Secp256k1Ecdsa, and -- in the SDK's representation
1157    // -- the Secp256r1 raw signature carried inside the WebAuthn envelope).
1158    // Variant 2 on-chain is WebAuthn, but for *simulation* the inner
1159    // PartialAuthenticatorAssertionResponse is allowed to be all zeros: the
1160    // simulator never actually verifies the signature, and a 64-byte zero
1161    // payload with the variant tag and length prefix has the same shape as
1162    // the live signature.
1163    match variant {
1164        0..=2 => {
1165            let mut out = Vec::with_capacity(1 + 1 + 64);
1166            out.push(variant);
1167            out.push(64);
1168            out.extend(std::iter::repeat_n(0u8, 64));
1169            Some(out)
1170        }
1171        _ => None,
1172    }
1173}
1174
1175/// Parses a `BCS(MultiKeyPublicKey)` blob into its variant tags and threshold.
1176///
1177/// Wire layout: `num_keys || (variant || ULEB128(len) || bytes) * n || threshold`.
1178#[cfg(feature = "ed25519")]
1179fn parse_multi_key_pubkey(bytes: &[u8]) -> AptosResult<(Vec<u8>, u8)> {
1180    if bytes.is_empty() {
1181        return Err(crate::error::AptosError::transaction(
1182            "simulate(): MultiKey public_key_bytes is empty",
1183        ));
1184    }
1185    let num_keys = bytes[0] as usize;
1186    let mut offset = 1;
1187    let mut variants = Vec::with_capacity(num_keys);
1188    for _ in 0..num_keys {
1189        if offset >= bytes.len() {
1190            return Err(crate::error::AptosError::transaction(
1191                "simulate(): MultiKey public_key truncated at variant tag",
1192            ));
1193        }
1194        let variant = bytes[offset];
1195        variants.push(variant);
1196        offset += 1;
1197        // ULEB128(len)
1198        let (len, len_bytes) = decode_uleb128_internal(&bytes[offset..])?;
1199        offset += len_bytes;
1200        offset = offset.checked_add(len).ok_or_else(|| {
1201            crate::error::AptosError::transaction("simulate(): MultiKey public_key overflow")
1202        })?;
1203        if offset > bytes.len() {
1204            return Err(crate::error::AptosError::transaction(
1205                "simulate(): MultiKey public_key truncated at key bytes",
1206            ));
1207        }
1208    }
1209    if offset >= bytes.len() {
1210        return Err(crate::error::AptosError::transaction(
1211            "simulate(): MultiKey public_key missing threshold byte",
1212        ));
1213    }
1214    let threshold = bytes[offset];
1215    Ok((variants, threshold))
1216}
1217
1218/// Minimal ULEB128 decoder local to the simulation helper.
1219#[cfg(feature = "ed25519")]
1220fn decode_uleb128_internal(bytes: &[u8]) -> AptosResult<(usize, usize)> {
1221    let mut value: usize = 0;
1222    let mut shift = 0;
1223    for (i, &b) in bytes.iter().enumerate() {
1224        value |= ((b & 0x7F) as usize) << shift;
1225        if (b & 0x80) == 0 {
1226            return Ok((value, i + 1));
1227        }
1228        shift += 7;
1229        if shift >= 64 {
1230            break;
1231        }
1232    }
1233    Err(crate::error::AptosError::transaction(
1234        "simulate(): malformed ULEB128 in public key",
1235    ))
1236}
1237
1238#[cfg(test)]
1239mod tests {
1240    use super::*;
1241    use crate::transaction::authenticator::{
1242        Ed25519PublicKey, Ed25519Signature, TransactionAuthenticator,
1243    };
1244    use crate::transaction::payload::{EntryFunction, TransactionPayload};
1245    use crate::transaction::simulation::SimulateQueryOptions;
1246    use crate::transaction::types::{
1247        FeePayerRawTransaction, MultiAgentRawTransaction, RawTransaction, SignedTransaction,
1248    };
1249    use crate::types::ChainId;
1250    use wiremock::{
1251        Mock, MockServer, ResponseTemplate,
1252        matchers::{method, path, path_regex},
1253    };
1254
1255    #[test]
1256    fn test_aptos_client_creation() {
1257        let aptos = Aptos::testnet();
1258        assert!(aptos.is_ok());
1259    }
1260
1261    #[test]
1262    fn test_chain_id() {
1263        let aptos = Aptos::testnet().unwrap();
1264        assert_eq!(aptos.chain_id(), ChainId::testnet());
1265
1266        let aptos = Aptos::mainnet().unwrap();
1267        assert_eq!(aptos.chain_id(), ChainId::mainnet());
1268    }
1269
1270    fn create_mock_aptos(server: &MockServer) -> Aptos {
1271        let url = format!("{}/v1", server.uri());
1272        let config = AptosConfig::custom(&url).unwrap().without_retry();
1273        Aptos::new(config).unwrap()
1274    }
1275
1276    fn create_minimal_signed_transaction() -> SignedTransaction {
1277        let raw = RawTransaction::new(
1278            AccountAddress::ONE,
1279            0,
1280            TransactionPayload::EntryFunction(
1281                EntryFunction::apt_transfer(AccountAddress::ONE, 0).unwrap(),
1282            ),
1283            100_000,
1284            100,
1285            std::time::SystemTime::now()
1286                .duration_since(std::time::UNIX_EPOCH)
1287                .unwrap()
1288                .as_secs()
1289                .saturating_add(600),
1290            ChainId::testnet(),
1291        );
1292        SignedTransaction::new(
1293            raw,
1294            TransactionAuthenticator::Ed25519 {
1295                public_key: Ed25519PublicKey([0u8; 32]),
1296                signature: Ed25519Signature([0u8; 64]),
1297            },
1298        )
1299    }
1300
1301    fn simulate_response_json() -> serde_json::Value {
1302        serde_json::json!([{
1303            "success": true,
1304            "vm_status": "Executed successfully",
1305            "gas_used": "1500",
1306            "max_gas_amount": "200000",
1307            "gas_unit_price": "100",
1308            "hash": "0xabc",
1309            "changes": [],
1310            "events": []
1311        }])
1312    }
1313
1314    #[tokio::test]
1315    async fn test_get_sequence_number() {
1316        let server = MockServer::start().await;
1317
1318        Mock::given(method("GET"))
1319            .and(path_regex(r"/v1/accounts/0x[0-9a-f]+"))
1320            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1321                "sequence_number": "42",
1322                "authentication_key": "0x0000000000000000000000000000000000000000000000000000000000000001"
1323            })))
1324            .expect(1)
1325            .mount(&server)
1326            .await;
1327
1328        let aptos = create_mock_aptos(&server);
1329        let seq = aptos
1330            .get_sequence_number(AccountAddress::ONE)
1331            .await
1332            .unwrap();
1333        assert_eq!(seq, 42);
1334    }
1335
1336    #[tokio::test]
1337    async fn test_get_balance() {
1338        let server = MockServer::start().await;
1339
1340        // get_balance now uses view function instead of CoinStore resource
1341        Mock::given(method("POST"))
1342            .and(path("/v1/view"))
1343            .respond_with(
1344                ResponseTemplate::new(200).set_body_json(serde_json::json!(["5000000000"])),
1345            )
1346            .expect(1)
1347            .mount(&server)
1348            .await;
1349
1350        let aptos = create_mock_aptos(&server);
1351        let balance = aptos.get_balance(AccountAddress::ONE).await.unwrap();
1352        assert_eq!(balance, 5_000_000_000);
1353    }
1354
1355    #[tokio::test]
1356    async fn test_get_resources_via_fullnode() {
1357        let server = MockServer::start().await;
1358
1359        Mock::given(method("GET"))
1360            .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/resources"))
1361            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([
1362                {"type": "0x1::account::Account", "data": {}},
1363                {"type": "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>", "data": {}}
1364            ])))
1365            .expect(1)
1366            .mount(&server)
1367            .await;
1368
1369        let aptos = create_mock_aptos(&server);
1370        let resources = aptos
1371            .fullnode()
1372            .get_account_resources(AccountAddress::ONE)
1373            .await
1374            .unwrap();
1375        assert_eq!(resources.data.len(), 2);
1376    }
1377
1378    #[tokio::test]
1379    async fn test_ledger_info() {
1380        let server = MockServer::start().await;
1381
1382        Mock::given(method("GET"))
1383            .and(path("/v1"))
1384            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1385                "chain_id": 2,
1386                "epoch": "100",
1387                "ledger_version": "12345",
1388                "oldest_ledger_version": "0",
1389                "ledger_timestamp": "1000000",
1390                "node_role": "full_node",
1391                "oldest_block_height": "0",
1392                "block_height": "5000"
1393            })))
1394            .expect(1)
1395            .mount(&server)
1396            .await;
1397
1398        let aptos = create_mock_aptos(&server);
1399        let info = aptos.ledger_info().await.unwrap();
1400        assert_eq!(info.version().unwrap(), 12345);
1401    }
1402
1403    #[tokio::test]
1404    async fn test_config_builder() {
1405        let config = AptosConfig::testnet().with_timeout(Duration::from_mins(1));
1406
1407        let aptos = Aptos::new(config).unwrap();
1408        assert_eq!(aptos.chain_id(), ChainId::testnet());
1409    }
1410
1411    #[tokio::test]
1412    async fn test_fullnode_accessor() {
1413        let server = MockServer::start().await;
1414        let aptos = create_mock_aptos(&server);
1415
1416        // Can access fullnode client directly
1417        let fullnode = aptos.fullnode();
1418        assert!(fullnode.base_url().as_str().contains(&server.uri()));
1419    }
1420
1421    #[cfg(feature = "ed25519")]
1422    #[tokio::test]
1423    async fn test_build_transaction() {
1424        let server = MockServer::start().await;
1425
1426        // Mock for getting account
1427        Mock::given(method("GET"))
1428            .and(path_regex(r"/v1/accounts/0x[0-9a-f]+"))
1429            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1430                "sequence_number": "0",
1431                "authentication_key": "0x0000000000000000000000000000000000000000000000000000000000000001"
1432            })))
1433            .expect(1)
1434            .mount(&server)
1435            .await;
1436
1437        // Mock for gas price
1438        Mock::given(method("GET"))
1439            .and(path("/v1/estimate_gas_price"))
1440            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1441                "gas_estimate": 100
1442            })))
1443            .expect(1)
1444            .mount(&server)
1445            .await;
1446
1447        // Mock for ledger info (needed for chain_id resolution on custom networks)
1448        Mock::given(method("GET"))
1449            .and(path("/v1"))
1450            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1451                "chain_id": 4,
1452                "epoch": "1",
1453                "ledger_version": "100",
1454                "oldest_ledger_version": "0",
1455                "ledger_timestamp": "1000000",
1456                "node_role": "full_node",
1457                "oldest_block_height": "0",
1458                "block_height": "50"
1459            })))
1460            .expect(1)
1461            .mount(&server)
1462            .await;
1463
1464        let aptos = create_mock_aptos(&server);
1465        let account = crate::account::Ed25519Account::generate();
1466        let recipient = AccountAddress::from_hex("0x123").unwrap();
1467        let payload = crate::transaction::EntryFunction::apt_transfer(recipient, 1000).unwrap();
1468
1469        let raw_txn = aptos
1470            .build_transaction(&account, payload.into())
1471            .await
1472            .unwrap();
1473        assert_eq!(raw_txn.sender, account.address());
1474        assert_eq!(raw_txn.sequence_number, 0);
1475    }
1476
1477    #[cfg(feature = "indexer")]
1478    #[tokio::test]
1479    async fn test_indexer_accessor() {
1480        let aptos = Aptos::testnet().unwrap();
1481        let indexer = aptos.indexer();
1482        assert!(indexer.is_some());
1483    }
1484
1485    #[tokio::test]
1486    async fn test_account_exists_true() {
1487        let server = MockServer::start().await;
1488
1489        Mock::given(method("GET"))
1490            .and(path_regex(r"^/v1/accounts/0x[0-9a-f]+$"))
1491            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1492                "sequence_number": "10",
1493                "authentication_key": "0x0000000000000000000000000000000000000000000000000000000000000001"
1494            })))
1495            .expect(1)
1496            .mount(&server)
1497            .await;
1498
1499        let aptos = create_mock_aptos(&server);
1500        let exists = aptos.account_exists(AccountAddress::ONE).await.unwrap();
1501        assert!(exists);
1502    }
1503
1504    #[tokio::test]
1505    async fn test_account_exists_false() {
1506        let server = MockServer::start().await;
1507
1508        Mock::given(method("GET"))
1509            .and(path_regex(r"^/v1/accounts/0x[0-9a-f]+$"))
1510            .respond_with(ResponseTemplate::new(404).set_body_json(serde_json::json!({
1511                "message": "Account not found",
1512                "error_code": "account_not_found"
1513            })))
1514            .expect(1)
1515            .mount(&server)
1516            .await;
1517
1518        let aptos = create_mock_aptos(&server);
1519        let exists = aptos.account_exists(AccountAddress::ONE).await.unwrap();
1520        assert!(!exists);
1521    }
1522
1523    #[tokio::test]
1524    async fn test_view_function() {
1525        let server = MockServer::start().await;
1526
1527        Mock::given(method("POST"))
1528            .and(path("/v1/view"))
1529            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!(["1000000"])))
1530            .expect(1)
1531            .mount(&server)
1532            .await;
1533
1534        let aptos = create_mock_aptos(&server);
1535        let result: Vec<serde_json::Value> = aptos
1536            .view(
1537                "0x1::coin::balance",
1538                vec!["0x1::aptos_coin::AptosCoin".to_string()],
1539                vec![serde_json::json!("0x1")],
1540            )
1541            .await
1542            .unwrap();
1543
1544        assert_eq!(result.len(), 1);
1545        assert_eq!(result[0].as_str().unwrap(), "1000000");
1546    }
1547
1548    #[tokio::test]
1549    async fn test_chain_id_from_config() {
1550        let aptos = Aptos::mainnet().unwrap();
1551        assert_eq!(aptos.chain_id(), ChainId::mainnet());
1552
1553        // Devnet's chain ID is intentionally reported as 0 (unknown) at
1554        // construction time -- the value is reset whenever devnet itself
1555        // is reset, so any hardcoded value would go stale. The Aptos client
1556        // populates the live chain ID lazily via `ensure_chain_id`, which is
1557        // exercised on the network and not in this offline unit test.
1558        let aptos = Aptos::devnet().unwrap();
1559        assert_eq!(aptos.chain_id(), ChainId::new(0));
1560    }
1561
1562    #[tokio::test]
1563    async fn test_custom_config() {
1564        let server = MockServer::start().await;
1565        let url = format!("{}/v1", server.uri());
1566        let config = AptosConfig::custom(&url).unwrap();
1567        let aptos = Aptos::new(config).unwrap();
1568
1569        // Custom config should have unknown chain ID
1570        assert_eq!(aptos.chain_id(), ChainId::new(0));
1571    }
1572
1573    // ---------------------------------------------------------------
1574    // build_zero_signed_authenticator: cover every signature scheme
1575    // the SDK can sign for and confirm the helper does NOT fall back to
1576    // the Ed25519 path for non-Ed25519 accounts. This was the subject
1577    // of a Copilot review comment and now has explicit regression
1578    // coverage.
1579    // ---------------------------------------------------------------
1580
1581    #[cfg(feature = "ed25519")]
1582    #[test]
1583    fn test_zero_signed_authenticator_ed25519() {
1584        use crate::account::Ed25519Account;
1585        use crate::transaction::TransactionAuthenticator;
1586
1587        let account = Ed25519Account::generate();
1588        let auth = super::build_zero_signed_authenticator(&account).unwrap();
1589        match auth {
1590            TransactionAuthenticator::Ed25519 {
1591                public_key,
1592                signature,
1593            } => {
1594                assert_eq!(public_key.0, account.public_key().to_bytes());
1595                assert_eq!(signature.0, [0u8; 64]);
1596            }
1597            other => panic!("expected TransactionAuthenticator::Ed25519, got {other:?}"),
1598        }
1599    }
1600
1601    #[cfg(all(feature = "ed25519", feature = "secp256k1"))]
1602    #[test]
1603    fn test_zero_signed_authenticator_single_key_secp256k1() {
1604        use crate::account::Secp256k1Account;
1605        use crate::transaction::TransactionAuthenticator;
1606        use crate::transaction::authenticator::AccountAuthenticator;
1607
1608        let account = Secp256k1Account::generate();
1609        let auth = super::build_zero_signed_authenticator(&account).unwrap();
1610        // Secp256k1Account is a SingleKey, so it must wrap in SingleSender +
1611        // SingleKey. No Ed25519 variant must appear anywhere.
1612        let TransactionAuthenticator::SingleSender { sender } = auth else {
1613            panic!("expected SingleSender, got {auth:?}");
1614        };
1615        let AccountAuthenticator::SingleKey {
1616            public_key,
1617            signature,
1618        } = sender
1619        else {
1620            panic!("expected AccountAuthenticator::SingleKey");
1621        };
1622        // Public key bytes are passed through unchanged.
1623        assert_eq!(public_key, account.public_key_bytes());
1624        // Signature is a zeroed BCS-encoded `AnySignature::Secp256k1Ecdsa`
1625        // (variant=1, len=64, 64 zero bytes).
1626        assert_eq!(signature.len(), 1 + 1 + 64);
1627        assert_eq!(signature[0], 0x01, "variant tag must match secp256k1");
1628        assert_eq!(signature[1], 64, "ULEB128(64)");
1629        assert!(signature[2..].iter().all(|b| *b == 0), "all-zero signature");
1630    }
1631
1632    #[cfg(feature = "ed25519")]
1633    #[test]
1634    fn test_zero_signed_authenticator_single_key_ed25519() {
1635        use crate::account::Ed25519SingleKeyAccount;
1636        use crate::transaction::TransactionAuthenticator;
1637        use crate::transaction::authenticator::AccountAuthenticator;
1638
1639        // Ed25519SingleKeyAccount uses scheme SINGLE_KEY_SCHEME and exposes
1640        // `public_key_bytes` as BCS(AnyPublicKey::Ed25519), not the raw 32-byte
1641        // pubkey, so the previous Ed25519-only fast path would have rejected
1642        // this account.
1643        let account = Ed25519SingleKeyAccount::generate();
1644        let auth = super::build_zero_signed_authenticator(&account).unwrap();
1645        let TransactionAuthenticator::SingleSender { sender } = auth else {
1646            panic!("expected SingleSender for Ed25519SingleKey, got {auth:?}");
1647        };
1648        let AccountAuthenticator::SingleKey {
1649            public_key,
1650            signature,
1651        } = sender
1652        else {
1653            panic!("expected AccountAuthenticator::SingleKey");
1654        };
1655        assert_eq!(public_key, account.public_key_bytes());
1656        assert_eq!(signature[0], 0x00, "AnySignature::Ed25519 variant");
1657        assert_eq!(signature[1], 64, "ULEB128(64)");
1658        assert!(signature[2..].iter().all(|b| *b == 0));
1659    }
1660
1661    #[cfg(feature = "ed25519")]
1662    #[test]
1663    fn test_zero_signed_authenticator_multi_ed25519() {
1664        use crate::account::MultiEd25519Account;
1665        use crate::crypto::Ed25519PrivateKey;
1666        use crate::transaction::TransactionAuthenticator;
1667
1668        // 2-of-3 multi-ed25519.
1669        let keys: Vec<_> = (0..3).map(|_| Ed25519PrivateKey::generate()).collect();
1670        let account = MultiEd25519Account::new(keys, 2).unwrap();
1671        let auth = super::build_zero_signed_authenticator(&account).unwrap();
1672        let TransactionAuthenticator::MultiEd25519 {
1673            public_key,
1674            signature,
1675        } = auth
1676        else {
1677            panic!("expected MultiEd25519, got {auth:?}");
1678        };
1679        assert_eq!(public_key, account.public_key_bytes());
1680        // signature = 2 * 64 zero bytes + 4-byte bitmap, MSB-first bits 0+1 set.
1681        assert_eq!(signature.len(), 2 * 64 + 4);
1682        assert!(signature[..128].iter().all(|b| *b == 0));
1683        assert_eq!(signature[128], 0b1100_0000, "bits 0 and 1 set (MSB-first)");
1684        assert_eq!(&signature[129..], &[0u8, 0u8, 0u8]);
1685    }
1686
1687    #[cfg(all(feature = "ed25519", feature = "secp256k1"))]
1688    #[test]
1689    fn test_zero_signed_authenticator_multi_key() {
1690        use crate::account::{AnyPrivateKey, MultiKeyAccount};
1691        use crate::crypto::{Ed25519PrivateKey, Secp256k1PrivateKey};
1692        use crate::transaction::TransactionAuthenticator;
1693        use crate::transaction::authenticator::AccountAuthenticator;
1694
1695        let keys = vec![
1696            AnyPrivateKey::ed25519(Ed25519PrivateKey::generate()),
1697            AnyPrivateKey::secp256k1(Secp256k1PrivateKey::generate()),
1698            AnyPrivateKey::ed25519(Ed25519PrivateKey::generate()),
1699        ];
1700        let account = MultiKeyAccount::new(keys, 2).unwrap();
1701        let auth = super::build_zero_signed_authenticator(&account).unwrap();
1702        let TransactionAuthenticator::SingleSender { sender } = auth else {
1703            panic!("expected SingleSender for MultiKey, got {auth:?}");
1704        };
1705        let AccountAuthenticator::MultiKey {
1706            public_key,
1707            signature,
1708        } = sender
1709        else {
1710            panic!("expected AccountAuthenticator::MultiKey");
1711        };
1712        assert_eq!(public_key, account.public_key_bytes());
1713        // signature = ULEB128(2) || two zero AnySignatures || ULEB128(4) || 4-byte bitmap.
1714        // First zero AnySignature is for the Ed25519 key at index 0 (variant 0).
1715        // Second is for the Secp256k1 key at index 1 (variant 1).
1716        // No reason for them to share variants; this regression-guards
1717        // the bug-prone assumption that all single-key accounts are Ed25519.
1718        assert_eq!(signature[0], 2, "num_sigs ULEB128");
1719        assert_eq!(signature[1], 0x00, "first AnySignature variant (Ed25519)");
1720        assert_eq!(
1721            signature[1 + 1 + 1 + 64],
1722            0x01,
1723            "second AnySignature variant (Secp256k1)"
1724        );
1725    }
1726
1727    #[tokio::test]
1728    async fn test_simulate_signed_with_options() {
1729        let server = MockServer::start().await;
1730
1731        Mock::given(method("POST"))
1732            .and(path("/v1/transactions/simulate"))
1733            .and(|req: &wiremock::Request| {
1734                req.url
1735                    .query()
1736                    .is_some_and(|q| q.contains("estimate_gas_unit_price=true"))
1737            })
1738            .respond_with(
1739                ResponseTemplate::new(200).set_body_json(serde_json::json!([{
1740                    "success": true,
1741                    "vm_status": "Executed successfully",
1742                    "gas_used": "1500",
1743                    "max_gas_amount": "200000",
1744                    "gas_unit_price": "100",
1745                    "hash": "0xabc",
1746                    "changes": [],
1747                    "events": []
1748                }])),
1749            )
1750            .expect(1)
1751            .mount(&server)
1752            .await;
1753
1754        let raw = RawTransaction::new(
1755            AccountAddress::ONE,
1756            0,
1757            TransactionPayload::EntryFunction(
1758                EntryFunction::apt_transfer(AccountAddress::ONE, 0).unwrap(),
1759            ),
1760            100_000,
1761            100,
1762            std::time::SystemTime::now()
1763                .duration_since(std::time::UNIX_EPOCH)
1764                .unwrap()
1765                .as_secs()
1766                .saturating_add(600),
1767            ChainId::testnet(),
1768        );
1769        let signed = SignedTransaction::new(
1770            raw,
1771            TransactionAuthenticator::Ed25519 {
1772                public_key: Ed25519PublicKey([0u8; 32]),
1773                signature: Ed25519Signature([0u8; 64]),
1774            },
1775        );
1776
1777        let aptos = create_mock_aptos(&server);
1778        let options = SimulateQueryOptions::new().estimate_gas_unit_price(true);
1779        let result = aptos
1780            .simulate_signed_with_options(&signed, options)
1781            .await
1782            .unwrap();
1783
1784        assert!(result.success());
1785        assert_eq!(result.gas_used(), 1500);
1786        assert_eq!(result.gas_unit_price(), 100);
1787    }
1788
1789    #[tokio::test]
1790    async fn test_simulate_signed_without_options() {
1791        let server = MockServer::start().await;
1792
1793        Mock::given(method("POST"))
1794            .and(path("/v1/transactions/simulate"))
1795            .and(|req: &wiremock::Request| {
1796                req.url.query().is_none_or(|q| {
1797                    !q.contains("estimate_gas_unit_price=")
1798                        && !q.contains("estimate_max_gas_amount=")
1799                        && !q.contains("estimate_prioritized_gas_unit_price=")
1800                })
1801            })
1802            .respond_with(ResponseTemplate::new(200).set_body_json(simulate_response_json()))
1803            .expect(1)
1804            .mount(&server)
1805            .await;
1806
1807        let aptos = create_mock_aptos(&server);
1808        let signed = create_minimal_signed_transaction();
1809        let result = aptos.simulate_signed(&signed).await.unwrap();
1810        assert!(result.success());
1811    }
1812
1813    #[tokio::test]
1814    async fn test_simulate_multi_agent_without_options() {
1815        let server = MockServer::start().await;
1816
1817        Mock::given(method("POST"))
1818            .and(path("/v1/transactions/simulate"))
1819            .and(|req: &wiremock::Request| {
1820                req.url.query().is_none_or(|q| {
1821                    !q.contains("estimate_gas_unit_price=")
1822                        && !q.contains("estimate_max_gas_amount=")
1823                        && !q.contains("estimate_prioritized_gas_unit_price=")
1824                })
1825            })
1826            .respond_with(ResponseTemplate::new(200).set_body_json(simulate_response_json()))
1827            .expect(1)
1828            .mount(&server)
1829            .await;
1830
1831        let aptos = create_mock_aptos(&server);
1832        let multi_agent = MultiAgentRawTransaction::new(
1833            create_minimal_signed_transaction().raw_txn,
1834            vec![AccountAddress::from_hex("0x2").unwrap()],
1835        );
1836        let result = aptos
1837            .simulate_multi_agent(&multi_agent, None)
1838            .await
1839            .unwrap();
1840        assert!(result.success());
1841    }
1842
1843    #[tokio::test]
1844    async fn test_simulate_multi_agent_with_options() {
1845        let server = MockServer::start().await;
1846
1847        Mock::given(method("POST"))
1848            .and(path("/v1/transactions/simulate"))
1849            .and(|req: &wiremock::Request| {
1850                req.url
1851                    .query()
1852                    .is_some_and(|q| q.contains("estimate_max_gas_amount=true"))
1853            })
1854            .respond_with(ResponseTemplate::new(200).set_body_json(simulate_response_json()))
1855            .expect(1)
1856            .mount(&server)
1857            .await;
1858
1859        let aptos = create_mock_aptos(&server);
1860        let multi_agent = MultiAgentRawTransaction::new(
1861            create_minimal_signed_transaction().raw_txn,
1862            vec![AccountAddress::from_hex("0x2").unwrap()],
1863        );
1864        let options = SimulateQueryOptions::new().estimate_max_gas_amount(true);
1865        let result = aptos
1866            .simulate_multi_agent(&multi_agent, Some(options))
1867            .await
1868            .unwrap();
1869        assert!(result.success());
1870    }
1871
1872    #[tokio::test]
1873    async fn test_simulate_fee_payer_without_options() {
1874        let server = MockServer::start().await;
1875
1876        Mock::given(method("POST"))
1877            .and(path("/v1/transactions/simulate"))
1878            .and(|req: &wiremock::Request| {
1879                req.url.query().is_none_or(|q| {
1880                    !q.contains("estimate_gas_unit_price=")
1881                        && !q.contains("estimate_max_gas_amount=")
1882                        && !q.contains("estimate_prioritized_gas_unit_price=")
1883                })
1884            })
1885            .respond_with(ResponseTemplate::new(200).set_body_json(simulate_response_json()))
1886            .expect(1)
1887            .mount(&server)
1888            .await;
1889
1890        let aptos = create_mock_aptos(&server);
1891        let fee_payer_txn = FeePayerRawTransaction::new_simple(
1892            create_minimal_signed_transaction().raw_txn,
1893            AccountAddress::THREE,
1894        );
1895        let result = aptos
1896            .simulate_fee_payer(&fee_payer_txn, None)
1897            .await
1898            .unwrap();
1899        assert!(result.success());
1900    }
1901
1902    #[tokio::test]
1903    async fn test_simulate_fee_payer_with_options() {
1904        let server = MockServer::start().await;
1905
1906        Mock::given(method("POST"))
1907            .and(path("/v1/transactions/simulate"))
1908            .and(|req: &wiremock::Request| {
1909                req.url
1910                    .query()
1911                    .is_some_and(|q| q.contains("estimate_prioritized_gas_unit_price=true"))
1912            })
1913            .respond_with(ResponseTemplate::new(200).set_body_json(simulate_response_json()))
1914            .expect(1)
1915            .mount(&server)
1916            .await;
1917
1918        let aptos = create_mock_aptos(&server);
1919        let fee_payer_txn = FeePayerRawTransaction::new_simple(
1920            create_minimal_signed_transaction().raw_txn,
1921            AccountAddress::THREE,
1922        );
1923        let options = SimulateQueryOptions::new().estimate_prioritized_gas_unit_price(true);
1924        let result = aptos
1925            .simulate_fee_payer(&fee_payer_txn, options)
1926            .await
1927            .unwrap();
1928        assert!(result.success());
1929    }
1930}