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: 2_000_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::atomic::AtomicU8,
533}
534
535impl<'a> BatchOperations<'a> {
536 pub fn new(client: &'a FullnodeClient, chain_id: &'a std::sync::atomic::AtomicU8) -> Self {
538 Self { client, chain_id }
539 }
540
541 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 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 pub async fn build<A: Account>(
564 &self,
565 account: &A,
566 payloads: Vec<TransactionPayload>,
567 ) -> AptosResult<SignedTransactionBatch> {
568 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 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 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 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) .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 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 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}