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