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}