1use crate::account::Account;
40use crate::error::{MovementError, MovementResult};
41use crate::transaction::authenticator::{AccountAuthenticator, TransactionAuthenticator};
42use crate::transaction::builder::{
43 DEFAULT_EXPIRATION_SECONDS, DEFAULT_GAS_UNIT_PRICE, DEFAULT_MAX_GAS_AMOUNT,
44};
45use crate::transaction::payload::TransactionPayload;
46use crate::transaction::types::{FeePayerRawTransaction, RawTransaction, SignedTransaction};
47use crate::types::{AccountAddress, ChainId};
48use std::time::{SystemTime, UNIX_EPOCH};
49
50#[derive(Debug, Clone, Default)]
78pub struct SponsoredTransactionBuilder {
79 sender_address: Option<AccountAddress>,
80 sequence_number: Option<u64>,
81 secondary_addresses: Vec<AccountAddress>,
82 fee_payer_address: Option<AccountAddress>,
83 payload: Option<TransactionPayload>,
84 max_gas_amount: u64,
85 gas_unit_price: u64,
86 expiration_timestamp_secs: Option<u64>,
87 chain_id: Option<ChainId>,
88}
89
90impl SponsoredTransactionBuilder {
91 #[must_use]
93 pub fn new() -> Self {
94 Self {
95 sender_address: None,
96 sequence_number: None,
97 secondary_addresses: Vec::new(),
98 fee_payer_address: None,
99 payload: None,
100 max_gas_amount: DEFAULT_MAX_GAS_AMOUNT,
101 gas_unit_price: DEFAULT_GAS_UNIT_PRICE,
102 expiration_timestamp_secs: None,
103 chain_id: None,
104 }
105 }
106
107 #[must_use]
109 pub fn sender(mut self, address: AccountAddress) -> Self {
110 self.sender_address = Some(address);
111 self
112 }
113
114 #[must_use]
116 pub fn sequence_number(mut self, sequence_number: u64) -> Self {
117 self.sequence_number = Some(sequence_number);
118 self
119 }
120
121 #[must_use]
126 pub fn secondary_signer(mut self, address: AccountAddress) -> Self {
127 self.secondary_addresses.push(address);
128 self
129 }
130
131 #[must_use]
133 pub fn secondary_signers(mut self, addresses: &[AccountAddress]) -> Self {
134 self.secondary_addresses.extend(addresses);
135 self
136 }
137
138 #[must_use]
140 pub fn fee_payer(mut self, address: AccountAddress) -> Self {
141 self.fee_payer_address = Some(address);
142 self
143 }
144
145 #[must_use]
147 pub fn payload(mut self, payload: TransactionPayload) -> Self {
148 self.payload = Some(payload);
149 self
150 }
151
152 #[must_use]
154 pub fn max_gas_amount(mut self, max_gas_amount: u64) -> Self {
155 self.max_gas_amount = max_gas_amount;
156 self
157 }
158
159 #[must_use]
161 pub fn gas_unit_price(mut self, gas_unit_price: u64) -> Self {
162 self.gas_unit_price = gas_unit_price;
163 self
164 }
165
166 #[must_use]
168 pub fn expiration_timestamp_secs(mut self, expiration_timestamp_secs: u64) -> Self {
169 self.expiration_timestamp_secs = Some(expiration_timestamp_secs);
170 self
171 }
172
173 #[must_use]
175 pub fn expiration_from_now(mut self, seconds: u64) -> Self {
176 let now = SystemTime::now()
177 .duration_since(UNIX_EPOCH)
178 .unwrap_or_default()
179 .as_secs();
180 self.expiration_timestamp_secs = Some(now + seconds);
181 self
182 }
183
184 #[must_use]
186 pub fn chain_id(mut self, chain_id: ChainId) -> Self {
187 self.chain_id = Some(chain_id);
188 self
189 }
190
191 pub fn build(self) -> MovementResult<FeePayerRawTransaction> {
200 let sender = self
201 .sender_address
202 .ok_or_else(|| MovementError::transaction("sender is required"))?;
203 let sequence_number = self
204 .sequence_number
205 .ok_or_else(|| MovementError::transaction("sequence_number is required"))?;
206 let payload = self
207 .payload
208 .ok_or_else(|| MovementError::transaction("payload is required"))?;
209 let chain_id = self
210 .chain_id
211 .ok_or_else(|| MovementError::transaction("chain_id is required"))?;
212 let fee_payer_address = self
213 .fee_payer_address
214 .ok_or_else(|| MovementError::transaction("fee_payer is required"))?;
215
216 let expiration_timestamp_secs = self.expiration_timestamp_secs.unwrap_or_else(|| {
218 SystemTime::now()
219 .duration_since(UNIX_EPOCH)
220 .unwrap_or_default()
221 .as_secs()
222 .saturating_add(DEFAULT_EXPIRATION_SECONDS)
223 });
224
225 let raw_txn = RawTransaction::new(
226 sender,
227 sequence_number,
228 payload,
229 self.max_gas_amount,
230 self.gas_unit_price,
231 expiration_timestamp_secs,
232 chain_id,
233 );
234
235 Ok(FeePayerRawTransaction {
236 raw_txn,
237 secondary_signer_addresses: self.secondary_addresses,
238 fee_payer_address,
239 })
240 }
241
242 pub fn build_and_sign<S, F>(
263 self,
264 sender: &S,
265 secondary_signers: &[&dyn Account],
266 fee_payer: &F,
267 ) -> MovementResult<SignedTransaction>
268 where
269 S: Account,
270 F: Account,
271 {
272 let fee_payer_txn = self.build()?;
273 sign_sponsored_transaction(&fee_payer_txn, sender, secondary_signers, fee_payer)
274 }
275}
276
277pub fn sign_sponsored_transaction<S, F>(
303 fee_payer_txn: &FeePayerRawTransaction,
304 sender: &S,
305 secondary_signers: &[&dyn Account],
306 fee_payer: &F,
307) -> MovementResult<SignedTransaction>
308where
309 S: Account,
310 F: Account,
311{
312 let signing_message = fee_payer_txn.signing_message()?;
313
314 let sender_signature = sender.sign(&signing_message)?;
316 let sender_public_key = sender.public_key_bytes();
317 let sender_auth = make_account_authenticator(
318 sender.signature_scheme(),
319 sender_public_key,
320 sender_signature,
321 )?;
322
323 let mut secondary_auths = Vec::with_capacity(secondary_signers.len());
325 for signer in secondary_signers {
326 let signature = signer.sign(&signing_message)?;
327 let public_key = signer.public_key_bytes();
328 secondary_auths.push(make_account_authenticator(
329 signer.signature_scheme(),
330 public_key,
331 signature,
332 )?);
333 }
334
335 let fee_payer_signature = fee_payer.sign(&signing_message)?;
337 let fee_payer_public_key = fee_payer.public_key_bytes();
338 let fee_payer_auth = make_account_authenticator(
339 fee_payer.signature_scheme(),
340 fee_payer_public_key,
341 fee_payer_signature,
342 )?;
343
344 let authenticator = TransactionAuthenticator::fee_payer(
345 sender_auth,
346 fee_payer_txn.secondary_signer_addresses.clone(),
347 secondary_auths,
348 fee_payer_txn.fee_payer_address,
349 fee_payer_auth,
350 );
351
352 Ok(SignedTransaction::new(
353 fee_payer_txn.raw_txn.clone(),
354 authenticator,
355 ))
356}
357
358fn make_account_authenticator(
364 scheme: u8,
365 public_key: Vec<u8>,
366 signature: Vec<u8>,
367) -> MovementResult<AccountAuthenticator> {
368 match scheme {
369 crate::crypto::ED25519_SCHEME => Ok(AccountAuthenticator::ed25519(public_key, signature)),
370 crate::crypto::MULTI_ED25519_SCHEME => Ok(AccountAuthenticator::MultiEd25519 {
371 public_key,
372 signature,
373 }),
374 crate::crypto::SINGLE_KEY_SCHEME => {
375 Ok(AccountAuthenticator::single_key(public_key, signature))
376 }
377 crate::crypto::MULTI_KEY_SCHEME => {
378 Ok(AccountAuthenticator::multi_key(public_key, signature))
379 }
380 _ => Err(MovementError::InvalidSignature(format!(
381 "unknown signature scheme: {scheme}"
382 ))),
383 }
384}
385
386#[derive(Debug, Clone)]
392pub struct PartiallySigned {
393 pub fee_payer_txn: FeePayerRawTransaction,
395 pub sender_auth: Option<AccountAuthenticator>,
397 pub secondary_auths: Vec<Option<AccountAuthenticator>>,
399 pub fee_payer_auth: Option<AccountAuthenticator>,
401}
402
403impl PartiallySigned {
404 pub fn new(fee_payer_txn: FeePayerRawTransaction) -> Self {
406 let num_secondary = fee_payer_txn.secondary_signer_addresses.len();
407 Self {
408 fee_payer_txn,
409 sender_auth: None,
410 secondary_auths: vec![None; num_secondary],
411 fee_payer_auth: None,
412 }
413 }
414
415 pub fn sign_as_sender<A: Account>(&mut self, sender: &A) -> MovementResult<()> {
422 let signing_message = self.fee_payer_txn.signing_message()?;
423 let signature = sender.sign(&signing_message)?;
424 let public_key = sender.public_key_bytes();
425 self.sender_auth = Some(make_account_authenticator(
426 sender.signature_scheme(),
427 public_key,
428 signature,
429 )?);
430 Ok(())
431 }
432
433 pub fn sign_as_secondary<A: Account>(
440 &mut self,
441 index: usize,
442 signer: &A,
443 ) -> MovementResult<()> {
444 if index >= self.secondary_auths.len() {
445 return Err(MovementError::transaction(format!(
446 "secondary signer index {} out of bounds (max {})",
447 index,
448 self.secondary_auths.len()
449 )));
450 }
451
452 let signing_message = self.fee_payer_txn.signing_message()?;
453 let signature = signer.sign(&signing_message)?;
454 let public_key = signer.public_key_bytes();
455 self.secondary_auths[index] = Some(make_account_authenticator(
456 signer.signature_scheme(),
457 public_key,
458 signature,
459 )?);
460 Ok(())
461 }
462
463 pub fn sign_as_fee_payer<A: Account>(&mut self, fee_payer: &A) -> MovementResult<()> {
470 let signing_message = self.fee_payer_txn.signing_message()?;
471 let signature = fee_payer.sign(&signing_message)?;
472 let public_key = fee_payer.public_key_bytes();
473 self.fee_payer_auth = Some(make_account_authenticator(
474 fee_payer.signature_scheme(),
475 public_key,
476 signature,
477 )?);
478 Ok(())
479 }
480
481 pub fn is_complete(&self) -> bool {
483 self.sender_auth.is_some()
484 && self.fee_payer_auth.is_some()
485 && self.secondary_auths.iter().all(Option::is_some)
486 }
487
488 pub fn finalize(self) -> MovementResult<SignedTransaction> {
496 let sender_auth = self
497 .sender_auth
498 .ok_or_else(|| MovementError::transaction("missing sender signature"))?;
499 let fee_payer_auth = self
500 .fee_payer_auth
501 .ok_or_else(|| MovementError::transaction("missing fee payer signature"))?;
502
503 let secondary_auths: Result<Vec<_>, _> = self
504 .secondary_auths
505 .into_iter()
506 .enumerate()
507 .map(|(i, auth)| {
508 auth.ok_or_else(|| {
509 MovementError::transaction(format!("missing secondary signer {i} signature"))
510 })
511 })
512 .collect();
513 let secondary_auths = secondary_auths?;
514
515 let authenticator = TransactionAuthenticator::fee_payer(
516 sender_auth,
517 self.fee_payer_txn.secondary_signer_addresses.clone(),
518 secondary_auths,
519 self.fee_payer_txn.fee_payer_address,
520 fee_payer_auth,
521 );
522
523 Ok(SignedTransaction::new(
524 self.fee_payer_txn.raw_txn,
525 authenticator,
526 ))
527 }
528}
529
530pub trait Sponsor: Account + Sized {
535 fn sponsor<S: Account>(
563 &self,
564 sender: &S,
565 sender_sequence_number: u64,
566 payload: TransactionPayload,
567 chain_id: ChainId,
568 ) -> MovementResult<SignedTransaction> {
569 SponsoredTransactionBuilder::new()
570 .sender(sender.address())
571 .sequence_number(sender_sequence_number)
572 .fee_payer(self.address())
573 .payload(payload)
574 .chain_id(chain_id)
575 .build_and_sign(sender, &[], self)
576 }
577
578 fn sponsor_with_gas<S: Account>(
584 &self,
585 sender: &S,
586 sender_sequence_number: u64,
587 payload: TransactionPayload,
588 chain_id: ChainId,
589 max_gas_amount: u64,
590 gas_unit_price: u64,
591 ) -> MovementResult<SignedTransaction> {
592 SponsoredTransactionBuilder::new()
593 .sender(sender.address())
594 .sequence_number(sender_sequence_number)
595 .fee_payer(self.address())
596 .payload(payload)
597 .chain_id(chain_id)
598 .max_gas_amount(max_gas_amount)
599 .gas_unit_price(gas_unit_price)
600 .build_and_sign(sender, &[], self)
601 }
602}
603
604impl<A: Account + Sized> Sponsor for A {}
606
607pub fn sponsor_transaction<S, F>(
630 sender: &S,
631 sender_sequence_number: u64,
632 fee_payer: &F,
633 payload: TransactionPayload,
634 chain_id: ChainId,
635) -> MovementResult<SignedTransaction>
636where
637 S: Account,
638 F: Account,
639{
640 SponsoredTransactionBuilder::new()
641 .sender(sender.address())
642 .sequence_number(sender_sequence_number)
643 .fee_payer(fee_payer.address())
644 .payload(payload)
645 .chain_id(chain_id)
646 .build_and_sign(sender, &[], fee_payer)
647}
648
649#[cfg(test)]
650mod tests {
651 use super::*;
652 use crate::transaction::payload::EntryFunction;
653
654 #[test]
655 fn test_builder_missing_sender() {
656 let recipient = AccountAddress::from_hex("0x123").unwrap();
657 let result = SponsoredTransactionBuilder::new()
658 .sequence_number(0)
659 .fee_payer(AccountAddress::ONE)
660 .payload(TransactionPayload::EntryFunction(
661 EntryFunction::apt_transfer(recipient, 1000).unwrap(),
662 ))
663 .chain_id(ChainId::testnet())
664 .build();
665
666 assert!(result.is_err());
667 assert!(result.unwrap_err().to_string().contains("sender"));
668 }
669
670 #[test]
671 fn test_builder_missing_fee_payer() {
672 let recipient = AccountAddress::from_hex("0x123").unwrap();
673 let result = SponsoredTransactionBuilder::new()
674 .sender(AccountAddress::ONE)
675 .sequence_number(0)
676 .payload(TransactionPayload::EntryFunction(
677 EntryFunction::apt_transfer(recipient, 1000).unwrap(),
678 ))
679 .chain_id(ChainId::testnet())
680 .build();
681
682 assert!(result.is_err());
683 assert!(result.unwrap_err().to_string().contains("fee_payer"));
684 }
685
686 #[test]
687 fn test_builder_complete() {
688 let recipient = AccountAddress::from_hex("0x123").unwrap();
689 let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
690
691 let fee_payer_txn = SponsoredTransactionBuilder::new()
692 .sender(AccountAddress::ONE)
693 .sequence_number(5)
694 .fee_payer(AccountAddress::from_hex("0x3").unwrap())
695 .payload(payload.into())
696 .chain_id(ChainId::testnet())
697 .max_gas_amount(100_000)
698 .gas_unit_price(150)
699 .build()
700 .unwrap();
701
702 assert_eq!(fee_payer_txn.raw_txn.sender, AccountAddress::ONE);
703 assert_eq!(fee_payer_txn.raw_txn.sequence_number, 5);
704 assert_eq!(fee_payer_txn.raw_txn.max_gas_amount, 100_000);
705 assert_eq!(fee_payer_txn.raw_txn.gas_unit_price, 150);
706 assert_eq!(
707 fee_payer_txn.fee_payer_address,
708 AccountAddress::from_hex("0x3").unwrap()
709 );
710 }
711
712 #[test]
713 fn test_partially_signed_completion_check() {
714 let recipient = AccountAddress::from_hex("0x123").unwrap();
715 let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
716
717 let fee_payer_txn = SponsoredTransactionBuilder::new()
718 .sender(AccountAddress::ONE)
719 .sequence_number(0)
720 .fee_payer(AccountAddress::from_hex("0x3").unwrap())
721 .payload(payload.into())
722 .chain_id(ChainId::testnet())
723 .build()
724 .unwrap();
725
726 let partially_signed = PartiallySigned::new(fee_payer_txn);
727 assert!(!partially_signed.is_complete());
728 }
729
730 #[test]
731 fn test_partially_signed_finalize_incomplete() {
732 let recipient = AccountAddress::from_hex("0x123").unwrap();
733 let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
734
735 let fee_payer_txn = SponsoredTransactionBuilder::new()
736 .sender(AccountAddress::ONE)
737 .sequence_number(0)
738 .fee_payer(AccountAddress::from_hex("0x3").unwrap())
739 .payload(payload.into())
740 .chain_id(ChainId::testnet())
741 .build()
742 .unwrap();
743
744 let partially_signed = PartiallySigned::new(fee_payer_txn);
745 let result = partially_signed.finalize();
746
747 assert!(result.is_err());
748 assert!(result.unwrap_err().to_string().contains("missing"));
749 }
750
751 #[cfg(feature = "ed25519")]
752 #[test]
753 fn test_full_sponsored_transaction() {
754 use crate::account::Ed25519Account;
755
756 let sender = Ed25519Account::generate();
757 let fee_payer = Ed25519Account::generate();
758 let recipient = AccountAddress::from_hex("0x123").unwrap();
759
760 let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
761
762 let signed_txn = SponsoredTransactionBuilder::new()
763 .sender(sender.address())
764 .sequence_number(0)
765 .fee_payer(fee_payer.address())
766 .payload(payload.into())
767 .chain_id(ChainId::testnet())
768 .build_and_sign(&sender, &[], &fee_payer)
769 .unwrap();
770
771 assert_eq!(signed_txn.raw_txn.sender, sender.address());
773 assert!(matches!(
774 signed_txn.authenticator,
775 TransactionAuthenticator::FeePayer { .. }
776 ));
777 }
778
779 #[cfg(feature = "ed25519")]
780 #[test]
781 fn test_sponsor_trait() {
782 use crate::account::Ed25519Account;
783
784 let sender = Ed25519Account::generate();
785 let sponsor = Ed25519Account::generate();
786 let recipient = AccountAddress::from_hex("0x123").unwrap();
787
788 let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
789
790 let signed_txn = sponsor
792 .sponsor(&sender, 0, payload.into(), ChainId::testnet())
793 .unwrap();
794
795 assert_eq!(signed_txn.raw_txn.sender, sender.address());
796 }
797
798 #[cfg(feature = "ed25519")]
799 #[test]
800 fn test_sponsor_transaction_fn() {
801 use crate::account::Ed25519Account;
802
803 let sender = Ed25519Account::generate();
804 let fee_payer = Ed25519Account::generate();
805 let recipient = AccountAddress::from_hex("0x123").unwrap();
806
807 let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
808
809 let signed_txn =
811 sponsor_transaction(&sender, 0, &fee_payer, payload.into(), ChainId::testnet())
812 .unwrap();
813
814 assert_eq!(signed_txn.raw_txn.sender, sender.address());
815 }
816
817 #[cfg(feature = "ed25519")]
818 #[test]
819 fn test_partially_signed_flow() {
820 use crate::account::Ed25519Account;
821
822 let sender = Ed25519Account::generate();
823 let fee_payer = Ed25519Account::generate();
824 let recipient = AccountAddress::from_hex("0x123").unwrap();
825
826 let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
827
828 let fee_payer_txn = SponsoredTransactionBuilder::new()
830 .sender(sender.address())
831 .sequence_number(0)
832 .fee_payer(fee_payer.address())
833 .payload(payload.into())
834 .chain_id(ChainId::testnet())
835 .build()
836 .unwrap();
837
838 let mut partially_signed = PartiallySigned::new(fee_payer_txn);
840
841 assert!(!partially_signed.is_complete());
843
844 partially_signed.sign_as_sender(&sender).unwrap();
846 assert!(!partially_signed.is_complete());
847
848 partially_signed.sign_as_fee_payer(&fee_payer).unwrap();
850 assert!(partially_signed.is_complete());
851
852 let signed_txn = partially_signed.finalize().unwrap();
854 assert_eq!(signed_txn.raw_txn.sender, sender.address());
855 }
856
857 #[test]
858 fn test_builder_missing_sequence_number() {
859 let recipient = AccountAddress::from_hex("0x123").unwrap();
860 let result = SponsoredTransactionBuilder::new()
861 .sender(AccountAddress::ONE)
862 .fee_payer(AccountAddress::from_hex("0x3").unwrap())
863 .payload(TransactionPayload::EntryFunction(
864 EntryFunction::apt_transfer(recipient, 1000).unwrap(),
865 ))
866 .chain_id(ChainId::testnet())
867 .build();
868
869 assert!(result.is_err());
870 assert!(result.unwrap_err().to_string().contains("sequence_number"));
871 }
872
873 #[test]
874 fn test_builder_missing_payload() {
875 let result = SponsoredTransactionBuilder::new()
876 .sender(AccountAddress::ONE)
877 .sequence_number(0)
878 .fee_payer(AccountAddress::from_hex("0x3").unwrap())
879 .chain_id(ChainId::testnet())
880 .build();
881
882 assert!(result.is_err());
883 assert!(result.unwrap_err().to_string().contains("payload"));
884 }
885
886 #[test]
887 fn test_builder_missing_chain_id() {
888 let recipient = AccountAddress::from_hex("0x123").unwrap();
889 let result = SponsoredTransactionBuilder::new()
890 .sender(AccountAddress::ONE)
891 .sequence_number(0)
892 .fee_payer(AccountAddress::from_hex("0x3").unwrap())
893 .payload(TransactionPayload::EntryFunction(
894 EntryFunction::apt_transfer(recipient, 1000).unwrap(),
895 ))
896 .build();
897
898 assert!(result.is_err());
899 assert!(result.unwrap_err().to_string().contains("chain_id"));
900 }
901
902 #[test]
903 fn test_builder_secondary_signers() {
904 let recipient = AccountAddress::from_hex("0x123").unwrap();
905 let secondary1 = AccountAddress::from_hex("0x4").unwrap();
906 let secondary2 = AccountAddress::from_hex("0x5").unwrap();
907
908 let fee_payer_txn = SponsoredTransactionBuilder::new()
909 .sender(AccountAddress::ONE)
910 .sequence_number(0)
911 .fee_payer(AccountAddress::from_hex("0x3").unwrap())
912 .secondary_signer(secondary1)
913 .secondary_signers(&[secondary2])
914 .payload(TransactionPayload::EntryFunction(
915 EntryFunction::apt_transfer(recipient, 1000).unwrap(),
916 ))
917 .chain_id(ChainId::testnet())
918 .build()
919 .unwrap();
920
921 assert_eq!(fee_payer_txn.secondary_signer_addresses.len(), 2);
922 assert_eq!(fee_payer_txn.secondary_signer_addresses[0], secondary1);
923 assert_eq!(fee_payer_txn.secondary_signer_addresses[1], secondary2);
924 }
925
926 #[test]
927 fn test_builder_expiration_timestamp() {
928 let recipient = AccountAddress::from_hex("0x123").unwrap();
929 let expiration = 1_234_567_890_u64;
930
931 let fee_payer_txn = SponsoredTransactionBuilder::new()
932 .sender(AccountAddress::ONE)
933 .sequence_number(0)
934 .fee_payer(AccountAddress::from_hex("0x3").unwrap())
935 .payload(TransactionPayload::EntryFunction(
936 EntryFunction::apt_transfer(recipient, 1000).unwrap(),
937 ))
938 .chain_id(ChainId::testnet())
939 .expiration_timestamp_secs(expiration)
940 .build()
941 .unwrap();
942
943 assert_eq!(fee_payer_txn.raw_txn.expiration_timestamp_secs, expiration);
944 }
945
946 #[test]
947 fn test_builder_expiration_from_now() {
948 let recipient = AccountAddress::from_hex("0x123").unwrap();
949
950 let fee_payer_txn = SponsoredTransactionBuilder::new()
951 .sender(AccountAddress::ONE)
952 .sequence_number(0)
953 .fee_payer(AccountAddress::from_hex("0x3").unwrap())
954 .payload(TransactionPayload::EntryFunction(
955 EntryFunction::apt_transfer(recipient, 1000).unwrap(),
956 ))
957 .chain_id(ChainId::testnet())
958 .expiration_from_now(60)
959 .build()
960 .unwrap();
961
962 let now = SystemTime::now()
964 .duration_since(UNIX_EPOCH)
965 .unwrap()
966 .as_secs();
967 assert!(fee_payer_txn.raw_txn.expiration_timestamp_secs >= now);
968 assert!(fee_payer_txn.raw_txn.expiration_timestamp_secs <= now + 65);
969 }
970
971 #[test]
972 fn test_builder_default() {
973 let builder = SponsoredTransactionBuilder::default();
974 assert!(builder.sender_address.is_none());
975 assert!(builder.sequence_number.is_none());
976 assert!(builder.fee_payer_address.is_none());
977 assert!(builder.payload.is_none());
978 assert!(builder.chain_id.is_none());
979 }
983
984 #[test]
985 fn test_builder_new_defaults() {
986 let builder = SponsoredTransactionBuilder::new();
987 assert!(builder.sender_address.is_none());
988 assert!(builder.sequence_number.is_none());
989 assert!(builder.fee_payer_address.is_none());
990 assert!(builder.payload.is_none());
991 assert!(builder.chain_id.is_none());
992 assert_eq!(builder.max_gas_amount, DEFAULT_MAX_GAS_AMOUNT);
993 assert_eq!(builder.gas_unit_price, DEFAULT_GAS_UNIT_PRICE);
994 }
995
996 #[test]
997 fn test_builder_debug() {
998 let builder = SponsoredTransactionBuilder::new().sender(AccountAddress::ONE);
999 let debug = format!("{builder:?}");
1000 assert!(debug.contains("SponsoredTransactionBuilder"));
1001 }
1002
1003 #[cfg(feature = "ed25519")]
1004 #[test]
1005 fn test_partially_signed_with_secondary_signers() {
1006 use crate::account::Ed25519Account;
1007
1008 let sender = Ed25519Account::generate();
1009 let secondary = Ed25519Account::generate();
1010 let fee_payer = Ed25519Account::generate();
1011 let recipient = AccountAddress::from_hex("0x123").unwrap();
1012
1013 let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
1014
1015 let fee_payer_txn = SponsoredTransactionBuilder::new()
1017 .sender(sender.address())
1018 .sequence_number(0)
1019 .secondary_signer(secondary.address())
1020 .fee_payer(fee_payer.address())
1021 .payload(payload.into())
1022 .chain_id(ChainId::testnet())
1023 .build()
1024 .unwrap();
1025
1026 let mut partially_signed = PartiallySigned::new(fee_payer_txn);
1027
1028 assert!(!partially_signed.is_complete());
1030
1031 partially_signed.sign_as_sender(&sender).unwrap();
1032 assert!(!partially_signed.is_complete());
1033
1034 partially_signed.sign_as_secondary(0, &secondary).unwrap();
1035 assert!(!partially_signed.is_complete());
1036
1037 partially_signed.sign_as_fee_payer(&fee_payer).unwrap();
1038 assert!(partially_signed.is_complete());
1039
1040 let signed = partially_signed.finalize().unwrap();
1041 assert_eq!(signed.raw_txn.sender, sender.address());
1042 }
1043
1044 #[cfg(feature = "ed25519")]
1045 #[test]
1046 fn test_partially_signed_secondary_index_out_of_bounds() {
1047 use crate::account::Ed25519Account;
1048
1049 let sender = Ed25519Account::generate();
1050 let fee_payer = Ed25519Account::generate();
1051 let secondary = Ed25519Account::generate();
1052 let recipient = AccountAddress::from_hex("0x123").unwrap();
1053
1054 let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
1055
1056 let fee_payer_txn = SponsoredTransactionBuilder::new()
1058 .sender(sender.address())
1059 .sequence_number(0)
1060 .fee_payer(fee_payer.address())
1061 .payload(payload.into())
1062 .chain_id(ChainId::testnet())
1063 .build()
1064 .unwrap();
1065
1066 let mut partially_signed = PartiallySigned::new(fee_payer_txn);
1067
1068 let result = partially_signed.sign_as_secondary(0, &secondary);
1070 assert!(result.is_err());
1071 assert!(result.unwrap_err().to_string().contains("out of bounds"));
1072 }
1073
1074 #[cfg(feature = "ed25519")]
1075 #[test]
1076 fn test_partially_signed_finalize_missing_secondary() {
1077 use crate::account::Ed25519Account;
1078
1079 let sender = Ed25519Account::generate();
1080 let fee_payer = Ed25519Account::generate();
1081 let recipient = AccountAddress::from_hex("0x123").unwrap();
1082
1083 let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
1084
1085 let fee_payer_txn = SponsoredTransactionBuilder::new()
1087 .sender(sender.address())
1088 .sequence_number(0)
1089 .secondary_signer(AccountAddress::from_hex("0x5").unwrap())
1090 .fee_payer(fee_payer.address())
1091 .payload(payload.into())
1092 .chain_id(ChainId::testnet())
1093 .build()
1094 .unwrap();
1095
1096 let mut partially_signed = PartiallySigned::new(fee_payer_txn);
1097
1098 partially_signed.sign_as_sender(&sender).unwrap();
1100 partially_signed.sign_as_fee_payer(&fee_payer).unwrap();
1101
1102 let result = partially_signed.finalize();
1104 assert!(result.is_err());
1105 assert!(result.unwrap_err().to_string().contains("secondary signer"));
1106 }
1107
1108 #[cfg(feature = "ed25519")]
1109 #[test]
1110 fn test_sponsor_with_gas() {
1111 use crate::account::Ed25519Account;
1112
1113 let sender = Ed25519Account::generate();
1114 let sponsor = Ed25519Account::generate();
1115 let recipient = AccountAddress::from_hex("0x123").unwrap();
1116
1117 let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
1118
1119 let signed_txn = sponsor
1120 .sponsor_with_gas(&sender, 0, payload.into(), ChainId::testnet(), 50000, 200)
1121 .unwrap();
1122
1123 assert_eq!(signed_txn.raw_txn.sender, sender.address());
1124 assert_eq!(signed_txn.raw_txn.max_gas_amount, 50000);
1125 assert_eq!(signed_txn.raw_txn.gas_unit_price, 200);
1126 }
1127
1128 #[test]
1129 fn test_partially_signed_debug() {
1130 let recipient = AccountAddress::from_hex("0x123").unwrap();
1131 let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
1132
1133 let fee_payer_txn = SponsoredTransactionBuilder::new()
1134 .sender(AccountAddress::ONE)
1135 .sequence_number(0)
1136 .fee_payer(AccountAddress::from_hex("0x3").unwrap())
1137 .payload(payload.into())
1138 .chain_id(ChainId::testnet())
1139 .build()
1140 .unwrap();
1141
1142 let partially_signed = PartiallySigned::new(fee_payer_txn);
1143 let debug = format!("{partially_signed:?}");
1144 assert!(debug.contains("PartiallySigned"));
1145 }
1146}