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: 200_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::RwLock<ChainId>,
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::RwLock<ChainId>) -> 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        {
544            let chain_id = self.chain_id.read().expect("chain_id lock poisoned");
545            if chain_id.id() > 0 {
546                return Ok(*chain_id);
547            }
548        }
549        // Chain ID is unknown; fetch from node
550        let response = self.client.get_ledger_info().await?;
551        let info = response.into_inner();
552        let new_chain_id = ChainId::new(info.chain_id);
553        *self.chain_id.write().expect("chain_id lock poisoned") = new_chain_id;
554        Ok(new_chain_id)
555    }
556
557    /// Builds a batch of transactions for an account.
558    ///
559    /// This automatically fetches the current sequence number, gas price,
560    /// and chain ID (if unknown).
561    ///
562    /// # Errors
563    ///
564    /// Returns an error if fetching the sequence number fails, fetching gas price fails, or building/signing the batch fails.
565    pub async fn build<A: Account>(
566        &self,
567        account: &A,
568        payloads: Vec<TransactionPayload>,
569    ) -> AptosResult<SignedTransactionBatch> {
570        // Fetch sequence number, gas price, and chain ID in parallel
571        let (sequence_number, gas_estimation, chain_id) = tokio::join!(
572            self.client.get_sequence_number(account.address()),
573            self.client.estimate_gas_price(),
574            self.resolve_chain_id()
575        );
576        let sequence_number = sequence_number?;
577        let gas_estimation = gas_estimation?;
578        let chain_id = chain_id?;
579
580        let batch = TransactionBatchBuilder::new()
581            .sender(account.address())
582            .starting_sequence_number(sequence_number)
583            .chain_id(chain_id)
584            .gas_unit_price(gas_estimation.data.recommended())
585            .add_payloads(payloads)
586            .build_and_sign(account)?;
587
588        Ok(batch)
589    }
590
591    /// Builds and submits a batch of transactions in parallel.
592    ///
593    /// # Errors
594    ///
595    /// Returns an error if building the batch fails.
596    pub async fn submit<A: Account>(
597        &self,
598        account: &A,
599        payloads: Vec<TransactionPayload>,
600    ) -> AptosResult<Vec<BatchTransactionResult>> {
601        let batch = self.build(account, payloads).await?;
602        Ok(batch.submit_all(self.client).await)
603    }
604
605    /// Builds, submits, and waits for a batch of transactions.
606    ///
607    /// # Errors
608    ///
609    /// Returns an error if building the batch fails (e.g., fetching sequence number or gas price),
610    /// signing the batch fails, or any transaction submission/waiting fails.
611    pub async fn submit_and_wait<A: Account>(
612        &self,
613        account: &A,
614        payloads: Vec<TransactionPayload>,
615        timeout: Option<Duration>,
616    ) -> AptosResult<Vec<BatchTransactionResult>> {
617        let batch = self.build(account, payloads).await?;
618        Ok(batch.submit_and_wait_all(self.client, timeout).await)
619    }
620
621    /// Creates multiple APT transfers as a batch.
622    ///
623    /// # Errors
624    ///
625    /// Returns an error if any transfer payload creation fails (e.g., invalid recipient address),
626    /// building the batch fails, or submitting/waiting for transactions fails.
627    pub async fn transfer_apt<A: Account>(
628        &self,
629        sender: &A,
630        transfers: Vec<(AccountAddress, u64)>,
631    ) -> AptosResult<Vec<BatchTransactionResult>> {
632        use crate::transaction::EntryFunction;
633
634        let payloads: Vec<_> = transfers
635            .into_iter()
636            .map(|(recipient, amount)| {
637                EntryFunction::apt_transfer(recipient, amount).map(TransactionPayload::from)
638            })
639            .collect::<AptosResult<Vec<_>>>()?;
640
641        self.submit_and_wait(sender, payloads, None).await
642    }
643}
644
645#[cfg(test)]
646mod tests {
647    use super::*;
648
649    #[test]
650    fn test_batch_builder_missing_fields() {
651        let builder = TransactionBatchBuilder::new().add_payload(TransactionPayload::Script(
652            crate::transaction::Script {
653                code: vec![],
654                type_args: vec![],
655                args: vec![],
656            },
657        ));
658
659        let result = builder.build();
660        assert!(result.is_err());
661    }
662
663    #[test]
664    fn test_batch_builder_complete() {
665        let builder = TransactionBatchBuilder::new()
666            .sender(AccountAddress::ONE)
667            .starting_sequence_number(0)
668            .chain_id(ChainId::testnet())
669            .gas_unit_price(100)
670            .add_payload(TransactionPayload::Script(crate::transaction::Script {
671                code: vec![],
672                type_args: vec![],
673                args: vec![],
674            }))
675            .add_payload(TransactionPayload::Script(crate::transaction::Script {
676                code: vec![],
677                type_args: vec![],
678                args: vec![],
679            }));
680
681        let transactions = builder.build().unwrap();
682        assert_eq!(transactions.len(), 2);
683        assert_eq!(transactions[0].sequence_number, 0);
684        assert_eq!(transactions[1].sequence_number, 1);
685    }
686
687    #[test]
688    fn test_batch_builder_sequence_numbers() {
689        let builder = TransactionBatchBuilder::new()
690            .sender(AccountAddress::ONE)
691            .starting_sequence_number(10)
692            .chain_id(ChainId::testnet())
693            .add_payload(TransactionPayload::Script(crate::transaction::Script {
694                code: vec![],
695                type_args: vec![],
696                args: vec![],
697            }))
698            .add_payload(TransactionPayload::Script(crate::transaction::Script {
699                code: vec![],
700                type_args: vec![],
701                args: vec![],
702            }))
703            .add_payload(TransactionPayload::Script(crate::transaction::Script {
704                code: vec![],
705                type_args: vec![],
706                args: vec![],
707            }));
708
709        let transactions = builder.build().unwrap();
710        assert_eq!(transactions.len(), 3);
711        assert_eq!(transactions[0].sequence_number, 10);
712        assert_eq!(transactions[1].sequence_number, 11);
713        assert_eq!(transactions[2].sequence_number, 12);
714    }
715
716    #[test]
717    fn test_batch_summary() {
718        let results = vec![
719            BatchTransactionResult {
720                index: 0,
721                transaction: create_dummy_signed_txn(),
722                result: Ok(BatchTransactionStatus::Confirmed {
723                    hash: "0x1".to_string(),
724                    success: true,
725                    version: 100,
726                    gas_used: 500,
727                }),
728            },
729            BatchTransactionResult {
730                index: 1,
731                transaction: create_dummy_signed_txn(),
732                result: Ok(BatchTransactionStatus::Confirmed {
733                    hash: "0x2".to_string(),
734                    success: true,
735                    version: 101,
736                    gas_used: 600,
737                }),
738            },
739            BatchTransactionResult {
740                index: 2,
741                transaction: create_dummy_signed_txn(),
742                result: Ok(BatchTransactionStatus::Confirmed {
743                    hash: "0x3".to_string(),
744                    success: false,
745                    version: 102,
746                    gas_used: 100,
747                }),
748            },
749        ];
750
751        let summary = BatchSummary::from_results(&results);
752        assert_eq!(summary.total, 3);
753        assert_eq!(summary.succeeded, 2);
754        assert_eq!(summary.failed, 1);
755        assert_eq!(summary.pending, 0);
756        assert_eq!(summary.total_gas_used, 1200);
757        assert!(!summary.all_succeeded());
758        assert!(summary.has_failures());
759    }
760
761    #[test]
762    fn test_batch_status_methods() {
763        let pending = BatchTransactionStatus::Pending {
764            hash: "0x123".to_string(),
765        };
766        assert_eq!(pending.hash(), Some("0x123"));
767        assert!(!pending.is_success());
768        assert!(!pending.is_failed());
769
770        let confirmed_success = BatchTransactionStatus::Confirmed {
771            hash: "0x456".to_string(),
772            success: true,
773            version: 100,
774            gas_used: 500,
775        };
776        assert_eq!(confirmed_success.hash(), Some("0x456"));
777        assert!(confirmed_success.is_success());
778        assert!(!confirmed_success.is_failed());
779
780        let confirmed_failed = BatchTransactionStatus::Confirmed {
781            hash: "0x789".to_string(),
782            success: false,
783            version: 101,
784            gas_used: 100,
785        };
786        assert!(!confirmed_failed.is_success());
787        assert!(confirmed_failed.is_failed());
788
789        let failed = BatchTransactionStatus::Failed {
790            error: "timeout".to_string(),
791        };
792        assert!(failed.hash().is_none());
793        assert!(!failed.is_success());
794        assert!(failed.is_failed());
795    }
796
797    #[cfg(feature = "ed25519")]
798    #[test]
799    fn test_batch_build_and_sign() {
800        use crate::account::Ed25519Account;
801
802        let account = Ed25519Account::generate();
803        let batch = TransactionBatchBuilder::new()
804            .sender(account.address())
805            .starting_sequence_number(0)
806            .chain_id(ChainId::testnet())
807            .add_payload(TransactionPayload::Script(crate::transaction::Script {
808                code: vec![],
809                type_args: vec![],
810                args: vec![],
811            }))
812            .add_payload(TransactionPayload::Script(crate::transaction::Script {
813                code: vec![],
814                type_args: vec![],
815                args: vec![],
816            }))
817            .build_and_sign(&account)
818            .unwrap();
819
820        assert_eq!(batch.len(), 2);
821    }
822
823    fn create_dummy_signed_txn() -> SignedTransaction {
824        use crate::transaction::TransactionAuthenticator;
825
826        let raw_txn = RawTransaction {
827            sender: AccountAddress::ONE,
828            sequence_number: 0,
829            payload: TransactionPayload::Script(crate::transaction::Script {
830                code: vec![],
831                type_args: vec![],
832                args: vec![],
833            }),
834            max_gas_amount: 200_000,
835            gas_unit_price: 100,
836            expiration_timestamp_secs: 0,
837            chain_id: ChainId::testnet(),
838        };
839
840        SignedTransaction {
841            raw_txn,
842            authenticator: TransactionAuthenticator::ed25519(vec![0u8; 32], vec![0u8; 64]),
843        }
844    }
845
846    #[test]
847    fn test_batch_summary_all_succeeded() {
848        let results = vec![
849            BatchTransactionResult {
850                index: 0,
851                transaction: create_dummy_signed_txn(),
852                result: Ok(BatchTransactionStatus::Confirmed {
853                    hash: "0x1".to_string(),
854                    success: true,
855                    version: 100,
856                    gas_used: 500,
857                }),
858            },
859            BatchTransactionResult {
860                index: 1,
861                transaction: create_dummy_signed_txn(),
862                result: Ok(BatchTransactionStatus::Confirmed {
863                    hash: "0x2".to_string(),
864                    success: true,
865                    version: 101,
866                    gas_used: 600,
867                }),
868            },
869        ];
870
871        let summary = BatchSummary::from_results(&results);
872        assert_eq!(summary.total, 2);
873        assert_eq!(summary.succeeded, 2);
874        assert_eq!(summary.failed, 0);
875        assert!(summary.all_succeeded());
876        assert!(!summary.has_failures());
877    }
878
879    #[test]
880    fn test_batch_summary_with_pending() {
881        let results = vec![
882            BatchTransactionResult {
883                index: 0,
884                transaction: create_dummy_signed_txn(),
885                result: Ok(BatchTransactionStatus::Pending {
886                    hash: "0x1".to_string(),
887                }),
888            },
889            BatchTransactionResult {
890                index: 1,
891                transaction: create_dummy_signed_txn(),
892                result: Ok(BatchTransactionStatus::Confirmed {
893                    hash: "0x2".to_string(),
894                    success: true,
895                    version: 101,
896                    gas_used: 600,
897                }),
898            },
899        ];
900
901        let summary = BatchSummary::from_results(&results);
902        assert_eq!(summary.total, 2);
903        assert_eq!(summary.succeeded, 1);
904        assert_eq!(summary.pending, 1);
905        assert!(!summary.all_succeeded());
906    }
907
908    #[test]
909    fn test_batch_summary_with_errors() {
910        let results = vec![BatchTransactionResult {
911            index: 0,
912            transaction: create_dummy_signed_txn(),
913            result: Err(AptosError::Transaction("failed".to_string())),
914        }];
915
916        let summary = BatchSummary::from_results(&results);
917        assert_eq!(summary.total, 1);
918        assert_eq!(summary.failed, 1);
919        assert!(summary.has_failures());
920    }
921
922    #[test]
923    fn test_batch_builder_with_max_gas() {
924        let builder = TransactionBatchBuilder::new()
925            .sender(AccountAddress::ONE)
926            .starting_sequence_number(0)
927            .chain_id(ChainId::testnet())
928            .max_gas_amount(500_000)
929            .add_payload(TransactionPayload::Script(crate::transaction::Script {
930                code: vec![],
931                type_args: vec![],
932                args: vec![],
933            }));
934
935        let transactions = builder.build().unwrap();
936        assert_eq!(transactions.len(), 1);
937        assert_eq!(transactions[0].max_gas_amount, 500_000);
938    }
939
940    #[test]
941    fn test_batch_builder_with_expiration() {
942        let builder = TransactionBatchBuilder::new()
943            .sender(AccountAddress::ONE)
944            .starting_sequence_number(0)
945            .chain_id(ChainId::testnet())
946            .expiration_secs(3600) // 1 hour from now
947            .add_payload(TransactionPayload::Script(crate::transaction::Script {
948                code: vec![],
949                type_args: vec![],
950                args: vec![],
951            }));
952
953        let transactions = builder.build().unwrap();
954        // Expiration should be set to some future timestamp (> current time)
955        assert!(transactions[0].expiration_timestamp_secs > 0);
956    }
957
958    #[test]
959    fn test_batch_builder_empty_payloads() {
960        let builder = TransactionBatchBuilder::new()
961            .sender(AccountAddress::ONE)
962            .starting_sequence_number(0)
963            .chain_id(ChainId::testnet());
964
965        // Empty payloads returns empty vec, not error
966        let result = builder.build();
967        assert!(result.is_ok());
968        assert_eq!(result.unwrap().len(), 0);
969    }
970
971    #[test]
972    fn test_batch_result_transaction_accessor() {
973        let signed_txn = create_dummy_signed_txn();
974        let result = BatchTransactionResult {
975            index: 0,
976            transaction: signed_txn.clone(),
977            result: Ok(BatchTransactionStatus::Pending {
978                hash: "0x123".to_string(),
979            }),
980        };
981
982        assert_eq!(result.index, 0);
983        assert_eq!(result.transaction.raw_txn.sender, AccountAddress::ONE);
984    }
985
986    #[test]
987    fn test_batch_builder_default() {
988        let builder = TransactionBatchBuilder::default();
989        assert!(builder.is_empty());
990        assert_eq!(builder.len(), 0);
991    }
992
993    #[test]
994    fn test_batch_builder_len_and_is_empty() {
995        let builder = TransactionBatchBuilder::new();
996        assert!(builder.is_empty());
997        assert_eq!(builder.len(), 0);
998
999        let builder = builder.add_payload(TransactionPayload::Script(crate::transaction::Script {
1000            code: vec![],
1001            type_args: vec![],
1002            args: vec![],
1003        }));
1004        assert!(!builder.is_empty());
1005        assert_eq!(builder.len(), 1);
1006    }
1007
1008    #[test]
1009    fn test_batch_builder_add_payloads() {
1010        let payloads = vec![
1011            TransactionPayload::Script(crate::transaction::Script {
1012                code: vec![1],
1013                type_args: vec![],
1014                args: vec![],
1015            }),
1016            TransactionPayload::Script(crate::transaction::Script {
1017                code: vec![2],
1018                type_args: vec![],
1019                args: vec![],
1020            }),
1021            TransactionPayload::Script(crate::transaction::Script {
1022                code: vec![3],
1023                type_args: vec![],
1024                args: vec![],
1025            }),
1026        ];
1027
1028        let builder = TransactionBatchBuilder::new()
1029            .sender(AccountAddress::ONE)
1030            .starting_sequence_number(0)
1031            .chain_id(ChainId::testnet())
1032            .add_payloads(payloads);
1033
1034        assert_eq!(builder.len(), 3);
1035
1036        let transactions = builder.build().unwrap();
1037        assert_eq!(transactions.len(), 3);
1038    }
1039
1040    #[test]
1041    fn test_batch_builder_missing_sequence_number() {
1042        let builder = TransactionBatchBuilder::new()
1043            .sender(AccountAddress::ONE)
1044            .chain_id(ChainId::testnet())
1045            .add_payload(TransactionPayload::Script(crate::transaction::Script {
1046                code: vec![],
1047                type_args: vec![],
1048                args: vec![],
1049            }));
1050
1051        let result = builder.build();
1052        assert!(result.is_err());
1053        assert!(result.unwrap_err().to_string().contains("sequence_number"));
1054    }
1055
1056    #[test]
1057    fn test_batch_builder_missing_chain_id() {
1058        let builder = TransactionBatchBuilder::new()
1059            .sender(AccountAddress::ONE)
1060            .starting_sequence_number(0)
1061            .add_payload(TransactionPayload::Script(crate::transaction::Script {
1062                code: vec![],
1063                type_args: vec![],
1064                args: vec![],
1065            }));
1066
1067        let result = builder.build();
1068        assert!(result.is_err());
1069        assert!(result.unwrap_err().to_string().contains("chain_id"));
1070    }
1071
1072    #[test]
1073    fn test_batch_summary_empty() {
1074        let results: Vec<BatchTransactionResult> = vec![];
1075        let summary = BatchSummary::from_results(&results);
1076        assert_eq!(summary.total, 0);
1077        assert_eq!(summary.succeeded, 0);
1078        assert_eq!(summary.failed, 0);
1079        assert_eq!(summary.pending, 0);
1080        assert_eq!(summary.total_gas_used, 0);
1081        assert!(summary.all_succeeded());
1082        assert!(!summary.has_failures());
1083    }
1084
1085    #[test]
1086    fn test_batch_status_failed_variant() {
1087        let failed = BatchTransactionStatus::Failed {
1088            error: "connection timeout".to_string(),
1089        };
1090        assert!(failed.is_failed());
1091        assert!(!failed.is_success());
1092        assert!(failed.hash().is_none());
1093    }
1094
1095    #[test]
1096    fn test_signed_transaction_batch_len() {
1097        let batch = SignedTransactionBatch {
1098            transactions: vec![create_dummy_signed_txn(), create_dummy_signed_txn()],
1099        };
1100        assert_eq!(batch.len(), 2);
1101        assert!(!batch.is_empty());
1102    }
1103
1104    #[test]
1105    fn test_signed_transaction_batch_iter() {
1106        let txn1 = create_dummy_signed_txn();
1107        let txn2 = create_dummy_signed_txn();
1108        let batch = SignedTransactionBatch {
1109            transactions: vec![txn1, txn2],
1110        };
1111
1112        let collected: Vec<_> = batch.transactions.iter().collect();
1113        assert_eq!(collected.len(), 2);
1114    }
1115
1116    #[test]
1117    fn test_batch_builder_gas_settings() {
1118        let builder = TransactionBatchBuilder::new()
1119            .max_gas_amount(50000)
1120            .gas_unit_price(200)
1121            .expiration_secs(120);
1122
1123        assert_eq!(builder.max_gas_amount, 50000);
1124        assert_eq!(builder.gas_unit_price, 200);
1125        assert_eq!(builder.expiration_secs, 120);
1126    }
1127
1128    #[test]
1129    fn test_batch_builder_missing_sender() {
1130        let builder = TransactionBatchBuilder::new()
1131            .starting_sequence_number(0)
1132            .chain_id(ChainId::testnet())
1133            .add_payload(TransactionPayload::Script(crate::transaction::Script {
1134                code: vec![],
1135                type_args: vec![],
1136                args: vec![],
1137            }));
1138
1139        let result = builder.build();
1140        assert!(result.is_err());
1141        assert!(result.unwrap_err().to_string().contains("sender"));
1142    }
1143
1144    #[test]
1145    fn test_batch_summary_with_failures() {
1146        let txn = create_dummy_signed_txn();
1147        let results = vec![
1148            BatchTransactionResult {
1149                index: 0,
1150                transaction: txn.clone(),
1151                result: Ok(BatchTransactionStatus::Failed {
1152                    error: "error".to_string(),
1153                }),
1154            },
1155            BatchTransactionResult {
1156                index: 1,
1157                transaction: txn,
1158                result: Err(AptosError::Transaction("test".to_string())),
1159            },
1160        ];
1161
1162        let summary = BatchSummary::from_results(&results);
1163        assert_eq!(summary.total, 2);
1164        assert_eq!(summary.failed, 2);
1165        assert!(summary.has_failures());
1166    }
1167
1168    #[test]
1169    fn test_batch_status_confirmed_variant() {
1170        let status = BatchTransactionStatus::Confirmed {
1171            hash: "0xabc".to_string(),
1172            success: true,
1173            version: 1,
1174            gas_used: 150,
1175        };
1176        assert!(status.is_success());
1177        assert!(!status.is_failed());
1178        assert_eq!(status.hash(), Some("0xabc"));
1179    }
1180
1181    #[test]
1182    fn test_batch_status_pending_variant() {
1183        let status = BatchTransactionStatus::Pending {
1184            hash: "0xdef".to_string(),
1185        };
1186        assert!(!status.is_success());
1187        assert!(!status.is_failed());
1188        assert_eq!(status.hash(), Some("0xdef"));
1189    }
1190
1191    #[test]
1192    fn test_signed_transaction_batch_new() {
1193        let txn1 = create_dummy_signed_txn();
1194        let txn2 = create_dummy_signed_txn();
1195        let batch = SignedTransactionBatch::new(vec![txn1, txn2]);
1196        assert_eq!(batch.len(), 2);
1197    }
1198
1199    #[test]
1200    fn test_signed_transaction_batch_transactions() {
1201        let txn1 = create_dummy_signed_txn();
1202        let txn2 = create_dummy_signed_txn();
1203        let batch = SignedTransactionBatch::new(vec![txn1, txn2]);
1204
1205        let txns = batch.transactions();
1206        assert_eq!(txns.len(), 2);
1207    }
1208
1209    #[test]
1210    fn test_signed_transaction_batch_into_transactions() {
1211        let txn1 = create_dummy_signed_txn();
1212        let txn2 = create_dummy_signed_txn();
1213        let batch = SignedTransactionBatch::new(vec![txn1, txn2]);
1214
1215        let txns = batch.into_transactions();
1216        assert_eq!(txns.len(), 2);
1217    }
1218
1219    #[test]
1220    fn test_signed_transaction_batch_empty() {
1221        let batch = SignedTransactionBatch::new(vec![]);
1222        assert!(batch.is_empty());
1223        assert_eq!(batch.len(), 0);
1224    }
1225
1226    #[test]
1227    fn test_batch_transaction_result_accessors() {
1228        let txn = create_dummy_signed_txn();
1229        let result = BatchTransactionResult {
1230            index: 5,
1231            transaction: txn.clone(),
1232            result: Ok(BatchTransactionStatus::Confirmed {
1233                hash: "0x123".to_string(),
1234                success: true,
1235                version: 1,
1236                gas_used: 100,
1237            }),
1238        };
1239
1240        assert_eq!(result.index, 5);
1241        assert!(result.result.is_ok());
1242    }
1243
1244    #[test]
1245    fn test_batch_builder_debug() {
1246        let builder = TransactionBatchBuilder::new().sender(AccountAddress::ONE);
1247        let debug = format!("{builder:?}");
1248        assert!(debug.contains("TransactionBatchBuilder"));
1249    }
1250
1251    #[test]
1252    fn test_signed_transaction_batch_debug() {
1253        let batch = SignedTransactionBatch::new(vec![create_dummy_signed_txn()]);
1254        let debug = format!("{batch:?}");
1255        assert!(debug.contains("SignedTransactionBatch"));
1256    }
1257
1258    #[test]
1259    fn test_batch_summary_debug() {
1260        let summary = BatchSummary {
1261            total: 5,
1262            succeeded: 3,
1263            failed: 1,
1264            pending: 1,
1265            total_gas_used: 500,
1266        };
1267        let debug = format!("{summary:?}");
1268        assert!(debug.contains("BatchSummary"));
1269    }
1270
1271    #[test]
1272    fn test_batch_transaction_status_debug() {
1273        let status = BatchTransactionStatus::Confirmed {
1274            hash: "0x123".to_string(),
1275            success: true,
1276            version: 1,
1277            gas_used: 100,
1278        };
1279        let debug = format!("{status:?}");
1280        assert!(debug.contains("Confirmed"));
1281    }
1282}