Skip to main content

aptos_sdk/transaction/
batch.rs

1//! Transaction batching for efficient multi-transaction submission.
2//!
3//! This module provides utilities for building, signing, and submitting
4//! multiple transactions efficiently with automatic sequence number management.
5//!
6//! # Overview
7//!
8//! Transaction batching is useful when you need to:
9//! - Submit multiple transfers at once
10//! - Execute a series of contract calls
11//! - Perform bulk operations efficiently
12//!
13//! # Example
14//!
15//! ```rust,ignore
16//! use aptos_sdk::transaction::batch::TransactionBatch;
17//!
18//! let batch = TransactionBatch::new(&aptos, &sender)
19//!     .add(payload1)
20//!     .add(payload2)
21//!     .add(payload3)
22//!     .build()
23//!     .await?;
24//!
25//! // Submit all transactions in parallel
26//! let results = batch.submit_all().await;
27//!
28//! // Or submit and wait for all to complete
29//! let results = batch.submit_and_wait_all().await;
30//! ```
31
32use crate::account::Account;
33use crate::api::FullnodeClient;
34
35use crate::error::{AptosError, AptosResult};
36use crate::transaction::{
37    RawTransaction, SignedTransaction, TransactionBuilder, TransactionPayload,
38    builder::sign_transaction,
39};
40use crate::types::{AccountAddress, ChainId};
41use futures::future::join_all;
42use std::time::Duration;
43
44/// Result of a single transaction in a batch.
45#[derive(Debug)]
46pub struct BatchTransactionResult {
47    /// Index of the transaction in the batch.
48    pub index: usize,
49    /// The signed transaction that was submitted.
50    pub transaction: SignedTransaction,
51    /// Result of the submission/execution.
52    pub result: Result<BatchTransactionStatus, AptosError>,
53}
54
55/// Status of a batch transaction after submission.
56#[derive(Debug, Clone)]
57pub enum BatchTransactionStatus {
58    /// Transaction was submitted and is pending.
59    Pending {
60        /// The transaction hash.
61        hash: String,
62    },
63    /// Transaction was submitted and confirmed.
64    Confirmed {
65        /// The transaction hash.
66        hash: String,
67        /// Whether the transaction succeeded on-chain.
68        success: bool,
69        /// The transaction version.
70        version: u64,
71        /// Gas used by the transaction.
72        gas_used: u64,
73    },
74    /// Transaction failed to submit.
75    Failed {
76        /// Error message.
77        error: String,
78    },
79}
80
81impl BatchTransactionStatus {
82    /// Returns the transaction hash if available.
83    pub fn hash(&self) -> Option<&str> {
84        match self {
85            BatchTransactionStatus::Pending { hash }
86            | BatchTransactionStatus::Confirmed { hash, .. } => Some(hash),
87            BatchTransactionStatus::Failed { .. } => None,
88        }
89    }
90
91    /// Returns true if the transaction is confirmed and successful.
92    pub fn is_success(&self) -> bool {
93        matches!(
94            self,
95            BatchTransactionStatus::Confirmed { success: true, .. }
96        )
97    }
98
99    /// Returns true if the transaction failed.
100    pub fn is_failed(&self) -> bool {
101        matches!(self, BatchTransactionStatus::Failed { .. })
102            || matches!(
103                self,
104                BatchTransactionStatus::Confirmed { success: false, .. }
105            )
106    }
107}
108
109/// Builder for creating a batch of transactions.
110///
111/// This builder handles:
112/// - Automatic sequence number management
113/// - Gas estimation
114/// - Transaction signing
115///
116/// # Example
117///
118/// ```rust,ignore
119/// let batch = TransactionBatchBuilder::new()
120///     .sender(account.address())
121///     .starting_sequence_number(10)
122///     .chain_id(ChainId::testnet())
123///     .gas_unit_price(100)
124///     .add_payload(payload1)
125///     .add_payload(payload2)
126///     .build_and_sign(&account)?;
127/// ```
128#[derive(Debug, Clone)]
129pub struct TransactionBatchBuilder {
130    sender: Option<AccountAddress>,
131    starting_sequence_number: Option<u64>,
132    chain_id: Option<ChainId>,
133    gas_unit_price: u64,
134    max_gas_amount: u64,
135    expiration_secs: u64,
136    payloads: Vec<TransactionPayload>,
137}
138
139impl Default for TransactionBatchBuilder {
140    fn default() -> Self {
141        Self::new()
142    }
143}
144
145impl TransactionBatchBuilder {
146    /// Creates a new batch builder.
147    #[must_use]
148    pub fn new() -> Self {
149        Self {
150            sender: None,
151            starting_sequence_number: None,
152            chain_id: None,
153            gas_unit_price: 100,
154            max_gas_amount: 2_000_000,
155            expiration_secs: 600,
156            payloads: Vec::new(),
157        }
158    }
159
160    /// Sets the sender address.
161    #[must_use]
162    pub fn sender(mut self, sender: AccountAddress) -> Self {
163        self.sender = Some(sender);
164        self
165    }
166
167    /// Sets the starting sequence number.
168    ///
169    /// Each transaction in the batch will use an incrementing sequence number
170    /// starting from this value.
171    #[must_use]
172    pub fn starting_sequence_number(mut self, seq: u64) -> Self {
173        self.starting_sequence_number = Some(seq);
174        self
175    }
176
177    /// Sets the chain ID.
178    #[must_use]
179    pub fn chain_id(mut self, chain_id: ChainId) -> Self {
180        self.chain_id = Some(chain_id);
181        self
182    }
183
184    /// Sets the gas unit price for all transactions.
185    #[must_use]
186    pub fn gas_unit_price(mut self, price: u64) -> Self {
187        self.gas_unit_price = price;
188        self
189    }
190
191    /// Sets the maximum gas amount for all transactions.
192    #[must_use]
193    pub fn max_gas_amount(mut self, amount: u64) -> Self {
194        self.max_gas_amount = amount;
195        self
196    }
197
198    /// Sets the expiration time in seconds from now.
199    #[must_use]
200    pub fn expiration_secs(mut self, secs: u64) -> Self {
201        self.expiration_secs = secs;
202        self
203    }
204
205    /// Adds a transaction payload to the batch.
206    #[must_use]
207    pub fn add_payload(mut self, payload: TransactionPayload) -> Self {
208        self.payloads.push(payload);
209        self
210    }
211
212    /// Adds multiple transaction payloads to the batch.
213    #[must_use]
214    pub fn add_payloads(mut self, payloads: impl IntoIterator<Item = TransactionPayload>) -> Self {
215        self.payloads.extend(payloads);
216        self
217    }
218
219    /// Returns the number of transactions in the batch.
220    pub fn len(&self) -> usize {
221        self.payloads.len()
222    }
223
224    /// Returns true if the batch is empty.
225    pub fn is_empty(&self) -> bool {
226        self.payloads.is_empty()
227    }
228
229    /// Builds raw transactions without signing.
230    ///
231    /// # Errors
232    ///
233    /// Returns an error if `sender`, `starting_sequence_number`, or `chain_id` is not set, or if building any transaction fails.
234    pub fn build(self) -> AptosResult<Vec<RawTransaction>> {
235        let sender = self
236            .sender
237            .ok_or_else(|| AptosError::Transaction("sender is required".into()))?;
238        let starting_seq = self.starting_sequence_number.ok_or_else(|| {
239            AptosError::Transaction("starting_sequence_number is required".into())
240        })?;
241        let chain_id = self
242            .chain_id
243            .ok_or_else(|| AptosError::Transaction("chain_id is required".into()))?;
244
245        let mut transactions = Vec::with_capacity(self.payloads.len());
246
247        for (i, payload) in self.payloads.into_iter().enumerate() {
248            // SECURITY: Use checked arithmetic to prevent sequence number overflow
249            let sequence_number = starting_seq
250                .checked_add(i as u64)
251                .ok_or_else(|| AptosError::Transaction("sequence number overflow".into()))?;
252
253            let txn = TransactionBuilder::new()
254                .sender(sender)
255                .sequence_number(sequence_number)
256                .payload(payload)
257                .gas_unit_price(self.gas_unit_price)
258                .max_gas_amount(self.max_gas_amount)
259                .chain_id(chain_id)
260                .expiration_from_now(self.expiration_secs)
261                .build()?;
262            transactions.push(txn);
263        }
264
265        Ok(transactions)
266    }
267
268    /// Builds and signs all transactions in the batch.
269    ///
270    /// # Errors
271    ///
272    /// Returns an error if building the transactions fails or if signing any transaction fails.
273    pub fn build_and_sign<A: Account>(self, account: &A) -> AptosResult<SignedTransactionBatch> {
274        let raw_transactions = self.build()?;
275        let mut signed = Vec::with_capacity(raw_transactions.len());
276
277        for raw_txn in raw_transactions {
278            let signed_txn = sign_transaction(&raw_txn, account)?;
279            signed.push(signed_txn);
280        }
281
282        Ok(SignedTransactionBatch {
283            transactions: signed,
284        })
285    }
286}
287
288/// A batch of signed transactions ready for submission.
289#[derive(Debug, Clone)]
290pub struct SignedTransactionBatch {
291    transactions: Vec<SignedTransaction>,
292}
293
294impl SignedTransactionBatch {
295    /// Creates a new batch from signed transactions.
296    pub fn new(transactions: Vec<SignedTransaction>) -> Self {
297        Self { transactions }
298    }
299
300    /// Returns the transactions in the batch.
301    pub fn transactions(&self) -> &[SignedTransaction] {
302        &self.transactions
303    }
304
305    /// Consumes the batch and returns the transactions.
306    pub fn into_transactions(self) -> Vec<SignedTransaction> {
307        self.transactions
308    }
309
310    /// Returns the number of transactions in the batch.
311    pub fn len(&self) -> usize {
312        self.transactions.len()
313    }
314
315    /// Returns true if the batch is empty.
316    pub fn is_empty(&self) -> bool {
317        self.transactions.is_empty()
318    }
319
320    /// Submits all transactions in parallel.
321    ///
322    /// Returns immediately after submission without waiting for confirmation.
323    pub async fn submit_all(self, client: &FullnodeClient) -> Vec<BatchTransactionResult> {
324        let futures: Vec<_> = self
325            .transactions
326            .into_iter()
327            .enumerate()
328            .map(|(index, txn)| {
329                let client = client.clone();
330                async move {
331                    let result = client.submit_transaction(&txn).await;
332                    BatchTransactionResult {
333                        index,
334                        transaction: txn,
335                        result: result.map(|resp| BatchTransactionStatus::Pending {
336                            hash: resp.data.hash.to_string(),
337                        }),
338                    }
339                }
340            })
341            .collect();
342
343        join_all(futures).await
344    }
345
346    /// Submits all transactions in parallel and waits for confirmation.
347    ///
348    /// Each transaction is submitted and then waited on independently.
349    pub async fn submit_and_wait_all(
350        self,
351        client: &FullnodeClient,
352        timeout: Option<Duration>,
353    ) -> Vec<BatchTransactionResult> {
354        let futures: Vec<_> = self
355            .transactions
356            .into_iter()
357            .enumerate()
358            .map(|(index, txn)| {
359                let client = client.clone();
360                async move {
361                    let result = submit_and_wait_single(&client, &txn, timeout).await;
362                    BatchTransactionResult {
363                        index,
364                        transaction: txn,
365                        result,
366                    }
367                }
368            })
369            .collect();
370
371        join_all(futures).await
372    }
373
374    /// Submits transactions sequentially (one at a time).
375    ///
376    /// This is slower but may be needed if transactions depend on each other.
377    pub async fn submit_sequential(self, client: &FullnodeClient) -> Vec<BatchTransactionResult> {
378        let mut results = Vec::with_capacity(self.transactions.len());
379
380        for (index, txn) in self.transactions.into_iter().enumerate() {
381            let result = client.submit_transaction(&txn).await;
382            results.push(BatchTransactionResult {
383                index,
384                transaction: txn,
385                result: result.map(|resp| BatchTransactionStatus::Pending {
386                    hash: resp.data.hash.to_string(),
387                }),
388            });
389        }
390
391        results
392    }
393
394    /// Submits transactions sequentially and waits for each to complete.
395    ///
396    /// This ensures each transaction is confirmed before submitting the next.
397    pub async fn submit_and_wait_sequential(
398        self,
399        client: &FullnodeClient,
400        timeout: Option<Duration>,
401    ) -> Vec<BatchTransactionResult> {
402        let mut results = Vec::with_capacity(self.transactions.len());
403
404        for (index, txn) in self.transactions.into_iter().enumerate() {
405            let result = submit_and_wait_single(client, &txn, timeout).await;
406            results.push(BatchTransactionResult {
407                index,
408                transaction: txn.clone(),
409                result,
410            });
411
412            // Stop on first failure if sequential
413            if results.last().is_some_and(|r| r.result.is_err()) {
414                break;
415            }
416        }
417
418        results
419    }
420}
421
422/// Helper to submit and wait for a single transaction.
423async fn submit_and_wait_single(
424    client: &FullnodeClient,
425    txn: &SignedTransaction,
426    timeout: Option<Duration>,
427) -> Result<BatchTransactionStatus, AptosError> {
428    let response = client.submit_and_wait(txn, timeout).await?;
429    let data = response.into_inner();
430
431    let hash = data
432        .get("hash")
433        .and_then(|v| v.as_str())
434        .unwrap_or("")
435        .to_string();
436    let success = data
437        .get("success")
438        .and_then(serde_json::Value::as_bool)
439        .unwrap_or(false);
440    let version = data
441        .get("version")
442        .and_then(serde_json::Value::as_str)
443        .and_then(|s| s.parse().ok())
444        .unwrap_or(0);
445    let gas_used = data
446        .get("gas_used")
447        .and_then(|v| v.as_str())
448        .and_then(|s| s.parse().ok())
449        .unwrap_or(0);
450
451    Ok(BatchTransactionStatus::Confirmed {
452        hash,
453        success,
454        version,
455        gas_used,
456    })
457}
458
459/// Summary of batch execution results.
460#[derive(Debug, Clone)]
461pub struct BatchSummary {
462    /// Total number of transactions.
463    pub total: usize,
464    /// Number of successful transactions.
465    pub succeeded: usize,
466    /// Number of failed transactions.
467    pub failed: usize,
468    /// Number of pending transactions.
469    pub pending: usize,
470    /// Total gas used across all confirmed transactions.
471    pub total_gas_used: u64,
472}
473
474impl BatchSummary {
475    /// Creates a summary from batch results.
476    pub fn from_results(results: &[BatchTransactionResult]) -> Self {
477        let mut succeeded = 0;
478        let mut failed = 0;
479        let mut pending = 0;
480        let mut total_gas_used = 0u64;
481
482        for result in results {
483            match &result.result {
484                Ok(status) => match status {
485                    BatchTransactionStatus::Confirmed {
486                        success, gas_used, ..
487                    } => {
488                        if *success {
489                            succeeded += 1;
490                        } else {
491                            failed += 1;
492                        }
493                        total_gas_used = total_gas_used.saturating_add(*gas_used);
494                    }
495                    BatchTransactionStatus::Pending { .. } => {
496                        pending += 1;
497                    }
498                    BatchTransactionStatus::Failed { .. } => {
499                        failed += 1;
500                    }
501                },
502                Err(_) => {
503                    failed += 1;
504                }
505            }
506        }
507
508        Self {
509            total: results.len(),
510            succeeded,
511            failed,
512            pending,
513            total_gas_used,
514        }
515    }
516
517    /// Returns true if all transactions succeeded.
518    pub fn all_succeeded(&self) -> bool {
519        self.succeeded == self.total
520    }
521
522    /// Returns true if any transaction failed.
523    pub fn has_failures(&self) -> bool {
524        self.failed > 0
525    }
526}
527
528/// High-level batch operations for the Aptos client.
529#[allow(missing_debug_implementations)] // Contains references that may not implement Debug
530pub struct BatchOperations<'a> {
531    client: &'a FullnodeClient,
532    chain_id: &'a std::sync::atomic::AtomicU8,
533}
534
535impl<'a> BatchOperations<'a> {
536    /// Creates a new batch operations helper.
537    pub fn new(client: &'a FullnodeClient, chain_id: &'a std::sync::atomic::AtomicU8) -> Self {
538        Self { client, chain_id }
539    }
540
541    /// Resolves the chain ID, fetching from the node if unknown.
542    async fn resolve_chain_id(&self) -> AptosResult<ChainId> {
543        let id = self.chain_id.load(std::sync::atomic::Ordering::Relaxed);
544        if id > 0 {
545            return Ok(ChainId::new(id));
546        }
547        // Chain ID is unknown; fetch from node
548        let response = self.client.get_ledger_info().await?;
549        let info = response.into_inner();
550        self.chain_id
551            .store(info.chain_id, std::sync::atomic::Ordering::Relaxed);
552        Ok(ChainId::new(info.chain_id))
553    }
554
555    /// Builds a batch of transactions for an account.
556    ///
557    /// This automatically fetches the current sequence number, gas price,
558    /// and chain ID (if unknown).
559    ///
560    /// # Errors
561    ///
562    /// Returns an error if fetching the sequence number fails, fetching gas price fails, or building/signing the batch fails.
563    pub async fn build<A: Account>(
564        &self,
565        account: &A,
566        payloads: Vec<TransactionPayload>,
567    ) -> AptosResult<SignedTransactionBatch> {
568        // Fetch sequence number, gas price, and chain ID in parallel
569        let (sequence_number, gas_estimation, chain_id) = tokio::join!(
570            self.client.get_sequence_number(account.address()),
571            self.client.estimate_gas_price(),
572            self.resolve_chain_id()
573        );
574        let sequence_number = sequence_number?;
575        let gas_estimation = gas_estimation?;
576        let chain_id = chain_id?;
577
578        let batch = TransactionBatchBuilder::new()
579            .sender(account.address())
580            .starting_sequence_number(sequence_number)
581            .chain_id(chain_id)
582            .gas_unit_price(gas_estimation.data.recommended())
583            .add_payloads(payloads)
584            .build_and_sign(account)?;
585
586        Ok(batch)
587    }
588
589    /// Builds and submits a batch of transactions in parallel.
590    ///
591    /// # Errors
592    ///
593    /// Returns an error if building the batch fails.
594    pub async fn submit<A: Account>(
595        &self,
596        account: &A,
597        payloads: Vec<TransactionPayload>,
598    ) -> AptosResult<Vec<BatchTransactionResult>> {
599        let batch = self.build(account, payloads).await?;
600        Ok(batch.submit_all(self.client).await)
601    }
602
603    /// Builds, submits, and waits for a batch of transactions.
604    ///
605    /// # Errors
606    ///
607    /// Returns an error if building the batch fails (e.g., fetching sequence number or gas price),
608    /// signing the batch fails, or any transaction submission/waiting fails.
609    pub async fn submit_and_wait<A: Account>(
610        &self,
611        account: &A,
612        payloads: Vec<TransactionPayload>,
613        timeout: Option<Duration>,
614    ) -> AptosResult<Vec<BatchTransactionResult>> {
615        let batch = self.build(account, payloads).await?;
616        Ok(batch.submit_and_wait_all(self.client, timeout).await)
617    }
618
619    /// Creates multiple APT transfers as a batch.
620    ///
621    /// # Errors
622    ///
623    /// Returns an error if any transfer payload creation fails (e.g., invalid recipient address),
624    /// building the batch fails, or submitting/waiting for transactions fails.
625    pub async fn transfer_apt<A: Account>(
626        &self,
627        sender: &A,
628        transfers: Vec<(AccountAddress, u64)>,
629    ) -> AptosResult<Vec<BatchTransactionResult>> {
630        use crate::transaction::EntryFunction;
631
632        let payloads: Vec<_> = transfers
633            .into_iter()
634            .map(|(recipient, amount)| {
635                EntryFunction::apt_transfer(recipient, amount).map(TransactionPayload::from)
636            })
637            .collect::<AptosResult<Vec<_>>>()?;
638
639        self.submit_and_wait(sender, payloads, None).await
640    }
641}
642
643#[cfg(test)]
644mod tests {
645    use super::*;
646
647    #[test]
648    fn test_batch_builder_missing_fields() {
649        let builder = TransactionBatchBuilder::new().add_payload(TransactionPayload::Script(
650            crate::transaction::Script {
651                code: vec![],
652                type_args: vec![],
653                args: vec![],
654            },
655        ));
656
657        let result = builder.build();
658        assert!(result.is_err());
659    }
660
661    #[test]
662    fn test_batch_builder_complete() {
663        let builder = TransactionBatchBuilder::new()
664            .sender(AccountAddress::ONE)
665            .starting_sequence_number(0)
666            .chain_id(ChainId::testnet())
667            .gas_unit_price(100)
668            .add_payload(TransactionPayload::Script(crate::transaction::Script {
669                code: vec![],
670                type_args: vec![],
671                args: vec![],
672            }))
673            .add_payload(TransactionPayload::Script(crate::transaction::Script {
674                code: vec![],
675                type_args: vec![],
676                args: vec![],
677            }));
678
679        let transactions = builder.build().unwrap();
680        assert_eq!(transactions.len(), 2);
681        assert_eq!(transactions[0].sequence_number, 0);
682        assert_eq!(transactions[1].sequence_number, 1);
683    }
684
685    #[test]
686    fn test_batch_builder_sequence_numbers() {
687        let builder = TransactionBatchBuilder::new()
688            .sender(AccountAddress::ONE)
689            .starting_sequence_number(10)
690            .chain_id(ChainId::testnet())
691            .add_payload(TransactionPayload::Script(crate::transaction::Script {
692                code: vec![],
693                type_args: vec![],
694                args: vec![],
695            }))
696            .add_payload(TransactionPayload::Script(crate::transaction::Script {
697                code: vec![],
698                type_args: vec![],
699                args: vec![],
700            }))
701            .add_payload(TransactionPayload::Script(crate::transaction::Script {
702                code: vec![],
703                type_args: vec![],
704                args: vec![],
705            }));
706
707        let transactions = builder.build().unwrap();
708        assert_eq!(transactions.len(), 3);
709        assert_eq!(transactions[0].sequence_number, 10);
710        assert_eq!(transactions[1].sequence_number, 11);
711        assert_eq!(transactions[2].sequence_number, 12);
712    }
713
714    #[test]
715    fn test_batch_summary() {
716        let results = vec![
717            BatchTransactionResult {
718                index: 0,
719                transaction: create_dummy_signed_txn(),
720                result: Ok(BatchTransactionStatus::Confirmed {
721                    hash: "0x1".to_string(),
722                    success: true,
723                    version: 100,
724                    gas_used: 500,
725                }),
726            },
727            BatchTransactionResult {
728                index: 1,
729                transaction: create_dummy_signed_txn(),
730                result: Ok(BatchTransactionStatus::Confirmed {
731                    hash: "0x2".to_string(),
732                    success: true,
733                    version: 101,
734                    gas_used: 600,
735                }),
736            },
737            BatchTransactionResult {
738                index: 2,
739                transaction: create_dummy_signed_txn(),
740                result: Ok(BatchTransactionStatus::Confirmed {
741                    hash: "0x3".to_string(),
742                    success: false,
743                    version: 102,
744                    gas_used: 100,
745                }),
746            },
747        ];
748
749        let summary = BatchSummary::from_results(&results);
750        assert_eq!(summary.total, 3);
751        assert_eq!(summary.succeeded, 2);
752        assert_eq!(summary.failed, 1);
753        assert_eq!(summary.pending, 0);
754        assert_eq!(summary.total_gas_used, 1200);
755        assert!(!summary.all_succeeded());
756        assert!(summary.has_failures());
757    }
758
759    #[test]
760    fn test_batch_status_methods() {
761        let pending = BatchTransactionStatus::Pending {
762            hash: "0x123".to_string(),
763        };
764        assert_eq!(pending.hash(), Some("0x123"));
765        assert!(!pending.is_success());
766        assert!(!pending.is_failed());
767
768        let confirmed_success = BatchTransactionStatus::Confirmed {
769            hash: "0x456".to_string(),
770            success: true,
771            version: 100,
772            gas_used: 500,
773        };
774        assert_eq!(confirmed_success.hash(), Some("0x456"));
775        assert!(confirmed_success.is_success());
776        assert!(!confirmed_success.is_failed());
777
778        let confirmed_failed = BatchTransactionStatus::Confirmed {
779            hash: "0x789".to_string(),
780            success: false,
781            version: 101,
782            gas_used: 100,
783        };
784        assert!(!confirmed_failed.is_success());
785        assert!(confirmed_failed.is_failed());
786
787        let failed = BatchTransactionStatus::Failed {
788            error: "timeout".to_string(),
789        };
790        assert!(failed.hash().is_none());
791        assert!(!failed.is_success());
792        assert!(failed.is_failed());
793    }
794
795    #[cfg(feature = "ed25519")]
796    #[test]
797    fn test_batch_build_and_sign() {
798        use crate::account::Ed25519Account;
799
800        let account = Ed25519Account::generate();
801        let batch = TransactionBatchBuilder::new()
802            .sender(account.address())
803            .starting_sequence_number(0)
804            .chain_id(ChainId::testnet())
805            .add_payload(TransactionPayload::Script(crate::transaction::Script {
806                code: vec![],
807                type_args: vec![],
808                args: vec![],
809            }))
810            .add_payload(TransactionPayload::Script(crate::transaction::Script {
811                code: vec![],
812                type_args: vec![],
813                args: vec![],
814            }))
815            .build_and_sign(&account)
816            .unwrap();
817
818        assert_eq!(batch.len(), 2);
819    }
820
821    fn create_dummy_signed_txn() -> SignedTransaction {
822        use crate::transaction::TransactionAuthenticator;
823
824        let raw_txn = RawTransaction {
825            sender: AccountAddress::ONE,
826            sequence_number: 0,
827            payload: TransactionPayload::Script(crate::transaction::Script {
828                code: vec![],
829                type_args: vec![],
830                args: vec![],
831            }),
832            max_gas_amount: 200_000,
833            gas_unit_price: 100,
834            expiration_timestamp_secs: 0,
835            chain_id: ChainId::testnet(),
836        };
837
838        SignedTransaction {
839            raw_txn,
840            authenticator: TransactionAuthenticator::ed25519(vec![0u8; 32], vec![0u8; 64]),
841        }
842    }
843
844    #[test]
845    fn test_batch_summary_all_succeeded() {
846        let results = vec![
847            BatchTransactionResult {
848                index: 0,
849                transaction: create_dummy_signed_txn(),
850                result: Ok(BatchTransactionStatus::Confirmed {
851                    hash: "0x1".to_string(),
852                    success: true,
853                    version: 100,
854                    gas_used: 500,
855                }),
856            },
857            BatchTransactionResult {
858                index: 1,
859                transaction: create_dummy_signed_txn(),
860                result: Ok(BatchTransactionStatus::Confirmed {
861                    hash: "0x2".to_string(),
862                    success: true,
863                    version: 101,
864                    gas_used: 600,
865                }),
866            },
867        ];
868
869        let summary = BatchSummary::from_results(&results);
870        assert_eq!(summary.total, 2);
871        assert_eq!(summary.succeeded, 2);
872        assert_eq!(summary.failed, 0);
873        assert!(summary.all_succeeded());
874        assert!(!summary.has_failures());
875    }
876
877    #[test]
878    fn test_batch_summary_with_pending() {
879        let results = vec![
880            BatchTransactionResult {
881                index: 0,
882                transaction: create_dummy_signed_txn(),
883                result: Ok(BatchTransactionStatus::Pending {
884                    hash: "0x1".to_string(),
885                }),
886            },
887            BatchTransactionResult {
888                index: 1,
889                transaction: create_dummy_signed_txn(),
890                result: Ok(BatchTransactionStatus::Confirmed {
891                    hash: "0x2".to_string(),
892                    success: true,
893                    version: 101,
894                    gas_used: 600,
895                }),
896            },
897        ];
898
899        let summary = BatchSummary::from_results(&results);
900        assert_eq!(summary.total, 2);
901        assert_eq!(summary.succeeded, 1);
902        assert_eq!(summary.pending, 1);
903        assert!(!summary.all_succeeded());
904    }
905
906    #[test]
907    fn test_batch_summary_with_errors() {
908        let results = vec![BatchTransactionResult {
909            index: 0,
910            transaction: create_dummy_signed_txn(),
911            result: Err(AptosError::Transaction("failed".to_string())),
912        }];
913
914        let summary = BatchSummary::from_results(&results);
915        assert_eq!(summary.total, 1);
916        assert_eq!(summary.failed, 1);
917        assert!(summary.has_failures());
918    }
919
920    #[test]
921    fn test_batch_builder_with_max_gas() {
922        let builder = TransactionBatchBuilder::new()
923            .sender(AccountAddress::ONE)
924            .starting_sequence_number(0)
925            .chain_id(ChainId::testnet())
926            .max_gas_amount(500_000)
927            .add_payload(TransactionPayload::Script(crate::transaction::Script {
928                code: vec![],
929                type_args: vec![],
930                args: vec![],
931            }));
932
933        let transactions = builder.build().unwrap();
934        assert_eq!(transactions.len(), 1);
935        assert_eq!(transactions[0].max_gas_amount, 500_000);
936    }
937
938    #[test]
939    fn test_batch_builder_with_expiration() {
940        let builder = TransactionBatchBuilder::new()
941            .sender(AccountAddress::ONE)
942            .starting_sequence_number(0)
943            .chain_id(ChainId::testnet())
944            .expiration_secs(3600) // 1 hour from now
945            .add_payload(TransactionPayload::Script(crate::transaction::Script {
946                code: vec![],
947                type_args: vec![],
948                args: vec![],
949            }));
950
951        let transactions = builder.build().unwrap();
952        // Expiration should be set to some future timestamp (> current time)
953        assert!(transactions[0].expiration_timestamp_secs > 0);
954    }
955
956    #[test]
957    fn test_batch_builder_empty_payloads() {
958        let builder = TransactionBatchBuilder::new()
959            .sender(AccountAddress::ONE)
960            .starting_sequence_number(0)
961            .chain_id(ChainId::testnet());
962
963        // Empty payloads returns empty vec, not error
964        let result = builder.build();
965        assert!(result.is_ok());
966        assert_eq!(result.unwrap().len(), 0);
967    }
968
969    #[test]
970    fn test_batch_result_transaction_accessor() {
971        let signed_txn = create_dummy_signed_txn();
972        let result = BatchTransactionResult {
973            index: 0,
974            transaction: signed_txn.clone(),
975            result: Ok(BatchTransactionStatus::Pending {
976                hash: "0x123".to_string(),
977            }),
978        };
979
980        assert_eq!(result.index, 0);
981        assert_eq!(result.transaction.raw_txn.sender, AccountAddress::ONE);
982    }
983
984    #[test]
985    fn test_batch_builder_default() {
986        let builder = TransactionBatchBuilder::default();
987        assert!(builder.is_empty());
988        assert_eq!(builder.len(), 0);
989    }
990
991    #[test]
992    fn test_batch_builder_len_and_is_empty() {
993        let builder = TransactionBatchBuilder::new();
994        assert!(builder.is_empty());
995        assert_eq!(builder.len(), 0);
996
997        let builder = builder.add_payload(TransactionPayload::Script(crate::transaction::Script {
998            code: vec![],
999            type_args: vec![],
1000            args: vec![],
1001        }));
1002        assert!(!builder.is_empty());
1003        assert_eq!(builder.len(), 1);
1004    }
1005
1006    #[test]
1007    fn test_batch_builder_add_payloads() {
1008        let payloads = vec![
1009            TransactionPayload::Script(crate::transaction::Script {
1010                code: vec![1],
1011                type_args: vec![],
1012                args: vec![],
1013            }),
1014            TransactionPayload::Script(crate::transaction::Script {
1015                code: vec![2],
1016                type_args: vec![],
1017                args: vec![],
1018            }),
1019            TransactionPayload::Script(crate::transaction::Script {
1020                code: vec![3],
1021                type_args: vec![],
1022                args: vec![],
1023            }),
1024        ];
1025
1026        let builder = TransactionBatchBuilder::new()
1027            .sender(AccountAddress::ONE)
1028            .starting_sequence_number(0)
1029            .chain_id(ChainId::testnet())
1030            .add_payloads(payloads);
1031
1032        assert_eq!(builder.len(), 3);
1033
1034        let transactions = builder.build().unwrap();
1035        assert_eq!(transactions.len(), 3);
1036    }
1037
1038    #[test]
1039    fn test_batch_builder_missing_sequence_number() {
1040        let builder = TransactionBatchBuilder::new()
1041            .sender(AccountAddress::ONE)
1042            .chain_id(ChainId::testnet())
1043            .add_payload(TransactionPayload::Script(crate::transaction::Script {
1044                code: vec![],
1045                type_args: vec![],
1046                args: vec![],
1047            }));
1048
1049        let result = builder.build();
1050        assert!(result.is_err());
1051        assert!(result.unwrap_err().to_string().contains("sequence_number"));
1052    }
1053
1054    #[test]
1055    fn test_batch_builder_missing_chain_id() {
1056        let builder = TransactionBatchBuilder::new()
1057            .sender(AccountAddress::ONE)
1058            .starting_sequence_number(0)
1059            .add_payload(TransactionPayload::Script(crate::transaction::Script {
1060                code: vec![],
1061                type_args: vec![],
1062                args: vec![],
1063            }));
1064
1065        let result = builder.build();
1066        assert!(result.is_err());
1067        assert!(result.unwrap_err().to_string().contains("chain_id"));
1068    }
1069
1070    #[test]
1071    fn test_batch_summary_empty() {
1072        let results: Vec<BatchTransactionResult> = vec![];
1073        let summary = BatchSummary::from_results(&results);
1074        assert_eq!(summary.total, 0);
1075        assert_eq!(summary.succeeded, 0);
1076        assert_eq!(summary.failed, 0);
1077        assert_eq!(summary.pending, 0);
1078        assert_eq!(summary.total_gas_used, 0);
1079        assert!(summary.all_succeeded());
1080        assert!(!summary.has_failures());
1081    }
1082
1083    #[test]
1084    fn test_batch_status_failed_variant() {
1085        let failed = BatchTransactionStatus::Failed {
1086            error: "connection timeout".to_string(),
1087        };
1088        assert!(failed.is_failed());
1089        assert!(!failed.is_success());
1090        assert!(failed.hash().is_none());
1091    }
1092
1093    #[test]
1094    fn test_signed_transaction_batch_len() {
1095        let batch = SignedTransactionBatch {
1096            transactions: vec![create_dummy_signed_txn(), create_dummy_signed_txn()],
1097        };
1098        assert_eq!(batch.len(), 2);
1099        assert!(!batch.is_empty());
1100    }
1101
1102    #[test]
1103    fn test_signed_transaction_batch_iter() {
1104        let txn1 = create_dummy_signed_txn();
1105        let txn2 = create_dummy_signed_txn();
1106        let batch = SignedTransactionBatch {
1107            transactions: vec![txn1, txn2],
1108        };
1109
1110        let collected: Vec<_> = batch.transactions.iter().collect();
1111        assert_eq!(collected.len(), 2);
1112    }
1113
1114    #[test]
1115    fn test_batch_builder_gas_settings() {
1116        let builder = TransactionBatchBuilder::new()
1117            .max_gas_amount(50000)
1118            .gas_unit_price(200)
1119            .expiration_secs(120);
1120
1121        assert_eq!(builder.max_gas_amount, 50000);
1122        assert_eq!(builder.gas_unit_price, 200);
1123        assert_eq!(builder.expiration_secs, 120);
1124    }
1125
1126    #[test]
1127    fn test_batch_builder_missing_sender() {
1128        let builder = TransactionBatchBuilder::new()
1129            .starting_sequence_number(0)
1130            .chain_id(ChainId::testnet())
1131            .add_payload(TransactionPayload::Script(crate::transaction::Script {
1132                code: vec![],
1133                type_args: vec![],
1134                args: vec![],
1135            }));
1136
1137        let result = builder.build();
1138        assert!(result.is_err());
1139        assert!(result.unwrap_err().to_string().contains("sender"));
1140    }
1141
1142    #[test]
1143    fn test_batch_summary_with_failures() {
1144        let txn = create_dummy_signed_txn();
1145        let results = vec![
1146            BatchTransactionResult {
1147                index: 0,
1148                transaction: txn.clone(),
1149                result: Ok(BatchTransactionStatus::Failed {
1150                    error: "error".to_string(),
1151                }),
1152            },
1153            BatchTransactionResult {
1154                index: 1,
1155                transaction: txn,
1156                result: Err(AptosError::Transaction("test".to_string())),
1157            },
1158        ];
1159
1160        let summary = BatchSummary::from_results(&results);
1161        assert_eq!(summary.total, 2);
1162        assert_eq!(summary.failed, 2);
1163        assert!(summary.has_failures());
1164    }
1165
1166    #[test]
1167    fn test_batch_status_confirmed_variant() {
1168        let status = BatchTransactionStatus::Confirmed {
1169            hash: "0xabc".to_string(),
1170            success: true,
1171            version: 1,
1172            gas_used: 150,
1173        };
1174        assert!(status.is_success());
1175        assert!(!status.is_failed());
1176        assert_eq!(status.hash(), Some("0xabc"));
1177    }
1178
1179    #[test]
1180    fn test_batch_status_pending_variant() {
1181        let status = BatchTransactionStatus::Pending {
1182            hash: "0xdef".to_string(),
1183        };
1184        assert!(!status.is_success());
1185        assert!(!status.is_failed());
1186        assert_eq!(status.hash(), Some("0xdef"));
1187    }
1188
1189    #[test]
1190    fn test_signed_transaction_batch_new() {
1191        let txn1 = create_dummy_signed_txn();
1192        let txn2 = create_dummy_signed_txn();
1193        let batch = SignedTransactionBatch::new(vec![txn1, txn2]);
1194        assert_eq!(batch.len(), 2);
1195    }
1196
1197    #[test]
1198    fn test_signed_transaction_batch_transactions() {
1199        let txn1 = create_dummy_signed_txn();
1200        let txn2 = create_dummy_signed_txn();
1201        let batch = SignedTransactionBatch::new(vec![txn1, txn2]);
1202
1203        let txns = batch.transactions();
1204        assert_eq!(txns.len(), 2);
1205    }
1206
1207    #[test]
1208    fn test_signed_transaction_batch_into_transactions() {
1209        let txn1 = create_dummy_signed_txn();
1210        let txn2 = create_dummy_signed_txn();
1211        let batch = SignedTransactionBatch::new(vec![txn1, txn2]);
1212
1213        let txns = batch.into_transactions();
1214        assert_eq!(txns.len(), 2);
1215    }
1216
1217    #[test]
1218    fn test_signed_transaction_batch_empty() {
1219        let batch = SignedTransactionBatch::new(vec![]);
1220        assert!(batch.is_empty());
1221        assert_eq!(batch.len(), 0);
1222    }
1223
1224    #[test]
1225    fn test_batch_transaction_result_accessors() {
1226        let txn = create_dummy_signed_txn();
1227        let result = BatchTransactionResult {
1228            index: 5,
1229            transaction: txn.clone(),
1230            result: Ok(BatchTransactionStatus::Confirmed {
1231                hash: "0x123".to_string(),
1232                success: true,
1233                version: 1,
1234                gas_used: 100,
1235            }),
1236        };
1237
1238        assert_eq!(result.index, 5);
1239        assert!(result.result.is_ok());
1240    }
1241
1242    #[test]
1243    fn test_batch_builder_debug() {
1244        let builder = TransactionBatchBuilder::new().sender(AccountAddress::ONE);
1245        let debug = format!("{builder:?}");
1246        assert!(debug.contains("TransactionBatchBuilder"));
1247    }
1248
1249    #[test]
1250    fn test_signed_transaction_batch_debug() {
1251        let batch = SignedTransactionBatch::new(vec![create_dummy_signed_txn()]);
1252        let debug = format!("{batch:?}");
1253        assert!(debug.contains("SignedTransactionBatch"));
1254    }
1255
1256    #[test]
1257    fn test_batch_summary_debug() {
1258        let summary = BatchSummary {
1259            total: 5,
1260            succeeded: 3,
1261            failed: 1,
1262            pending: 1,
1263            total_gas_used: 500,
1264        };
1265        let debug = format!("{summary:?}");
1266        assert!(debug.contains("BatchSummary"));
1267    }
1268
1269    #[test]
1270    fn test_batch_transaction_status_debug() {
1271        let status = BatchTransactionStatus::Confirmed {
1272            hash: "0x123".to_string(),
1273            success: true,
1274            version: 1,
1275            gas_used: 100,
1276        };
1277        let debug = format!("{status:?}");
1278        assert!(debug.contains("Confirmed"));
1279    }
1280}