1use crate::account::Account;
40use crate::crypto::{AnyPublicKey, AnySignature, MultiKeyPublicKey, MultiKeySignature};
41use crate::error::{AptosError, AptosResult};
42use crate::transaction::authenticator::{AccountAuthenticator, TransactionAuthenticator};
43use crate::transaction::builder::{
44 DEFAULT_EXPIRATION_SECONDS, DEFAULT_GAS_UNIT_PRICE, DEFAULT_MAX_GAS_AMOUNT,
45};
46use crate::transaction::payload::TransactionPayload;
47use crate::transaction::types::{FeePayerRawTransaction, RawTransaction, SignedTransaction};
48use crate::types::{AccountAddress, ChainId};
49use std::time::{SystemTime, UNIX_EPOCH};
50
51#[derive(Debug, Clone, Default)]
79pub struct SponsoredTransactionBuilder {
80 sender_address: Option<AccountAddress>,
81 sequence_number: Option<u64>,
82 secondary_addresses: Vec<AccountAddress>,
83 fee_payer_address: Option<AccountAddress>,
84 payload: Option<TransactionPayload>,
85 max_gas_amount: u64,
86 gas_unit_price: u64,
87 expiration_timestamp_secs: Option<u64>,
88 chain_id: Option<ChainId>,
89}
90
91impl SponsoredTransactionBuilder {
92 #[must_use]
94 pub fn new() -> Self {
95 Self {
96 sender_address: None,
97 sequence_number: None,
98 secondary_addresses: Vec::new(),
99 fee_payer_address: None,
100 payload: None,
101 max_gas_amount: DEFAULT_MAX_GAS_AMOUNT,
102 gas_unit_price: DEFAULT_GAS_UNIT_PRICE,
103 expiration_timestamp_secs: None,
104 chain_id: None,
105 }
106 }
107
108 #[must_use]
110 pub fn sender(mut self, address: AccountAddress) -> Self {
111 self.sender_address = Some(address);
112 self
113 }
114
115 #[must_use]
117 pub fn sequence_number(mut self, sequence_number: u64) -> Self {
118 self.sequence_number = Some(sequence_number);
119 self
120 }
121
122 #[must_use]
127 pub fn secondary_signer(mut self, address: AccountAddress) -> Self {
128 self.secondary_addresses.push(address);
129 self
130 }
131
132 #[must_use]
134 pub fn secondary_signers(mut self, addresses: &[AccountAddress]) -> Self {
135 self.secondary_addresses.extend(addresses);
136 self
137 }
138
139 #[must_use]
141 pub fn fee_payer(mut self, address: AccountAddress) -> Self {
142 self.fee_payer_address = Some(address);
143 self
144 }
145
146 #[must_use]
148 pub fn payload(mut self, payload: TransactionPayload) -> Self {
149 self.payload = Some(payload);
150 self
151 }
152
153 #[must_use]
155 pub fn max_gas_amount(mut self, max_gas_amount: u64) -> Self {
156 self.max_gas_amount = max_gas_amount;
157 self
158 }
159
160 #[must_use]
162 pub fn gas_unit_price(mut self, gas_unit_price: u64) -> Self {
163 self.gas_unit_price = gas_unit_price;
164 self
165 }
166
167 #[must_use]
169 pub fn expiration_timestamp_secs(mut self, expiration_timestamp_secs: u64) -> Self {
170 self.expiration_timestamp_secs = Some(expiration_timestamp_secs);
171 self
172 }
173
174 #[must_use]
178 pub fn expiration_from_now(mut self, seconds: u64) -> Self {
179 let now = SystemTime::now()
180 .duration_since(UNIX_EPOCH)
181 .unwrap_or_default()
182 .as_secs();
183 self.expiration_timestamp_secs = Some(now.saturating_add(seconds));
184 self
185 }
186
187 #[must_use]
189 pub fn chain_id(mut self, chain_id: ChainId) -> Self {
190 self.chain_id = Some(chain_id);
191 self
192 }
193
194 pub fn build(self) -> AptosResult<FeePayerRawTransaction> {
203 let sender = self
204 .sender_address
205 .ok_or_else(|| AptosError::transaction("sender is required"))?;
206 let sequence_number = self
207 .sequence_number
208 .ok_or_else(|| AptosError::transaction("sequence_number is required"))?;
209 let payload = self
210 .payload
211 .ok_or_else(|| AptosError::transaction("payload is required"))?;
212 let chain_id = self
213 .chain_id
214 .ok_or_else(|| AptosError::transaction("chain_id is required"))?;
215 let fee_payer_address = self
216 .fee_payer_address
217 .ok_or_else(|| AptosError::transaction("fee_payer is required"))?;
218
219 let expiration_timestamp_secs = self.expiration_timestamp_secs.unwrap_or_else(|| {
221 SystemTime::now()
222 .duration_since(UNIX_EPOCH)
223 .unwrap_or_default()
224 .as_secs()
225 .saturating_add(DEFAULT_EXPIRATION_SECONDS)
226 });
227
228 let raw_txn = RawTransaction::new(
229 sender,
230 sequence_number,
231 payload,
232 self.max_gas_amount,
233 self.gas_unit_price,
234 expiration_timestamp_secs,
235 chain_id,
236 );
237
238 Ok(FeePayerRawTransaction {
239 raw_txn,
240 secondary_signer_addresses: self.secondary_addresses,
241 fee_payer_address,
242 })
243 }
244
245 pub fn build_and_sign<S, F>(
266 self,
267 sender: &S,
268 secondary_signers: &[&dyn Account],
269 fee_payer: &F,
270 ) -> AptosResult<SignedTransaction>
271 where
272 S: Account,
273 F: Account,
274 {
275 let fee_payer_txn = self.build()?;
276 sign_sponsored_transaction(&fee_payer_txn, sender, secondary_signers, fee_payer)
277 }
278}
279
280pub fn sign_sponsored_transaction<S, F>(
306 fee_payer_txn: &FeePayerRawTransaction,
307 sender: &S,
308 secondary_signers: &[&dyn Account],
309 fee_payer: &F,
310) -> AptosResult<SignedTransaction>
311where
312 S: Account,
313 F: Account,
314{
315 let signing_message = fee_payer_txn.signing_message()?;
316
317 let sender_signature = sender.sign(&signing_message)?;
319 let sender_public_key = sender.public_key_bytes();
320 let sender_auth = make_account_authenticator(
321 sender.signature_scheme(),
322 sender_public_key,
323 sender_signature,
324 )?;
325
326 let mut secondary_auths = Vec::with_capacity(secondary_signers.len());
328 for signer in secondary_signers {
329 let signature = signer.sign(&signing_message)?;
330 let public_key = signer.public_key_bytes();
331 secondary_auths.push(make_account_authenticator(
332 signer.signature_scheme(),
333 public_key,
334 signature,
335 )?);
336 }
337
338 let fee_payer_signature = fee_payer.sign(&signing_message)?;
340 let fee_payer_public_key = fee_payer.public_key_bytes();
341 let fee_payer_auth = make_account_authenticator(
342 fee_payer.signature_scheme(),
343 fee_payer_public_key,
344 fee_payer_signature,
345 )?;
346
347 let authenticator = TransactionAuthenticator::fee_payer(
348 sender_auth,
349 fee_payer_txn.secondary_signer_addresses.clone(),
350 secondary_auths,
351 fee_payer_txn.fee_payer_address,
352 fee_payer_auth,
353 );
354
355 Ok(SignedTransaction::new(
356 fee_payer_txn.raw_txn.clone(),
357 authenticator,
358 ))
359}
360
361fn make_account_authenticator(
367 scheme: u8,
368 public_key: Vec<u8>,
369 signature: Vec<u8>,
370) -> AptosResult<AccountAuthenticator> {
371 match scheme {
372 crate::crypto::ED25519_SCHEME => Ok(AccountAuthenticator::ed25519(public_key, signature)),
373 crate::crypto::MULTI_ED25519_SCHEME => Ok(AccountAuthenticator::MultiEd25519 {
374 public_key,
375 signature,
376 }),
377 crate::crypto::SINGLE_KEY_SCHEME => {
378 let _ = AnyPublicKey::from_bcs_bytes(&public_key)?;
379 let _ = AnySignature::from_bcs_bytes(&signature)?;
380 Ok(AccountAuthenticator::single_key(public_key, signature))
381 }
382 crate::crypto::MULTI_KEY_SCHEME => {
383 let _ = MultiKeyPublicKey::from_bytes(&public_key)?;
384 let _ = MultiKeySignature::from_bytes(&signature)?;
385 Ok(AccountAuthenticator::multi_key(public_key, signature))
386 }
387 _ => Err(AptosError::InvalidSignature(format!(
388 "unknown signature scheme: {scheme}"
389 ))),
390 }
391}
392
393#[derive(Debug, Clone)]
399pub struct PartiallySigned {
400 pub fee_payer_txn: FeePayerRawTransaction,
402 pub sender_auth: Option<AccountAuthenticator>,
404 pub secondary_auths: Vec<Option<AccountAuthenticator>>,
406 pub fee_payer_auth: Option<AccountAuthenticator>,
408}
409
410impl PartiallySigned {
411 pub fn new(fee_payer_txn: FeePayerRawTransaction) -> Self {
413 let num_secondary = fee_payer_txn.secondary_signer_addresses.len();
414 Self {
415 fee_payer_txn,
416 sender_auth: None,
417 secondary_auths: vec![None; num_secondary],
418 fee_payer_auth: None,
419 }
420 }
421
422 pub fn sign_as_sender<A: Account>(&mut self, sender: &A) -> AptosResult<()> {
429 let signing_message = self.fee_payer_txn.signing_message()?;
430 let signature = sender.sign(&signing_message)?;
431 let public_key = sender.public_key_bytes();
432 self.sender_auth = Some(make_account_authenticator(
433 sender.signature_scheme(),
434 public_key,
435 signature,
436 )?);
437 Ok(())
438 }
439
440 pub fn sign_as_secondary<A: Account>(&mut self, index: usize, signer: &A) -> AptosResult<()> {
447 if index >= self.secondary_auths.len() {
448 return Err(AptosError::transaction(format!(
449 "secondary signer index {} out of bounds (max {})",
450 index,
451 self.secondary_auths.len()
452 )));
453 }
454
455 let signing_message = self.fee_payer_txn.signing_message()?;
456 let signature = signer.sign(&signing_message)?;
457 let public_key = signer.public_key_bytes();
458 self.secondary_auths[index] = Some(make_account_authenticator(
459 signer.signature_scheme(),
460 public_key,
461 signature,
462 )?);
463 Ok(())
464 }
465
466 pub fn sign_as_fee_payer<A: Account>(&mut self, fee_payer: &A) -> AptosResult<()> {
473 let signing_message = self.fee_payer_txn.signing_message()?;
474 let signature = fee_payer.sign(&signing_message)?;
475 let public_key = fee_payer.public_key_bytes();
476 self.fee_payer_auth = Some(make_account_authenticator(
477 fee_payer.signature_scheme(),
478 public_key,
479 signature,
480 )?);
481 Ok(())
482 }
483
484 pub fn is_complete(&self) -> bool {
486 self.sender_auth.is_some()
487 && self.fee_payer_auth.is_some()
488 && self.secondary_auths.iter().all(Option::is_some)
489 }
490
491 pub fn finalize(self) -> AptosResult<SignedTransaction> {
499 let sender_auth = self
500 .sender_auth
501 .ok_or_else(|| AptosError::transaction("missing sender signature"))?;
502 let fee_payer_auth = self
503 .fee_payer_auth
504 .ok_or_else(|| AptosError::transaction("missing fee payer signature"))?;
505
506 let secondary_auths: Result<Vec<_>, _> = self
507 .secondary_auths
508 .into_iter()
509 .enumerate()
510 .map(|(i, auth)| {
511 auth.ok_or_else(|| {
512 AptosError::transaction(format!("missing secondary signer {i} signature"))
513 })
514 })
515 .collect();
516 let secondary_auths = secondary_auths?;
517
518 let authenticator = TransactionAuthenticator::fee_payer(
519 sender_auth,
520 self.fee_payer_txn.secondary_signer_addresses.clone(),
521 secondary_auths,
522 self.fee_payer_txn.fee_payer_address,
523 fee_payer_auth,
524 );
525
526 Ok(SignedTransaction::new(
527 self.fee_payer_txn.raw_txn,
528 authenticator,
529 ))
530 }
531}
532
533pub trait Sponsor: Account + Sized {
538 fn sponsor<S: Account>(
566 &self,
567 sender: &S,
568 sender_sequence_number: u64,
569 payload: TransactionPayload,
570 chain_id: ChainId,
571 ) -> AptosResult<SignedTransaction> {
572 SponsoredTransactionBuilder::new()
573 .sender(sender.address())
574 .sequence_number(sender_sequence_number)
575 .fee_payer(self.address())
576 .payload(payload)
577 .chain_id(chain_id)
578 .build_and_sign(sender, &[], self)
579 }
580
581 fn sponsor_with_gas<S: Account>(
587 &self,
588 sender: &S,
589 sender_sequence_number: u64,
590 payload: TransactionPayload,
591 chain_id: ChainId,
592 max_gas_amount: u64,
593 gas_unit_price: u64,
594 ) -> AptosResult<SignedTransaction> {
595 SponsoredTransactionBuilder::new()
596 .sender(sender.address())
597 .sequence_number(sender_sequence_number)
598 .fee_payer(self.address())
599 .payload(payload)
600 .chain_id(chain_id)
601 .max_gas_amount(max_gas_amount)
602 .gas_unit_price(gas_unit_price)
603 .build_and_sign(sender, &[], self)
604 }
605}
606
607impl<A: Account + Sized> Sponsor for A {}
609
610pub fn sponsor_transaction<S, F>(
633 sender: &S,
634 sender_sequence_number: u64,
635 fee_payer: &F,
636 payload: TransactionPayload,
637 chain_id: ChainId,
638) -> AptosResult<SignedTransaction>
639where
640 S: Account,
641 F: Account,
642{
643 SponsoredTransactionBuilder::new()
644 .sender(sender.address())
645 .sequence_number(sender_sequence_number)
646 .fee_payer(fee_payer.address())
647 .payload(payload)
648 .chain_id(chain_id)
649 .build_and_sign(sender, &[], fee_payer)
650}
651
652#[cfg(test)]
653mod tests {
654 use super::*;
655 use crate::transaction::payload::EntryFunction;
656
657 #[test]
658 fn test_builder_missing_sender() {
659 let recipient = AccountAddress::from_hex("0x123").unwrap();
660 let result = SponsoredTransactionBuilder::new()
661 .sequence_number(0)
662 .fee_payer(AccountAddress::ONE)
663 .payload(TransactionPayload::EntryFunction(
664 EntryFunction::apt_transfer(recipient, 1000).unwrap(),
665 ))
666 .chain_id(ChainId::testnet())
667 .build();
668
669 assert!(result.is_err());
670 assert!(result.unwrap_err().to_string().contains("sender"));
671 }
672
673 #[test]
674 fn test_builder_missing_fee_payer() {
675 let recipient = AccountAddress::from_hex("0x123").unwrap();
676 let result = SponsoredTransactionBuilder::new()
677 .sender(AccountAddress::ONE)
678 .sequence_number(0)
679 .payload(TransactionPayload::EntryFunction(
680 EntryFunction::apt_transfer(recipient, 1000).unwrap(),
681 ))
682 .chain_id(ChainId::testnet())
683 .build();
684
685 assert!(result.is_err());
686 assert!(result.unwrap_err().to_string().contains("fee_payer"));
687 }
688
689 #[test]
690 fn test_builder_complete() {
691 let recipient = AccountAddress::from_hex("0x123").unwrap();
692 let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
693
694 let fee_payer_txn = SponsoredTransactionBuilder::new()
695 .sender(AccountAddress::ONE)
696 .sequence_number(5)
697 .fee_payer(AccountAddress::from_hex("0x3").unwrap())
698 .payload(payload.into())
699 .chain_id(ChainId::testnet())
700 .max_gas_amount(100_000)
701 .gas_unit_price(150)
702 .build()
703 .unwrap();
704
705 assert_eq!(fee_payer_txn.raw_txn.sender, AccountAddress::ONE);
706 assert_eq!(fee_payer_txn.raw_txn.sequence_number, 5);
707 assert_eq!(fee_payer_txn.raw_txn.max_gas_amount, 100_000);
708 assert_eq!(fee_payer_txn.raw_txn.gas_unit_price, 150);
709 assert_eq!(
710 fee_payer_txn.fee_payer_address,
711 AccountAddress::from_hex("0x3").unwrap()
712 );
713 }
714
715 #[test]
716 fn test_partially_signed_completion_check() {
717 let recipient = AccountAddress::from_hex("0x123").unwrap();
718 let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
719
720 let fee_payer_txn = SponsoredTransactionBuilder::new()
721 .sender(AccountAddress::ONE)
722 .sequence_number(0)
723 .fee_payer(AccountAddress::from_hex("0x3").unwrap())
724 .payload(payload.into())
725 .chain_id(ChainId::testnet())
726 .build()
727 .unwrap();
728
729 let partially_signed = PartiallySigned::new(fee_payer_txn);
730 assert!(!partially_signed.is_complete());
731 }
732
733 #[test]
734 fn test_partially_signed_finalize_incomplete() {
735 let recipient = AccountAddress::from_hex("0x123").unwrap();
736 let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
737
738 let fee_payer_txn = SponsoredTransactionBuilder::new()
739 .sender(AccountAddress::ONE)
740 .sequence_number(0)
741 .fee_payer(AccountAddress::from_hex("0x3").unwrap())
742 .payload(payload.into())
743 .chain_id(ChainId::testnet())
744 .build()
745 .unwrap();
746
747 let partially_signed = PartiallySigned::new(fee_payer_txn);
748 let result = partially_signed.finalize();
749
750 assert!(result.is_err());
751 assert!(result.unwrap_err().to_string().contains("missing"));
752 }
753
754 #[cfg(feature = "ed25519")]
755 #[test]
756 fn test_full_sponsored_transaction() {
757 use crate::account::Ed25519Account;
758
759 let sender = Ed25519Account::generate();
760 let fee_payer = Ed25519Account::generate();
761 let recipient = AccountAddress::from_hex("0x123").unwrap();
762
763 let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
764
765 let signed_txn = SponsoredTransactionBuilder::new()
766 .sender(sender.address())
767 .sequence_number(0)
768 .fee_payer(fee_payer.address())
769 .payload(payload.into())
770 .chain_id(ChainId::testnet())
771 .build_and_sign(&sender, &[], &fee_payer)
772 .unwrap();
773
774 assert_eq!(signed_txn.raw_txn.sender, sender.address());
776 assert!(matches!(
777 signed_txn.authenticator,
778 TransactionAuthenticator::FeePayer { .. }
779 ));
780 }
781
782 #[cfg(feature = "ed25519")]
783 #[test]
784 fn test_sponsor_trait() {
785 use crate::account::Ed25519Account;
786
787 let sender = Ed25519Account::generate();
788 let sponsor = Ed25519Account::generate();
789 let recipient = AccountAddress::from_hex("0x123").unwrap();
790
791 let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
792
793 let signed_txn = sponsor
795 .sponsor(&sender, 0, payload.into(), ChainId::testnet())
796 .unwrap();
797
798 assert_eq!(signed_txn.raw_txn.sender, sender.address());
799 }
800
801 #[cfg(feature = "ed25519")]
802 #[test]
803 fn test_sponsor_transaction_fn() {
804 use crate::account::Ed25519Account;
805
806 let sender = Ed25519Account::generate();
807 let fee_payer = Ed25519Account::generate();
808 let recipient = AccountAddress::from_hex("0x123").unwrap();
809
810 let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
811
812 let signed_txn =
814 sponsor_transaction(&sender, 0, &fee_payer, payload.into(), ChainId::testnet())
815 .unwrap();
816
817 assert_eq!(signed_txn.raw_txn.sender, sender.address());
818 }
819
820 #[cfg(feature = "ed25519")]
821 #[test]
822 fn test_partially_signed_flow() {
823 use crate::account::Ed25519Account;
824
825 let sender = Ed25519Account::generate();
826 let fee_payer = Ed25519Account::generate();
827 let recipient = AccountAddress::from_hex("0x123").unwrap();
828
829 let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
830
831 let fee_payer_txn = SponsoredTransactionBuilder::new()
833 .sender(sender.address())
834 .sequence_number(0)
835 .fee_payer(fee_payer.address())
836 .payload(payload.into())
837 .chain_id(ChainId::testnet())
838 .build()
839 .unwrap();
840
841 let mut partially_signed = PartiallySigned::new(fee_payer_txn);
843
844 assert!(!partially_signed.is_complete());
846
847 partially_signed.sign_as_sender(&sender).unwrap();
849 assert!(!partially_signed.is_complete());
850
851 partially_signed.sign_as_fee_payer(&fee_payer).unwrap();
853 assert!(partially_signed.is_complete());
854
855 let signed_txn = partially_signed.finalize().unwrap();
857 assert_eq!(signed_txn.raw_txn.sender, sender.address());
858 }
859
860 #[test]
861 fn test_builder_missing_sequence_number() {
862 let recipient = AccountAddress::from_hex("0x123").unwrap();
863 let result = SponsoredTransactionBuilder::new()
864 .sender(AccountAddress::ONE)
865 .fee_payer(AccountAddress::from_hex("0x3").unwrap())
866 .payload(TransactionPayload::EntryFunction(
867 EntryFunction::apt_transfer(recipient, 1000).unwrap(),
868 ))
869 .chain_id(ChainId::testnet())
870 .build();
871
872 assert!(result.is_err());
873 assert!(result.unwrap_err().to_string().contains("sequence_number"));
874 }
875
876 #[test]
877 fn test_builder_missing_payload() {
878 let result = SponsoredTransactionBuilder::new()
879 .sender(AccountAddress::ONE)
880 .sequence_number(0)
881 .fee_payer(AccountAddress::from_hex("0x3").unwrap())
882 .chain_id(ChainId::testnet())
883 .build();
884
885 assert!(result.is_err());
886 assert!(result.unwrap_err().to_string().contains("payload"));
887 }
888
889 #[test]
890 fn test_builder_missing_chain_id() {
891 let recipient = AccountAddress::from_hex("0x123").unwrap();
892 let result = SponsoredTransactionBuilder::new()
893 .sender(AccountAddress::ONE)
894 .sequence_number(0)
895 .fee_payer(AccountAddress::from_hex("0x3").unwrap())
896 .payload(TransactionPayload::EntryFunction(
897 EntryFunction::apt_transfer(recipient, 1000).unwrap(),
898 ))
899 .build();
900
901 assert!(result.is_err());
902 assert!(result.unwrap_err().to_string().contains("chain_id"));
903 }
904
905 #[test]
906 fn test_builder_secondary_signers() {
907 let recipient = AccountAddress::from_hex("0x123").unwrap();
908 let secondary1 = AccountAddress::from_hex("0x4").unwrap();
909 let secondary2 = AccountAddress::from_hex("0x5").unwrap();
910
911 let fee_payer_txn = SponsoredTransactionBuilder::new()
912 .sender(AccountAddress::ONE)
913 .sequence_number(0)
914 .fee_payer(AccountAddress::from_hex("0x3").unwrap())
915 .secondary_signer(secondary1)
916 .secondary_signers(&[secondary2])
917 .payload(TransactionPayload::EntryFunction(
918 EntryFunction::apt_transfer(recipient, 1000).unwrap(),
919 ))
920 .chain_id(ChainId::testnet())
921 .build()
922 .unwrap();
923
924 assert_eq!(fee_payer_txn.secondary_signer_addresses.len(), 2);
925 assert_eq!(fee_payer_txn.secondary_signer_addresses[0], secondary1);
926 assert_eq!(fee_payer_txn.secondary_signer_addresses[1], secondary2);
927 }
928
929 #[test]
930 fn test_builder_expiration_timestamp() {
931 let recipient = AccountAddress::from_hex("0x123").unwrap();
932 let expiration = 1_234_567_890_u64;
933
934 let fee_payer_txn = SponsoredTransactionBuilder::new()
935 .sender(AccountAddress::ONE)
936 .sequence_number(0)
937 .fee_payer(AccountAddress::from_hex("0x3").unwrap())
938 .payload(TransactionPayload::EntryFunction(
939 EntryFunction::apt_transfer(recipient, 1000).unwrap(),
940 ))
941 .chain_id(ChainId::testnet())
942 .expiration_timestamp_secs(expiration)
943 .build()
944 .unwrap();
945
946 assert_eq!(fee_payer_txn.raw_txn.expiration_timestamp_secs, expiration);
947 }
948
949 #[test]
950 fn test_builder_expiration_from_now() {
951 let recipient = AccountAddress::from_hex("0x123").unwrap();
952
953 let fee_payer_txn = SponsoredTransactionBuilder::new()
954 .sender(AccountAddress::ONE)
955 .sequence_number(0)
956 .fee_payer(AccountAddress::from_hex("0x3").unwrap())
957 .payload(TransactionPayload::EntryFunction(
958 EntryFunction::apt_transfer(recipient, 1000).unwrap(),
959 ))
960 .chain_id(ChainId::testnet())
961 .expiration_from_now(60)
962 .build()
963 .unwrap();
964
965 let now = SystemTime::now()
967 .duration_since(UNIX_EPOCH)
968 .unwrap()
969 .as_secs();
970 assert!(fee_payer_txn.raw_txn.expiration_timestamp_secs >= now);
971 assert!(fee_payer_txn.raw_txn.expiration_timestamp_secs <= now + 65);
972 }
973
974 #[test]
975 fn test_builder_default() {
976 let builder = SponsoredTransactionBuilder::default();
977 assert!(builder.sender_address.is_none());
978 assert!(builder.sequence_number.is_none());
979 assert!(builder.fee_payer_address.is_none());
980 assert!(builder.payload.is_none());
981 assert!(builder.chain_id.is_none());
982 }
986
987 #[test]
988 fn test_builder_new_defaults() {
989 let builder = SponsoredTransactionBuilder::new();
990 assert!(builder.sender_address.is_none());
991 assert!(builder.sequence_number.is_none());
992 assert!(builder.fee_payer_address.is_none());
993 assert!(builder.payload.is_none());
994 assert!(builder.chain_id.is_none());
995 assert_eq!(builder.max_gas_amount, DEFAULT_MAX_GAS_AMOUNT);
996 assert_eq!(builder.gas_unit_price, DEFAULT_GAS_UNIT_PRICE);
997 }
998
999 #[test]
1000 fn test_builder_debug() {
1001 let builder = SponsoredTransactionBuilder::new().sender(AccountAddress::ONE);
1002 let debug = format!("{builder:?}");
1003 assert!(debug.contains("SponsoredTransactionBuilder"));
1004 }
1005
1006 #[cfg(feature = "ed25519")]
1007 #[test]
1008 fn test_partially_signed_with_secondary_signers() {
1009 use crate::account::Ed25519Account;
1010
1011 let sender = Ed25519Account::generate();
1012 let secondary = Ed25519Account::generate();
1013 let fee_payer = Ed25519Account::generate();
1014 let recipient = AccountAddress::from_hex("0x123").unwrap();
1015
1016 let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
1017
1018 let fee_payer_txn = SponsoredTransactionBuilder::new()
1020 .sender(sender.address())
1021 .sequence_number(0)
1022 .secondary_signer(secondary.address())
1023 .fee_payer(fee_payer.address())
1024 .payload(payload.into())
1025 .chain_id(ChainId::testnet())
1026 .build()
1027 .unwrap();
1028
1029 let mut partially_signed = PartiallySigned::new(fee_payer_txn);
1030
1031 assert!(!partially_signed.is_complete());
1033
1034 partially_signed.sign_as_sender(&sender).unwrap();
1035 assert!(!partially_signed.is_complete());
1036
1037 partially_signed.sign_as_secondary(0, &secondary).unwrap();
1038 assert!(!partially_signed.is_complete());
1039
1040 partially_signed.sign_as_fee_payer(&fee_payer).unwrap();
1041 assert!(partially_signed.is_complete());
1042
1043 let signed = partially_signed.finalize().unwrap();
1044 assert_eq!(signed.raw_txn.sender, sender.address());
1045 }
1046
1047 #[cfg(feature = "ed25519")]
1048 #[test]
1049 fn test_partially_signed_flow_with_single_key_accounts() {
1050 use crate::account::Ed25519SingleKeyAccount;
1051 use crate::transaction::authenticator::{AccountAuthenticator, TransactionAuthenticator};
1052
1053 let sender = Ed25519SingleKeyAccount::generate();
1054 let fee_payer = Ed25519SingleKeyAccount::generate();
1055 let recipient = AccountAddress::from_hex("0x123").unwrap();
1056 let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
1057
1058 let fee_payer_txn = SponsoredTransactionBuilder::new()
1059 .sender(sender.address())
1060 .sequence_number(0)
1061 .fee_payer(fee_payer.address())
1062 .payload(payload.into())
1063 .chain_id(ChainId::testnet())
1064 .build()
1065 .unwrap();
1066
1067 let mut partially_signed = PartiallySigned::new(fee_payer_txn);
1068 partially_signed.sign_as_sender(&sender).unwrap();
1069 partially_signed.sign_as_fee_payer(&fee_payer).unwrap();
1070 let signed = partially_signed.finalize().unwrap();
1071 match &signed.authenticator {
1072 TransactionAuthenticator::FeePayer {
1073 sender,
1074 fee_payer_signer,
1075 ..
1076 } => {
1077 assert!(matches!(sender, AccountAuthenticator::SingleKey { .. }));
1078 assert!(matches!(
1079 fee_payer_signer,
1080 AccountAuthenticator::SingleKey { .. }
1081 ));
1082 }
1083 _ => panic!("expected FeePayer authenticator"),
1084 }
1085 signed.verify_signature().unwrap();
1086 }
1087
1088 #[cfg(feature = "ed25519")]
1089 #[test]
1090 fn test_partially_signed_flow_with_multi_key_accounts() {
1091 use crate::account::{AnyPrivateKey, MultiKeyAccount};
1092 use crate::crypto::Ed25519PrivateKey;
1093 use crate::transaction::authenticator::{AccountAuthenticator, TransactionAuthenticator};
1094
1095 let sender = MultiKeyAccount::new(
1096 vec![AnyPrivateKey::ed25519(Ed25519PrivateKey::generate())],
1097 1,
1098 )
1099 .unwrap();
1100 let fee_payer = MultiKeyAccount::new(
1101 vec![AnyPrivateKey::ed25519(Ed25519PrivateKey::generate())],
1102 1,
1103 )
1104 .unwrap();
1105 let recipient = AccountAddress::from_hex("0x123").unwrap();
1106 let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
1107
1108 let fee_payer_txn = SponsoredTransactionBuilder::new()
1109 .sender(sender.address())
1110 .sequence_number(0)
1111 .fee_payer(fee_payer.address())
1112 .payload(payload.into())
1113 .chain_id(ChainId::testnet())
1114 .build()
1115 .unwrap();
1116
1117 let mut partially_signed = PartiallySigned::new(fee_payer_txn);
1118 partially_signed.sign_as_sender(&sender).unwrap();
1119 partially_signed.sign_as_fee_payer(&fee_payer).unwrap();
1120 let signed = partially_signed.finalize().unwrap();
1121 match &signed.authenticator {
1122 TransactionAuthenticator::FeePayer {
1123 sender,
1124 fee_payer_signer,
1125 ..
1126 } => {
1127 assert!(matches!(sender, AccountAuthenticator::MultiKey { .. }));
1128 assert!(matches!(
1129 fee_payer_signer,
1130 AccountAuthenticator::MultiKey { .. }
1131 ));
1132 }
1133 _ => panic!("expected FeePayer authenticator"),
1134 }
1135 signed.verify_signature().unwrap();
1136 }
1137
1138 #[cfg(feature = "ed25519")]
1139 #[test]
1140 fn test_partially_signed_secondary_index_out_of_bounds() {
1141 use crate::account::Ed25519Account;
1142
1143 let sender = Ed25519Account::generate();
1144 let fee_payer = Ed25519Account::generate();
1145 let secondary = Ed25519Account::generate();
1146 let recipient = AccountAddress::from_hex("0x123").unwrap();
1147
1148 let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
1149
1150 let fee_payer_txn = SponsoredTransactionBuilder::new()
1152 .sender(sender.address())
1153 .sequence_number(0)
1154 .fee_payer(fee_payer.address())
1155 .payload(payload.into())
1156 .chain_id(ChainId::testnet())
1157 .build()
1158 .unwrap();
1159
1160 let mut partially_signed = PartiallySigned::new(fee_payer_txn);
1161
1162 let result = partially_signed.sign_as_secondary(0, &secondary);
1164 assert!(result.is_err());
1165 assert!(result.unwrap_err().to_string().contains("out of bounds"));
1166 }
1167
1168 #[cfg(feature = "ed25519")]
1169 #[test]
1170 fn test_partially_signed_finalize_missing_secondary() {
1171 use crate::account::Ed25519Account;
1172
1173 let sender = Ed25519Account::generate();
1174 let fee_payer = Ed25519Account::generate();
1175 let recipient = AccountAddress::from_hex("0x123").unwrap();
1176
1177 let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
1178
1179 let fee_payer_txn = SponsoredTransactionBuilder::new()
1181 .sender(sender.address())
1182 .sequence_number(0)
1183 .secondary_signer(AccountAddress::from_hex("0x5").unwrap())
1184 .fee_payer(fee_payer.address())
1185 .payload(payload.into())
1186 .chain_id(ChainId::testnet())
1187 .build()
1188 .unwrap();
1189
1190 let mut partially_signed = PartiallySigned::new(fee_payer_txn);
1191
1192 partially_signed.sign_as_sender(&sender).unwrap();
1194 partially_signed.sign_as_fee_payer(&fee_payer).unwrap();
1195
1196 let result = partially_signed.finalize();
1198 assert!(result.is_err());
1199 assert!(result.unwrap_err().to_string().contains("secondary signer"));
1200 }
1201
1202 #[cfg(feature = "ed25519")]
1203 #[test]
1204 fn test_sponsor_with_gas() {
1205 use crate::account::Ed25519Account;
1206
1207 let sender = Ed25519Account::generate();
1208 let sponsor = Ed25519Account::generate();
1209 let recipient = AccountAddress::from_hex("0x123").unwrap();
1210
1211 let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
1212
1213 let signed_txn = sponsor
1214 .sponsor_with_gas(&sender, 0, payload.into(), ChainId::testnet(), 50000, 200)
1215 .unwrap();
1216
1217 assert_eq!(signed_txn.raw_txn.sender, sender.address());
1218 assert_eq!(signed_txn.raw_txn.max_gas_amount, 50000);
1219 assert_eq!(signed_txn.raw_txn.gas_unit_price, 200);
1220 }
1221
1222 #[test]
1223 fn test_partially_signed_debug() {
1224 let recipient = AccountAddress::from_hex("0x123").unwrap();
1225 let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
1226
1227 let fee_payer_txn = SponsoredTransactionBuilder::new()
1228 .sender(AccountAddress::ONE)
1229 .sequence_number(0)
1230 .fee_payer(AccountAddress::from_hex("0x3").unwrap())
1231 .payload(payload.into())
1232 .chain_id(ChainId::testnet())
1233 .build()
1234 .unwrap();
1235
1236 let partially_signed = PartiallySigned::new(fee_payer_txn);
1237 let debug = format!("{partially_signed:?}");
1238 assert!(debug.contains("PartiallySigned"));
1239 }
1240}