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    /// Returns an Aptos Names Service (ANS) client bound to this client's
155    /// fullnode and network.
156    ///
157    /// ANS is only deployed on mainnet, testnet, and localnet; on other
158    /// networks the returned client's methods error unless you instead build
159    /// one with [`crate::api::AnsClient::with_router_address`].
160    pub fn ans(&self) -> crate::api::AnsClient {
161        crate::api::AnsClient::new((*self.fullnode).clone())
162    }
163
164    // === Ledger Info ===
165
166    /// Gets the current ledger information.
167    ///
168    /// As a side effect, this also resolves the chain ID if it was unknown
169    /// (e.g., for custom network configurations).
170    ///
171    /// # Errors
172    ///
173    /// Returns an error if the HTTP request fails, the API returns an error status code,
174    /// or the response cannot be parsed.
175    pub async fn ledger_info(&self) -> AptosResult<crate::api::response::LedgerInfo> {
176        let response = self.fullnode.get_ledger_info().await?;
177        let info = response.into_inner();
178
179        // Update chain_id if it was unknown (custom network).
180        // NOTE: The load-then-store pattern has a benign TOCTOU race: multiple
181        // threads may concurrently see chain_id == 0 and all store the same
182        // value from the ledger info response. This is safe because they always
183        // store the identical chain_id value returned by the node.
184        if self.chain_id.load(Ordering::Relaxed) == 0 && info.chain_id > 0 {
185            self.chain_id.store(info.chain_id, Ordering::Relaxed);
186        }
187
188        Ok(info)
189    }
190
191    /// Returns the current chain ID.
192    ///
193    /// For networks with a fixed, well-known chain ID (mainnet = 1,
194    /// testnet = 2, local = 4), this returns it immediately. For **devnet** and
195    /// **custom** networks the chain ID is not fixed, so this returns
196    /// `ChainId(0)` until it is resolved via [`ensure_chain_id`](Self::ensure_chain_id)
197    /// or any method that makes a request to the node (e.g., [`build_transaction`](Self::build_transaction),
198    /// [`ledger_info`](Self::ledger_info)). Devnet is included here because it is
199    /// wiped and re-genesised regularly, so its chain ID changes over time and
200    /// must be discovered from the node rather than hardcoded.
201    ///
202    pub fn chain_id(&self) -> ChainId {
203        ChainId::new(self.chain_id.load(Ordering::Relaxed))
204    }
205
206    /// Resolves the chain ID from the node if it is unknown.
207    ///
208    /// For networks with a fixed chain ID (mainnet, testnet, local), this
209    /// returns it immediately without making a network request. For devnet and
210    /// custom networks (chain ID 0 until resolved), this fetches the ledger
211    /// info from the node to discover the actual chain ID and caches it for
212    /// future use.
213    ///
214    /// This is called automatically by [`build_transaction`](Self::build_transaction)
215    /// and other transaction methods, so you typically don't need to call it
216    /// directly unless you need the chain ID before building a transaction.
217    ///
218    /// # Errors
219    ///
220    /// Returns an error if the HTTP request to fetch ledger info fails.
221    ///
222    pub async fn ensure_chain_id(&self) -> AptosResult<ChainId> {
223        let id = self.chain_id.load(Ordering::Relaxed);
224        if id > 0 {
225            return Ok(ChainId::new(id));
226        }
227        // Chain ID is unknown; fetch from node
228        let response = self.fullnode.get_ledger_info().await?;
229        let info = response.into_inner();
230        self.chain_id.store(info.chain_id, Ordering::Relaxed);
231        Ok(ChainId::new(info.chain_id))
232    }
233
234    // === Account ===
235
236    /// Gets the sequence number for an account.
237    ///
238    /// # Errors
239    ///
240    /// Returns an error if the HTTP request fails, the API returns an error status code
241    /// (e.g., account not found 404), or the response cannot be parsed.
242    pub async fn get_sequence_number(&self, address: AccountAddress) -> AptosResult<u64> {
243        self.fullnode.get_sequence_number(address).await
244    }
245
246    /// Gets the APT balance for an account.
247    ///
248    /// # Errors
249    ///
250    /// Returns an error if the HTTP request fails, the API returns an error status code,
251    /// or the response cannot be parsed.
252    pub async fn get_balance(&self, address: AccountAddress) -> AptosResult<u64> {
253        self.fullnode.get_account_balance(address).await
254    }
255
256    /// Checks if an account exists.
257    ///
258    /// # Errors
259    ///
260    /// Returns an error if the HTTP request fails or the API returns an error status code
261    /// other than 404 (not found). A 404 error is handled gracefully and returns `Ok(false)`.
262    pub async fn account_exists(&self, address: AccountAddress) -> AptosResult<bool> {
263        match self.fullnode.get_account(address).await {
264            Ok(_) => Ok(true),
265            Err(AptosError::Api {
266                status_code: 404, ..
267            }) => Ok(false),
268            Err(e) => Err(e),
269        }
270    }
271
272    // === Transactions ===
273
274    /// Builds a transaction for the given account.
275    ///
276    /// This automatically fetches the sequence number and gas price.
277    ///
278    /// # Errors
279    ///
280    /// Returns an error if fetching the sequence number fails, fetching the gas price fails,
281    /// or if the transaction builder fails to construct a valid transaction (e.g., missing
282    /// required fields).
283    pub async fn build_transaction<A: Account>(
284        &self,
285        sender: &A,
286        payload: TransactionPayload,
287    ) -> AptosResult<RawTransaction> {
288        // Fetch sequence number, gas price, and chain ID in parallel
289        let (sequence_number, gas_estimation, chain_id) = tokio::join!(
290            self.get_sequence_number(sender.address()),
291            self.fullnode.estimate_gas_price(),
292            self.ensure_chain_id()
293        );
294        let sequence_number = sequence_number?;
295        let gas_estimation = gas_estimation?;
296        let chain_id = chain_id?;
297
298        TransactionBuilder::new()
299            .sender(sender.address())
300            .sequence_number(sequence_number)
301            .payload(payload)
302            .gas_unit_price(gas_estimation.data.recommended())
303            .chain_id(chain_id)
304            .expiration_from_now(600)
305            .build()
306    }
307
308    /// Signs and submits a transaction.
309    ///
310    /// # Errors
311    ///
312    /// Returns an error if building the transaction fails, signing fails (e.g., invalid key),
313    /// the transaction cannot be serialized to BCS, the HTTP request fails, or the API returns
314    /// an error status code.
315    #[cfg(feature = "ed25519")]
316    pub async fn sign_and_submit<A: Account>(
317        &self,
318        account: &A,
319        payload: TransactionPayload,
320    ) -> AptosResult<AptosResponse<PendingTransaction>> {
321        let raw_txn = self.build_transaction(account, payload).await?;
322        let signed = crate::transaction::builder::sign_transaction(&raw_txn, account)?;
323        self.fullnode.submit_transaction(&signed).await
324    }
325
326    /// Signs, submits, and waits for a transaction to complete.
327    ///
328    /// # Errors
329    ///
330    /// Returns an error if building the transaction fails, signing fails, submission fails,
331    /// the transaction times out waiting for commitment, the transaction execution fails,
332    /// or any HTTP/API errors occur.
333    #[cfg(feature = "ed25519")]
334    pub async fn sign_submit_and_wait<A: Account>(
335        &self,
336        account: &A,
337        payload: TransactionPayload,
338        timeout: Option<Duration>,
339    ) -> AptosResult<AptosResponse<serde_json::Value>> {
340        let raw_txn = self.build_transaction(account, payload).await?;
341        let signed = crate::transaction::builder::sign_transaction(&raw_txn, account)?;
342        self.fullnode.submit_and_wait(&signed, timeout).await
343    }
344
345    /// Builds an orderless transaction for `sender`.
346    ///
347    /// Orderless transactions use a random replay-protection **nonce** instead
348    /// of the account's sequence number, so they can be submitted in any order
349    /// (or concurrently) within a short expiration window. On the wire the chain
350    /// encodes this by setting the transaction's sequence number to
351    /// [`u64::MAX`] and carrying the nonce in a
352    /// [`TransactionPayload::Payload`](crate::transaction::TransactionPayload::Payload)
353    /// (see [`TransactionPayload::into_orderless`](crate::transaction::TransactionPayload::into_orderless)).
354    ///
355    /// `payload` must be an entry-function or script payload. Pass `nonce` to
356    /// reuse a specific nonce or `None` to generate a fresh random one. The
357    /// expiration defaults to 60 seconds, the recommended short window for
358    /// nonce-based replay protection.
359    ///
360    /// # Errors
361    ///
362    /// Returns an error if fetching the gas price or chain ID fails, if
363    /// `payload` is not an entry-function or script payload, or if the
364    /// transaction builder fails.
365    pub async fn build_orderless_transaction<A: Account>(
366        &self,
367        sender: &A,
368        payload: TransactionPayload,
369        nonce: Option<u64>,
370    ) -> AptosResult<RawTransaction> {
371        // Orderless transactions do not consume a sequence number, so only the
372        // gas price and chain ID need to be fetched.
373        let (gas_estimation, chain_id) =
374            tokio::join!(self.fullnode.estimate_gas_price(), self.ensure_chain_id());
375        let gas_estimation = gas_estimation?;
376        let chain_id = chain_id?;
377
378        let nonce = nonce.unwrap_or_else(|| rand::RngCore::next_u64(&mut rand::rngs::OsRng));
379        let orderless_payload = payload.into_orderless(nonce)?;
380
381        TransactionBuilder::new()
382            .sender(sender.address())
383            .sequence_number(u64::MAX)
384            .payload(orderless_payload)
385            .gas_unit_price(gas_estimation.data.recommended())
386            .chain_id(chain_id)
387            .expiration_from_now(60)
388            .build()
389    }
390
391    /// Builds, signs, and submits an orderless transaction.
392    ///
393    /// See [`build_orderless_transaction`](Self::build_orderless_transaction)
394    /// for the meaning of `payload` and `nonce`.
395    ///
396    /// # Errors
397    ///
398    /// Returns an error if building the transaction fails, signing fails, the
399    /// transaction cannot be serialized to BCS, the HTTP request fails, or the
400    /// API returns an error status code.
401    #[cfg(feature = "ed25519")]
402    pub async fn sign_and_submit_orderless<A: Account>(
403        &self,
404        account: &A,
405        payload: TransactionPayload,
406        nonce: Option<u64>,
407    ) -> AptosResult<AptosResponse<PendingTransaction>> {
408        let raw_txn = self
409            .build_orderless_transaction(account, payload, nonce)
410            .await?;
411        let signed = crate::transaction::builder::sign_transaction(&raw_txn, account)?;
412        self.fullnode.submit_transaction(&signed).await
413    }
414
415    /// Builds, signs, submits, and waits for an orderless transaction.
416    ///
417    /// See [`build_orderless_transaction`](Self::build_orderless_transaction)
418    /// for the meaning of `payload` and `nonce`.
419    ///
420    /// # Errors
421    ///
422    /// Returns an error if building the transaction fails, signing fails,
423    /// submission fails, the transaction times out waiting for commitment, the
424    /// transaction execution fails, or any HTTP/API errors occur.
425    #[cfg(feature = "ed25519")]
426    pub async fn sign_submit_and_wait_orderless<A: Account>(
427        &self,
428        account: &A,
429        payload: TransactionPayload,
430        nonce: Option<u64>,
431        timeout: Option<Duration>,
432    ) -> AptosResult<AptosResponse<serde_json::Value>> {
433        let raw_txn = self
434            .build_orderless_transaction(account, payload, nonce)
435            .await?;
436        let signed = crate::transaction::builder::sign_transaction(&raw_txn, account)?;
437        self.fullnode.submit_and_wait(&signed, timeout).await
438    }
439
440    /// Submits a pre-signed transaction.
441    ///
442    /// # Errors
443    ///
444    /// Returns an error if the transaction cannot be serialized to BCS, the HTTP request fails,
445    /// or the API returns an error status code.
446    pub async fn submit_transaction(
447        &self,
448        signed_txn: &SignedTransaction,
449    ) -> AptosResult<AptosResponse<PendingTransaction>> {
450        self.fullnode.submit_transaction(signed_txn).await
451    }
452
453    /// Submits and waits for a pre-signed transaction.
454    ///
455    /// # Errors
456    ///
457    /// Returns an error if transaction submission fails, the transaction times out waiting
458    /// for commitment, the transaction execution fails (`vm_status` indicates failure),
459    /// or any HTTP/API errors occur.
460    pub async fn submit_and_wait(
461        &self,
462        signed_txn: &SignedTransaction,
463        timeout: Option<Duration>,
464    ) -> AptosResult<AptosResponse<serde_json::Value>> {
465        self.fullnode.submit_and_wait(signed_txn, timeout).await
466    }
467
468    /// Simulates a transaction.
469    ///
470    /// # Errors
471    ///
472    /// Returns an error if the transaction cannot be serialized to BCS, the HTTP request fails,
473    /// the API returns an error status code, or the response cannot be parsed as JSON.
474    ///
475    /// Note: [`FullnodeClient::simulate_transaction`] rewrites authenticators for the simulate
476    /// endpoint before sending; callers may pass a normally signed transaction.
477    pub async fn simulate_transaction(
478        &self,
479        signed_txn: &SignedTransaction,
480    ) -> AptosResult<AptosResponse<Vec<serde_json::Value>>> {
481        self.fullnode.simulate_transaction(signed_txn).await
482    }
483
484    /// Simulates a transaction and returns a parsed result.
485    ///
486    /// This method provides a more ergonomic way to simulate transactions
487    /// with detailed result parsing.
488    ///
489    /// # Example
490    ///
491    /// ```rust,ignore
492    /// let result = aptos.simulate(&account, payload).await?;
493    /// if result.success() {
494    ///     println!("Gas estimate: {}", result.gas_used());
495    /// } else {
496    ///     println!("Would fail: {}", result.error_message().unwrap_or_default());
497    /// }
498    /// ```
499    ///
500    /// # Errors
501    ///
502    /// Returns an error if building the transaction fails, signing fails, simulation fails,
503    /// or the simulation response cannot be parsed.
504    #[cfg(feature = "ed25519")]
505    pub async fn simulate<A: Account>(
506        &self,
507        account: &A,
508        payload: TransactionPayload,
509    ) -> AptosResult<crate::transaction::SimulationResult> {
510        use crate::transaction::SignedTransaction;
511
512        let raw_txn = self.build_transaction(account, payload).await?;
513
514        // The simulation endpoint *rejects* valid signatures
515        // (it returns 400 "Simulated transactions must not have a valid
516        // signature") because its job is gas estimation, not actual
517        // execution. We attach the account's real public key (the simulator
518        // still uses it to walk the signing-message hash) and a zeroed
519        // signature of the appropriate shape for the account's
520        // signature scheme.
521        let auth = build_zero_signed_authenticator(account)?;
522        let signed = SignedTransaction::new(raw_txn, auth);
523
524        let response = self.fullnode.simulate_transaction(&signed).await?;
525        crate::transaction::SimulationResult::from_response(response.into_inner())
526    }
527
528    /// Simulates a transaction with a pre-built signed transaction.
529    ///
530    /// For gas estimation options (e.g. `estimate_gas_unit_price`), use
531    /// [`simulate_signed_with_options`](Self::simulate_signed_with_options).
532    ///
533    /// Authenticators are rewritten client-side before the HTTP request (see
534    /// [`SignedTransaction::for_simulate_endpoint`]); you do not need to swap in
535    /// [`AccountAuthenticator::NoAccountAuthenticator`](crate::transaction::authenticator::AccountAuthenticator::no_account_authenticator)
536    /// manually to avoid the fullnode's "must not have a valid signature" error.
537    ///
538    /// # Errors
539    ///
540    /// Returns an error if simulation fails or the simulation response cannot be parsed.
541    pub async fn simulate_signed(
542        &self,
543        signed_txn: &SignedTransaction,
544    ) -> AptosResult<SimulationResult> {
545        let response = self.fullnode.simulate_transaction(signed_txn).await?;
546        SimulationResult::from_response(response.into_inner())
547    }
548
549    /// Simulates a signed transaction with query options for the node.
550    ///
551    /// Use this when you need [`SimulateQueryOptions`] (e.g. `estimate_gas_unit_price`,
552    /// `estimate_max_gas_amount`). For the common case without options, use
553    /// [`simulate_signed`](Self::simulate_signed) instead.
554    ///
555    /// Like [`simulate_signed`](Self::simulate_signed), this applies
556    /// [`SignedTransaction::for_simulate_endpoint`] before calling the fullnode.
557    ///
558    /// # Errors
559    ///
560    /// Returns an error if simulation fails or the simulation response cannot be parsed.
561    pub async fn simulate_signed_with_options(
562        &self,
563        signed_txn: &SignedTransaction,
564        options: SimulateQueryOptions,
565    ) -> AptosResult<SimulationResult> {
566        let response = self
567            .fullnode
568            .simulate_transaction_with_options(signed_txn, Some(options))
569            .await?;
570        SimulationResult::from_response(response.into_inner())
571    }
572
573    /// Simulates a multi-agent transaction without requiring real signatures.
574    ///
575    /// Builds a simulation-only signed transaction (using
576    /// [`crate::transaction::authenticator::AccountAuthenticator::NoAccountAuthenticator`]) and sends it to the
577    /// simulate endpoint. Use this to check outcome and gas before collecting
578    /// signatures from sender and secondary signers.
579    ///
580    /// # Example
581    ///
582    /// ```rust,ignore
583    /// let multi_agent = MultiAgentRawTransaction::new(raw_txn, secondary_addresses);
584    /// let result = aptos.simulate_multi_agent(&multi_agent, None).await?;
585    /// if result.success() {
586    ///     println!("Gas: {}", result.gas_used());
587    /// }
588    /// ```
589    ///
590    /// # Errors
591    ///
592    /// Returns an error if the simulate request fails or the response cannot be parsed.
593    pub async fn simulate_multi_agent(
594        &self,
595        multi_agent: &MultiAgentRawTransaction,
596        options: impl Into<Option<SimulateQueryOptions>>,
597    ) -> AptosResult<SimulationResult> {
598        let signed = build_simulation_signed_multi_agent(multi_agent);
599        match options.into() {
600            None => self.simulate_signed(&signed).await,
601            Some(opts) => self.simulate_signed_with_options(&signed, opts).await,
602        }
603    }
604
605    /// Simulates a fee-payer (sponsored) transaction without requiring real signatures.
606    ///
607    /// Builds a simulation-only signed transaction (using
608    /// [`crate::transaction::authenticator::AccountAuthenticator::NoAccountAuthenticator`]) and sends it to the
609    /// simulate endpoint. Use this to check outcome and gas before collecting
610    /// signatures from sender, secondary signers, and fee payer.
611    ///
612    /// # Example
613    ///
614    /// ```rust,ignore
615    /// let fee_payer_txn = FeePayerRawTransaction::new_simple(raw_txn, fee_payer_address);
616    /// let result = aptos.simulate_fee_payer(&fee_payer_txn, None).await?;
617    /// if result.success() {
618    ///     println!("Gas: {}", result.gas_used());
619    /// }
620    /// ```
621    ///
622    /// # Errors
623    ///
624    /// Returns an error if the simulate request fails or the response cannot be parsed.
625    pub async fn simulate_fee_payer(
626        &self,
627        fee_payer_txn: &FeePayerRawTransaction,
628        options: impl Into<Option<SimulateQueryOptions>>,
629    ) -> AptosResult<SimulationResult> {
630        let signed = build_simulation_signed_fee_payer(fee_payer_txn);
631        match options.into() {
632            None => self.simulate_signed(&signed).await,
633            Some(opts) => self.simulate_signed_with_options(&signed, opts).await,
634        }
635    }
636
637    /// Estimates gas for a transaction by simulating it.
638    ///
639    /// Returns the estimated gas usage with a 20% safety margin.
640    ///
641    /// # Example
642    ///
643    /// ```rust,ignore
644    /// let gas = aptos.estimate_gas(&account, payload).await?;
645    /// println!("Estimated gas: {}", gas);
646    /// ```
647    ///
648    /// # Errors
649    ///
650    /// Returns an error if simulation fails or if the simulation indicates the transaction
651    /// would fail (returns [`AptosError::SimulationFailed`]).
652    #[cfg(feature = "ed25519")]
653    pub async fn estimate_gas<A: Account>(
654        &self,
655        account: &A,
656        payload: TransactionPayload,
657    ) -> AptosResult<u64> {
658        let result = self.simulate(account, payload).await?;
659        if result.success() {
660            Ok(result.safe_gas_estimate())
661        } else {
662            Err(AptosError::SimulationFailed(
663                result
664                    .error_message()
665                    .unwrap_or_else(|| result.vm_status().to_string()),
666            ))
667        }
668    }
669
670    /// Simulates and submits a transaction if successful.
671    ///
672    /// This is a "dry run" approach that first simulates the transaction
673    /// to verify it will succeed before actually submitting it.
674    ///
675    /// # Example
676    ///
677    /// ```rust,ignore
678    /// let result = aptos.simulate_and_submit(&account, payload).await?;
679    /// println!("Transaction submitted: {}", result.hash);
680    /// ```
681    ///
682    /// # Errors
683    ///
684    /// Returns an error if building the transaction fails, signing fails, simulation fails,
685    /// the simulation indicates the transaction would fail (returns [`AptosError::SimulationFailed`]),
686    /// or transaction submission fails.
687    #[cfg(feature = "ed25519")]
688    pub async fn simulate_and_submit<A: Account>(
689        &self,
690        account: &A,
691        payload: TransactionPayload,
692    ) -> AptosResult<AptosResponse<PendingTransaction>> {
693        // First simulate with an intentionally invalid (zeroed) signature;
694        // the node rejects real signatures on the simulate endpoint.
695        let raw_txn = self.build_transaction(account, payload).await?;
696        let sim_auth = build_zero_signed_authenticator(account)?;
697        let sim_signed = SignedTransaction::new(raw_txn.clone(), sim_auth);
698        let sim_response = self.fullnode.simulate_transaction(&sim_signed).await?;
699        let sim_result =
700            crate::transaction::SimulationResult::from_response(sim_response.into_inner())?;
701
702        if sim_result.failed() {
703            return Err(AptosError::SimulationFailed(
704                sim_result
705                    .error_message()
706                    .unwrap_or_else(|| sim_result.vm_status().to_string()),
707            ));
708        }
709
710        let signed = crate::transaction::builder::sign_transaction(&raw_txn, account)?;
711        self.fullnode.submit_transaction(&signed).await
712    }
713
714    /// Simulates, submits, and waits for a transaction.
715    ///
716    /// Like `simulate_and_submit` but also waits for the transaction to complete.
717    ///
718    /// # Errors
719    ///
720    /// Returns an error if building the transaction fails, signing fails, simulation fails,
721    /// the simulation indicates the transaction would fail (returns [`AptosError::SimulationFailed`]),
722    /// submission fails, the transaction times out waiting for commitment, or the transaction
723    /// execution fails.
724    #[cfg(feature = "ed25519")]
725    pub async fn simulate_submit_and_wait<A: Account>(
726        &self,
727        account: &A,
728        payload: TransactionPayload,
729        timeout: Option<Duration>,
730    ) -> AptosResult<AptosResponse<serde_json::Value>> {
731        // First simulate with an intentionally invalid (zeroed) signature;
732        // the node rejects real signatures on the simulate endpoint.
733        let raw_txn = self.build_transaction(account, payload).await?;
734        let sim_auth = build_zero_signed_authenticator(account)?;
735        let sim_signed = SignedTransaction::new(raw_txn.clone(), sim_auth);
736        let sim_response = self.fullnode.simulate_transaction(&sim_signed).await?;
737        let sim_result =
738            crate::transaction::SimulationResult::from_response(sim_response.into_inner())?;
739
740        if sim_result.failed() {
741            return Err(AptosError::SimulationFailed(
742                sim_result
743                    .error_message()
744                    .unwrap_or_else(|| sim_result.vm_status().to_string()),
745            ));
746        }
747
748        let signed = crate::transaction::builder::sign_transaction(&raw_txn, account)?;
749        self.fullnode.submit_and_wait(&signed, timeout).await
750    }
751
752    // === Transfers ===
753
754    /// Transfers APT from one account to another.
755    ///
756    /// # Errors
757    ///
758    /// Returns an error if building the transfer payload fails (e.g., invalid address),
759    /// signing fails, submission fails, the transaction times out, or the transaction
760    /// execution fails.
761    #[cfg(feature = "ed25519")]
762    pub async fn transfer_apt<A: Account>(
763        &self,
764        sender: &A,
765        recipient: AccountAddress,
766        amount: u64,
767    ) -> AptosResult<AptosResponse<serde_json::Value>> {
768        let payload = EntryFunction::apt_transfer(recipient, amount)?;
769        self.sign_submit_and_wait(sender, payload.into(), None)
770            .await
771    }
772
773    /// Transfers a coin from one account to another.
774    ///
775    /// # Errors
776    ///
777    /// Returns an error if building the transfer payload fails (e.g., invalid type tag or address),
778    /// signing fails, submission fails, the transaction times out, or the transaction
779    /// execution fails.
780    #[cfg(feature = "ed25519")]
781    pub async fn transfer_coin<A: Account>(
782        &self,
783        sender: &A,
784        recipient: AccountAddress,
785        coin_type: TypeTag,
786        amount: u64,
787    ) -> AptosResult<AptosResponse<serde_json::Value>> {
788        let payload = EntryFunction::coin_transfer(coin_type, recipient, amount)?;
789        self.sign_submit_and_wait(sender, payload.into(), None)
790            .await
791    }
792
793    /// Transfers a fungible asset (FA standard) from one account to another.
794    ///
795    /// Uses `0x1::primary_fungible_store::transfer`, moving `amount` units of
796    /// the asset identified by `metadata` (the address of its
797    /// `0x1::fungible_asset::Metadata` object) between the sender's and
798    /// recipient's primary stores, creating the recipient's store if needed.
799    /// This is the current-standard counterpart to [`transfer_coin`](Self::transfer_coin)
800    /// and matches the TypeScript SDK's `transferFungibleAsset`.
801    ///
802    /// # Errors
803    ///
804    /// Returns an error if building the transfer payload fails (e.g. invalid
805    /// address), signing fails, submission fails, the transaction times out, or
806    /// the transaction execution fails.
807    #[cfg(feature = "ed25519")]
808    pub async fn transfer_fungible_asset<A: Account>(
809        &self,
810        sender: &A,
811        metadata: AccountAddress,
812        recipient: AccountAddress,
813        amount: u64,
814    ) -> AptosResult<AptosResponse<serde_json::Value>> {
815        let payload = crate::transaction::InputEntryFunctionData::transfer_fungible_asset(
816            metadata, recipient, amount,
817        )?;
818        self.sign_submit_and_wait(sender, payload, None).await
819    }
820
821    // === Objects & Digital Assets ===
822
823    /// Transfers ownership of an object to another address
824    /// (`0x1::object::transfer_call`).
825    ///
826    /// # Errors
827    ///
828    /// Returns an error if building the payload fails, signing fails, submission
829    /// fails, the transaction times out, or execution fails.
830    #[cfg(feature = "ed25519")]
831    pub async fn transfer_object<A: Account>(
832        &self,
833        owner: &A,
834        object: AccountAddress,
835        to: AccountAddress,
836    ) -> AptosResult<AptosResponse<serde_json::Value>> {
837        let payload = crate::transaction::InputEntryFunctionData::transfer_object(object, to)?;
838        self.sign_submit_and_wait(owner, payload, None).await
839    }
840
841    /// Transfers a digital asset (NFT) to another address
842    /// (`0x1::object::transfer` with the `0x4::token::Token` type), matching the
843    /// TypeScript SDK's `transferDigitalAsset`.
844    ///
845    /// # Errors
846    ///
847    /// Returns an error if building the payload fails, signing fails, submission
848    /// fails, the transaction times out, or execution fails.
849    #[cfg(feature = "ed25519")]
850    pub async fn transfer_digital_asset<A: Account>(
851        &self,
852        owner: &A,
853        token: AccountAddress,
854        to: AccountAddress,
855    ) -> AptosResult<AptosResponse<serde_json::Value>> {
856        let payload =
857            crate::transaction::InputEntryFunctionData::transfer_digital_asset(token, to)?;
858        self.sign_submit_and_wait(owner, payload, None).await
859    }
860
861    // === Authentication key rotation ===
862
863    /// Rotates `current`'s authentication key to `new_account`'s key.
864    ///
865    /// Builds and signs a [`RotationProofChallenge`](crate::account::RotationProofChallenge)
866    /// with both keys (proving control of the current account and ownership of
867    /// the new key), submits `0x1::account::rotate_authentication_key`, and waits
868    /// for it to commit. The account address is unchanged; only its
869    /// authentication key changes, so `new_account` must sign subsequent
870    /// transactions from this address.
871    ///
872    /// The transaction is sent (and signed) by `current`, using the same
873    /// sequence number embedded in the challenge.
874    ///
875    /// # Errors
876    ///
877    /// Returns an error if fetching the sequence number, gas price, or chain ID
878    /// fails; if either challenge signature cannot be produced; if the payload or
879    /// transaction cannot be built; or if submission/execution fails.
880    #[cfg(feature = "ed25519")]
881    pub async fn rotate_auth_key<A: Account, B: Account>(
882        &self,
883        current: &A,
884        new_account: &B,
885        timeout: Option<Duration>,
886    ) -> AptosResult<AptosResponse<serde_json::Value>> {
887        // The challenge's sequence number MUST match the transaction's, so fetch
888        // it once and reuse it for both.
889        let sequence_number = self.get_sequence_number(current.address()).await?;
890        let payload =
891            crate::account::build_rotate_auth_key_payload(current, new_account, sequence_number)?;
892
893        let (gas_estimation, chain_id) =
894            tokio::join!(self.fullnode.estimate_gas_price(), self.ensure_chain_id());
895        let gas_estimation = gas_estimation?;
896        let chain_id = chain_id?;
897
898        let raw_txn = TransactionBuilder::new()
899            .sender(current.address())
900            .sequence_number(sequence_number)
901            .payload(payload)
902            .gas_unit_price(gas_estimation.data.recommended())
903            .chain_id(chain_id)
904            .expiration_from_now(600)
905            .build()?;
906        let signed = crate::transaction::builder::sign_transaction(&raw_txn, current)?;
907        self.fullnode.submit_and_wait(&signed, timeout).await
908    }
909
910    // === Tables ===
911
912    /// Reads an item from a Move table by its key.
913    ///
914    /// Thin wrapper over [`FullnodeClient::get_table_item`] returning the stored
915    /// value directly. See that method for the meaning of `handle`, `key_type`,
916    /// `value_type`, and `key`.
917    ///
918    /// # Errors
919    ///
920    /// Returns an error if the request fails, the API returns an error status
921    /// code (including 404 when the key is absent), or the response cannot be
922    /// parsed as JSON.
923    pub async fn get_table_item(
924        &self,
925        handle: &str,
926        key_type: &str,
927        value_type: &str,
928        key: serde_json::Value,
929    ) -> AptosResult<serde_json::Value> {
930        let response = self
931            .fullnode
932            .get_table_item(handle, key_type, value_type, key)
933            .await?;
934        Ok(response.into_inner())
935    }
936
937    // === View Functions ===
938
939    /// Calls a view function using JSON encoding.
940    ///
941    /// For lossless serialization of large integers, use [`view_bcs`](Self::view_bcs) instead.
942    ///
943    /// # Errors
944    ///
945    /// Returns an error if the HTTP request fails, the API returns an error status code,
946    /// or the response cannot be parsed as JSON.
947    pub async fn view(
948        &self,
949        function: &str,
950        type_args: Vec<String>,
951        args: Vec<serde_json::Value>,
952    ) -> AptosResult<Vec<serde_json::Value>> {
953        let response = self.fullnode.view(function, type_args, args).await?;
954        Ok(response.into_inner())
955    }
956
957    /// Calls a view function using BCS encoding for both inputs and outputs.
958    ///
959    /// This method provides lossless serialization by using BCS (Binary Canonical Serialization)
960    /// instead of JSON, which is important for large integers (u128, u256) and other types
961    /// where JSON can lose precision.
962    ///
963    /// # Type Parameter
964    ///
965    /// * `T` - The expected return type. Must implement `serde::de::DeserializeOwned`.
966    ///
967    /// # Arguments
968    ///
969    /// * `function` - The fully qualified function name (e.g., `0x1::coin::balance`)
970    /// * `type_args` - Type arguments as strings (e.g., `0x1::aptos_coin::AptosCoin`)
971    /// * `args` - Pre-serialized BCS arguments as byte vectors
972    ///
973    /// # Example
974    ///
975    /// ```rust,ignore
976    /// use aptos_sdk::{Aptos, AptosConfig, AccountAddress};
977    ///
978    /// let aptos = Aptos::new(AptosConfig::testnet())?;
979    /// let owner = AccountAddress::from_hex("0x1")?;
980    ///
981    /// // BCS-encode the argument
982    /// let args = vec![aptos_bcs::to_bytes(&owner)?];
983    ///
984    /// // Call view function with typed return
985    /// let balance: u64 = aptos.view_bcs(
986    ///     "0x1::coin::balance",
987    ///     vec!["0x1::aptos_coin::AptosCoin".to_string()],
988    ///     args,
989    /// ).await?;
990    /// ```
991    ///
992    /// # Errors
993    ///
994    /// Returns an error if the HTTP request fails, the API returns an error status code,
995    /// or the BCS deserialization fails.
996    pub async fn view_bcs<T: serde::de::DeserializeOwned>(
997        &self,
998        function: &str,
999        type_args: Vec<String>,
1000        args: Vec<Vec<u8>>,
1001    ) -> AptosResult<T> {
1002        let response = self.fullnode.view_bcs(function, type_args, args).await?;
1003        let bytes = response.into_inner();
1004        aptos_bcs::from_bytes(&bytes).map_err(|e| AptosError::Bcs(e.to_string()))
1005    }
1006
1007    /// Calls a view function with BCS inputs and returns raw BCS bytes.
1008    ///
1009    /// Use this when you need to manually deserialize the response or when
1010    /// the return type is complex or dynamic.
1011    ///
1012    /// # Errors
1013    ///
1014    /// Returns an error if the HTTP request fails or the API returns an error status code.
1015    pub async fn view_bcs_raw(
1016        &self,
1017        function: &str,
1018        type_args: Vec<String>,
1019        args: Vec<Vec<u8>>,
1020    ) -> AptosResult<Vec<u8>> {
1021        let response = self.fullnode.view_bcs(function, type_args, args).await?;
1022        Ok(response.into_inner())
1023    }
1024
1025    // === Faucet ===
1026
1027    /// Funds an account using the faucet.
1028    ///
1029    /// This method waits for the faucet transactions to be confirmed before returning.
1030    ///
1031    /// Some Aptos faucets (notably devnet) cap the amount delivered per request to a
1032    /// fixed value (typically 1 APT / 100,000,000 octas) regardless of the requested
1033    /// amount. This method automatically issues additional faucet requests, up to a
1034    /// reasonable limit, until the account's balance has been topped up by at least
1035    /// `amount` octas. The returned vector contains the transaction hashes from every
1036    /// underlying faucet call.
1037    ///
1038    /// # Errors
1039    ///
1040    /// Returns an error if the faucet feature is not enabled, the faucet request fails
1041    /// (e.g., rate limiting 429, server error 500), waiting for transaction confirmation
1042    /// times out, any HTTP/API errors occur, or if the requested amount cannot be
1043    /// delivered after several attempts.
1044    #[cfg(feature = "faucet")]
1045    pub async fn fund_account(
1046        &self,
1047        address: AccountAddress,
1048        amount: u64,
1049    ) -> AptosResult<Vec<String>> {
1050        // Hard-cap on how many faucet calls we'll make to satisfy a single
1051        // `fund_account` request. This prevents unbounded faucet usage if the
1052        // faucet is silently dropping requests.
1053        const MAX_FAUCET_ATTEMPTS: u32 = 16;
1054
1055        let faucet = self
1056            .faucet
1057            .as_ref()
1058            .ok_or_else(|| AptosError::FeatureNotEnabled("faucet".into()))?;
1059
1060        // Snapshot the starting balance (0 if the account doesn't yet exist).
1061        let starting_balance = self.get_balance(address).await.unwrap_or(0);
1062        let target_balance = starting_balance.saturating_add(amount);
1063
1064        let mut all_hashes: Vec<String> = Vec::new();
1065        let mut current_balance = starting_balance;
1066        let mut attempts = 0u32;
1067
1068        while current_balance < target_balance && attempts < MAX_FAUCET_ATTEMPTS {
1069            attempts += 1;
1070            let still_needed = target_balance.saturating_sub(current_balance);
1071            let txn_hashes = faucet.fund(address, still_needed).await?;
1072
1073            // Parse hashes for waiting on confirmation.
1074            let hashes: Vec<HashValue> = txn_hashes
1075                .iter()
1076                .filter_map(|hash_str| {
1077                    let hash_str_clean = hash_str.strip_prefix("0x").unwrap_or(hash_str);
1078                    HashValue::from_hex(hash_str_clean).ok()
1079                })
1080                .collect();
1081
1082            // Wait for all faucet transactions in this batch to be confirmed in parallel.
1083            let wait_futures: Vec<_> = hashes
1084                .iter()
1085                .map(|hash| {
1086                    self.fullnode
1087                        .wait_for_transaction(hash, Some(Duration::from_mins(1)))
1088                })
1089                .collect();
1090            let results = futures::future::join_all(wait_futures).await;
1091            for result in results {
1092                result?;
1093            }
1094
1095            all_hashes.extend(txn_hashes);
1096
1097            // Re-read balance; if it didn't move, the faucet isn't going to help.
1098            let new_balance = self.get_balance(address).await.unwrap_or(current_balance);
1099            if new_balance <= current_balance {
1100                return Err(AptosError::api(
1101                    400,
1102                    format!(
1103                        "faucet returned successful response but balance did not increase (\
1104                         attempts={attempts}, balance={new_balance}, requested top-up={amount})"
1105                    ),
1106                ));
1107            }
1108            current_balance = new_balance;
1109        }
1110
1111        if current_balance < target_balance {
1112            return Err(AptosError::api(
1113                429,
1114                format!(
1115                    "faucet could not deliver {amount} octas in {attempts} attempts \
1116                     (starting balance={starting_balance}, current balance={current_balance})"
1117                ),
1118            ));
1119        }
1120
1121        Ok(all_hashes)
1122    }
1123
1124    #[cfg(all(feature = "faucet", feature = "ed25519"))]
1125    /// Creates a funded account.
1126    ///
1127    /// # Errors
1128    ///
1129    /// Returns an error if funding the account fails (see [`Self::fund_account`] for details).
1130    pub async fn create_funded_account(
1131        &self,
1132        amount: u64,
1133    ) -> AptosResult<crate::account::Ed25519Account> {
1134        let account = crate::account::Ed25519Account::generate();
1135        self.fund_account(account.address(), amount).await?;
1136        Ok(account)
1137    }
1138
1139    // === Transaction Batching ===
1140
1141    /// Returns a batch operations helper for submitting multiple transactions.
1142    ///
1143    /// # Example
1144    ///
1145    /// ```rust,ignore
1146    /// let aptos = Aptos::testnet()?;
1147    ///
1148    /// // Build and submit batch of transfers
1149    /// let payloads = vec![
1150    ///     EntryFunction::apt_transfer(addr1, 1000)?.into(),
1151    ///     EntryFunction::apt_transfer(addr2, 2000)?.into(),
1152    ///     EntryFunction::apt_transfer(addr3, 3000)?.into(),
1153    /// ];
1154    ///
1155    /// let results = aptos.batch().submit_and_wait(&sender, payloads, None).await?;
1156    /// ```
1157    pub fn batch(&self) -> crate::transaction::BatchOperations<'_> {
1158        crate::transaction::BatchOperations::new(&self.fullnode, &self.chain_id)
1159    }
1160
1161    /// Submits multiple transactions in parallel.
1162    ///
1163    /// This is a convenience method that builds, signs, and submits
1164    /// multiple transactions at once.
1165    ///
1166    /// # Arguments
1167    ///
1168    /// * `account` - The account to sign with
1169    /// * `payloads` - The transaction payloads to submit
1170    ///
1171    /// # Returns
1172    ///
1173    /// Results for each transaction in the batch.
1174    ///
1175    /// # Errors
1176    ///
1177    /// Returns an error if building any transaction fails, signing fails, or submission fails
1178    /// for any transaction in the batch.
1179    #[cfg(feature = "ed25519")]
1180    pub async fn submit_batch<A: Account>(
1181        &self,
1182        account: &A,
1183        payloads: Vec<TransactionPayload>,
1184    ) -> AptosResult<Vec<crate::transaction::BatchTransactionResult>> {
1185        self.batch().submit(account, payloads).await
1186    }
1187
1188    /// Submits multiple transactions and waits for all to complete.
1189    ///
1190    /// # Arguments
1191    ///
1192    /// * `account` - The account to sign with
1193    /// * `payloads` - The transaction payloads to submit
1194    /// * `timeout` - Optional timeout for waiting
1195    ///
1196    /// # Returns
1197    ///
1198    /// Results for each transaction in the batch.
1199    ///
1200    /// # Errors
1201    ///
1202    /// Returns an error if building any transaction fails, signing fails, submission fails,
1203    /// any transaction times out waiting for commitment, or any transaction execution fails.
1204    #[cfg(feature = "ed25519")]
1205    pub async fn submit_batch_and_wait<A: Account>(
1206        &self,
1207        account: &A,
1208        payloads: Vec<TransactionPayload>,
1209        timeout: Option<Duration>,
1210    ) -> AptosResult<Vec<crate::transaction::BatchTransactionResult>> {
1211        self.batch()
1212            .submit_and_wait(account, payloads, timeout)
1213            .await
1214    }
1215
1216    /// Transfers APT to multiple recipients in a batch.
1217    ///
1218    /// # Arguments
1219    ///
1220    /// * `sender` - The sending account
1221    /// * `transfers` - List of (recipient, amount) pairs
1222    ///
1223    /// # Example
1224    ///
1225    /// ```rust,ignore
1226    /// let results = aptos.batch_transfer_apt(&sender, vec![
1227    ///     (addr1, 1_000_000),  // 0.01 APT
1228    ///     (addr2, 2_000_000),  // 0.02 APT
1229    ///     (addr3, 3_000_000),  // 0.03 APT
1230    /// ]).await?;
1231    /// ```
1232    ///
1233    /// # Errors
1234    ///
1235    /// Returns an error if building any transfer payload fails, signing fails, submission fails,
1236    /// any transaction times out, or any transaction execution fails.
1237    #[cfg(feature = "ed25519")]
1238    pub async fn batch_transfer_apt<A: Account>(
1239        &self,
1240        sender: &A,
1241        transfers: Vec<(AccountAddress, u64)>,
1242    ) -> AptosResult<Vec<crate::transaction::BatchTransactionResult>> {
1243        self.batch().transfer_apt(sender, transfers).await
1244    }
1245}
1246
1247// The simulation helpers below are only reachable from `Aptos::simulate`,
1248// which is `#[cfg(feature = "ed25519")]`. Mirror that gate on the helpers so
1249// `cargo clippy -p aptos-sdk --no-default-features` does not flag them as
1250// dead code.
1251
1252// Backticks on every identifier in this module-internal doc comment keep
1253// `clippy::doc_markdown` happy and make the rendering clearer too.
1254
1255#[cfg(feature = "ed25519")]
1256/// Builds a [`TransactionAuthenticator`] containing the account's real
1257/// public key paired with an **all-zero** signature of the correct shape
1258/// for the account's signature scheme.
1259///
1260/// Used by [`Aptos::simulate`] / [`Aptos::estimate_gas`]: the on-chain
1261/// simulation endpoint rejects transactions carrying a valid signature
1262/// (it is a gas-estimation tool, not an execution tool) but it does need
1263/// to walk the signing-message hash to estimate gas correctly. A
1264/// well-shaped zero-signed authenticator is exactly what it expects.
1265///
1266/// Supported schemes (everything the SDK can already produce signatures for):
1267/// * `ED25519_SCHEME` -- `Ed25519` 32-byte pubkey + 64-byte zero signature.
1268/// * `MULTI_ED25519_SCHEME` -- `MultiEd25519` pubkey/signature wrapped as
1269///   `TransactionAuthenticator::MultiEd25519`; signature is `64 * t` zero
1270///   bytes plus a 4-byte bitmap with bits `0..t` set, where `t` is the
1271///   account's threshold (recovered from the pubkey's last byte).
1272/// * `SINGLE_KEY_SCHEME` -- Wraps any single-key account (`Ed25519SingleKey`,
1273///   `Secp256k1`, `Secp256r1`, `WebAuthn`) by emitting a zeroed `AnySignature`
1274///   whose variant tag matches the account's pubkey variant.
1275/// * `MULTI_KEY_SCHEME` -- Wraps a `MultiKey` account by emitting a zeroed
1276///   `MultiKeySignature` whose `AnySignature` variants match the pubkeys.
1277fn build_zero_signed_authenticator<A: Account>(
1278    account: &A,
1279) -> AptosResult<crate::transaction::TransactionAuthenticator> {
1280    use crate::crypto::{
1281        ED25519_SCHEME, MULTI_ED25519_SCHEME, MULTI_KEY_SCHEME, SINGLE_KEY_SCHEME,
1282    };
1283    use crate::transaction::TransactionAuthenticator;
1284    use crate::transaction::authenticator::{
1285        AccountAuthenticator, Ed25519PublicKey, Ed25519Signature,
1286    };
1287
1288    let pubkey_bytes = account.public_key_bytes();
1289    let scheme = account.signature_scheme();
1290
1291    match scheme {
1292        // Single Ed25519: 32-byte pubkey, 64-byte zero signature, top-level
1293        // TransactionAuthenticator::Ed25519 variant.
1294        s if s == ED25519_SCHEME => {
1295            let pubkey_arr: [u8; 32] = pubkey_bytes.as_slice().try_into().map_err(|_| {
1296                crate::error::AptosError::transaction(
1297                    "simulate(): Ed25519 account exposed a non-32-byte public key",
1298                )
1299            })?;
1300            Ok(TransactionAuthenticator::Ed25519 {
1301                public_key: Ed25519PublicKey(pubkey_arr),
1302                signature: Ed25519Signature([0u8; 64]),
1303            })
1304        }
1305
1306        // MultiEd25519: the pubkey blob is `pk_0 || pk_1 || ... || pk_{n-1} || threshold`
1307        // where each `pk_i` is 32 bytes. We can recover `n` and `t`, then emit
1308        // `t` zero signatures plus a bitmap with bits 0..t set (MSB-first
1309        // ordering, matching `MultiEd25519Signature::new`).
1310        s if s == MULTI_ED25519_SCHEME => {
1311            const PK_LEN: usize = 32;
1312            const SIG_LEN: usize = 64;
1313            if pubkey_bytes.is_empty() || !(pubkey_bytes.len() - 1).is_multiple_of(PK_LEN) {
1314                return Err(crate::error::AptosError::transaction(
1315                    "simulate(): MultiEd25519 public_key_bytes has invalid length",
1316                ));
1317            }
1318            let threshold = *pubkey_bytes.last().unwrap() as usize;
1319            if threshold == 0 {
1320                return Err(crate::error::AptosError::transaction(
1321                    "simulate(): MultiEd25519 threshold cannot be zero",
1322                ));
1323            }
1324            // MSB-first bitmap, threshold bits set starting at index 0.
1325            let mut bitmap = [0u8; 4];
1326            for i in 0..threshold {
1327                let byte = i / 8;
1328                let bit = i % 8;
1329                bitmap[byte] |= 0b1000_0000_u8 >> bit;
1330            }
1331            let mut signature = Vec::with_capacity(threshold * SIG_LEN + 4);
1332            signature.extend(std::iter::repeat_n(0u8, threshold * SIG_LEN));
1333            signature.extend_from_slice(&bitmap);
1334            Ok(TransactionAuthenticator::MultiEd25519 {
1335                public_key: pubkey_bytes,
1336                signature,
1337            })
1338        }
1339
1340        // SingleKey: pubkey is already `BCS(AnyPublicKey)` (variant + ULEB128(len) + bytes).
1341        // We mirror the variant tag in a matching zero AnySignature and wrap
1342        // in a SingleSender top-level TransactionAuthenticator.
1343        s if s == SINGLE_KEY_SCHEME => {
1344            let zero_sig = zero_any_signature_for_pubkey(&pubkey_bytes).ok_or_else(|| {
1345                crate::error::AptosError::transaction(
1346                    "simulate(): unsupported AnyPublicKey variant in SingleKey account",
1347                )
1348            })?;
1349            Ok(TransactionAuthenticator::single_sender(
1350                AccountAuthenticator::single_key(pubkey_bytes, zero_sig),
1351            ))
1352        }
1353
1354        // MultiKey: pubkey is `num_keys || (variant || ULEB128(len) || bytes) * n || threshold`.
1355        // Emit a zeroed MultiKeySignature: one zero AnySignature per pubkey
1356        // for the first `threshold` keys, plus the BCS BitVec length prefix
1357        // and a bitmap with bits 0..threshold set (MSB-first).
1358        s if s == MULTI_KEY_SCHEME => {
1359            let (variants, threshold) = parse_multi_key_pubkey(&pubkey_bytes)?;
1360            if threshold == 0 || (threshold as usize) > variants.len() {
1361                return Err(crate::error::AptosError::transaction(
1362                    "simulate(): invalid MultiKey threshold",
1363                ));
1364            }
1365            let mut sig_bytes = Vec::with_capacity(1 + threshold as usize * 66 + 1 + 4);
1366            sig_bytes.push(threshold); // ULEB128(num_sigs); fits in 1 byte for n <= 32.
1367            for variant in variants.iter().take(threshold as usize) {
1368                let zero_sig = zero_any_signature_for_variant(*variant).ok_or_else(|| {
1369                    crate::error::AptosError::transaction(
1370                        "simulate(): unsupported AnyPublicKey variant in MultiKey account",
1371                    )
1372                })?;
1373                sig_bytes.extend_from_slice(&zero_sig);
1374            }
1375            // BitVec length prefix + 4-byte bitmap (MSB-first).
1376            sig_bytes.push(4);
1377            let mut bitmap = [0u8; 4];
1378            for i in 0..threshold as usize {
1379                let byte = i / 8;
1380                let bit = i % 8;
1381                bitmap[byte] |= 0b1000_0000_u8 >> bit;
1382            }
1383            sig_bytes.extend_from_slice(&bitmap);
1384            Ok(TransactionAuthenticator::single_sender(
1385                AccountAuthenticator::multi_key(pubkey_bytes, sig_bytes),
1386            ))
1387        }
1388
1389        _ => Err(crate::error::AptosError::transaction(format!(
1390            "simulate(): unsupported signature scheme {scheme}; \
1391             use simulate_signed() with a hand-built zero-signed transaction"
1392        ))),
1393    }
1394}
1395
1396/// Builds a BCS-encoded zero `AnySignature` whose variant tag matches the
1397/// `AnyPublicKey` carried by the given `SingleKey` pubkey blob. Returns
1398/// `None` for unknown variants.
1399#[cfg(feature = "ed25519")]
1400fn zero_any_signature_for_pubkey(any_public_key_bcs: &[u8]) -> Option<Vec<u8>> {
1401    let variant = *any_public_key_bcs.first()?;
1402    zero_any_signature_for_variant(variant)
1403}
1404
1405/// Builds a BCS-encoded zero `AnySignature` for the given variant tag.
1406#[cfg(feature = "ed25519")]
1407fn zero_any_signature_for_variant(variant: u8) -> Option<Vec<u8>> {
1408    // For all SDK-supported variants, the inner signature payload is 64
1409    // bytes (Ed25519, Secp256k1Ecdsa, and -- in the SDK's representation
1410    // -- the Secp256r1 raw signature carried inside the WebAuthn envelope).
1411    // Variant 2 on-chain is WebAuthn, but for *simulation* the inner
1412    // PartialAuthenticatorAssertionResponse is allowed to be all zeros: the
1413    // simulator never actually verifies the signature, and a 64-byte zero
1414    // payload with the variant tag and length prefix has the same shape as
1415    // the live signature.
1416    match variant {
1417        0..=2 => {
1418            let mut out = Vec::with_capacity(1 + 1 + 64);
1419            out.push(variant);
1420            out.push(64);
1421            out.extend(std::iter::repeat_n(0u8, 64));
1422            Some(out)
1423        }
1424        _ => None,
1425    }
1426}
1427
1428/// Parses a `BCS(MultiKeyPublicKey)` blob into its variant tags and threshold.
1429///
1430/// Wire layout: `num_keys || (variant || ULEB128(len) || bytes) * n || threshold`.
1431#[cfg(feature = "ed25519")]
1432fn parse_multi_key_pubkey(bytes: &[u8]) -> AptosResult<(Vec<u8>, u8)> {
1433    if bytes.is_empty() {
1434        return Err(crate::error::AptosError::transaction(
1435            "simulate(): MultiKey public_key_bytes is empty",
1436        ));
1437    }
1438    let num_keys = bytes[0] as usize;
1439    let mut offset = 1;
1440    let mut variants = Vec::with_capacity(num_keys);
1441    for _ in 0..num_keys {
1442        if offset >= bytes.len() {
1443            return Err(crate::error::AptosError::transaction(
1444                "simulate(): MultiKey public_key truncated at variant tag",
1445            ));
1446        }
1447        let variant = bytes[offset];
1448        variants.push(variant);
1449        offset += 1;
1450        // ULEB128(len)
1451        let (len, len_bytes) = decode_uleb128_internal(&bytes[offset..])?;
1452        offset += len_bytes;
1453        offset = offset.checked_add(len).ok_or_else(|| {
1454            crate::error::AptosError::transaction("simulate(): MultiKey public_key overflow")
1455        })?;
1456        if offset > bytes.len() {
1457            return Err(crate::error::AptosError::transaction(
1458                "simulate(): MultiKey public_key truncated at key bytes",
1459            ));
1460        }
1461    }
1462    if offset >= bytes.len() {
1463        return Err(crate::error::AptosError::transaction(
1464            "simulate(): MultiKey public_key missing threshold byte",
1465        ));
1466    }
1467    let threshold = bytes[offset];
1468    Ok((variants, threshold))
1469}
1470
1471/// Minimal ULEB128 decoder local to the simulation helper.
1472#[cfg(feature = "ed25519")]
1473fn decode_uleb128_internal(bytes: &[u8]) -> AptosResult<(usize, usize)> {
1474    let mut value: usize = 0;
1475    let mut shift = 0;
1476    for (i, &b) in bytes.iter().enumerate() {
1477        value |= ((b & 0x7F) as usize) << shift;
1478        if (b & 0x80) == 0 {
1479            return Ok((value, i + 1));
1480        }
1481        shift += 7;
1482        if shift >= 64 {
1483            break;
1484        }
1485    }
1486    Err(crate::error::AptosError::transaction(
1487        "simulate(): malformed ULEB128 in public key",
1488    ))
1489}
1490
1491#[cfg(test)]
1492mod tests {
1493    use super::*;
1494    use crate::transaction::authenticator::{
1495        Ed25519PublicKey, Ed25519Signature, TransactionAuthenticator,
1496    };
1497    use crate::transaction::payload::{EntryFunction, TransactionPayload};
1498    use crate::transaction::simulation::SimulateQueryOptions;
1499    use crate::transaction::types::{
1500        FeePayerRawTransaction, MultiAgentRawTransaction, RawTransaction, SignedTransaction,
1501    };
1502    use crate::types::ChainId;
1503    use wiremock::{
1504        Mock, MockServer, ResponseTemplate,
1505        matchers::{method, path, path_regex},
1506    };
1507
1508    #[test]
1509    fn test_aptos_client_creation() {
1510        let aptos = Aptos::testnet();
1511        assert!(aptos.is_ok());
1512    }
1513
1514    #[test]
1515    fn test_chain_id() {
1516        let aptos = Aptos::testnet().unwrap();
1517        assert_eq!(aptos.chain_id(), ChainId::testnet());
1518
1519        let aptos = Aptos::mainnet().unwrap();
1520        assert_eq!(aptos.chain_id(), ChainId::mainnet());
1521    }
1522
1523    #[test]
1524    fn test_ans_accessor() {
1525        // ans() returns a client bound to this client's network; mainnet has a
1526        // built-in router contract address, so resolution succeeds.
1527        let aptos = Aptos::mainnet().unwrap();
1528        assert!(aptos.ans().router_address().is_ok());
1529    }
1530
1531    fn create_mock_aptos(server: &MockServer) -> Aptos {
1532        let url = format!("{}/v1", server.uri());
1533        let config = AptosConfig::custom(&url).unwrap().without_retry();
1534        Aptos::new(config).unwrap()
1535    }
1536
1537    fn create_minimal_signed_transaction() -> SignedTransaction {
1538        let raw = RawTransaction::new(
1539            AccountAddress::ONE,
1540            0,
1541            TransactionPayload::EntryFunction(
1542                EntryFunction::apt_transfer(AccountAddress::ONE, 0).unwrap(),
1543            ),
1544            100_000,
1545            100,
1546            std::time::SystemTime::now()
1547                .duration_since(std::time::UNIX_EPOCH)
1548                .unwrap()
1549                .as_secs()
1550                .saturating_add(600),
1551            ChainId::testnet(),
1552        );
1553        SignedTransaction::new(
1554            raw,
1555            TransactionAuthenticator::Ed25519 {
1556                public_key: Ed25519PublicKey([0u8; 32]),
1557                signature: Ed25519Signature([0u8; 64]),
1558            },
1559        )
1560    }
1561
1562    fn simulate_response_json() -> serde_json::Value {
1563        serde_json::json!([{
1564            "success": true,
1565            "vm_status": "Executed successfully",
1566            "gas_used": "1500",
1567            "max_gas_amount": "200000",
1568            "gas_unit_price": "100",
1569            "hash": "0xabc",
1570            "changes": [],
1571            "events": []
1572        }])
1573    }
1574
1575    #[tokio::test]
1576    async fn test_get_sequence_number() {
1577        let server = MockServer::start().await;
1578
1579        Mock::given(method("GET"))
1580            .and(path_regex(r"/v1/accounts/0x[0-9a-f]+"))
1581            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1582                "sequence_number": "42",
1583                "authentication_key": "0x0000000000000000000000000000000000000000000000000000000000000001"
1584            })))
1585            .expect(1)
1586            .mount(&server)
1587            .await;
1588
1589        let aptos = create_mock_aptos(&server);
1590        let seq = aptos
1591            .get_sequence_number(AccountAddress::ONE)
1592            .await
1593            .unwrap();
1594        assert_eq!(seq, 42);
1595    }
1596
1597    #[tokio::test]
1598    async fn test_get_balance() {
1599        let server = MockServer::start().await;
1600
1601        // get_balance now uses view function instead of CoinStore resource
1602        Mock::given(method("POST"))
1603            .and(path("/v1/view"))
1604            .respond_with(
1605                ResponseTemplate::new(200).set_body_json(serde_json::json!(["5000000000"])),
1606            )
1607            .expect(1)
1608            .mount(&server)
1609            .await;
1610
1611        let aptos = create_mock_aptos(&server);
1612        let balance = aptos.get_balance(AccountAddress::ONE).await.unwrap();
1613        assert_eq!(balance, 5_000_000_000);
1614    }
1615
1616    #[tokio::test]
1617    async fn test_get_resources_via_fullnode() {
1618        let server = MockServer::start().await;
1619
1620        Mock::given(method("GET"))
1621            .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/resources"))
1622            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([
1623                {"type": "0x1::account::Account", "data": {}},
1624                {"type": "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>", "data": {}}
1625            ])))
1626            .expect(1)
1627            .mount(&server)
1628            .await;
1629
1630        let aptos = create_mock_aptos(&server);
1631        let resources = aptos
1632            .fullnode()
1633            .get_account_resources(AccountAddress::ONE)
1634            .await
1635            .unwrap();
1636        assert_eq!(resources.data.len(), 2);
1637    }
1638
1639    #[tokio::test]
1640    async fn test_ledger_info() {
1641        let server = MockServer::start().await;
1642
1643        Mock::given(method("GET"))
1644            .and(path("/v1"))
1645            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1646                "chain_id": 2,
1647                "epoch": "100",
1648                "ledger_version": "12345",
1649                "oldest_ledger_version": "0",
1650                "ledger_timestamp": "1000000",
1651                "node_role": "full_node",
1652                "oldest_block_height": "0",
1653                "block_height": "5000"
1654            })))
1655            .expect(1)
1656            .mount(&server)
1657            .await;
1658
1659        let aptos = create_mock_aptos(&server);
1660        let info = aptos.ledger_info().await.unwrap();
1661        assert_eq!(info.version().unwrap(), 12345);
1662    }
1663
1664    #[tokio::test]
1665    async fn test_config_builder() {
1666        let config = AptosConfig::testnet().with_timeout(Duration::from_mins(1));
1667
1668        let aptos = Aptos::new(config).unwrap();
1669        assert_eq!(aptos.chain_id(), ChainId::testnet());
1670    }
1671
1672    #[tokio::test]
1673    async fn test_fullnode_accessor() {
1674        let server = MockServer::start().await;
1675        let aptos = create_mock_aptos(&server);
1676
1677        // Can access fullnode client directly
1678        let fullnode = aptos.fullnode();
1679        assert!(fullnode.base_url().as_str().contains(&server.uri()));
1680    }
1681
1682    #[cfg(feature = "ed25519")]
1683    #[tokio::test]
1684    async fn test_build_transaction() {
1685        let server = MockServer::start().await;
1686
1687        // Mock for getting account
1688        Mock::given(method("GET"))
1689            .and(path_regex(r"/v1/accounts/0x[0-9a-f]+"))
1690            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1691                "sequence_number": "0",
1692                "authentication_key": "0x0000000000000000000000000000000000000000000000000000000000000001"
1693            })))
1694            .expect(1)
1695            .mount(&server)
1696            .await;
1697
1698        // Mock for gas price
1699        Mock::given(method("GET"))
1700            .and(path("/v1/estimate_gas_price"))
1701            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1702                "gas_estimate": 100
1703            })))
1704            .expect(1)
1705            .mount(&server)
1706            .await;
1707
1708        // Mock for ledger info (needed for chain_id resolution on custom networks)
1709        Mock::given(method("GET"))
1710            .and(path("/v1"))
1711            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1712                "chain_id": 4,
1713                "epoch": "1",
1714                "ledger_version": "100",
1715                "oldest_ledger_version": "0",
1716                "ledger_timestamp": "1000000",
1717                "node_role": "full_node",
1718                "oldest_block_height": "0",
1719                "block_height": "50"
1720            })))
1721            .expect(1)
1722            .mount(&server)
1723            .await;
1724
1725        let aptos = create_mock_aptos(&server);
1726        let account = crate::account::Ed25519Account::generate();
1727        let recipient = AccountAddress::from_hex("0x123").unwrap();
1728        let payload = crate::transaction::EntryFunction::apt_transfer(recipient, 1000).unwrap();
1729
1730        let raw_txn = aptos
1731            .build_transaction(&account, payload.into())
1732            .await
1733            .unwrap();
1734        assert_eq!(raw_txn.sender, account.address());
1735        assert_eq!(raw_txn.sequence_number, 0);
1736    }
1737
1738    #[cfg(feature = "indexer")]
1739    #[tokio::test]
1740    async fn test_indexer_accessor() {
1741        let aptos = Aptos::testnet().unwrap();
1742        let indexer = aptos.indexer();
1743        assert!(indexer.is_some());
1744    }
1745
1746    #[tokio::test]
1747    async fn test_account_exists_true() {
1748        let server = MockServer::start().await;
1749
1750        Mock::given(method("GET"))
1751            .and(path_regex(r"^/v1/accounts/0x[0-9a-f]+$"))
1752            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1753                "sequence_number": "10",
1754                "authentication_key": "0x0000000000000000000000000000000000000000000000000000000000000001"
1755            })))
1756            .expect(1)
1757            .mount(&server)
1758            .await;
1759
1760        let aptos = create_mock_aptos(&server);
1761        let exists = aptos.account_exists(AccountAddress::ONE).await.unwrap();
1762        assert!(exists);
1763    }
1764
1765    #[tokio::test]
1766    async fn test_account_exists_false() {
1767        let server = MockServer::start().await;
1768
1769        Mock::given(method("GET"))
1770            .and(path_regex(r"^/v1/accounts/0x[0-9a-f]+$"))
1771            .respond_with(ResponseTemplate::new(404).set_body_json(serde_json::json!({
1772                "message": "Account not found",
1773                "error_code": "account_not_found"
1774            })))
1775            .expect(1)
1776            .mount(&server)
1777            .await;
1778
1779        let aptos = create_mock_aptos(&server);
1780        let exists = aptos.account_exists(AccountAddress::ONE).await.unwrap();
1781        assert!(!exists);
1782    }
1783
1784    #[tokio::test]
1785    async fn test_view_function() {
1786        let server = MockServer::start().await;
1787
1788        Mock::given(method("POST"))
1789            .and(path("/v1/view"))
1790            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!(["1000000"])))
1791            .expect(1)
1792            .mount(&server)
1793            .await;
1794
1795        let aptos = create_mock_aptos(&server);
1796        let result: Vec<serde_json::Value> = aptos
1797            .view(
1798                "0x1::coin::balance",
1799                vec!["0x1::aptos_coin::AptosCoin".to_string()],
1800                vec![serde_json::json!("0x1")],
1801            )
1802            .await
1803            .unwrap();
1804
1805        assert_eq!(result.len(), 1);
1806        assert_eq!(result[0].as_str().unwrap(), "1000000");
1807    }
1808
1809    #[tokio::test]
1810    async fn test_chain_id_from_config() {
1811        let aptos = Aptos::mainnet().unwrap();
1812        assert_eq!(aptos.chain_id(), ChainId::mainnet());
1813
1814        // Devnet's chain ID is intentionally reported as 0 (unknown) at
1815        // construction time -- the value is reset whenever devnet itself
1816        // is reset, so any hardcoded value would go stale. The Aptos client
1817        // populates the live chain ID lazily via `ensure_chain_id`, which is
1818        // exercised on the network and not in this offline unit test.
1819        let aptos = Aptos::devnet().unwrap();
1820        assert_eq!(aptos.chain_id(), ChainId::new(0));
1821    }
1822
1823    #[tokio::test]
1824    async fn test_custom_config() {
1825        let server = MockServer::start().await;
1826        let url = format!("{}/v1", server.uri());
1827        let config = AptosConfig::custom(&url).unwrap();
1828        let aptos = Aptos::new(config).unwrap();
1829
1830        // Custom config should have unknown chain ID
1831        assert_eq!(aptos.chain_id(), ChainId::new(0));
1832    }
1833
1834    // ---------------------------------------------------------------
1835    // build_zero_signed_authenticator: cover every signature scheme
1836    // the SDK can sign for and confirm the helper does NOT fall back to
1837    // the Ed25519 path for non-Ed25519 accounts. This was the subject
1838    // of a Copilot review comment and now has explicit regression
1839    // coverage.
1840    // ---------------------------------------------------------------
1841
1842    #[cfg(feature = "ed25519")]
1843    #[test]
1844    fn test_zero_signed_authenticator_ed25519() {
1845        use crate::account::Ed25519Account;
1846        use crate::transaction::TransactionAuthenticator;
1847
1848        let account = Ed25519Account::generate();
1849        let auth = super::build_zero_signed_authenticator(&account).unwrap();
1850        match auth {
1851            TransactionAuthenticator::Ed25519 {
1852                public_key,
1853                signature,
1854            } => {
1855                assert_eq!(public_key.0, account.public_key().to_bytes());
1856                assert_eq!(signature.0, [0u8; 64]);
1857            }
1858            other => panic!("expected TransactionAuthenticator::Ed25519, got {other:?}"),
1859        }
1860    }
1861
1862    #[cfg(all(feature = "ed25519", feature = "secp256k1"))]
1863    #[test]
1864    fn test_zero_signed_authenticator_single_key_secp256k1() {
1865        use crate::account::Secp256k1Account;
1866        use crate::transaction::TransactionAuthenticator;
1867        use crate::transaction::authenticator::AccountAuthenticator;
1868
1869        let account = Secp256k1Account::generate();
1870        let auth = super::build_zero_signed_authenticator(&account).unwrap();
1871        // Secp256k1Account is a SingleKey, so it must wrap in SingleSender +
1872        // SingleKey. No Ed25519 variant must appear anywhere.
1873        let TransactionAuthenticator::SingleSender { sender } = auth else {
1874            panic!("expected SingleSender, got {auth:?}");
1875        };
1876        let AccountAuthenticator::SingleKey {
1877            public_key,
1878            signature,
1879        } = sender
1880        else {
1881            panic!("expected AccountAuthenticator::SingleKey");
1882        };
1883        // Public key bytes are passed through unchanged.
1884        assert_eq!(public_key, account.public_key_bytes());
1885        // Signature is a zeroed BCS-encoded `AnySignature::Secp256k1Ecdsa`
1886        // (variant=1, len=64, 64 zero bytes).
1887        assert_eq!(signature.len(), 1 + 1 + 64);
1888        assert_eq!(signature[0], 0x01, "variant tag must match secp256k1");
1889        assert_eq!(signature[1], 64, "ULEB128(64)");
1890        assert!(signature[2..].iter().all(|b| *b == 0), "all-zero signature");
1891    }
1892
1893    #[cfg(feature = "ed25519")]
1894    #[test]
1895    fn test_zero_signed_authenticator_single_key_ed25519() {
1896        use crate::account::Ed25519SingleKeyAccount;
1897        use crate::transaction::TransactionAuthenticator;
1898        use crate::transaction::authenticator::AccountAuthenticator;
1899
1900        // Ed25519SingleKeyAccount uses scheme SINGLE_KEY_SCHEME and exposes
1901        // `public_key_bytes` as BCS(AnyPublicKey::Ed25519), not the raw 32-byte
1902        // pubkey, so the previous Ed25519-only fast path would have rejected
1903        // this account.
1904        let account = Ed25519SingleKeyAccount::generate();
1905        let auth = super::build_zero_signed_authenticator(&account).unwrap();
1906        let TransactionAuthenticator::SingleSender { sender } = auth else {
1907            panic!("expected SingleSender for Ed25519SingleKey, got {auth:?}");
1908        };
1909        let AccountAuthenticator::SingleKey {
1910            public_key,
1911            signature,
1912        } = sender
1913        else {
1914            panic!("expected AccountAuthenticator::SingleKey");
1915        };
1916        assert_eq!(public_key, account.public_key_bytes());
1917        assert_eq!(signature[0], 0x00, "AnySignature::Ed25519 variant");
1918        assert_eq!(signature[1], 64, "ULEB128(64)");
1919        assert!(signature[2..].iter().all(|b| *b == 0));
1920    }
1921
1922    #[cfg(feature = "ed25519")]
1923    #[test]
1924    fn test_zero_signed_authenticator_multi_ed25519() {
1925        use crate::account::MultiEd25519Account;
1926        use crate::crypto::Ed25519PrivateKey;
1927        use crate::transaction::TransactionAuthenticator;
1928
1929        // 2-of-3 multi-ed25519.
1930        let keys: Vec<_> = (0..3).map(|_| Ed25519PrivateKey::generate()).collect();
1931        let account = MultiEd25519Account::new(keys, 2).unwrap();
1932        let auth = super::build_zero_signed_authenticator(&account).unwrap();
1933        let TransactionAuthenticator::MultiEd25519 {
1934            public_key,
1935            signature,
1936        } = auth
1937        else {
1938            panic!("expected MultiEd25519, got {auth:?}");
1939        };
1940        assert_eq!(public_key, account.public_key_bytes());
1941        // signature = 2 * 64 zero bytes + 4-byte bitmap, MSB-first bits 0+1 set.
1942        assert_eq!(signature.len(), 2 * 64 + 4);
1943        assert!(signature[..128].iter().all(|b| *b == 0));
1944        assert_eq!(signature[128], 0b1100_0000, "bits 0 and 1 set (MSB-first)");
1945        assert_eq!(&signature[129..], &[0u8, 0u8, 0u8]);
1946    }
1947
1948    #[cfg(all(feature = "ed25519", feature = "secp256k1"))]
1949    #[test]
1950    fn test_zero_signed_authenticator_multi_key() {
1951        use crate::account::{AnyPrivateKey, MultiKeyAccount};
1952        use crate::crypto::{Ed25519PrivateKey, Secp256k1PrivateKey};
1953        use crate::transaction::TransactionAuthenticator;
1954        use crate::transaction::authenticator::AccountAuthenticator;
1955
1956        let keys = vec![
1957            AnyPrivateKey::ed25519(Ed25519PrivateKey::generate()),
1958            AnyPrivateKey::secp256k1(Secp256k1PrivateKey::generate()),
1959            AnyPrivateKey::ed25519(Ed25519PrivateKey::generate()),
1960        ];
1961        let account = MultiKeyAccount::new(keys, 2).unwrap();
1962        let auth = super::build_zero_signed_authenticator(&account).unwrap();
1963        let TransactionAuthenticator::SingleSender { sender } = auth else {
1964            panic!("expected SingleSender for MultiKey, got {auth:?}");
1965        };
1966        let AccountAuthenticator::MultiKey {
1967            public_key,
1968            signature,
1969        } = sender
1970        else {
1971            panic!("expected AccountAuthenticator::MultiKey");
1972        };
1973        assert_eq!(public_key, account.public_key_bytes());
1974        // signature = ULEB128(2) || two zero AnySignatures || ULEB128(4) || 4-byte bitmap.
1975        // First zero AnySignature is for the Ed25519 key at index 0 (variant 0).
1976        // Second is for the Secp256k1 key at index 1 (variant 1).
1977        // No reason for them to share variants; this regression-guards
1978        // the bug-prone assumption that all single-key accounts are Ed25519.
1979        assert_eq!(signature[0], 2, "num_sigs ULEB128");
1980        assert_eq!(signature[1], 0x00, "first AnySignature variant (Ed25519)");
1981        assert_eq!(
1982            signature[1 + 1 + 1 + 64],
1983            0x01,
1984            "second AnySignature variant (Secp256k1)"
1985        );
1986    }
1987
1988    #[tokio::test]
1989    async fn test_simulate_signed_with_options() {
1990        let server = MockServer::start().await;
1991
1992        Mock::given(method("POST"))
1993            .and(path("/v1/transactions/simulate"))
1994            .and(|req: &wiremock::Request| {
1995                req.url
1996                    .query()
1997                    .is_some_and(|q| q.contains("estimate_gas_unit_price=true"))
1998            })
1999            .respond_with(
2000                ResponseTemplate::new(200).set_body_json(serde_json::json!([{
2001                    "success": true,
2002                    "vm_status": "Executed successfully",
2003                    "gas_used": "1500",
2004                    "max_gas_amount": "200000",
2005                    "gas_unit_price": "100",
2006                    "hash": "0xabc",
2007                    "changes": [],
2008                    "events": []
2009                }])),
2010            )
2011            .expect(1)
2012            .mount(&server)
2013            .await;
2014
2015        let raw = RawTransaction::new(
2016            AccountAddress::ONE,
2017            0,
2018            TransactionPayload::EntryFunction(
2019                EntryFunction::apt_transfer(AccountAddress::ONE, 0).unwrap(),
2020            ),
2021            100_000,
2022            100,
2023            std::time::SystemTime::now()
2024                .duration_since(std::time::UNIX_EPOCH)
2025                .unwrap()
2026                .as_secs()
2027                .saturating_add(600),
2028            ChainId::testnet(),
2029        );
2030        let signed = SignedTransaction::new(
2031            raw,
2032            TransactionAuthenticator::Ed25519 {
2033                public_key: Ed25519PublicKey([0u8; 32]),
2034                signature: Ed25519Signature([0u8; 64]),
2035            },
2036        );
2037
2038        let aptos = create_mock_aptos(&server);
2039        let options = SimulateQueryOptions::new().estimate_gas_unit_price(true);
2040        let result = aptos
2041            .simulate_signed_with_options(&signed, options)
2042            .await
2043            .unwrap();
2044
2045        assert!(result.success());
2046        assert_eq!(result.gas_used(), 1500);
2047        assert_eq!(result.gas_unit_price(), 100);
2048    }
2049
2050    #[tokio::test]
2051    async fn test_simulate_signed_without_options() {
2052        let server = MockServer::start().await;
2053
2054        Mock::given(method("POST"))
2055            .and(path("/v1/transactions/simulate"))
2056            .and(|req: &wiremock::Request| {
2057                req.url.query().is_none_or(|q| {
2058                    !q.contains("estimate_gas_unit_price=")
2059                        && !q.contains("estimate_max_gas_amount=")
2060                        && !q.contains("estimate_prioritized_gas_unit_price=")
2061                })
2062            })
2063            .respond_with(ResponseTemplate::new(200).set_body_json(simulate_response_json()))
2064            .expect(1)
2065            .mount(&server)
2066            .await;
2067
2068        let aptos = create_mock_aptos(&server);
2069        let signed = create_minimal_signed_transaction();
2070        let result = aptos.simulate_signed(&signed).await.unwrap();
2071        assert!(result.success());
2072    }
2073
2074    #[tokio::test]
2075    async fn test_simulate_multi_agent_without_options() {
2076        let server = MockServer::start().await;
2077
2078        Mock::given(method("POST"))
2079            .and(path("/v1/transactions/simulate"))
2080            .and(|req: &wiremock::Request| {
2081                req.url.query().is_none_or(|q| {
2082                    !q.contains("estimate_gas_unit_price=")
2083                        && !q.contains("estimate_max_gas_amount=")
2084                        && !q.contains("estimate_prioritized_gas_unit_price=")
2085                })
2086            })
2087            .respond_with(ResponseTemplate::new(200).set_body_json(simulate_response_json()))
2088            .expect(1)
2089            .mount(&server)
2090            .await;
2091
2092        let aptos = create_mock_aptos(&server);
2093        let multi_agent = MultiAgentRawTransaction::new(
2094            create_minimal_signed_transaction().raw_txn,
2095            vec![AccountAddress::from_hex("0x2").unwrap()],
2096        );
2097        let result = aptos
2098            .simulate_multi_agent(&multi_agent, None)
2099            .await
2100            .unwrap();
2101        assert!(result.success());
2102    }
2103
2104    #[tokio::test]
2105    async fn test_simulate_multi_agent_with_options() {
2106        let server = MockServer::start().await;
2107
2108        Mock::given(method("POST"))
2109            .and(path("/v1/transactions/simulate"))
2110            .and(|req: &wiremock::Request| {
2111                req.url
2112                    .query()
2113                    .is_some_and(|q| q.contains("estimate_max_gas_amount=true"))
2114            })
2115            .respond_with(ResponseTemplate::new(200).set_body_json(simulate_response_json()))
2116            .expect(1)
2117            .mount(&server)
2118            .await;
2119
2120        let aptos = create_mock_aptos(&server);
2121        let multi_agent = MultiAgentRawTransaction::new(
2122            create_minimal_signed_transaction().raw_txn,
2123            vec![AccountAddress::from_hex("0x2").unwrap()],
2124        );
2125        let options = SimulateQueryOptions::new().estimate_max_gas_amount(true);
2126        let result = aptos
2127            .simulate_multi_agent(&multi_agent, Some(options))
2128            .await
2129            .unwrap();
2130        assert!(result.success());
2131    }
2132
2133    #[tokio::test]
2134    async fn test_simulate_fee_payer_without_options() {
2135        let server = MockServer::start().await;
2136
2137        Mock::given(method("POST"))
2138            .and(path("/v1/transactions/simulate"))
2139            .and(|req: &wiremock::Request| {
2140                req.url.query().is_none_or(|q| {
2141                    !q.contains("estimate_gas_unit_price=")
2142                        && !q.contains("estimate_max_gas_amount=")
2143                        && !q.contains("estimate_prioritized_gas_unit_price=")
2144                })
2145            })
2146            .respond_with(ResponseTemplate::new(200).set_body_json(simulate_response_json()))
2147            .expect(1)
2148            .mount(&server)
2149            .await;
2150
2151        let aptos = create_mock_aptos(&server);
2152        let fee_payer_txn = FeePayerRawTransaction::new_simple(
2153            create_minimal_signed_transaction().raw_txn,
2154            AccountAddress::THREE,
2155        );
2156        let result = aptos
2157            .simulate_fee_payer(&fee_payer_txn, None)
2158            .await
2159            .unwrap();
2160        assert!(result.success());
2161    }
2162
2163    #[tokio::test]
2164    async fn test_simulate_fee_payer_with_options() {
2165        let server = MockServer::start().await;
2166
2167        Mock::given(method("POST"))
2168            .and(path("/v1/transactions/simulate"))
2169            .and(|req: &wiremock::Request| {
2170                req.url
2171                    .query()
2172                    .is_some_and(|q| q.contains("estimate_prioritized_gas_unit_price=true"))
2173            })
2174            .respond_with(ResponseTemplate::new(200).set_body_json(simulate_response_json()))
2175            .expect(1)
2176            .mount(&server)
2177            .await;
2178
2179        let aptos = create_mock_aptos(&server);
2180        let fee_payer_txn = FeePayerRawTransaction::new_simple(
2181            create_minimal_signed_transaction().raw_txn,
2182            AccountAddress::THREE,
2183        );
2184        let options = SimulateQueryOptions::new().estimate_prioritized_gas_unit_price(true);
2185        let result = aptos
2186            .simulate_fee_payer(&fee_payer_txn, options)
2187            .await
2188            .unwrap();
2189        assert!(result.success());
2190    }
2191
2192    // ---------------------------------------------------------------
2193    // Shared mock helpers for the transaction build / submit / wait
2194    // flow. Each mounts one fullnode endpoint on the given server.
2195    // ---------------------------------------------------------------
2196
2197    /// A full 32-byte transaction hash used across submit/wait mocks.
2198    const TEST_TXN_HASH: &str =
2199        "0x0000000000000000000000000000000000000000000000000000000000000001";
2200
2201    async fn mount_seq_number(server: &MockServer, seq: u64) {
2202        Mock::given(method("GET"))
2203            .and(path_regex(r"^/v1/accounts/0x[0-9a-f]+$"))
2204            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2205                "sequence_number": seq.to_string(),
2206                "authentication_key": "0x0000000000000000000000000000000000000000000000000000000000000001"
2207            })))
2208            .mount(server)
2209            .await;
2210    }
2211
2212    async fn mount_gas_price(server: &MockServer) {
2213        Mock::given(method("GET"))
2214            .and(path("/v1/estimate_gas_price"))
2215            .respond_with(
2216                ResponseTemplate::new(200)
2217                    .set_body_json(serde_json::json!({ "gas_estimate": 100 })),
2218            )
2219            .mount(server)
2220            .await;
2221    }
2222
2223    async fn mount_ledger(server: &MockServer, chain_id: u8) {
2224        Mock::given(method("GET"))
2225            .and(path("/v1"))
2226            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2227                "chain_id": chain_id,
2228                "epoch": "1",
2229                "ledger_version": "100",
2230                "oldest_ledger_version": "0",
2231                "ledger_timestamp": "1000000",
2232                "node_role": "full_node",
2233                "oldest_block_height": "0",
2234                "block_height": "50"
2235            })))
2236            .mount(server)
2237            .await;
2238    }
2239
2240    fn pending_txn_json() -> serde_json::Value {
2241        serde_json::json!({
2242            "hash": TEST_TXN_HASH,
2243            "sender": "0x1",
2244            "sequence_number": "0",
2245            "max_gas_amount": "200000",
2246            "gas_unit_price": "100",
2247            "expiration_timestamp_secs": "1000000000"
2248        })
2249    }
2250
2251    fn committed_txn_json() -> serde_json::Value {
2252        serde_json::json!({
2253            "type": "user_transaction",
2254            "version": "12345",
2255            "hash": TEST_TXN_HASH,
2256            "success": true,
2257            "vm_status": "Executed successfully"
2258        })
2259    }
2260
2261    async fn mount_submit(server: &MockServer) {
2262        Mock::given(method("POST"))
2263            .and(path("/v1/transactions"))
2264            .respond_with(ResponseTemplate::new(202).set_body_json(pending_txn_json()))
2265            .mount(server)
2266            .await;
2267    }
2268
2269    async fn mount_wait(server: &MockServer) {
2270        Mock::given(method("GET"))
2271            .and(path_regex(r"^/v1/transactions/by_hash/0x[0-9a-f]+$"))
2272            .respond_with(ResponseTemplate::new(200).set_body_json(committed_txn_json()))
2273            .mount(server)
2274            .await;
2275    }
2276
2277    async fn mount_simulate(server: &MockServer) {
2278        Mock::given(method("POST"))
2279            .and(path("/v1/transactions/simulate"))
2280            .respond_with(ResponseTemplate::new(200).set_body_json(simulate_response_json()))
2281            .mount(server)
2282            .await;
2283    }
2284
2285    /// Mounts every endpoint required for `build_transaction` on a custom
2286    /// (chain-id 0) network: sequence number, gas price, and ledger info.
2287    async fn mount_build_flow(server: &MockServer, seq: u64, chain_id: u8) {
2288        mount_seq_number(server, seq).await;
2289        mount_gas_price(server).await;
2290        mount_ledger(server, chain_id).await;
2291    }
2292
2293    // ---------------------------------------------------------------
2294    // ensure_chain_id
2295    // ---------------------------------------------------------------
2296
2297    #[tokio::test]
2298    async fn test_ensure_chain_id_known_no_request() {
2299        // Testnet has a fixed chain ID, so ensure_chain_id must not touch the
2300        // network. Point at a server with no mounted routes: any request would
2301        // fail, proving no request was made.
2302        let server = MockServer::start().await;
2303        let aptos = Aptos::testnet().unwrap();
2304        // (server is unused on purpose; keep it alive to mirror the harness)
2305        let _ = &server;
2306        let id = aptos.ensure_chain_id().await.unwrap();
2307        assert_eq!(id, ChainId::testnet());
2308    }
2309
2310    #[tokio::test]
2311    async fn test_ensure_chain_id_fetches_from_node() {
2312        let server = MockServer::start().await;
2313        mount_ledger(&server, 7).await;
2314
2315        let aptos = create_mock_aptos(&server);
2316        // Custom config starts unknown.
2317        assert_eq!(aptos.chain_id(), ChainId::new(0));
2318
2319        let id = aptos.ensure_chain_id().await.unwrap();
2320        assert_eq!(id, ChainId::new(7));
2321        // Cached for subsequent calls.
2322        assert_eq!(aptos.chain_id(), ChainId::new(7));
2323    }
2324
2325    #[tokio::test]
2326    async fn test_ledger_info_populates_chain_id() {
2327        let server = MockServer::start().await;
2328        mount_ledger(&server, 9).await;
2329
2330        let aptos = create_mock_aptos(&server);
2331        assert_eq!(aptos.chain_id(), ChainId::new(0));
2332        let info = aptos.ledger_info().await.unwrap();
2333        assert_eq!(info.chain_id, 9);
2334        // ledger_info() resolves the previously-unknown chain ID as a side effect.
2335        assert_eq!(aptos.chain_id(), ChainId::new(9));
2336    }
2337
2338    // ---------------------------------------------------------------
2339    // submit / wait (pre-signed)
2340    // ---------------------------------------------------------------
2341
2342    #[tokio::test]
2343    async fn test_submit_transaction() {
2344        let server = MockServer::start().await;
2345        mount_submit(&server).await;
2346
2347        let aptos = create_mock_aptos(&server);
2348        let signed = create_minimal_signed_transaction();
2349        let pending = aptos.submit_transaction(&signed).await.unwrap();
2350        assert_eq!(pending.into_inner().hash().to_string(), TEST_TXN_HASH);
2351    }
2352
2353    #[tokio::test]
2354    async fn test_submit_and_wait() {
2355        let server = MockServer::start().await;
2356        mount_submit(&server).await;
2357        mount_wait(&server).await;
2358
2359        let aptos = create_mock_aptos(&server);
2360        let signed = create_minimal_signed_transaction();
2361        let response = aptos.submit_and_wait(&signed, None).await.unwrap();
2362        assert_eq!(
2363            response
2364                .into_inner()
2365                .get("success")
2366                .and_then(serde_json::Value::as_bool),
2367            Some(true)
2368        );
2369    }
2370
2371    #[tokio::test]
2372    async fn test_simulate_transaction_raw() {
2373        let server = MockServer::start().await;
2374        mount_simulate(&server).await;
2375
2376        let aptos = create_mock_aptos(&server);
2377        let signed = create_minimal_signed_transaction();
2378        let response = aptos.simulate_transaction(&signed).await.unwrap();
2379        let data = response.into_inner();
2380        assert_eq!(data.len(), 1);
2381        assert_eq!(
2382            data[0].get("success").and_then(serde_json::Value::as_bool),
2383            Some(true)
2384        );
2385    }
2386
2387    // ---------------------------------------------------------------
2388    // build_orderless_transaction
2389    // ---------------------------------------------------------------
2390
2391    #[cfg(feature = "ed25519")]
2392    #[tokio::test]
2393    async fn test_build_orderless_transaction() {
2394        let server = MockServer::start().await;
2395        mount_gas_price(&server).await;
2396        mount_ledger(&server, 4).await;
2397
2398        let aptos = create_mock_aptos(&server);
2399        let account = crate::account::Ed25519Account::generate();
2400        let payload = crate::transaction::EntryFunction::apt_transfer(AccountAddress::ONE, 1000)
2401            .unwrap()
2402            .into();
2403
2404        let raw_txn = aptos
2405            .build_orderless_transaction(&account, payload, Some(99))
2406            .await
2407            .unwrap();
2408        // Orderless transactions encode a sentinel sequence number of u64::MAX.
2409        assert_eq!(raw_txn.sequence_number, u64::MAX);
2410        assert_eq!(raw_txn.sender, account.address());
2411        assert_eq!(raw_txn.chain_id, ChainId::new(4));
2412    }
2413
2414    // ---------------------------------------------------------------
2415    // sign_and_submit / sign_submit_and_wait (build + submit)
2416    // ---------------------------------------------------------------
2417
2418    #[cfg(feature = "ed25519")]
2419    #[tokio::test]
2420    async fn test_sign_and_submit() {
2421        let server = MockServer::start().await;
2422        mount_build_flow(&server, 0, 4).await;
2423        mount_submit(&server).await;
2424
2425        let aptos = create_mock_aptos(&server);
2426        let account = crate::account::Ed25519Account::generate();
2427        let payload = crate::transaction::EntryFunction::apt_transfer(AccountAddress::ONE, 1000)
2428            .unwrap()
2429            .into();
2430
2431        let pending = aptos.sign_and_submit(&account, payload).await.unwrap();
2432        assert_eq!(pending.into_inner().hash().to_string(), TEST_TXN_HASH);
2433    }
2434
2435    #[cfg(feature = "ed25519")]
2436    #[tokio::test]
2437    async fn test_sign_submit_and_wait() {
2438        let server = MockServer::start().await;
2439        mount_build_flow(&server, 0, 4).await;
2440        mount_submit(&server).await;
2441        mount_wait(&server).await;
2442
2443        let aptos = create_mock_aptos(&server);
2444        let account = crate::account::Ed25519Account::generate();
2445        let payload = crate::transaction::EntryFunction::apt_transfer(AccountAddress::ONE, 1000)
2446            .unwrap()
2447            .into();
2448
2449        let response = aptos
2450            .sign_submit_and_wait(&account, payload, None)
2451            .await
2452            .unwrap();
2453        assert_eq!(
2454            response
2455                .into_inner()
2456                .get("success")
2457                .and_then(serde_json::Value::as_bool),
2458            Some(true)
2459        );
2460    }
2461
2462    #[cfg(feature = "ed25519")]
2463    #[tokio::test]
2464    async fn test_sign_and_submit_orderless() {
2465        let server = MockServer::start().await;
2466        mount_gas_price(&server).await;
2467        mount_ledger(&server, 4).await;
2468        mount_submit(&server).await;
2469
2470        let aptos = create_mock_aptos(&server);
2471        let account = crate::account::Ed25519Account::generate();
2472        let payload = crate::transaction::EntryFunction::apt_transfer(AccountAddress::ONE, 1000)
2473            .unwrap()
2474            .into();
2475
2476        let pending = aptos
2477            .sign_and_submit_orderless(&account, payload, Some(7))
2478            .await
2479            .unwrap();
2480        assert_eq!(pending.into_inner().hash().to_string(), TEST_TXN_HASH);
2481    }
2482
2483    // ---------------------------------------------------------------
2484    // simulate (account) / estimate_gas / simulate_and_submit
2485    // ---------------------------------------------------------------
2486
2487    #[cfg(feature = "ed25519")]
2488    #[tokio::test]
2489    async fn test_simulate_with_account() {
2490        let server = MockServer::start().await;
2491        mount_build_flow(&server, 0, 4).await;
2492        mount_simulate(&server).await;
2493
2494        let aptos = create_mock_aptos(&server);
2495        let account = crate::account::Ed25519Account::generate();
2496        let payload = crate::transaction::EntryFunction::apt_transfer(AccountAddress::ONE, 1000)
2497            .unwrap()
2498            .into();
2499
2500        let result = aptos.simulate(&account, payload).await.unwrap();
2501        assert!(result.success());
2502        assert_eq!(result.gas_used(), 1500);
2503    }
2504
2505    #[cfg(feature = "ed25519")]
2506    #[tokio::test]
2507    async fn test_estimate_gas() {
2508        let server = MockServer::start().await;
2509        mount_build_flow(&server, 0, 4).await;
2510        mount_simulate(&server).await;
2511
2512        let aptos = create_mock_aptos(&server);
2513        let account = crate::account::Ed25519Account::generate();
2514        let payload = crate::transaction::EntryFunction::apt_transfer(AccountAddress::ONE, 1000)
2515            .unwrap()
2516            .into();
2517
2518        let gas = aptos.estimate_gas(&account, payload).await.unwrap();
2519        // safe_gas_estimate adds a 20% margin to the simulated 1500 gas units.
2520        assert_eq!(gas, 1800);
2521    }
2522
2523    #[cfg(feature = "ed25519")]
2524    #[tokio::test]
2525    async fn test_estimate_gas_simulation_failed() {
2526        let server = MockServer::start().await;
2527        mount_build_flow(&server, 0, 4).await;
2528        // Simulation reports failure -> estimate_gas surfaces SimulationFailed.
2529        Mock::given(method("POST"))
2530            .and(path("/v1/transactions/simulate"))
2531            .respond_with(
2532                ResponseTemplate::new(200).set_body_json(serde_json::json!([{
2533                    "success": false,
2534                    "vm_status": "Move abort in 0x1::coin",
2535                    "gas_used": "10",
2536                    "max_gas_amount": "200000",
2537                    "gas_unit_price": "100",
2538                    "hash": "0xabc",
2539                    "changes": [],
2540                    "events": []
2541                }])),
2542            )
2543            .mount(&server)
2544            .await;
2545
2546        let aptos = create_mock_aptos(&server);
2547        let account = crate::account::Ed25519Account::generate();
2548        let payload = crate::transaction::EntryFunction::apt_transfer(AccountAddress::ONE, 1000)
2549            .unwrap()
2550            .into();
2551
2552        let err = aptos.estimate_gas(&account, payload).await.unwrap_err();
2553        assert!(matches!(err, AptosError::SimulationFailed(_)));
2554    }
2555
2556    #[cfg(feature = "ed25519")]
2557    #[tokio::test]
2558    async fn test_simulate_and_submit() {
2559        let server = MockServer::start().await;
2560        mount_build_flow(&server, 0, 4).await;
2561        mount_simulate(&server).await;
2562        mount_submit(&server).await;
2563
2564        let aptos = create_mock_aptos(&server);
2565        let account = crate::account::Ed25519Account::generate();
2566        let payload = crate::transaction::EntryFunction::apt_transfer(AccountAddress::ONE, 1000)
2567            .unwrap()
2568            .into();
2569
2570        let pending = aptos.simulate_and_submit(&account, payload).await.unwrap();
2571        assert_eq!(pending.into_inner().hash().to_string(), TEST_TXN_HASH);
2572    }
2573
2574    #[cfg(feature = "ed25519")]
2575    #[tokio::test]
2576    async fn test_simulate_submit_and_wait() {
2577        let server = MockServer::start().await;
2578        mount_build_flow(&server, 0, 4).await;
2579        mount_simulate(&server).await;
2580        mount_submit(&server).await;
2581        mount_wait(&server).await;
2582
2583        let aptos = create_mock_aptos(&server);
2584        let account = crate::account::Ed25519Account::generate();
2585        let payload = crate::transaction::EntryFunction::apt_transfer(AccountAddress::ONE, 1000)
2586            .unwrap()
2587            .into();
2588
2589        let response = aptos
2590            .simulate_submit_and_wait(&account, payload, None)
2591            .await
2592            .unwrap();
2593        assert_eq!(
2594            response
2595                .into_inner()
2596                .get("success")
2597                .and_then(serde_json::Value::as_bool),
2598            Some(true)
2599        );
2600    }
2601
2602    // ---------------------------------------------------------------
2603    // Transfers (build + submit + wait full flow)
2604    // ---------------------------------------------------------------
2605
2606    #[cfg(feature = "ed25519")]
2607    #[tokio::test]
2608    async fn test_transfer_apt() {
2609        let server = MockServer::start().await;
2610        mount_build_flow(&server, 0, 4).await;
2611        mount_submit(&server).await;
2612        mount_wait(&server).await;
2613
2614        let aptos = create_mock_aptos(&server);
2615        let sender = crate::account::Ed25519Account::generate();
2616        let recipient = AccountAddress::from_hex("0x123").unwrap();
2617        let response = aptos.transfer_apt(&sender, recipient, 1000).await.unwrap();
2618        assert_eq!(
2619            response
2620                .into_inner()
2621                .get("success")
2622                .and_then(serde_json::Value::as_bool),
2623            Some(true)
2624        );
2625    }
2626
2627    #[cfg(feature = "ed25519")]
2628    #[tokio::test]
2629    async fn test_transfer_coin() {
2630        let server = MockServer::start().await;
2631        mount_build_flow(&server, 0, 4).await;
2632        mount_submit(&server).await;
2633        mount_wait(&server).await;
2634
2635        let aptos = create_mock_aptos(&server);
2636        let sender = crate::account::Ed25519Account::generate();
2637        let recipient = AccountAddress::from_hex("0x123").unwrap();
2638        let coin_type = TypeTag::aptos_coin();
2639        let response = aptos
2640            .transfer_coin(&sender, recipient, coin_type, 1000)
2641            .await
2642            .unwrap();
2643        assert_eq!(
2644            response
2645                .into_inner()
2646                .get("success")
2647                .and_then(serde_json::Value::as_bool),
2648            Some(true)
2649        );
2650    }
2651
2652    #[cfg(feature = "ed25519")]
2653    #[tokio::test]
2654    async fn test_transfer_fungible_asset() {
2655        let server = MockServer::start().await;
2656        mount_build_flow(&server, 0, 4).await;
2657        mount_submit(&server).await;
2658        mount_wait(&server).await;
2659
2660        let aptos = create_mock_aptos(&server);
2661        let sender = crate::account::Ed25519Account::generate();
2662        let metadata = AccountAddress::from_hex("0xa").unwrap();
2663        let recipient = AccountAddress::from_hex("0x123").unwrap();
2664        let response = aptos
2665            .transfer_fungible_asset(&sender, metadata, recipient, 1000)
2666            .await
2667            .unwrap();
2668        assert_eq!(
2669            response
2670                .into_inner()
2671                .get("success")
2672                .and_then(serde_json::Value::as_bool),
2673            Some(true)
2674        );
2675    }
2676
2677    #[cfg(feature = "ed25519")]
2678    #[tokio::test]
2679    async fn test_transfer_object() {
2680        let server = MockServer::start().await;
2681        mount_build_flow(&server, 0, 4).await;
2682        mount_submit(&server).await;
2683        mount_wait(&server).await;
2684
2685        let aptos = create_mock_aptos(&server);
2686        let owner = crate::account::Ed25519Account::generate();
2687        let object = AccountAddress::from_hex("0xb").unwrap();
2688        let to = AccountAddress::from_hex("0x123").unwrap();
2689        let response = aptos.transfer_object(&owner, object, to).await.unwrap();
2690        assert_eq!(
2691            response
2692                .into_inner()
2693                .get("success")
2694                .and_then(serde_json::Value::as_bool),
2695            Some(true)
2696        );
2697    }
2698
2699    #[cfg(feature = "ed25519")]
2700    #[tokio::test]
2701    async fn test_transfer_digital_asset() {
2702        let server = MockServer::start().await;
2703        mount_build_flow(&server, 0, 4).await;
2704        mount_submit(&server).await;
2705        mount_wait(&server).await;
2706
2707        let aptos = create_mock_aptos(&server);
2708        let owner = crate::account::Ed25519Account::generate();
2709        let token = AccountAddress::from_hex("0xc").unwrap();
2710        let to = AccountAddress::from_hex("0x123").unwrap();
2711        let response = aptos
2712            .transfer_digital_asset(&owner, token, to)
2713            .await
2714            .unwrap();
2715        assert_eq!(
2716            response
2717                .into_inner()
2718                .get("success")
2719                .and_then(serde_json::Value::as_bool),
2720            Some(true)
2721        );
2722    }
2723
2724    // ---------------------------------------------------------------
2725    // Tables & BCS view functions
2726    // ---------------------------------------------------------------
2727
2728    #[tokio::test]
2729    async fn test_get_table_item() {
2730        let server = MockServer::start().await;
2731        Mock::given(method("POST"))
2732            .and(path_regex(r"^/v1/tables/.+/item$"))
2733            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!("42")))
2734            .expect(1)
2735            .mount(&server)
2736            .await;
2737
2738        let aptos = create_mock_aptos(&server);
2739        let value = aptos
2740            .get_table_item("0xabc", "address", "u64", serde_json::json!("0x2"))
2741            .await
2742            .unwrap();
2743        assert_eq!(value.as_str(), Some("42"));
2744    }
2745
2746    #[tokio::test]
2747    async fn test_view_bcs_typed() {
2748        let server = MockServer::start().await;
2749        // view_bcs returns raw BCS bytes; encode a u64 the way the node would.
2750        let body = aptos_bcs::to_bytes(&1_000_000u64).unwrap();
2751        Mock::given(method("POST"))
2752            .and(path("/v1/view"))
2753            .respond_with(ResponseTemplate::new(200).set_body_bytes(body))
2754            .expect(1)
2755            .mount(&server)
2756            .await;
2757
2758        let aptos = create_mock_aptos(&server);
2759        let args = vec![aptos_bcs::to_bytes(&AccountAddress::ONE).unwrap()];
2760        let balance: u64 = aptos
2761            .view_bcs("0x1::coin::balance", vec![], args)
2762            .await
2763            .unwrap();
2764        assert_eq!(balance, 1_000_000);
2765    }
2766
2767    #[tokio::test]
2768    async fn test_view_bcs_raw() {
2769        let server = MockServer::start().await;
2770        let body = aptos_bcs::to_bytes(&7u64).unwrap();
2771        Mock::given(method("POST"))
2772            .and(path("/v1/view"))
2773            .respond_with(ResponseTemplate::new(200).set_body_bytes(body.clone()))
2774            .expect(1)
2775            .mount(&server)
2776            .await;
2777
2778        let aptos = create_mock_aptos(&server);
2779        let args = vec![aptos_bcs::to_bytes(&AccountAddress::ONE).unwrap()];
2780        let raw = aptos
2781            .view_bcs_raw("0x1::coin::balance", vec![], args)
2782            .await
2783            .unwrap();
2784        assert_eq!(raw, body);
2785    }
2786
2787    // ---------------------------------------------------------------
2788    // Batch helpers
2789    // ---------------------------------------------------------------
2790
2791    #[cfg(feature = "ed25519")]
2792    #[tokio::test]
2793    async fn test_submit_batch() {
2794        let server = MockServer::start().await;
2795        mount_build_flow(&server, 0, 4).await;
2796        mount_submit(&server).await;
2797
2798        let aptos = create_mock_aptos(&server);
2799        let account = crate::account::Ed25519Account::generate();
2800        let payloads: Vec<TransactionPayload> = vec![
2801            crate::transaction::EntryFunction::apt_transfer(AccountAddress::ONE, 1000)
2802                .unwrap()
2803                .into(),
2804            crate::transaction::EntryFunction::apt_transfer(
2805                AccountAddress::from_hex("0x2").unwrap(),
2806                2000,
2807            )
2808            .unwrap()
2809            .into(),
2810        ];
2811
2812        let results = aptos.submit_batch(&account, payloads).await.unwrap();
2813        assert_eq!(results.len(), 2);
2814    }
2815
2816    #[cfg(feature = "ed25519")]
2817    #[tokio::test]
2818    async fn test_batch_transfer_apt() {
2819        let server = MockServer::start().await;
2820        mount_build_flow(&server, 0, 4).await;
2821        mount_submit(&server).await;
2822        mount_wait(&server).await;
2823
2824        let aptos = create_mock_aptos(&server);
2825        let sender = crate::account::Ed25519Account::generate();
2826        let transfers = vec![
2827            (AccountAddress::from_hex("0x123").unwrap(), 1000u64),
2828            (AccountAddress::from_hex("0x456").unwrap(), 2000u64),
2829        ];
2830
2831        let results = aptos.batch_transfer_apt(&sender, transfers).await.unwrap();
2832        assert_eq!(results.len(), 2);
2833    }
2834}