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