1use 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#[derive(Debug)]
46pub struct BatchTransactionResult {
47 pub index: usize,
49 pub transaction: SignedTransaction,
51 pub result: Result<BatchTransactionStatus, AptosError>,
53}
54
55#[derive(Debug, Clone)]
57pub enum BatchTransactionStatus {
58 Pending {
60 hash: String,
62 },
63 Confirmed {
65 hash: String,
67 success: bool,
69 version: u64,
71 gas_used: u64,
73 },
74 Failed {
76 error: String,
78 },
79}
80
81impl BatchTransactionStatus {
82 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 pub fn is_success(&self) -> bool {
93 matches!(
94 self,
95 BatchTransactionStatus::Confirmed { success: true, .. }
96 )
97 }
98
99 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#[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 #[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 #[must_use]
162 pub fn sender(mut self, sender: AccountAddress) -> Self {
163 self.sender = Some(sender);
164 self
165 }
166
167 #[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 #[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 #[must_use]
186 pub fn gas_unit_price(mut self, price: u64) -> Self {
187 self.gas_unit_price = price;
188 self
189 }
190
191 #[must_use]
193 pub fn max_gas_amount(mut self, amount: u64) -> Self {
194 self.max_gas_amount = amount;
195 self
196 }
197
198 #[must_use]
200 pub fn expiration_secs(mut self, secs: u64) -> Self {
201 self.expiration_secs = secs;
202 self
203 }
204
205 #[must_use]
207 pub fn add_payload(mut self, payload: TransactionPayload) -> Self {
208 self.payloads.push(payload);
209 self
210 }
211
212 #[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 pub fn len(&self) -> usize {
221 self.payloads.len()
222 }
223
224 pub fn is_empty(&self) -> bool {
226 self.payloads.is_empty()
227 }
228
229 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 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 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#[derive(Debug, Clone)]
290pub struct SignedTransactionBatch {
291 transactions: Vec<SignedTransaction>,
292}
293
294impl SignedTransactionBatch {
295 pub fn new(transactions: Vec<SignedTransaction>) -> Self {
297 Self { transactions }
298 }
299
300 pub fn transactions(&self) -> &[SignedTransaction] {
302 &self.transactions
303 }
304
305 pub fn into_transactions(self) -> Vec<SignedTransaction> {
307 self.transactions
308 }
309
310 pub fn len(&self) -> usize {
312 self.transactions.len()
313 }
314
315 pub fn is_empty(&self) -> bool {
317 self.transactions.is_empty()
318 }
319
320 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 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 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 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 if results.last().is_some_and(|r| r.result.is_err()) {
414 break;
415 }
416 }
417
418 results
419 }
420}
421
422async 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#[derive(Debug, Clone)]
461pub struct BatchSummary {
462 pub total: usize,
464 pub succeeded: usize,
466 pub failed: usize,
468 pub pending: usize,
470 pub total_gas_used: u64,
472}
473
474impl BatchSummary {
475 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 pub fn all_succeeded(&self) -> bool {
519 self.succeeded == self.total
520 }
521
522 pub fn has_failures(&self) -> bool {
524 self.failed > 0
525 }
526}
527
528#[allow(missing_debug_implementations)] pub struct BatchOperations<'a> {
531 client: &'a FullnodeClient,
532 chain_id: &'a std::sync::RwLock<ChainId>,
533}
534
535impl<'a> BatchOperations<'a> {
536 pub fn new(client: &'a FullnodeClient, chain_id: &'a std::sync::RwLock<ChainId>) -> Self {
538 Self { client, chain_id }
539 }
540
541 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 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 pub async fn build<A: Account>(
566 &self,
567 account: &A,
568 payloads: Vec<TransactionPayload>,
569 ) -> AptosResult<SignedTransactionBatch> {
570 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 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 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 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) .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 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 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}