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    RawTransaction, SignedTransaction, TransactionBuilder, TransactionPayload,
11};
12use crate::types::{AccountAddress, ChainId};
13use std::sync::{Arc, RwLock};
14use std::time::Duration;
15
16#[cfg(feature = "ed25519")]
17use crate::transaction::EntryFunction;
18#[cfg(feature = "ed25519")]
19use crate::types::TypeTag;
20
21#[cfg(feature = "faucet")]
22use crate::api::FaucetClient;
23#[cfg(feature = "faucet")]
24use crate::types::HashValue;
25
26#[cfg(feature = "indexer")]
27use crate::api::IndexerClient;
28
29/// The main entry point for the Aptos SDK.
30///
31/// This struct provides a unified interface for interacting with the Aptos blockchain,
32/// including account management, transaction building and submission, and queries.
33///
34/// # Example
35///
36/// ```rust,no_run
37/// use aptos_sdk::{Aptos, AptosConfig};
38///
39/// #[tokio::main]
40/// async fn main() -> anyhow::Result<()> {
41///     // Create client for testnet
42///     let aptos = Aptos::new(AptosConfig::testnet())?;
43///
44///     // Get ledger info
45///     let ledger = aptos.ledger_info().await?;
46///     println!("Ledger version: {:?}", ledger.version());
47///
48///     Ok(())
49/// }
50/// ```
51#[derive(Debug)]
52pub struct Aptos {
53    config: AptosConfig,
54    fullnode: Arc<FullnodeClient>,
55    /// Resolved chain ID. Initialized from config; lazily fetched from node
56    /// for custom networks where the chain ID is unknown (0).
57    chain_id: RwLock<ChainId>,
58    #[cfg(feature = "faucet")]
59    faucet: Option<FaucetClient>,
60    #[cfg(feature = "indexer")]
61    indexer: Option<IndexerClient>,
62}
63
64impl Aptos {
65    /// Creates a new Aptos client with the given configuration.
66    ///
67    /// # Errors
68    ///
69    /// Returns an error if the HTTP client fails to build (e.g., invalid TLS configuration).
70    pub fn new(config: AptosConfig) -> AptosResult<Self> {
71        let fullnode = Arc::new(FullnodeClient::new(config.clone())?);
72
73        #[cfg(feature = "faucet")]
74        let faucet = FaucetClient::new(&config).ok();
75
76        #[cfg(feature = "indexer")]
77        let indexer = IndexerClient::new(&config).ok();
78
79        let chain_id = RwLock::new(config.chain_id());
80
81        Ok(Self {
82            config,
83            fullnode,
84            chain_id,
85            #[cfg(feature = "faucet")]
86            faucet,
87            #[cfg(feature = "indexer")]
88            indexer,
89        })
90    }
91
92    /// Creates a client for testnet with default settings.
93    ///
94    /// # Errors
95    ///
96    /// Returns an error if the HTTP client fails to build (e.g., invalid TLS configuration).
97    pub fn testnet() -> AptosResult<Self> {
98        Self::new(AptosConfig::testnet())
99    }
100
101    /// Creates a client for devnet with default settings.
102    ///
103    /// # Errors
104    ///
105    /// Returns an error if the HTTP client fails to build (e.g., invalid TLS configuration).
106    pub fn devnet() -> AptosResult<Self> {
107        Self::new(AptosConfig::devnet())
108    }
109
110    /// Creates a client for mainnet with default settings.
111    ///
112    /// # Errors
113    ///
114    /// Returns an error if the HTTP client fails to build (e.g., invalid TLS configuration).
115    pub fn mainnet() -> AptosResult<Self> {
116        Self::new(AptosConfig::mainnet())
117    }
118
119    /// Creates a client for local development network.
120    ///
121    /// # Errors
122    ///
123    /// Returns an error if the HTTP client fails to build (e.g., invalid TLS configuration).
124    pub fn local() -> AptosResult<Self> {
125        Self::new(AptosConfig::local())
126    }
127
128    /// Returns the configuration.
129    pub fn config(&self) -> &AptosConfig {
130        &self.config
131    }
132
133    /// Returns the fullnode client.
134    pub fn fullnode(&self) -> &FullnodeClient {
135        &self.fullnode
136    }
137
138    /// Returns the faucet client, if available.
139    #[cfg(feature = "faucet")]
140    pub fn faucet(&self) -> Option<&FaucetClient> {
141        self.faucet.as_ref()
142    }
143
144    /// Returns the indexer client, if available.
145    #[cfg(feature = "indexer")]
146    pub fn indexer(&self) -> Option<&IndexerClient> {
147        self.indexer.as_ref()
148    }
149
150    // === Ledger Info ===
151
152    /// Gets the current ledger information.
153    ///
154    /// As a side effect, this also resolves the chain ID if it was unknown
155    /// (e.g., for custom network configurations).
156    ///
157    /// # Errors
158    ///
159    /// Returns an error if the HTTP request fails, the API returns an error status code,
160    /// or the response cannot be parsed.
161    ///
162    /// # Panics
163    ///
164    /// Panics if the internal `chain_id` lock is poisoned (only possible if another thread
165    /// panicked while holding the lock).
166    pub async fn ledger_info(&self) -> AptosResult<crate::api::response::LedgerInfo> {
167        let response = self.fullnode.get_ledger_info().await?;
168        let info = response.into_inner();
169
170        // Update chain_id if it was unknown (custom network)
171        if self.chain_id.read().expect("chain_id lock poisoned").id() == 0 && info.chain_id > 0 {
172            *self.chain_id.write().expect("chain_id lock poisoned") = ChainId::new(info.chain_id);
173        }
174
175        Ok(info)
176    }
177
178    /// Returns the current chain ID.
179    ///
180    /// For known networks (mainnet, testnet, devnet, local), this returns the
181    /// well-known chain ID immediately. For custom networks, this returns
182    /// `ChainId(0)` until the chain ID is resolved via [`ensure_chain_id`](Self::ensure_chain_id)
183    /// or any method that makes a request to the node (e.g., [`build_transaction`](Self::build_transaction),
184    /// [`ledger_info`](Self::ledger_info)).
185    ///
186    /// # Panics
187    ///
188    /// Panics if the internal `chain_id` lock is poisoned.
189    pub fn chain_id(&self) -> ChainId {
190        *self.chain_id.read().expect("chain_id lock poisoned")
191    }
192
193    /// Resolves the chain ID from the node if it is unknown.
194    ///
195    /// For known networks, this returns the chain ID immediately without
196    /// making a network request. For custom networks (chain ID 0), this
197    /// fetches the ledger info from the node to discover the actual chain ID
198    /// and caches it for future use.
199    ///
200    /// This is called automatically by [`build_transaction`](Self::build_transaction)
201    /// and other transaction methods, so you typically don't need to call it
202    /// directly unless you need the chain ID before building a transaction.
203    ///
204    /// # Errors
205    ///
206    /// Returns an error if the HTTP request to fetch ledger info fails.
207    ///
208    /// # Panics
209    ///
210    /// Panics if the internal `chain_id` lock is poisoned.
211    pub async fn ensure_chain_id(&self) -> AptosResult<ChainId> {
212        {
213            let chain_id = self.chain_id.read().expect("chain_id lock poisoned");
214            if chain_id.id() > 0 {
215                return Ok(*chain_id);
216            }
217        }
218        // Chain ID is unknown; fetch from node
219        let response = self.fullnode.get_ledger_info().await?;
220        let info = response.into_inner();
221        let new_chain_id = ChainId::new(info.chain_id);
222        *self.chain_id.write().expect("chain_id lock poisoned") = new_chain_id;
223        Ok(new_chain_id)
224    }
225
226    // === Account ===
227
228    /// Gets the sequence number for an account.
229    ///
230    /// # Errors
231    ///
232    /// Returns an error if the HTTP request fails, the API returns an error status code
233    /// (e.g., account not found 404), or the response cannot be parsed.
234    pub async fn get_sequence_number(&self, address: AccountAddress) -> AptosResult<u64> {
235        self.fullnode.get_sequence_number(address).await
236    }
237
238    /// Gets the APT balance for an account.
239    ///
240    /// # Errors
241    ///
242    /// Returns an error if the HTTP request fails, the API returns an error status code,
243    /// or the response cannot be parsed.
244    pub async fn get_balance(&self, address: AccountAddress) -> AptosResult<u64> {
245        self.fullnode.get_account_balance(address).await
246    }
247
248    /// Checks if an account exists.
249    ///
250    /// # Errors
251    ///
252    /// Returns an error if the HTTP request fails or the API returns an error status code
253    /// other than 404 (not found). A 404 error is handled gracefully and returns `Ok(false)`.
254    pub async fn account_exists(&self, address: AccountAddress) -> AptosResult<bool> {
255        match self.fullnode.get_account(address).await {
256            Ok(_) => Ok(true),
257            Err(AptosError::Api {
258                status_code: 404, ..
259            }) => Ok(false),
260            Err(e) => Err(e),
261        }
262    }
263
264    // === Transactions ===
265
266    /// Builds a transaction for the given account.
267    ///
268    /// This automatically fetches the sequence number and gas price.
269    ///
270    /// # Errors
271    ///
272    /// Returns an error if fetching the sequence number fails, fetching the gas price fails,
273    /// or if the transaction builder fails to construct a valid transaction (e.g., missing
274    /// required fields).
275    pub async fn build_transaction<A: Account>(
276        &self,
277        sender: &A,
278        payload: TransactionPayload,
279    ) -> AptosResult<RawTransaction> {
280        // Fetch sequence number, gas price, and chain ID in parallel
281        let (sequence_number, gas_estimation, chain_id) = tokio::join!(
282            self.get_sequence_number(sender.address()),
283            self.fullnode.estimate_gas_price(),
284            self.ensure_chain_id()
285        );
286        let sequence_number = sequence_number?;
287        let gas_estimation = gas_estimation?;
288        let chain_id = chain_id?;
289
290        TransactionBuilder::new()
291            .sender(sender.address())
292            .sequence_number(sequence_number)
293            .payload(payload)
294            .gas_unit_price(gas_estimation.data.recommended())
295            .chain_id(chain_id)
296            .expiration_from_now(600)
297            .build()
298    }
299
300    /// Signs and submits a transaction.
301    ///
302    /// # Errors
303    ///
304    /// Returns an error if building the transaction fails, signing fails (e.g., invalid key),
305    /// the transaction cannot be serialized to BCS, the HTTP request fails, or the API returns
306    /// an error status code.
307    #[cfg(feature = "ed25519")]
308    pub async fn sign_and_submit<A: Account>(
309        &self,
310        account: &A,
311        payload: TransactionPayload,
312    ) -> AptosResult<AptosResponse<PendingTransaction>> {
313        let raw_txn = self.build_transaction(account, payload).await?;
314        let signed = crate::transaction::builder::sign_transaction(&raw_txn, account)?;
315        self.fullnode.submit_transaction(&signed).await
316    }
317
318    /// Signs, submits, and waits for a transaction to complete.
319    ///
320    /// # Errors
321    ///
322    /// Returns an error if building the transaction fails, signing fails, submission fails,
323    /// the transaction times out waiting for commitment, the transaction execution fails,
324    /// or any HTTP/API errors occur.
325    #[cfg(feature = "ed25519")]
326    pub async fn sign_submit_and_wait<A: Account>(
327        &self,
328        account: &A,
329        payload: TransactionPayload,
330        timeout: Option<Duration>,
331    ) -> AptosResult<AptosResponse<serde_json::Value>> {
332        let raw_txn = self.build_transaction(account, payload).await?;
333        let signed = crate::transaction::builder::sign_transaction(&raw_txn, account)?;
334        self.fullnode.submit_and_wait(&signed, timeout).await
335    }
336
337    /// Submits a pre-signed transaction.
338    ///
339    /// # Errors
340    ///
341    /// Returns an error if the transaction cannot be serialized to BCS, the HTTP request fails,
342    /// or the API returns an error status code.
343    pub async fn submit_transaction(
344        &self,
345        signed_txn: &SignedTransaction,
346    ) -> AptosResult<AptosResponse<PendingTransaction>> {
347        self.fullnode.submit_transaction(signed_txn).await
348    }
349
350    /// Submits and waits for a pre-signed transaction.
351    ///
352    /// # Errors
353    ///
354    /// Returns an error if transaction submission fails, the transaction times out waiting
355    /// for commitment, the transaction execution fails (`vm_status` indicates failure),
356    /// or any HTTP/API errors occur.
357    pub async fn submit_and_wait(
358        &self,
359        signed_txn: &SignedTransaction,
360        timeout: Option<Duration>,
361    ) -> AptosResult<AptosResponse<serde_json::Value>> {
362        self.fullnode.submit_and_wait(signed_txn, timeout).await
363    }
364
365    /// Simulates a transaction.
366    ///
367    /// # Errors
368    ///
369    /// Returns an error if the transaction cannot be serialized to BCS, the HTTP request fails,
370    /// the API returns an error status code, or the response cannot be parsed as JSON.
371    pub async fn simulate_transaction(
372        &self,
373        signed_txn: &SignedTransaction,
374    ) -> AptosResult<AptosResponse<Vec<serde_json::Value>>> {
375        self.fullnode.simulate_transaction(signed_txn).await
376    }
377
378    /// Simulates a transaction and returns a parsed result.
379    ///
380    /// This method provides a more ergonomic way to simulate transactions
381    /// with detailed result parsing.
382    ///
383    /// # Example
384    ///
385    /// ```rust,ignore
386    /// let result = aptos.simulate(&account, payload).await?;
387    /// if result.success() {
388    ///     println!("Gas estimate: {}", result.gas_used());
389    /// } else {
390    ///     println!("Would fail: {}", result.error_message().unwrap_or_default());
391    /// }
392    /// ```
393    ///
394    /// # Errors
395    ///
396    /// Returns an error if building the transaction fails, signing fails, simulation fails,
397    /// or the simulation response cannot be parsed.
398    #[cfg(feature = "ed25519")]
399    pub async fn simulate<A: Account>(
400        &self,
401        account: &A,
402        payload: TransactionPayload,
403    ) -> AptosResult<crate::transaction::SimulationResult> {
404        let raw_txn = self.build_transaction(account, payload).await?;
405        let signed = crate::transaction::builder::sign_transaction(&raw_txn, account)?;
406        let response = self.fullnode.simulate_transaction(&signed).await?;
407        crate::transaction::SimulationResult::from_response(response.into_inner())
408    }
409
410    /// Simulates a transaction with a pre-built signed transaction.
411    ///
412    /// # Errors
413    ///
414    /// Returns an error if simulation fails or the simulation response cannot be parsed.
415    pub async fn simulate_signed(
416        &self,
417        signed_txn: &SignedTransaction,
418    ) -> AptosResult<crate::transaction::SimulationResult> {
419        let response = self.fullnode.simulate_transaction(signed_txn).await?;
420        crate::transaction::SimulationResult::from_response(response.into_inner())
421    }
422
423    /// Estimates gas for a transaction by simulating it.
424    ///
425    /// Returns the estimated gas usage with a 20% safety margin.
426    ///
427    /// # Example
428    ///
429    /// ```rust,ignore
430    /// let gas = aptos.estimate_gas(&account, payload).await?;
431    /// println!("Estimated gas: {}", gas);
432    /// ```
433    ///
434    /// # Errors
435    ///
436    /// Returns an error if simulation fails or if the simulation indicates the transaction
437    /// would fail (returns [`AptosError::SimulationFailed`]).
438    #[cfg(feature = "ed25519")]
439    pub async fn estimate_gas<A: Account>(
440        &self,
441        account: &A,
442        payload: TransactionPayload,
443    ) -> AptosResult<u64> {
444        let result = self.simulate(account, payload).await?;
445        if result.success() {
446            Ok(result.safe_gas_estimate())
447        } else {
448            Err(AptosError::SimulationFailed(
449                result
450                    .error_message()
451                    .unwrap_or_else(|| result.vm_status().to_string()),
452            ))
453        }
454    }
455
456    /// Simulates and submits a transaction if successful.
457    ///
458    /// This is a "dry run" approach that first simulates the transaction
459    /// to verify it will succeed before actually submitting it.
460    ///
461    /// # Example
462    ///
463    /// ```rust,ignore
464    /// let result = aptos.simulate_and_submit(&account, payload).await?;
465    /// println!("Transaction submitted: {}", result.hash);
466    /// ```
467    ///
468    /// # Errors
469    ///
470    /// Returns an error if building the transaction fails, signing fails, simulation fails,
471    /// the simulation indicates the transaction would fail (returns [`AptosError::SimulationFailed`]),
472    /// or transaction submission fails.
473    #[cfg(feature = "ed25519")]
474    pub async fn simulate_and_submit<A: Account>(
475        &self,
476        account: &A,
477        payload: TransactionPayload,
478    ) -> AptosResult<AptosResponse<PendingTransaction>> {
479        // First simulate
480        let raw_txn = self.build_transaction(account, payload.clone()).await?;
481        let signed = crate::transaction::builder::sign_transaction(&raw_txn, account)?;
482        let sim_response = self.fullnode.simulate_transaction(&signed).await?;
483        let sim_result =
484            crate::transaction::SimulationResult::from_response(sim_response.into_inner())?;
485
486        if sim_result.failed() {
487            return Err(AptosError::SimulationFailed(
488                sim_result
489                    .error_message()
490                    .unwrap_or_else(|| sim_result.vm_status().to_string()),
491            ));
492        }
493
494        // Submit the same signed transaction
495        self.fullnode.submit_transaction(&signed).await
496    }
497
498    /// Simulates, submits, and waits for a transaction.
499    ///
500    /// Like `simulate_and_submit` but also waits for the transaction to complete.
501    ///
502    /// # Errors
503    ///
504    /// Returns an error if building the transaction fails, signing fails, simulation fails,
505    /// the simulation indicates the transaction would fail (returns [`AptosError::SimulationFailed`]),
506    /// submission fails, the transaction times out waiting for commitment, or the transaction
507    /// execution fails.
508    #[cfg(feature = "ed25519")]
509    pub async fn simulate_submit_and_wait<A: Account>(
510        &self,
511        account: &A,
512        payload: TransactionPayload,
513        timeout: Option<Duration>,
514    ) -> AptosResult<AptosResponse<serde_json::Value>> {
515        // First simulate
516        let raw_txn = self.build_transaction(account, payload.clone()).await?;
517        let signed = crate::transaction::builder::sign_transaction(&raw_txn, account)?;
518        let sim_response = self.fullnode.simulate_transaction(&signed).await?;
519        let sim_result =
520            crate::transaction::SimulationResult::from_response(sim_response.into_inner())?;
521
522        if sim_result.failed() {
523            return Err(AptosError::SimulationFailed(
524                sim_result
525                    .error_message()
526                    .unwrap_or_else(|| sim_result.vm_status().to_string()),
527            ));
528        }
529
530        // Submit and wait
531        self.fullnode.submit_and_wait(&signed, timeout).await
532    }
533
534    // === Transfers ===
535
536    /// Transfers APT from one account to another.
537    ///
538    /// # Errors
539    ///
540    /// Returns an error if building the transfer payload fails (e.g., invalid address),
541    /// signing fails, submission fails, the transaction times out, or the transaction
542    /// execution fails.
543    #[cfg(feature = "ed25519")]
544    pub async fn transfer_apt<A: Account>(
545        &self,
546        sender: &A,
547        recipient: AccountAddress,
548        amount: u64,
549    ) -> AptosResult<AptosResponse<serde_json::Value>> {
550        let payload = EntryFunction::apt_transfer(recipient, amount)?;
551        self.sign_submit_and_wait(sender, payload.into(), None)
552            .await
553    }
554
555    /// Transfers a coin from one account to another.
556    ///
557    /// # Errors
558    ///
559    /// Returns an error if building the transfer payload fails (e.g., invalid type tag or address),
560    /// signing fails, submission fails, the transaction times out, or the transaction
561    /// execution fails.
562    #[cfg(feature = "ed25519")]
563    pub async fn transfer_coin<A: Account>(
564        &self,
565        sender: &A,
566        recipient: AccountAddress,
567        coin_type: TypeTag,
568        amount: u64,
569    ) -> AptosResult<AptosResponse<serde_json::Value>> {
570        let payload = EntryFunction::coin_transfer(coin_type, recipient, amount)?;
571        self.sign_submit_and_wait(sender, payload.into(), None)
572            .await
573    }
574
575    // === View Functions ===
576
577    /// Calls a view function using JSON encoding.
578    ///
579    /// For lossless serialization of large integers, use [`view_bcs`](Self::view_bcs) instead.
580    ///
581    /// # Errors
582    ///
583    /// Returns an error if the HTTP request fails, the API returns an error status code,
584    /// or the response cannot be parsed as JSON.
585    pub async fn view(
586        &self,
587        function: &str,
588        type_args: Vec<String>,
589        args: Vec<serde_json::Value>,
590    ) -> AptosResult<Vec<serde_json::Value>> {
591        let response = self.fullnode.view(function, type_args, args).await?;
592        Ok(response.into_inner())
593    }
594
595    /// Calls a view function using BCS encoding for both inputs and outputs.
596    ///
597    /// This method provides lossless serialization by using BCS (Binary Canonical Serialization)
598    /// instead of JSON, which is important for large integers (u128, u256) and other types
599    /// where JSON can lose precision.
600    ///
601    /// # Type Parameter
602    ///
603    /// * `T` - The expected return type. Must implement `serde::de::DeserializeOwned`.
604    ///
605    /// # Arguments
606    ///
607    /// * `function` - The fully qualified function name (e.g., `0x1::coin::balance`)
608    /// * `type_args` - Type arguments as strings (e.g., `0x1::aptos_coin::AptosCoin`)
609    /// * `args` - Pre-serialized BCS arguments as byte vectors
610    ///
611    /// # Example
612    ///
613    /// ```rust,ignore
614    /// use aptos_sdk::{Aptos, AptosConfig, AccountAddress};
615    ///
616    /// let aptos = Aptos::new(AptosConfig::testnet())?;
617    /// let owner = AccountAddress::from_hex("0x1")?;
618    ///
619    /// // BCS-encode the argument
620    /// let args = vec![aptos_bcs::to_bytes(&owner)?];
621    ///
622    /// // Call view function with typed return
623    /// let balance: u64 = aptos.view_bcs(
624    ///     "0x1::coin::balance",
625    ///     vec!["0x1::aptos_coin::AptosCoin".to_string()],
626    ///     args,
627    /// ).await?;
628    /// ```
629    ///
630    /// # Errors
631    ///
632    /// Returns an error if the HTTP request fails, the API returns an error status code,
633    /// or the BCS deserialization fails.
634    pub async fn view_bcs<T: serde::de::DeserializeOwned>(
635        &self,
636        function: &str,
637        type_args: Vec<String>,
638        args: Vec<Vec<u8>>,
639    ) -> AptosResult<T> {
640        let response = self.fullnode.view_bcs(function, type_args, args).await?;
641        let bytes = response.into_inner();
642        aptos_bcs::from_bytes(&bytes).map_err(|e| AptosError::Bcs(e.to_string()))
643    }
644
645    /// Calls a view function with BCS inputs and returns raw BCS bytes.
646    ///
647    /// Use this when you need to manually deserialize the response or when
648    /// the return type is complex or dynamic.
649    ///
650    /// # Errors
651    ///
652    /// Returns an error if the HTTP request fails or the API returns an error status code.
653    pub async fn view_bcs_raw(
654        &self,
655        function: &str,
656        type_args: Vec<String>,
657        args: Vec<Vec<u8>>,
658    ) -> AptosResult<Vec<u8>> {
659        let response = self.fullnode.view_bcs(function, type_args, args).await?;
660        Ok(response.into_inner())
661    }
662
663    // === Faucet ===
664
665    /// Funds an account using the faucet.
666    ///
667    /// This method waits for the faucet transactions to be confirmed before returning.
668    ///
669    /// # Errors
670    ///
671    /// Returns an error if the faucet feature is not enabled, the faucet request fails
672    /// (e.g., rate limiting 429, server error 500), waiting for transaction confirmation
673    /// times out, or any HTTP/API errors occur.
674    #[cfg(feature = "faucet")]
675    pub async fn fund_account(
676        &self,
677        address: AccountAddress,
678        amount: u64,
679    ) -> AptosResult<Vec<String>> {
680        let faucet = self
681            .faucet
682            .as_ref()
683            .ok_or_else(|| AptosError::FeatureNotEnabled("faucet".into()))?;
684        let txn_hashes = faucet.fund(address, amount).await?;
685
686        // Parse hashes first to own them
687        let hashes: Vec<HashValue> = txn_hashes
688            .iter()
689            .filter_map(|hash_str| {
690                // Hash might have 0x prefix or not
691                let hash_str_clean = hash_str.strip_prefix("0x").unwrap_or(hash_str);
692                HashValue::from_hex(hash_str_clean).ok()
693            })
694            .collect();
695
696        // Wait for all faucet transactions to be confirmed in parallel
697        let wait_futures: Vec<_> = hashes
698            .iter()
699            .map(|hash| {
700                self.fullnode
701                    .wait_for_transaction(hash, Some(Duration::from_secs(60)))
702            })
703            .collect();
704
705        // Wait for all transactions in parallel
706        let results = futures::future::join_all(wait_futures).await;
707        for result in results {
708            result?;
709        }
710
711        Ok(txn_hashes)
712    }
713
714    #[cfg(all(feature = "faucet", feature = "ed25519"))]
715    /// Creates a funded account.
716    ///
717    /// # Errors
718    ///
719    /// Returns an error if funding the account fails (see [`Self::fund_account`] for details).
720    pub async fn create_funded_account(
721        &self,
722        amount: u64,
723    ) -> AptosResult<crate::account::Ed25519Account> {
724        let account = crate::account::Ed25519Account::generate();
725        self.fund_account(account.address(), amount).await?;
726        Ok(account)
727    }
728
729    // === Transaction Batching ===
730
731    /// Returns a batch operations helper for submitting multiple transactions.
732    ///
733    /// # Example
734    ///
735    /// ```rust,ignore
736    /// let aptos = Aptos::testnet()?;
737    ///
738    /// // Build and submit batch of transfers
739    /// let payloads = vec![
740    ///     EntryFunction::apt_transfer(addr1, 1000)?.into(),
741    ///     EntryFunction::apt_transfer(addr2, 2000)?.into(),
742    ///     EntryFunction::apt_transfer(addr3, 3000)?.into(),
743    /// ];
744    ///
745    /// let results = aptos.batch().submit_and_wait(&sender, payloads, None).await?;
746    /// ```
747    pub fn batch(&self) -> crate::transaction::BatchOperations<'_> {
748        crate::transaction::BatchOperations::new(&self.fullnode, &self.chain_id)
749    }
750
751    /// Submits multiple transactions in parallel.
752    ///
753    /// This is a convenience method that builds, signs, and submits
754    /// multiple transactions at once.
755    ///
756    /// # Arguments
757    ///
758    /// * `account` - The account to sign with
759    /// * `payloads` - The transaction payloads to submit
760    ///
761    /// # Returns
762    ///
763    /// Results for each transaction in the batch.
764    ///
765    /// # Errors
766    ///
767    /// Returns an error if building any transaction fails, signing fails, or submission fails
768    /// for any transaction in the batch.
769    #[cfg(feature = "ed25519")]
770    pub async fn submit_batch<A: Account>(
771        &self,
772        account: &A,
773        payloads: Vec<TransactionPayload>,
774    ) -> AptosResult<Vec<crate::transaction::BatchTransactionResult>> {
775        self.batch().submit(account, payloads).await
776    }
777
778    /// Submits multiple transactions and waits for all to complete.
779    ///
780    /// # Arguments
781    ///
782    /// * `account` - The account to sign with
783    /// * `payloads` - The transaction payloads to submit
784    /// * `timeout` - Optional timeout for waiting
785    ///
786    /// # Returns
787    ///
788    /// Results for each transaction in the batch.
789    ///
790    /// # Errors
791    ///
792    /// Returns an error if building any transaction fails, signing fails, submission fails,
793    /// any transaction times out waiting for commitment, or any transaction execution fails.
794    #[cfg(feature = "ed25519")]
795    pub async fn submit_batch_and_wait<A: Account>(
796        &self,
797        account: &A,
798        payloads: Vec<TransactionPayload>,
799        timeout: Option<Duration>,
800    ) -> AptosResult<Vec<crate::transaction::BatchTransactionResult>> {
801        self.batch()
802            .submit_and_wait(account, payloads, timeout)
803            .await
804    }
805
806    /// Transfers APT to multiple recipients in a batch.
807    ///
808    /// # Arguments
809    ///
810    /// * `sender` - The sending account
811    /// * `transfers` - List of (recipient, amount) pairs
812    ///
813    /// # Example
814    ///
815    /// ```rust,ignore
816    /// let results = aptos.batch_transfer_apt(&sender, vec![
817    ///     (addr1, 1_000_000),  // 0.01 APT
818    ///     (addr2, 2_000_000),  // 0.02 APT
819    ///     (addr3, 3_000_000),  // 0.03 APT
820    /// ]).await?;
821    /// ```
822    ///
823    /// # Errors
824    ///
825    /// Returns an error if building any transfer payload fails, signing fails, submission fails,
826    /// any transaction times out, or any transaction execution fails.
827    #[cfg(feature = "ed25519")]
828    pub async fn batch_transfer_apt<A: Account>(
829        &self,
830        sender: &A,
831        transfers: Vec<(AccountAddress, u64)>,
832    ) -> AptosResult<Vec<crate::transaction::BatchTransactionResult>> {
833        self.batch().transfer_apt(sender, transfers).await
834    }
835}
836
837#[cfg(test)]
838mod tests {
839    use super::*;
840    use wiremock::{
841        Mock, MockServer, ResponseTemplate,
842        matchers::{method, path, path_regex},
843    };
844
845    #[test]
846    fn test_aptos_client_creation() {
847        let aptos = Aptos::testnet();
848        assert!(aptos.is_ok());
849    }
850
851    #[test]
852    fn test_chain_id() {
853        let aptos = Aptos::testnet().unwrap();
854        assert_eq!(aptos.chain_id(), ChainId::testnet());
855
856        let aptos = Aptos::mainnet().unwrap();
857        assert_eq!(aptos.chain_id(), ChainId::mainnet());
858    }
859
860    fn create_mock_aptos(server: &MockServer) -> Aptos {
861        let url = format!("{}/v1", server.uri());
862        let config = AptosConfig::custom(&url).unwrap().without_retry();
863        Aptos::new(config).unwrap()
864    }
865
866    #[tokio::test]
867    async fn test_get_sequence_number() {
868        let server = MockServer::start().await;
869
870        Mock::given(method("GET"))
871            .and(path_regex(r"/v1/accounts/0x[0-9a-f]+"))
872            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
873                "sequence_number": "42",
874                "authentication_key": "0x0000000000000000000000000000000000000000000000000000000000000001"
875            })))
876            .expect(1)
877            .mount(&server)
878            .await;
879
880        let aptos = create_mock_aptos(&server);
881        let seq = aptos
882            .get_sequence_number(AccountAddress::ONE)
883            .await
884            .unwrap();
885        assert_eq!(seq, 42);
886    }
887
888    #[tokio::test]
889    async fn test_get_balance() {
890        let server = MockServer::start().await;
891
892        // get_balance now uses view function instead of CoinStore resource
893        Mock::given(method("POST"))
894            .and(path("/v1/view"))
895            .respond_with(
896                ResponseTemplate::new(200).set_body_json(serde_json::json!(["5000000000"])),
897            )
898            .expect(1)
899            .mount(&server)
900            .await;
901
902        let aptos = create_mock_aptos(&server);
903        let balance = aptos.get_balance(AccountAddress::ONE).await.unwrap();
904        assert_eq!(balance, 5_000_000_000);
905    }
906
907    #[tokio::test]
908    async fn test_get_resources_via_fullnode() {
909        let server = MockServer::start().await;
910
911        Mock::given(method("GET"))
912            .and(path_regex(r"/v1/accounts/0x[0-9a-f]+/resources"))
913            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([
914                {"type": "0x1::account::Account", "data": {}},
915                {"type": "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>", "data": {}}
916            ])))
917            .expect(1)
918            .mount(&server)
919            .await;
920
921        let aptos = create_mock_aptos(&server);
922        let resources = aptos
923            .fullnode()
924            .get_account_resources(AccountAddress::ONE)
925            .await
926            .unwrap();
927        assert_eq!(resources.data.len(), 2);
928    }
929
930    #[tokio::test]
931    async fn test_ledger_info() {
932        let server = MockServer::start().await;
933
934        Mock::given(method("GET"))
935            .and(path("/v1"))
936            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
937                "chain_id": 2,
938                "epoch": "100",
939                "ledger_version": "12345",
940                "oldest_ledger_version": "0",
941                "ledger_timestamp": "1000000",
942                "node_role": "full_node",
943                "oldest_block_height": "0",
944                "block_height": "5000"
945            })))
946            .expect(1)
947            .mount(&server)
948            .await;
949
950        let aptos = create_mock_aptos(&server);
951        let info = aptos.ledger_info().await.unwrap();
952        assert_eq!(info.version().unwrap(), 12345);
953    }
954
955    #[tokio::test]
956    async fn test_config_builder() {
957        let config = AptosConfig::testnet().with_timeout(Duration::from_secs(60));
958
959        let aptos = Aptos::new(config).unwrap();
960        assert_eq!(aptos.chain_id(), ChainId::testnet());
961    }
962
963    #[tokio::test]
964    async fn test_fullnode_accessor() {
965        let server = MockServer::start().await;
966        let aptos = create_mock_aptos(&server);
967
968        // Can access fullnode client directly
969        let fullnode = aptos.fullnode();
970        assert!(fullnode.base_url().as_str().contains(&server.uri()));
971    }
972
973    #[cfg(feature = "ed25519")]
974    #[tokio::test]
975    async fn test_build_transaction() {
976        let server = MockServer::start().await;
977
978        // Mock for getting account
979        Mock::given(method("GET"))
980            .and(path_regex(r"/v1/accounts/0x[0-9a-f]+"))
981            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
982                "sequence_number": "0",
983                "authentication_key": "0x0000000000000000000000000000000000000000000000000000000000000001"
984            })))
985            .expect(1)
986            .mount(&server)
987            .await;
988
989        // Mock for gas price
990        Mock::given(method("GET"))
991            .and(path("/v1/estimate_gas_price"))
992            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
993                "gas_estimate": 100
994            })))
995            .expect(1)
996            .mount(&server)
997            .await;
998
999        // Mock for ledger info (needed for chain_id resolution on custom networks)
1000        Mock::given(method("GET"))
1001            .and(path("/v1"))
1002            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1003                "chain_id": 4,
1004                "epoch": "1",
1005                "ledger_version": "100",
1006                "oldest_ledger_version": "0",
1007                "ledger_timestamp": "1000000",
1008                "node_role": "full_node",
1009                "oldest_block_height": "0",
1010                "block_height": "50"
1011            })))
1012            .expect(1)
1013            .mount(&server)
1014            .await;
1015
1016        let aptos = create_mock_aptos(&server);
1017        let account = crate::account::Ed25519Account::generate();
1018        let recipient = AccountAddress::from_hex("0x123").unwrap();
1019        let payload = crate::transaction::EntryFunction::apt_transfer(recipient, 1000).unwrap();
1020
1021        let raw_txn = aptos
1022            .build_transaction(&account, payload.into())
1023            .await
1024            .unwrap();
1025        assert_eq!(raw_txn.sender, account.address());
1026        assert_eq!(raw_txn.sequence_number, 0);
1027    }
1028
1029    #[cfg(feature = "indexer")]
1030    #[tokio::test]
1031    async fn test_indexer_accessor() {
1032        let aptos = Aptos::testnet().unwrap();
1033        let indexer = aptos.indexer();
1034        assert!(indexer.is_some());
1035    }
1036
1037    #[tokio::test]
1038    async fn test_account_exists_true() {
1039        let server = MockServer::start().await;
1040
1041        Mock::given(method("GET"))
1042            .and(path_regex(r"^/v1/accounts/0x[0-9a-f]+$"))
1043            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1044                "sequence_number": "10",
1045                "authentication_key": "0x0000000000000000000000000000000000000000000000000000000000000001"
1046            })))
1047            .expect(1)
1048            .mount(&server)
1049            .await;
1050
1051        let aptos = create_mock_aptos(&server);
1052        let exists = aptos.account_exists(AccountAddress::ONE).await.unwrap();
1053        assert!(exists);
1054    }
1055
1056    #[tokio::test]
1057    async fn test_account_exists_false() {
1058        let server = MockServer::start().await;
1059
1060        Mock::given(method("GET"))
1061            .and(path_regex(r"^/v1/accounts/0x[0-9a-f]+$"))
1062            .respond_with(ResponseTemplate::new(404).set_body_json(serde_json::json!({
1063                "message": "Account not found",
1064                "error_code": "account_not_found"
1065            })))
1066            .expect(1)
1067            .mount(&server)
1068            .await;
1069
1070        let aptos = create_mock_aptos(&server);
1071        let exists = aptos.account_exists(AccountAddress::ONE).await.unwrap();
1072        assert!(!exists);
1073    }
1074
1075    #[tokio::test]
1076    async fn test_view_function() {
1077        let server = MockServer::start().await;
1078
1079        Mock::given(method("POST"))
1080            .and(path("/v1/view"))
1081            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!(["1000000"])))
1082            .expect(1)
1083            .mount(&server)
1084            .await;
1085
1086        let aptos = create_mock_aptos(&server);
1087        let result: Vec<serde_json::Value> = aptos
1088            .view(
1089                "0x1::coin::balance",
1090                vec!["0x1::aptos_coin::AptosCoin".to_string()],
1091                vec![serde_json::json!("0x1")],
1092            )
1093            .await
1094            .unwrap();
1095
1096        assert_eq!(result.len(), 1);
1097        assert_eq!(result[0].as_str().unwrap(), "1000000");
1098    }
1099
1100    #[tokio::test]
1101    async fn test_chain_id_from_config() {
1102        let aptos = Aptos::mainnet().unwrap();
1103        assert_eq!(aptos.chain_id(), ChainId::mainnet());
1104
1105        let aptos = Aptos::devnet().unwrap();
1106        // Devnet uses chain_id 165
1107        assert_eq!(aptos.chain_id(), ChainId::new(165));
1108    }
1109
1110    #[tokio::test]
1111    async fn test_custom_config() {
1112        let server = MockServer::start().await;
1113        let url = format!("{}/v1", server.uri());
1114        let config = AptosConfig::custom(&url).unwrap();
1115        let aptos = Aptos::new(config).unwrap();
1116
1117        // Custom config should have unknown chain ID
1118        assert_eq!(aptos.chain_id(), ChainId::new(0));
1119    }
1120}