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