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}