1use crate::account::Account;
36use crate::api::FullnodeClient;
37
38use crate::error::{AptosError, AptosResult};
39use crate::transaction::{
40 RawTransaction, SignedTransaction, TransactionBuilder, TransactionPayload,
41 builder::sign_transaction,
42};
43use crate::types::{AccountAddress, ChainId};
44use futures::future::join_all;
45use std::time::Duration;
46
47#[derive(Debug)]
49pub struct BatchTransactionResult {
50 pub index: usize,
52 pub transaction: SignedTransaction,
54 pub result: Result<BatchTransactionStatus, AptosError>,
56}
57
58#[derive(Debug, Clone)]
60pub enum BatchTransactionStatus {
61 Pending {
63 hash: String,
65 },
66 Confirmed {
68 hash: String,
70 success: bool,
72 version: u64,
74 gas_used: u64,
76 },
77 Failed {
79 error: String,
81 },
82}
83
84impl BatchTransactionStatus {
85 pub fn hash(&self) -> Option<&str> {
87 match self {
88 BatchTransactionStatus::Pending { hash }
89 | BatchTransactionStatus::Confirmed { hash, .. } => Some(hash),
90 BatchTransactionStatus::Failed { .. } => None,
91 }
92 }
93
94 pub fn is_success(&self) -> bool {
96 matches!(
97 self,
98 BatchTransactionStatus::Confirmed { success: true, .. }
99 )
100 }
101
102 pub fn is_failed(&self) -> bool {
104 matches!(self, BatchTransactionStatus::Failed { .. })
105 || matches!(
106 self,
107 BatchTransactionStatus::Confirmed { success: false, .. }
108 )
109 }
110}
111
112#[derive(Debug, Clone)]
138pub struct TransactionBatchBuilder {
139 sender: Option<AccountAddress>,
140 starting_sequence_number: Option<u64>,
141 chain_id: Option<ChainId>,
142 gas_unit_price: u64,
143 max_gas_amount: u64,
144 expiration_secs: u64,
145 payloads: Vec<TransactionPayload>,
146}
147
148impl Default for TransactionBatchBuilder {
149 fn default() -> Self {
150 Self::new()
151 }
152}
153
154impl TransactionBatchBuilder {
155 #[must_use]
157 pub fn new() -> Self {
158 Self {
159 sender: None,
160 starting_sequence_number: None,
161 chain_id: None,
162 gas_unit_price: 100,
163 max_gas_amount: 2_000_000,
164 expiration_secs: 600,
165 payloads: Vec::new(),
166 }
167 }
168
169 #[must_use]
171 pub fn sender(mut self, sender: AccountAddress) -> Self {
172 self.sender = Some(sender);
173 self
174 }
175
176 #[must_use]
181 pub fn starting_sequence_number(mut self, seq: u64) -> Self {
182 self.starting_sequence_number = Some(seq);
183 self
184 }
185
186 #[must_use]
188 pub fn chain_id(mut self, chain_id: ChainId) -> Self {
189 self.chain_id = Some(chain_id);
190 self
191 }
192
193 #[must_use]
195 pub fn gas_unit_price(mut self, price: u64) -> Self {
196 self.gas_unit_price = price;
197 self
198 }
199
200 #[must_use]
202 pub fn max_gas_amount(mut self, amount: u64) -> Self {
203 self.max_gas_amount = amount;
204 self
205 }
206
207 #[must_use]
209 pub fn expiration_secs(mut self, secs: u64) -> Self {
210 self.expiration_secs = secs;
211 self
212 }
213
214 #[must_use]
216 pub fn add_payload(mut self, payload: TransactionPayload) -> Self {
217 self.payloads.push(payload);
218 self
219 }
220
221 #[must_use]
223 pub fn add_payloads(mut self, payloads: impl IntoIterator<Item = TransactionPayload>) -> Self {
224 self.payloads.extend(payloads);
225 self
226 }
227
228 pub fn len(&self) -> usize {
230 self.payloads.len()
231 }
232
233 pub fn is_empty(&self) -> bool {
235 self.payloads.is_empty()
236 }
237
238 pub fn build(self) -> AptosResult<Vec<RawTransaction>> {
244 let sender = self
245 .sender
246 .ok_or_else(|| AptosError::Transaction("sender is required".into()))?;
247 let starting_seq = self.starting_sequence_number.ok_or_else(|| {
248 AptosError::Transaction("starting_sequence_number is required".into())
249 })?;
250 let chain_id = self
251 .chain_id
252 .ok_or_else(|| AptosError::Transaction("chain_id is required".into()))?;
253
254 let mut transactions = Vec::with_capacity(self.payloads.len());
255
256 for (i, payload) in self.payloads.into_iter().enumerate() {
257 let sequence_number = starting_seq
259 .checked_add(i as u64)
260 .ok_or_else(|| AptosError::Transaction("sequence number overflow".into()))?;
261
262 let txn = TransactionBuilder::new()
263 .sender(sender)
264 .sequence_number(sequence_number)
265 .payload(payload)
266 .gas_unit_price(self.gas_unit_price)
267 .max_gas_amount(self.max_gas_amount)
268 .chain_id(chain_id)
269 .expiration_from_now(self.expiration_secs)
270 .build()?;
271 transactions.push(txn);
272 }
273
274 Ok(transactions)
275 }
276
277 pub fn build_and_sign<A: Account>(self, account: &A) -> AptosResult<SignedTransactionBatch> {
283 let raw_transactions = self.build()?;
284 let mut signed = Vec::with_capacity(raw_transactions.len());
285
286 for raw_txn in raw_transactions {
287 let signed_txn = sign_transaction(&raw_txn, account)?;
288 signed.push(signed_txn);
289 }
290
291 Ok(SignedTransactionBatch {
292 transactions: signed,
293 })
294 }
295}
296
297#[derive(Debug, Clone)]
299pub struct SignedTransactionBatch {
300 transactions: Vec<SignedTransaction>,
301}
302
303impl SignedTransactionBatch {
304 pub fn new(transactions: Vec<SignedTransaction>) -> Self {
306 Self { transactions }
307 }
308
309 pub fn transactions(&self) -> &[SignedTransaction] {
311 &self.transactions
312 }
313
314 pub fn into_transactions(self) -> Vec<SignedTransaction> {
316 self.transactions
317 }
318
319 pub fn len(&self) -> usize {
321 self.transactions.len()
322 }
323
324 pub fn is_empty(&self) -> bool {
326 self.transactions.is_empty()
327 }
328
329 pub async fn submit_all(self, client: &FullnodeClient) -> Vec<BatchTransactionResult> {
333 let futures: Vec<_> = self
334 .transactions
335 .into_iter()
336 .enumerate()
337 .map(|(index, txn)| {
338 let client = client.clone();
339 async move {
340 let result = client.submit_transaction(&txn).await;
341 BatchTransactionResult {
342 index,
343 transaction: txn,
344 result: result.map(|resp| BatchTransactionStatus::Pending {
345 hash: resp.data.hash.to_string(),
346 }),
347 }
348 }
349 })
350 .collect();
351
352 join_all(futures).await
353 }
354
355 pub async fn submit_and_wait_all(
359 self,
360 client: &FullnodeClient,
361 timeout: Option<Duration>,
362 ) -> Vec<BatchTransactionResult> {
363 let futures: Vec<_> = self
364 .transactions
365 .into_iter()
366 .enumerate()
367 .map(|(index, txn)| {
368 let client = client.clone();
369 async move {
370 let result = submit_and_wait_single(&client, &txn, timeout).await;
371 BatchTransactionResult {
372 index,
373 transaction: txn,
374 result,
375 }
376 }
377 })
378 .collect();
379
380 join_all(futures).await
381 }
382
383 pub async fn submit_sequential(self, client: &FullnodeClient) -> Vec<BatchTransactionResult> {
387 let mut results = Vec::with_capacity(self.transactions.len());
388
389 for (index, txn) in self.transactions.into_iter().enumerate() {
390 let result = client.submit_transaction(&txn).await;
391 results.push(BatchTransactionResult {
392 index,
393 transaction: txn,
394 result: result.map(|resp| BatchTransactionStatus::Pending {
395 hash: resp.data.hash.to_string(),
396 }),
397 });
398 }
399
400 results
401 }
402
403 pub async fn submit_and_wait_sequential(
407 self,
408 client: &FullnodeClient,
409 timeout: Option<Duration>,
410 ) -> Vec<BatchTransactionResult> {
411 let mut results = Vec::with_capacity(self.transactions.len());
412
413 for (index, txn) in self.transactions.into_iter().enumerate() {
414 let result = submit_and_wait_single(client, &txn, timeout).await;
415 results.push(BatchTransactionResult {
416 index,
417 transaction: txn.clone(),
418 result,
419 });
420
421 if results.last().is_some_and(|r| r.result.is_err()) {
423 break;
424 }
425 }
426
427 results
428 }
429}
430
431async fn submit_and_wait_single(
433 client: &FullnodeClient,
434 txn: &SignedTransaction,
435 timeout: Option<Duration>,
436) -> Result<BatchTransactionStatus, AptosError> {
437 let response = client.submit_and_wait(txn, timeout).await?;
438 let data = response.into_inner();
439
440 let hash = data
441 .get("hash")
442 .and_then(|v| v.as_str())
443 .unwrap_or("")
444 .to_string();
445 let success = data
446 .get("success")
447 .and_then(serde_json::Value::as_bool)
448 .unwrap_or(false);
449 let version = data
450 .get("version")
451 .and_then(serde_json::Value::as_str)
452 .and_then(|s| s.parse().ok())
453 .unwrap_or(0);
454 let gas_used = data
455 .get("gas_used")
456 .and_then(|v| v.as_str())
457 .and_then(|s| s.parse().ok())
458 .unwrap_or(0);
459
460 Ok(BatchTransactionStatus::Confirmed {
461 hash,
462 success,
463 version,
464 gas_used,
465 })
466}
467
468#[derive(Debug, Clone)]
470pub struct BatchSummary {
471 pub total: usize,
473 pub succeeded: usize,
475 pub failed: usize,
477 pub pending: usize,
479 pub total_gas_used: u64,
481}
482
483impl BatchSummary {
484 pub fn from_results(results: &[BatchTransactionResult]) -> Self {
486 let mut succeeded = 0;
487 let mut failed = 0;
488 let mut pending = 0;
489 let mut total_gas_used = 0u64;
490
491 for result in results {
492 match &result.result {
493 Ok(status) => match status {
494 BatchTransactionStatus::Confirmed {
495 success, gas_used, ..
496 } => {
497 if *success {
498 succeeded += 1;
499 } else {
500 failed += 1;
501 }
502 total_gas_used = total_gas_used.saturating_add(*gas_used);
503 }
504 BatchTransactionStatus::Pending { .. } => {
505 pending += 1;
506 }
507 BatchTransactionStatus::Failed { .. } => {
508 failed += 1;
509 }
510 },
511 Err(_) => {
512 failed += 1;
513 }
514 }
515 }
516
517 Self {
518 total: results.len(),
519 succeeded,
520 failed,
521 pending,
522 total_gas_used,
523 }
524 }
525
526 pub fn all_succeeded(&self) -> bool {
528 self.succeeded == self.total
529 }
530
531 pub fn has_failures(&self) -> bool {
533 self.failed > 0
534 }
535}
536
537#[allow(missing_debug_implementations)] pub struct BatchOperations<'a> {
540 client: &'a FullnodeClient,
541 chain_id: &'a std::sync::atomic::AtomicU8,
542}
543
544impl<'a> BatchOperations<'a> {
545 pub fn new(client: &'a FullnodeClient, chain_id: &'a std::sync::atomic::AtomicU8) -> Self {
547 Self { client, chain_id }
548 }
549
550 async fn resolve_chain_id(&self) -> AptosResult<ChainId> {
552 let id = self.chain_id.load(std::sync::atomic::Ordering::Relaxed);
553 if id > 0 {
554 return Ok(ChainId::new(id));
555 }
556 let response = self.client.get_ledger_info().await?;
558 let info = response.into_inner();
559 self.chain_id
560 .store(info.chain_id, std::sync::atomic::Ordering::Relaxed);
561 Ok(ChainId::new(info.chain_id))
562 }
563
564 pub async fn build<A: Account>(
573 &self,
574 account: &A,
575 payloads: Vec<TransactionPayload>,
576 ) -> AptosResult<SignedTransactionBatch> {
577 let (sequence_number, gas_estimation, chain_id) = tokio::join!(
579 self.client.get_sequence_number(account.address()),
580 self.client.estimate_gas_price(),
581 self.resolve_chain_id()
582 );
583 let sequence_number = sequence_number?;
584 let gas_estimation = gas_estimation?;
585 let chain_id = chain_id?;
586
587 let batch = TransactionBatchBuilder::new()
588 .sender(account.address())
589 .starting_sequence_number(sequence_number)
590 .chain_id(chain_id)
591 .gas_unit_price(gas_estimation.data.recommended())
592 .add_payloads(payloads)
593 .build_and_sign(account)?;
594
595 Ok(batch)
596 }
597
598 pub async fn submit<A: Account>(
604 &self,
605 account: &A,
606 payloads: Vec<TransactionPayload>,
607 ) -> AptosResult<Vec<BatchTransactionResult>> {
608 let batch = self.build(account, payloads).await?;
609 Ok(batch.submit_all(self.client).await)
610 }
611
612 pub async fn submit_and_wait<A: Account>(
619 &self,
620 account: &A,
621 payloads: Vec<TransactionPayload>,
622 timeout: Option<Duration>,
623 ) -> AptosResult<Vec<BatchTransactionResult>> {
624 let batch = self.build(account, payloads).await?;
625 Ok(batch.submit_and_wait_all(self.client, timeout).await)
626 }
627
628 pub async fn transfer_apt<A: Account>(
635 &self,
636 sender: &A,
637 transfers: Vec<(AccountAddress, u64)>,
638 ) -> AptosResult<Vec<BatchTransactionResult>> {
639 use crate::transaction::EntryFunction;
640
641 let payloads: Vec<_> = transfers
642 .into_iter()
643 .map(|(recipient, amount)| {
644 EntryFunction::apt_transfer(recipient, amount).map(TransactionPayload::from)
645 })
646 .collect::<AptosResult<Vec<_>>>()?;
647
648 self.submit_and_wait(sender, payloads, None).await
649 }
650}
651
652#[cfg(test)]
653mod tests {
654 use super::*;
655 use crate::config::AptosConfig;
656 use wiremock::{
657 Mock, MockServer, ResponseTemplate,
658 matchers::{method, path, path_regex},
659 };
660
661 fn create_mock_client(server: &MockServer) -> FullnodeClient {
664 let url = format!("{}/v1", server.uri());
665 let config = AptosConfig::custom(&url).unwrap().without_retry();
666 FullnodeClient::new(config).unwrap()
667 }
668
669 fn pending_txn_response() -> serde_json::Value {
671 serde_json::json!({
672 "hash": "0x0000000000000000000000000000000000000000000000000000000000000001",
673 "sender": "0x1",
674 "sequence_number": "0",
675 "max_gas_amount": "200000",
676 "gas_unit_price": "100",
677 "expiration_timestamp_secs": "1000000"
678 })
679 }
680
681 fn committed_txn_response() -> serde_json::Value {
683 serde_json::json!({
684 "type": "user_transaction",
685 "version": "12345",
686 "hash": "0x0000000000000000000000000000000000000000000000000000000000000001",
687 "success": true,
688 "vm_status": "Executed successfully",
689 "gas_used": "500"
690 })
691 }
692
693 #[test]
694 fn test_batch_builder_missing_fields() {
695 let builder = TransactionBatchBuilder::new().add_payload(TransactionPayload::Script(
696 crate::transaction::Script {
697 code: vec![],
698 type_args: vec![],
699 args: vec![],
700 },
701 ));
702
703 let result = builder.build();
704 assert!(result.is_err());
705 }
706
707 #[test]
708 fn test_batch_builder_complete() {
709 let builder = TransactionBatchBuilder::new()
710 .sender(AccountAddress::ONE)
711 .starting_sequence_number(0)
712 .chain_id(ChainId::testnet())
713 .gas_unit_price(100)
714 .add_payload(TransactionPayload::Script(crate::transaction::Script {
715 code: vec![],
716 type_args: vec![],
717 args: vec![],
718 }))
719 .add_payload(TransactionPayload::Script(crate::transaction::Script {
720 code: vec![],
721 type_args: vec![],
722 args: vec![],
723 }));
724
725 let transactions = builder.build().unwrap();
726 assert_eq!(transactions.len(), 2);
727 assert_eq!(transactions[0].sequence_number, 0);
728 assert_eq!(transactions[1].sequence_number, 1);
729 }
730
731 #[test]
732 fn test_batch_builder_sequence_numbers() {
733 let builder = TransactionBatchBuilder::new()
734 .sender(AccountAddress::ONE)
735 .starting_sequence_number(10)
736 .chain_id(ChainId::testnet())
737 .add_payload(TransactionPayload::Script(crate::transaction::Script {
738 code: vec![],
739 type_args: vec![],
740 args: vec![],
741 }))
742 .add_payload(TransactionPayload::Script(crate::transaction::Script {
743 code: vec![],
744 type_args: vec![],
745 args: vec![],
746 }))
747 .add_payload(TransactionPayload::Script(crate::transaction::Script {
748 code: vec![],
749 type_args: vec![],
750 args: vec![],
751 }));
752
753 let transactions = builder.build().unwrap();
754 assert_eq!(transactions.len(), 3);
755 assert_eq!(transactions[0].sequence_number, 10);
756 assert_eq!(transactions[1].sequence_number, 11);
757 assert_eq!(transactions[2].sequence_number, 12);
758 }
759
760 #[test]
761 fn test_batch_summary() {
762 let results = vec![
763 BatchTransactionResult {
764 index: 0,
765 transaction: create_dummy_signed_txn(),
766 result: Ok(BatchTransactionStatus::Confirmed {
767 hash: "0x1".to_string(),
768 success: true,
769 version: 100,
770 gas_used: 500,
771 }),
772 },
773 BatchTransactionResult {
774 index: 1,
775 transaction: create_dummy_signed_txn(),
776 result: Ok(BatchTransactionStatus::Confirmed {
777 hash: "0x2".to_string(),
778 success: true,
779 version: 101,
780 gas_used: 600,
781 }),
782 },
783 BatchTransactionResult {
784 index: 2,
785 transaction: create_dummy_signed_txn(),
786 result: Ok(BatchTransactionStatus::Confirmed {
787 hash: "0x3".to_string(),
788 success: false,
789 version: 102,
790 gas_used: 100,
791 }),
792 },
793 ];
794
795 let summary = BatchSummary::from_results(&results);
796 assert_eq!(summary.total, 3);
797 assert_eq!(summary.succeeded, 2);
798 assert_eq!(summary.failed, 1);
799 assert_eq!(summary.pending, 0);
800 assert_eq!(summary.total_gas_used, 1200);
801 assert!(!summary.all_succeeded());
802 assert!(summary.has_failures());
803 }
804
805 #[test]
806 fn test_batch_status_methods() {
807 let pending = BatchTransactionStatus::Pending {
808 hash: "0x123".to_string(),
809 };
810 assert_eq!(pending.hash(), Some("0x123"));
811 assert!(!pending.is_success());
812 assert!(!pending.is_failed());
813
814 let confirmed_success = BatchTransactionStatus::Confirmed {
815 hash: "0x456".to_string(),
816 success: true,
817 version: 100,
818 gas_used: 500,
819 };
820 assert_eq!(confirmed_success.hash(), Some("0x456"));
821 assert!(confirmed_success.is_success());
822 assert!(!confirmed_success.is_failed());
823
824 let confirmed_failed = BatchTransactionStatus::Confirmed {
825 hash: "0x789".to_string(),
826 success: false,
827 version: 101,
828 gas_used: 100,
829 };
830 assert!(!confirmed_failed.is_success());
831 assert!(confirmed_failed.is_failed());
832
833 let failed = BatchTransactionStatus::Failed {
834 error: "timeout".to_string(),
835 };
836 assert!(failed.hash().is_none());
837 assert!(!failed.is_success());
838 assert!(failed.is_failed());
839 }
840
841 #[cfg(feature = "ed25519")]
842 #[test]
843 fn test_batch_build_and_sign() {
844 use crate::account::Ed25519Account;
845
846 let account = Ed25519Account::generate();
847 let batch = TransactionBatchBuilder::new()
848 .sender(account.address())
849 .starting_sequence_number(0)
850 .chain_id(ChainId::testnet())
851 .add_payload(TransactionPayload::Script(crate::transaction::Script {
852 code: vec![],
853 type_args: vec![],
854 args: vec![],
855 }))
856 .add_payload(TransactionPayload::Script(crate::transaction::Script {
857 code: vec![],
858 type_args: vec![],
859 args: vec![],
860 }))
861 .build_and_sign(&account)
862 .unwrap();
863
864 assert_eq!(batch.len(), 2);
865 }
866
867 fn create_dummy_signed_txn() -> SignedTransaction {
868 use crate::transaction::TransactionAuthenticator;
869
870 let raw_txn = RawTransaction {
871 sender: AccountAddress::ONE,
872 sequence_number: 0,
873 payload: TransactionPayload::Script(crate::transaction::Script {
874 code: vec![],
875 type_args: vec![],
876 args: vec![],
877 }),
878 max_gas_amount: 200_000,
879 gas_unit_price: 100,
880 expiration_timestamp_secs: 0,
881 chain_id: ChainId::testnet(),
882 };
883
884 SignedTransaction {
885 raw_txn,
886 authenticator: TransactionAuthenticator::ed25519(vec![0u8; 32], vec![0u8; 64]),
887 }
888 }
889
890 #[test]
891 fn test_batch_summary_all_succeeded() {
892 let results = vec![
893 BatchTransactionResult {
894 index: 0,
895 transaction: create_dummy_signed_txn(),
896 result: Ok(BatchTransactionStatus::Confirmed {
897 hash: "0x1".to_string(),
898 success: true,
899 version: 100,
900 gas_used: 500,
901 }),
902 },
903 BatchTransactionResult {
904 index: 1,
905 transaction: create_dummy_signed_txn(),
906 result: Ok(BatchTransactionStatus::Confirmed {
907 hash: "0x2".to_string(),
908 success: true,
909 version: 101,
910 gas_used: 600,
911 }),
912 },
913 ];
914
915 let summary = BatchSummary::from_results(&results);
916 assert_eq!(summary.total, 2);
917 assert_eq!(summary.succeeded, 2);
918 assert_eq!(summary.failed, 0);
919 assert!(summary.all_succeeded());
920 assert!(!summary.has_failures());
921 }
922
923 #[test]
924 fn test_batch_summary_with_pending() {
925 let results = vec![
926 BatchTransactionResult {
927 index: 0,
928 transaction: create_dummy_signed_txn(),
929 result: Ok(BatchTransactionStatus::Pending {
930 hash: "0x1".to_string(),
931 }),
932 },
933 BatchTransactionResult {
934 index: 1,
935 transaction: create_dummy_signed_txn(),
936 result: Ok(BatchTransactionStatus::Confirmed {
937 hash: "0x2".to_string(),
938 success: true,
939 version: 101,
940 gas_used: 600,
941 }),
942 },
943 ];
944
945 let summary = BatchSummary::from_results(&results);
946 assert_eq!(summary.total, 2);
947 assert_eq!(summary.succeeded, 1);
948 assert_eq!(summary.pending, 1);
949 assert!(!summary.all_succeeded());
950 }
951
952 #[test]
953 fn test_batch_summary_with_errors() {
954 let results = vec![BatchTransactionResult {
955 index: 0,
956 transaction: create_dummy_signed_txn(),
957 result: Err(AptosError::Transaction("failed".to_string())),
958 }];
959
960 let summary = BatchSummary::from_results(&results);
961 assert_eq!(summary.total, 1);
962 assert_eq!(summary.failed, 1);
963 assert!(summary.has_failures());
964 }
965
966 #[test]
967 fn test_batch_builder_with_max_gas() {
968 let builder = TransactionBatchBuilder::new()
969 .sender(AccountAddress::ONE)
970 .starting_sequence_number(0)
971 .chain_id(ChainId::testnet())
972 .max_gas_amount(500_000)
973 .add_payload(TransactionPayload::Script(crate::transaction::Script {
974 code: vec![],
975 type_args: vec![],
976 args: vec![],
977 }));
978
979 let transactions = builder.build().unwrap();
980 assert_eq!(transactions.len(), 1);
981 assert_eq!(transactions[0].max_gas_amount, 500_000);
982 }
983
984 #[test]
985 fn test_batch_builder_with_expiration() {
986 let builder = TransactionBatchBuilder::new()
987 .sender(AccountAddress::ONE)
988 .starting_sequence_number(0)
989 .chain_id(ChainId::testnet())
990 .expiration_secs(3600) .add_payload(TransactionPayload::Script(crate::transaction::Script {
992 code: vec![],
993 type_args: vec![],
994 args: vec![],
995 }));
996
997 let transactions = builder.build().unwrap();
998 assert!(transactions[0].expiration_timestamp_secs > 0);
1000 }
1001
1002 #[test]
1003 fn test_batch_builder_empty_payloads() {
1004 let builder = TransactionBatchBuilder::new()
1005 .sender(AccountAddress::ONE)
1006 .starting_sequence_number(0)
1007 .chain_id(ChainId::testnet());
1008
1009 let result = builder.build();
1011 assert!(result.is_ok());
1012 assert_eq!(result.unwrap().len(), 0);
1013 }
1014
1015 #[test]
1016 fn test_batch_result_transaction_accessor() {
1017 let signed_txn = create_dummy_signed_txn();
1018 let result = BatchTransactionResult {
1019 index: 0,
1020 transaction: signed_txn.clone(),
1021 result: Ok(BatchTransactionStatus::Pending {
1022 hash: "0x123".to_string(),
1023 }),
1024 };
1025
1026 assert_eq!(result.index, 0);
1027 assert_eq!(result.transaction.raw_txn.sender, AccountAddress::ONE);
1028 }
1029
1030 #[test]
1031 fn test_batch_builder_default() {
1032 let builder = TransactionBatchBuilder::default();
1033 assert!(builder.is_empty());
1034 assert_eq!(builder.len(), 0);
1035 }
1036
1037 #[test]
1038 fn test_batch_builder_len_and_is_empty() {
1039 let builder = TransactionBatchBuilder::new();
1040 assert!(builder.is_empty());
1041 assert_eq!(builder.len(), 0);
1042
1043 let builder = builder.add_payload(TransactionPayload::Script(crate::transaction::Script {
1044 code: vec![],
1045 type_args: vec![],
1046 args: vec![],
1047 }));
1048 assert!(!builder.is_empty());
1049 assert_eq!(builder.len(), 1);
1050 }
1051
1052 #[test]
1053 fn test_batch_builder_add_payloads() {
1054 let payloads = vec![
1055 TransactionPayload::Script(crate::transaction::Script {
1056 code: vec![1],
1057 type_args: vec![],
1058 args: vec![],
1059 }),
1060 TransactionPayload::Script(crate::transaction::Script {
1061 code: vec![2],
1062 type_args: vec![],
1063 args: vec![],
1064 }),
1065 TransactionPayload::Script(crate::transaction::Script {
1066 code: vec![3],
1067 type_args: vec![],
1068 args: vec![],
1069 }),
1070 ];
1071
1072 let builder = TransactionBatchBuilder::new()
1073 .sender(AccountAddress::ONE)
1074 .starting_sequence_number(0)
1075 .chain_id(ChainId::testnet())
1076 .add_payloads(payloads);
1077
1078 assert_eq!(builder.len(), 3);
1079
1080 let transactions = builder.build().unwrap();
1081 assert_eq!(transactions.len(), 3);
1082 }
1083
1084 #[test]
1085 fn test_batch_builder_missing_sequence_number() {
1086 let builder = TransactionBatchBuilder::new()
1087 .sender(AccountAddress::ONE)
1088 .chain_id(ChainId::testnet())
1089 .add_payload(TransactionPayload::Script(crate::transaction::Script {
1090 code: vec![],
1091 type_args: vec![],
1092 args: vec![],
1093 }));
1094
1095 let result = builder.build();
1096 assert!(result.is_err());
1097 assert!(result.unwrap_err().to_string().contains("sequence_number"));
1098 }
1099
1100 #[test]
1101 fn test_batch_builder_missing_chain_id() {
1102 let builder = TransactionBatchBuilder::new()
1103 .sender(AccountAddress::ONE)
1104 .starting_sequence_number(0)
1105 .add_payload(TransactionPayload::Script(crate::transaction::Script {
1106 code: vec![],
1107 type_args: vec![],
1108 args: vec![],
1109 }));
1110
1111 let result = builder.build();
1112 assert!(result.is_err());
1113 assert!(result.unwrap_err().to_string().contains("chain_id"));
1114 }
1115
1116 #[test]
1117 fn test_batch_summary_empty() {
1118 let results: Vec<BatchTransactionResult> = vec![];
1119 let summary = BatchSummary::from_results(&results);
1120 assert_eq!(summary.total, 0);
1121 assert_eq!(summary.succeeded, 0);
1122 assert_eq!(summary.failed, 0);
1123 assert_eq!(summary.pending, 0);
1124 assert_eq!(summary.total_gas_used, 0);
1125 assert!(summary.all_succeeded());
1126 assert!(!summary.has_failures());
1127 }
1128
1129 #[test]
1130 fn test_batch_status_failed_variant() {
1131 let failed = BatchTransactionStatus::Failed {
1132 error: "connection timeout".to_string(),
1133 };
1134 assert!(failed.is_failed());
1135 assert!(!failed.is_success());
1136 assert!(failed.hash().is_none());
1137 }
1138
1139 #[test]
1140 fn test_signed_transaction_batch_len() {
1141 let batch = SignedTransactionBatch {
1142 transactions: vec![create_dummy_signed_txn(), create_dummy_signed_txn()],
1143 };
1144 assert_eq!(batch.len(), 2);
1145 assert!(!batch.is_empty());
1146 }
1147
1148 #[test]
1149 fn test_signed_transaction_batch_iter() {
1150 let txn1 = create_dummy_signed_txn();
1151 let txn2 = create_dummy_signed_txn();
1152 let batch = SignedTransactionBatch {
1153 transactions: vec![txn1, txn2],
1154 };
1155
1156 let collected: Vec<_> = batch.transactions.iter().collect();
1157 assert_eq!(collected.len(), 2);
1158 }
1159
1160 #[test]
1161 fn test_batch_builder_gas_settings() {
1162 let builder = TransactionBatchBuilder::new()
1163 .max_gas_amount(50000)
1164 .gas_unit_price(200)
1165 .expiration_secs(120);
1166
1167 assert_eq!(builder.max_gas_amount, 50000);
1168 assert_eq!(builder.gas_unit_price, 200);
1169 assert_eq!(builder.expiration_secs, 120);
1170 }
1171
1172 #[test]
1173 fn test_batch_builder_missing_sender() {
1174 let builder = TransactionBatchBuilder::new()
1175 .starting_sequence_number(0)
1176 .chain_id(ChainId::testnet())
1177 .add_payload(TransactionPayload::Script(crate::transaction::Script {
1178 code: vec![],
1179 type_args: vec![],
1180 args: vec![],
1181 }));
1182
1183 let result = builder.build();
1184 assert!(result.is_err());
1185 assert!(result.unwrap_err().to_string().contains("sender"));
1186 }
1187
1188 #[test]
1189 fn test_batch_summary_with_failures() {
1190 let txn = create_dummy_signed_txn();
1191 let results = vec![
1192 BatchTransactionResult {
1193 index: 0,
1194 transaction: txn.clone(),
1195 result: Ok(BatchTransactionStatus::Failed {
1196 error: "error".to_string(),
1197 }),
1198 },
1199 BatchTransactionResult {
1200 index: 1,
1201 transaction: txn,
1202 result: Err(AptosError::Transaction("test".to_string())),
1203 },
1204 ];
1205
1206 let summary = BatchSummary::from_results(&results);
1207 assert_eq!(summary.total, 2);
1208 assert_eq!(summary.failed, 2);
1209 assert!(summary.has_failures());
1210 }
1211
1212 #[test]
1213 fn test_batch_status_confirmed_variant() {
1214 let status = BatchTransactionStatus::Confirmed {
1215 hash: "0xabc".to_string(),
1216 success: true,
1217 version: 1,
1218 gas_used: 150,
1219 };
1220 assert!(status.is_success());
1221 assert!(!status.is_failed());
1222 assert_eq!(status.hash(), Some("0xabc"));
1223 }
1224
1225 #[test]
1226 fn test_batch_status_pending_variant() {
1227 let status = BatchTransactionStatus::Pending {
1228 hash: "0xdef".to_string(),
1229 };
1230 assert!(!status.is_success());
1231 assert!(!status.is_failed());
1232 assert_eq!(status.hash(), Some("0xdef"));
1233 }
1234
1235 #[test]
1236 fn test_signed_transaction_batch_new() {
1237 let txn1 = create_dummy_signed_txn();
1238 let txn2 = create_dummy_signed_txn();
1239 let batch = SignedTransactionBatch::new(vec![txn1, txn2]);
1240 assert_eq!(batch.len(), 2);
1241 }
1242
1243 #[test]
1244 fn test_signed_transaction_batch_transactions() {
1245 let txn1 = create_dummy_signed_txn();
1246 let txn2 = create_dummy_signed_txn();
1247 let batch = SignedTransactionBatch::new(vec![txn1, txn2]);
1248
1249 let txns = batch.transactions();
1250 assert_eq!(txns.len(), 2);
1251 }
1252
1253 #[test]
1254 fn test_signed_transaction_batch_into_transactions() {
1255 let txn1 = create_dummy_signed_txn();
1256 let txn2 = create_dummy_signed_txn();
1257 let batch = SignedTransactionBatch::new(vec![txn1, txn2]);
1258
1259 let txns = batch.into_transactions();
1260 assert_eq!(txns.len(), 2);
1261 }
1262
1263 #[test]
1264 fn test_signed_transaction_batch_empty() {
1265 let batch = SignedTransactionBatch::new(vec![]);
1266 assert!(batch.is_empty());
1267 assert_eq!(batch.len(), 0);
1268 }
1269
1270 #[test]
1271 fn test_batch_transaction_result_accessors() {
1272 let txn = create_dummy_signed_txn();
1273 let result = BatchTransactionResult {
1274 index: 5,
1275 transaction: txn.clone(),
1276 result: Ok(BatchTransactionStatus::Confirmed {
1277 hash: "0x123".to_string(),
1278 success: true,
1279 version: 1,
1280 gas_used: 100,
1281 }),
1282 };
1283
1284 assert_eq!(result.index, 5);
1285 assert!(result.result.is_ok());
1286 }
1287
1288 #[test]
1289 fn test_batch_builder_debug() {
1290 let builder = TransactionBatchBuilder::new().sender(AccountAddress::ONE);
1291 let debug = format!("{builder:?}");
1292 assert!(debug.contains("TransactionBatchBuilder"));
1293 }
1294
1295 #[test]
1296 fn test_signed_transaction_batch_debug() {
1297 let batch = SignedTransactionBatch::new(vec![create_dummy_signed_txn()]);
1298 let debug = format!("{batch:?}");
1299 assert!(debug.contains("SignedTransactionBatch"));
1300 }
1301
1302 #[test]
1303 fn test_batch_summary_debug() {
1304 let summary = BatchSummary {
1305 total: 5,
1306 succeeded: 3,
1307 failed: 1,
1308 pending: 1,
1309 total_gas_used: 500,
1310 };
1311 let debug = format!("{summary:?}");
1312 assert!(debug.contains("BatchSummary"));
1313 }
1314
1315 #[test]
1316 fn test_batch_transaction_status_debug() {
1317 let status = BatchTransactionStatus::Confirmed {
1318 hash: "0x123".to_string(),
1319 success: true,
1320 version: 1,
1321 gas_used: 100,
1322 };
1323 let debug = format!("{status:?}");
1324 assert!(debug.contains("Confirmed"));
1325 }
1326
1327 #[tokio::test]
1328 async fn test_submit_all_returns_pending() {
1329 let server = MockServer::start().await;
1330 Mock::given(method("POST"))
1331 .and(path("/v1/transactions"))
1332 .respond_with(ResponseTemplate::new(202).set_body_json(pending_txn_response()))
1333 .mount(&server)
1334 .await;
1335
1336 let client = create_mock_client(&server);
1337 let batch =
1338 SignedTransactionBatch::new(vec![create_dummy_signed_txn(), create_dummy_signed_txn()]);
1339 let results = batch.submit_all(&client).await;
1340
1341 assert_eq!(results.len(), 2);
1342 for (i, r) in results.iter().enumerate() {
1343 assert_eq!(r.index, i);
1344 let status = r.result.as_ref().unwrap();
1345 assert!(matches!(status, BatchTransactionStatus::Pending { .. }));
1346 assert_eq!(
1347 status.hash(),
1348 Some("0x0000000000000000000000000000000000000000000000000000000000000001")
1349 );
1350 }
1351 }
1352
1353 #[tokio::test]
1354 async fn test_submit_all_surfaces_submission_error() {
1355 let server = MockServer::start().await;
1356 Mock::given(method("POST"))
1357 .and(path("/v1/transactions"))
1358 .respond_with(ResponseTemplate::new(400).set_body_json(serde_json::json!({
1359 "message": "Invalid transaction",
1360 "error_code": "invalid_transaction_update"
1361 })))
1362 .mount(&server)
1363 .await;
1364
1365 let client = create_mock_client(&server);
1366 let batch = SignedTransactionBatch::new(vec![create_dummy_signed_txn()]);
1367 let results = batch.submit_all(&client).await;
1368
1369 assert_eq!(results.len(), 1);
1370 assert!(results[0].result.is_err());
1371 }
1372
1373 #[tokio::test]
1374 async fn test_submit_and_wait_all_confirmed() {
1375 let server = MockServer::start().await;
1376 Mock::given(method("POST"))
1377 .and(path("/v1/transactions"))
1378 .respond_with(ResponseTemplate::new(202).set_body_json(pending_txn_response()))
1379 .mount(&server)
1380 .await;
1381 Mock::given(method("GET"))
1382 .and(path_regex(r"^/v1/transactions/by_hash/0x[0-9a-f]+$"))
1383 .respond_with(ResponseTemplate::new(200).set_body_json(committed_txn_response()))
1384 .mount(&server)
1385 .await;
1386
1387 let client = create_mock_client(&server);
1388 let batch =
1389 SignedTransactionBatch::new(vec![create_dummy_signed_txn(), create_dummy_signed_txn()]);
1390 let results = batch
1391 .submit_and_wait_all(&client, Some(Duration::from_secs(5)))
1392 .await;
1393
1394 assert_eq!(results.len(), 2);
1395 for r in &results {
1396 match r.result.as_ref().unwrap() {
1397 BatchTransactionStatus::Confirmed {
1398 success,
1399 version,
1400 gas_used,
1401 hash,
1402 } => {
1403 assert!(*success);
1404 assert_eq!(*version, 12345);
1405 assert_eq!(*gas_used, 500);
1406 assert_eq!(
1407 hash,
1408 "0x0000000000000000000000000000000000000000000000000000000000000001"
1409 );
1410 }
1411 other => panic!("expected Confirmed, got {other:?}"),
1412 }
1413 }
1414 }
1415
1416 #[tokio::test]
1417 async fn test_submit_sequential_returns_pending() {
1418 let server = MockServer::start().await;
1419 Mock::given(method("POST"))
1420 .and(path("/v1/transactions"))
1421 .respond_with(ResponseTemplate::new(202).set_body_json(pending_txn_response()))
1422 .mount(&server)
1423 .await;
1424
1425 let client = create_mock_client(&server);
1426 let batch =
1427 SignedTransactionBatch::new(vec![create_dummy_signed_txn(), create_dummy_signed_txn()]);
1428 let results = batch.submit_sequential(&client).await;
1429
1430 assert_eq!(results.len(), 2);
1431 assert_eq!(results[0].index, 0);
1432 assert_eq!(results[1].index, 1);
1433 assert!(results.iter().all(|r| r.result.is_ok()));
1434 }
1435
1436 #[tokio::test]
1437 async fn test_submit_and_wait_sequential_confirmed() {
1438 let server = MockServer::start().await;
1439 Mock::given(method("POST"))
1440 .and(path("/v1/transactions"))
1441 .respond_with(ResponseTemplate::new(202).set_body_json(pending_txn_response()))
1442 .mount(&server)
1443 .await;
1444 Mock::given(method("GET"))
1445 .and(path_regex(r"^/v1/transactions/by_hash/0x[0-9a-f]+$"))
1446 .respond_with(ResponseTemplate::new(200).set_body_json(committed_txn_response()))
1447 .mount(&server)
1448 .await;
1449
1450 let client = create_mock_client(&server);
1451 let batch =
1452 SignedTransactionBatch::new(vec![create_dummy_signed_txn(), create_dummy_signed_txn()]);
1453 let results = batch.submit_and_wait_sequential(&client, None).await;
1454
1455 assert_eq!(results.len(), 2);
1457 assert!(results.iter().all(|r| r.result.is_ok()));
1458 }
1459
1460 #[tokio::test]
1461 async fn test_submit_and_wait_sequential_stops_on_failure() {
1462 let server = MockServer::start().await;
1463 Mock::given(method("POST"))
1466 .and(path("/v1/transactions"))
1467 .respond_with(ResponseTemplate::new(400).set_body_json(serde_json::json!({
1468 "message": "Invalid transaction",
1469 "error_code": "invalid_transaction_update"
1470 })))
1471 .mount(&server)
1472 .await;
1473
1474 let client = create_mock_client(&server);
1475 let batch =
1476 SignedTransactionBatch::new(vec![create_dummy_signed_txn(), create_dummy_signed_txn()]);
1477 let results = batch.submit_and_wait_sequential(&client, None).await;
1478
1479 assert_eq!(results.len(), 1, "should stop after first failure");
1480 assert!(results[0].result.is_err());
1481 }
1482
1483 #[tokio::test]
1484 async fn test_resolve_chain_id_uses_cached_value() {
1485 let server = MockServer::start().await;
1487 let client = create_mock_client(&server);
1488 let chain_id = std::sync::atomic::AtomicU8::new(4);
1489 let ops = BatchOperations::new(&client, &chain_id);
1490
1491 let resolved = ops.resolve_chain_id().await.unwrap();
1492 assert_eq!(resolved, ChainId::new(4));
1493 }
1494
1495 #[tokio::test]
1496 async fn test_resolve_chain_id_fetches_from_node() {
1497 let server = MockServer::start().await;
1498 Mock::given(method("GET"))
1499 .and(path("/v1"))
1500 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1501 "chain_id": 2,
1502 "epoch": "100",
1503 "ledger_version": "12345",
1504 "oldest_ledger_version": "0",
1505 "ledger_timestamp": "1000000",
1506 "node_role": "full_node",
1507 "oldest_block_height": "0",
1508 "block_height": "5000"
1509 })))
1510 .mount(&server)
1511 .await;
1512
1513 let client = create_mock_client(&server);
1514 let chain_id = std::sync::atomic::AtomicU8::new(0);
1515 let ops = BatchOperations::new(&client, &chain_id);
1516
1517 let resolved = ops.resolve_chain_id().await.unwrap();
1518 assert_eq!(resolved, ChainId::new(2));
1519 assert_eq!(chain_id.load(std::sync::atomic::Ordering::Relaxed), 2);
1521 }
1522
1523 #[cfg(feature = "ed25519")]
1524 #[tokio::test]
1525 async fn test_batch_operations_build() {
1526 use crate::account::Ed25519Account;
1527
1528 let server = MockServer::start().await;
1529 Mock::given(method("GET"))
1530 .and(path_regex(r"^/v1/accounts/0x[0-9a-f]+$"))
1531 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1532 "sequence_number": "7",
1533 "authentication_key": "0x0000000000000000000000000000000000000000000000000000000000000001"
1534 })))
1535 .mount(&server)
1536 .await;
1537 Mock::given(method("GET"))
1538 .and(path("/v1/estimate_gas_price"))
1539 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1540 "gas_estimate": 123
1541 })))
1542 .mount(&server)
1543 .await;
1544
1545 let client = create_mock_client(&server);
1546 let chain_id = std::sync::atomic::AtomicU8::new(4);
1548 let ops = BatchOperations::new(&client, &chain_id);
1549 let account = Ed25519Account::generate();
1550
1551 let payloads = vec![
1552 TransactionPayload::Script(crate::transaction::Script {
1553 code: vec![],
1554 type_args: vec![],
1555 args: vec![],
1556 }),
1557 TransactionPayload::Script(crate::transaction::Script {
1558 code: vec![],
1559 type_args: vec![],
1560 args: vec![],
1561 }),
1562 ];
1563 let batch = ops.build(&account, payloads).await.unwrap();
1564
1565 assert_eq!(batch.len(), 2);
1568 let txns = batch.transactions();
1569 assert_eq!(txns[0].raw_txn.sequence_number, 7);
1570 assert_eq!(txns[1].raw_txn.sequence_number, 8);
1571 assert_eq!(txns[0].raw_txn.gas_unit_price, 123);
1573 assert_eq!(txns[0].raw_txn.chain_id, ChainId::new(4));
1574 }
1575
1576 #[cfg(feature = "ed25519")]
1577 #[tokio::test]
1578 async fn test_batch_operations_submit() {
1579 use crate::account::Ed25519Account;
1580
1581 let server = MockServer::start().await;
1582 Mock::given(method("GET"))
1583 .and(path_regex(r"^/v1/accounts/0x[0-9a-f]+$"))
1584 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1585 "sequence_number": "0",
1586 "authentication_key": "0x0000000000000000000000000000000000000000000000000000000000000001"
1587 })))
1588 .mount(&server)
1589 .await;
1590 Mock::given(method("GET"))
1591 .and(path("/v1/estimate_gas_price"))
1592 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1593 "gas_estimate": 100
1594 })))
1595 .mount(&server)
1596 .await;
1597 Mock::given(method("POST"))
1598 .and(path("/v1/transactions"))
1599 .respond_with(ResponseTemplate::new(202).set_body_json(pending_txn_response()))
1600 .mount(&server)
1601 .await;
1602
1603 let client = create_mock_client(&server);
1604 let chain_id = std::sync::atomic::AtomicU8::new(4);
1605 let ops = BatchOperations::new(&client, &chain_id);
1606 let account = Ed25519Account::generate();
1607
1608 let payloads = vec![TransactionPayload::Script(crate::transaction::Script {
1609 code: vec![],
1610 type_args: vec![],
1611 args: vec![],
1612 })];
1613 let results = ops.submit(&account, payloads).await.unwrap();
1614
1615 assert_eq!(results.len(), 1);
1616 assert!(matches!(
1617 results[0].result.as_ref().unwrap(),
1618 BatchTransactionStatus::Pending { .. }
1619 ));
1620 }
1621
1622 #[cfg(feature = "ed25519")]
1623 #[tokio::test]
1624 async fn test_batch_operations_submit_and_wait() {
1625 use crate::account::Ed25519Account;
1626
1627 let server = MockServer::start().await;
1628 Mock::given(method("GET"))
1629 .and(path_regex(r"^/v1/accounts/0x[0-9a-f]+$"))
1630 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1631 "sequence_number": "0",
1632 "authentication_key": "0x0000000000000000000000000000000000000000000000000000000000000001"
1633 })))
1634 .mount(&server)
1635 .await;
1636 Mock::given(method("GET"))
1637 .and(path("/v1/estimate_gas_price"))
1638 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1639 "gas_estimate": 100
1640 })))
1641 .mount(&server)
1642 .await;
1643 Mock::given(method("POST"))
1644 .and(path("/v1/transactions"))
1645 .respond_with(ResponseTemplate::new(202).set_body_json(pending_txn_response()))
1646 .mount(&server)
1647 .await;
1648 Mock::given(method("GET"))
1649 .and(path_regex(r"^/v1/transactions/by_hash/0x[0-9a-f]+$"))
1650 .respond_with(ResponseTemplate::new(200).set_body_json(committed_txn_response()))
1651 .mount(&server)
1652 .await;
1653
1654 let client = create_mock_client(&server);
1655 let chain_id = std::sync::atomic::AtomicU8::new(4);
1656 let ops = BatchOperations::new(&client, &chain_id);
1657 let account = Ed25519Account::generate();
1658
1659 let payloads = vec![TransactionPayload::Script(crate::transaction::Script {
1660 code: vec![],
1661 type_args: vec![],
1662 args: vec![],
1663 })];
1664 let results = ops
1665 .submit_and_wait(&account, payloads, Some(Duration::from_secs(5)))
1666 .await
1667 .unwrap();
1668
1669 assert_eq!(results.len(), 1);
1670 assert!(matches!(
1671 results[0].result.as_ref().unwrap(),
1672 BatchTransactionStatus::Confirmed { success: true, .. }
1673 ));
1674 }
1675
1676 #[cfg(feature = "ed25519")]
1677 #[tokio::test]
1678 async fn test_batch_operations_transfer_apt() {
1679 use crate::account::Ed25519Account;
1680
1681 let server = MockServer::start().await;
1682 Mock::given(method("GET"))
1683 .and(path_regex(r"^/v1/accounts/0x[0-9a-f]+$"))
1684 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1685 "sequence_number": "0",
1686 "authentication_key": "0x0000000000000000000000000000000000000000000000000000000000000001"
1687 })))
1688 .mount(&server)
1689 .await;
1690 Mock::given(method("GET"))
1691 .and(path("/v1/estimate_gas_price"))
1692 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1693 "gas_estimate": 100
1694 })))
1695 .mount(&server)
1696 .await;
1697 Mock::given(method("POST"))
1698 .and(path("/v1/transactions"))
1699 .respond_with(ResponseTemplate::new(202).set_body_json(pending_txn_response()))
1700 .mount(&server)
1701 .await;
1702 Mock::given(method("GET"))
1703 .and(path_regex(r"^/v1/transactions/by_hash/0x[0-9a-f]+$"))
1704 .respond_with(ResponseTemplate::new(200).set_body_json(committed_txn_response()))
1705 .mount(&server)
1706 .await;
1707
1708 let client = create_mock_client(&server);
1709 let chain_id = std::sync::atomic::AtomicU8::new(4);
1710 let ops = BatchOperations::new(&client, &chain_id);
1711 let account = Ed25519Account::generate();
1712
1713 let transfers = vec![
1714 (AccountAddress::ONE, 100u64),
1715 (AccountAddress::from_hex("0x2").unwrap(), 200u64),
1716 ];
1717 let results = ops.transfer_apt(&account, transfers).await.unwrap();
1718
1719 assert_eq!(results.len(), 2);
1720 assert!(results.iter().all(|r| matches!(
1721 r.result.as_ref().unwrap(),
1722 BatchTransactionStatus::Confirmed { success: true, .. }
1723 )));
1724 }
1725}