Skip to main content

aptos_sdk/transaction/
sponsored.rs

1//! Sponsored transaction helpers.
2//!
3//! This module provides high-level utilities for creating and managing
4//! sponsored (fee payer) transactions, where one account pays the gas fees
5//! on behalf of another account.
6//!
7//! # Overview
8//!
9//! Sponsored transactions allow a "fee payer" account to pay the gas fees
10//! for a transaction initiated by a different "sender" account. This is useful for:
11//!
12//! - **Onboarding new users** - Users without APT can still execute transactions
13//! - **dApp subsidization** - Applications can pay gas fees for their users
14//! - **Gasless experiences** - Create seamless UX without exposing gas costs
15//!
16//! # Example
17//!
18//! ```rust,ignore
19//! use aptos_sdk::transaction::{SponsoredTransactionBuilder, EntryFunction};
20//!
21//! // Build a sponsored transaction
22//! let fee_payer_txn = SponsoredTransactionBuilder::new()
23//!     .sender(user_account.address())
24//!     .sequence_number(0)
25//!     .fee_payer(sponsor_account.address())
26//!     .payload(payload)
27//!     .chain_id(ChainId::testnet())
28//!     .build()?;
29//!
30//! // Sign with all parties
31//! let signed = sign_sponsored_transaction(
32//!     &fee_payer_txn,
33//!     &user_account,
34//!     &[],
35//!     &sponsor_account,
36//! )?;
37//! ```
38
39use crate::account::Account;
40use crate::error::{AptosError, AptosResult};
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/// A builder for constructing sponsored (fee payer) transactions.
51///
52/// This provides a fluent API for creating transactions where a fee payer
53/// account pays the gas fees on behalf of the sender.
54///
55/// # Example
56///
57/// ```rust,ignore
58/// use aptos_sdk::transaction::{SponsoredTransactionBuilder, EntryFunction};
59///
60/// // Build the fee payer transaction structure
61/// let fee_payer_txn = SponsoredTransactionBuilder::new()
62///     .sender(user_account.address())
63///     .sequence_number(0)
64///     .fee_payer(sponsor_account.address())
65///     .payload(payload)
66///     .chain_id(ChainId::testnet())
67///     .build()?;
68///
69/// // Then sign it
70/// let signed = sign_sponsored_transaction(
71///     &fee_payer_txn,
72///     &user_account,
73///     &[],  // no secondary signers
74///     &sponsor_account,
75/// )?;
76/// ```
77#[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    /// Creates a new sponsored transaction builder with default values.
92    #[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    /// Sets the sender address.
108    #[must_use]
109    pub fn sender(mut self, address: AccountAddress) -> Self {
110        self.sender_address = Some(address);
111        self
112    }
113
114    /// Sets the sender's sequence number.
115    #[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    /// Adds a secondary signer address to the transaction.
122    ///
123    /// Secondary signers are additional accounts that must sign the transaction.
124    /// This is useful for multi-party transactions.
125    #[must_use]
126    pub fn secondary_signer(mut self, address: AccountAddress) -> Self {
127        self.secondary_addresses.push(address);
128        self
129    }
130
131    /// Adds multiple secondary signer addresses to the transaction.
132    #[must_use]
133    pub fn secondary_signers(mut self, addresses: &[AccountAddress]) -> Self {
134        self.secondary_addresses.extend(addresses);
135        self
136    }
137
138    /// Sets the fee payer address.
139    #[must_use]
140    pub fn fee_payer(mut self, address: AccountAddress) -> Self {
141        self.fee_payer_address = Some(address);
142        self
143    }
144
145    /// Sets the transaction payload.
146    #[must_use]
147    pub fn payload(mut self, payload: TransactionPayload) -> Self {
148        self.payload = Some(payload);
149        self
150    }
151
152    /// Sets the maximum gas amount.
153    #[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    /// Sets the gas unit price in octas.
160    #[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    /// Sets the expiration timestamp in seconds since Unix epoch.
167    #[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    /// Sets the expiration time relative to now.
174    #[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    /// Sets the chain ID.
185    #[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    /// Builds the raw fee payer transaction (unsigned).
192    ///
193    /// This returns a `FeePayerRawTransaction` that can be signed later
194    /// by the sender, secondary signers, and fee payer.
195    ///
196    /// # Errors
197    ///
198    /// Returns an error if `sender`, `sequence_number`, `payload`, `chain_id`, or `fee_payer` is not set.
199    pub fn build(self) -> AptosResult<FeePayerRawTransaction> {
200        let sender = self
201            .sender_address
202            .ok_or_else(|| AptosError::transaction("sender is required"))?;
203        let sequence_number = self
204            .sequence_number
205            .ok_or_else(|| AptosError::transaction("sequence_number is required"))?;
206        let payload = self
207            .payload
208            .ok_or_else(|| AptosError::transaction("payload is required"))?;
209        let chain_id = self
210            .chain_id
211            .ok_or_else(|| AptosError::transaction("chain_id is required"))?;
212        let fee_payer_address = self
213            .fee_payer_address
214            .ok_or_else(|| AptosError::transaction("fee_payer is required"))?;
215
216        let expiration_timestamp_secs = self.expiration_timestamp_secs.unwrap_or_else(|| {
217            SystemTime::now()
218                .duration_since(UNIX_EPOCH)
219                .unwrap_or_default()
220                .as_secs()
221                .saturating_add(DEFAULT_EXPIRATION_SECONDS)
222                + 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    /// Builds and signs the transaction with all provided accounts.
243    ///
244    /// This is a convenience method that builds the transaction and signs it
245    /// in one step.
246    ///
247    /// # Example
248    ///
249    /// ```rust,ignore
250    /// let signed = SponsoredTransactionBuilder::new()
251    ///     .sender(user.address())
252    ///     .sequence_number(0)
253    ///     .fee_payer(sponsor.address())
254    ///     .payload(payload)
255    ///     .chain_id(ChainId::testnet())
256    ///     .build_and_sign(&user, &[], &sponsor)?;
257    /// ```
258    ///
259    /// # Errors
260    ///
261    /// Returns an error if building the transaction fails or if any signer fails to sign.
262    pub fn build_and_sign<S, F>(
263        self,
264        sender: &S,
265        secondary_signers: &[&dyn Account],
266        fee_payer: &F,
267    ) -> AptosResult<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
277/// Signs a sponsored (fee payer) transaction with all required signatures.
278///
279/// # Arguments
280///
281/// * `fee_payer_txn` - The unsigned fee payer transaction
282/// * `sender` - The sender account
283/// * `secondary_signers` - Additional signers (if any)
284/// * `fee_payer` - The account paying gas fees
285///
286/// # Example
287///
288/// ```rust,ignore
289/// use aptos_sdk::transaction::sign_sponsored_transaction;
290///
291/// let signed_txn = sign_sponsored_transaction(
292///     &fee_payer_txn,
293///     &sender_account,
294///     &[],  // No secondary signers
295///     &fee_payer_account,
296/// )?;
297/// ```
298///
299/// # Errors
300///
301/// Returns an error if generating the signing message fails or if any signer fails to sign.
302pub fn sign_sponsored_transaction<S, F>(
303    fee_payer_txn: &FeePayerRawTransaction,
304    sender: &S,
305    secondary_signers: &[&dyn Account],
306    fee_payer: &F,
307) -> AptosResult<SignedTransaction>
308where
309    S: Account,
310    F: Account,
311{
312    let signing_message = fee_payer_txn.signing_message()?;
313
314    // Sign with sender
315    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    // Sign with secondary signers
324    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    // Sign with fee payer
336    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
358/// Creates an account authenticator from signature components.
359///
360/// # Errors
361///
362/// Returns an error if the signature scheme is not recognized.
363fn make_account_authenticator(
364    scheme: u8,
365    public_key: Vec<u8>,
366    signature: Vec<u8>,
367) -> AptosResult<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(AptosError::InvalidSignature(format!(
381            "unknown signature scheme: {scheme}"
382        ))),
383    }
384}
385
386/// A partially signed sponsored transaction.
387///
388/// This represents a sponsored transaction that has been signed by some but
389/// not all required signers. It can be passed between parties for signature
390/// collection.
391#[derive(Debug, Clone)]
392pub struct PartiallySigned {
393    /// The underlying fee payer transaction.
394    pub fee_payer_txn: FeePayerRawTransaction,
395    /// Sender's signature (if signed).
396    pub sender_auth: Option<AccountAuthenticator>,
397    /// Secondary signer signatures.
398    pub secondary_auths: Vec<Option<AccountAuthenticator>>,
399    /// Fee payer's signature (if signed).
400    pub fee_payer_auth: Option<AccountAuthenticator>,
401}
402
403impl PartiallySigned {
404    /// Creates a new partially signed transaction.
405    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    /// Signs as the sender.
416    ///
417    /// # Errors
418    ///
419    /// Returns an error if generating the signing message fails, if signing fails,
420    /// or if the signature scheme is not recognized.
421    pub fn sign_as_sender<A: Account>(&mut self, sender: &A) -> AptosResult<()> {
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    /// Signs as a secondary signer at the given index.
434    ///
435    /// # Errors
436    ///
437    /// Returns an error if the index is out of bounds, if generating the signing message fails,
438    /// if signing fails, or if the signature scheme is not recognized.
439    pub fn sign_as_secondary<A: Account>(&mut self, index: usize, signer: &A) -> AptosResult<()> {
440        if index >= self.secondary_auths.len() {
441            return Err(AptosError::transaction(format!(
442                "secondary signer index {} out of bounds (max {})",
443                index,
444                self.secondary_auths.len()
445            )));
446        }
447
448        let signing_message = self.fee_payer_txn.signing_message()?;
449        let signature = signer.sign(&signing_message)?;
450        let public_key = signer.public_key_bytes();
451        self.secondary_auths[index] = Some(make_account_authenticator(
452            signer.signature_scheme(),
453            public_key,
454            signature,
455        )?);
456        Ok(())
457    }
458
459    /// Signs as the fee payer.
460    ///
461    /// # Errors
462    ///
463    /// Returns an error if generating the signing message fails, if signing fails,
464    /// or if the signature scheme is not recognized.
465    pub fn sign_as_fee_payer<A: Account>(&mut self, fee_payer: &A) -> AptosResult<()> {
466        let signing_message = self.fee_payer_txn.signing_message()?;
467        let signature = fee_payer.sign(&signing_message)?;
468        let public_key = fee_payer.public_key_bytes();
469        self.fee_payer_auth = Some(make_account_authenticator(
470            fee_payer.signature_scheme(),
471            public_key,
472            signature,
473        )?);
474        Ok(())
475    }
476
477    /// Checks if all required signatures have been collected.
478    pub fn is_complete(&self) -> bool {
479        self.sender_auth.is_some()
480            && self.fee_payer_auth.is_some()
481            && self.secondary_auths.iter().all(Option::is_some)
482    }
483
484    /// Finalizes the transaction if all signatures are present.
485    ///
486    /// Returns an error if any signatures are missing.
487    ///
488    /// # Errors
489    ///
490    /// Returns an error if the sender signature, fee payer signature, or any secondary signer signature is missing.
491    pub fn finalize(self) -> AptosResult<SignedTransaction> {
492        let sender_auth = self
493            .sender_auth
494            .ok_or_else(|| AptosError::transaction("missing sender signature"))?;
495        let fee_payer_auth = self
496            .fee_payer_auth
497            .ok_or_else(|| AptosError::transaction("missing fee payer signature"))?;
498
499        let secondary_auths: Result<Vec<_>, _> = self
500            .secondary_auths
501            .into_iter()
502            .enumerate()
503            .map(|(i, auth)| {
504                auth.ok_or_else(|| {
505                    AptosError::transaction(format!("missing secondary signer {i} signature"))
506                })
507            })
508            .collect();
509        let secondary_auths = secondary_auths?;
510
511        let authenticator = TransactionAuthenticator::fee_payer(
512            sender_auth,
513            self.fee_payer_txn.secondary_signer_addresses.clone(),
514            secondary_auths,
515            self.fee_payer_txn.fee_payer_address,
516            fee_payer_auth,
517        );
518
519        Ok(SignedTransaction::new(
520            self.fee_payer_txn.raw_txn,
521            authenticator,
522        ))
523    }
524}
525
526/// Extension trait that adds sponsorship capabilities to accounts.
527///
528/// This trait provides convenient methods for an account to sponsor
529/// transactions for other users.
530pub trait Sponsor: Account + Sized {
531    /// Sponsors a transaction for another account.
532    ///
533    /// Creates and signs a sponsored transaction where `self` pays the gas fees.
534    ///
535    /// # Arguments
536    ///
537    /// * `sender` - The account initiating the transaction
538    /// * `sender_sequence_number` - The sender's current sequence number
539    /// * `payload` - The transaction payload
540    /// * `chain_id` - The target chain ID
541    ///
542    /// # Example
543    ///
544    /// ```rust,ignore
545    /// use aptos_sdk::transaction::Sponsor;
546    ///
547    /// let signed_txn = sponsor_account.sponsor(
548    ///     &user_account,
549    ///     0,
550    ///     payload,
551    ///     ChainId::testnet(),
552    /// )?;
553    /// ```
554    ///
555    /// # Errors
556    ///
557    /// Returns an error if building the transaction fails or if any signer fails to sign.
558    fn sponsor<S: Account>(
559        &self,
560        sender: &S,
561        sender_sequence_number: u64,
562        payload: TransactionPayload,
563        chain_id: ChainId,
564    ) -> AptosResult<SignedTransaction> {
565        SponsoredTransactionBuilder::new()
566            .sender(sender.address())
567            .sequence_number(sender_sequence_number)
568            .fee_payer(self.address())
569            .payload(payload)
570            .chain_id(chain_id)
571            .build_and_sign(sender, &[], self)
572    }
573
574    /// Sponsors a transaction with custom gas settings.
575    ///
576    /// # Errors
577    ///
578    /// Returns an error if building the transaction fails or if any signer fails to sign.
579    fn sponsor_with_gas<S: Account>(
580        &self,
581        sender: &S,
582        sender_sequence_number: u64,
583        payload: TransactionPayload,
584        chain_id: ChainId,
585        max_gas_amount: u64,
586        gas_unit_price: u64,
587    ) -> AptosResult<SignedTransaction> {
588        SponsoredTransactionBuilder::new()
589            .sender(sender.address())
590            .sequence_number(sender_sequence_number)
591            .fee_payer(self.address())
592            .payload(payload)
593            .chain_id(chain_id)
594            .max_gas_amount(max_gas_amount)
595            .gas_unit_price(gas_unit_price)
596            .build_and_sign(sender, &[], self)
597    }
598}
599
600// Implement Sponsor for all Account types that are Sized
601impl<A: Account + Sized> Sponsor for A {}
602
603/// Creates a simple sponsored transaction with minimal configuration.
604///
605/// This is a convenience function for the common case of sponsoring a
606/// simple transaction without secondary signers.
607///
608/// # Example
609///
610/// ```rust,ignore
611/// use aptos_sdk::transaction::sponsor_transaction;
612///
613/// let signed = sponsor_transaction(
614///     &sender_account,
615///     sender_sequence_number,
616///     &sponsor_account,
617///     payload,
618///     ChainId::testnet(),
619/// )?;
620/// ```
621///
622/// # Errors
623///
624/// Returns an error if building the transaction fails or if any signer fails to sign.
625pub fn sponsor_transaction<S, F>(
626    sender: &S,
627    sender_sequence_number: u64,
628    fee_payer: &F,
629    payload: TransactionPayload,
630    chain_id: ChainId,
631) -> AptosResult<SignedTransaction>
632where
633    S: Account,
634    F: Account,
635{
636    SponsoredTransactionBuilder::new()
637        .sender(sender.address())
638        .sequence_number(sender_sequence_number)
639        .fee_payer(fee_payer.address())
640        .payload(payload)
641        .chain_id(chain_id)
642        .build_and_sign(sender, &[], fee_payer)
643}
644
645#[cfg(test)]
646mod tests {
647    use super::*;
648    use crate::transaction::payload::EntryFunction;
649
650    #[test]
651    fn test_builder_missing_sender() {
652        let recipient = AccountAddress::from_hex("0x123").unwrap();
653        let result = SponsoredTransactionBuilder::new()
654            .sequence_number(0)
655            .fee_payer(AccountAddress::ONE)
656            .payload(TransactionPayload::EntryFunction(
657                EntryFunction::apt_transfer(recipient, 1000).unwrap(),
658            ))
659            .chain_id(ChainId::testnet())
660            .build();
661
662        assert!(result.is_err());
663        assert!(result.unwrap_err().to_string().contains("sender"));
664    }
665
666    #[test]
667    fn test_builder_missing_fee_payer() {
668        let recipient = AccountAddress::from_hex("0x123").unwrap();
669        let result = SponsoredTransactionBuilder::new()
670            .sender(AccountAddress::ONE)
671            .sequence_number(0)
672            .payload(TransactionPayload::EntryFunction(
673                EntryFunction::apt_transfer(recipient, 1000).unwrap(),
674            ))
675            .chain_id(ChainId::testnet())
676            .build();
677
678        assert!(result.is_err());
679        assert!(result.unwrap_err().to_string().contains("fee_payer"));
680    }
681
682    #[test]
683    fn test_builder_complete() {
684        let recipient = AccountAddress::from_hex("0x123").unwrap();
685        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
686
687        let fee_payer_txn = SponsoredTransactionBuilder::new()
688            .sender(AccountAddress::ONE)
689            .sequence_number(5)
690            .fee_payer(AccountAddress::from_hex("0x3").unwrap())
691            .payload(payload.into())
692            .chain_id(ChainId::testnet())
693            .max_gas_amount(100_000)
694            .gas_unit_price(150)
695            .build()
696            .unwrap();
697
698        assert_eq!(fee_payer_txn.raw_txn.sender, AccountAddress::ONE);
699        assert_eq!(fee_payer_txn.raw_txn.sequence_number, 5);
700        assert_eq!(fee_payer_txn.raw_txn.max_gas_amount, 100_000);
701        assert_eq!(fee_payer_txn.raw_txn.gas_unit_price, 150);
702        assert_eq!(
703            fee_payer_txn.fee_payer_address,
704            AccountAddress::from_hex("0x3").unwrap()
705        );
706    }
707
708    #[test]
709    fn test_partially_signed_completion_check() {
710        let recipient = AccountAddress::from_hex("0x123").unwrap();
711        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
712
713        let fee_payer_txn = SponsoredTransactionBuilder::new()
714            .sender(AccountAddress::ONE)
715            .sequence_number(0)
716            .fee_payer(AccountAddress::from_hex("0x3").unwrap())
717            .payload(payload.into())
718            .chain_id(ChainId::testnet())
719            .build()
720            .unwrap();
721
722        let partially_signed = PartiallySigned::new(fee_payer_txn);
723        assert!(!partially_signed.is_complete());
724    }
725
726    #[test]
727    fn test_partially_signed_finalize_incomplete() {
728        let recipient = AccountAddress::from_hex("0x123").unwrap();
729        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
730
731        let fee_payer_txn = SponsoredTransactionBuilder::new()
732            .sender(AccountAddress::ONE)
733            .sequence_number(0)
734            .fee_payer(AccountAddress::from_hex("0x3").unwrap())
735            .payload(payload.into())
736            .chain_id(ChainId::testnet())
737            .build()
738            .unwrap();
739
740        let partially_signed = PartiallySigned::new(fee_payer_txn);
741        let result = partially_signed.finalize();
742
743        assert!(result.is_err());
744        assert!(result.unwrap_err().to_string().contains("missing"));
745    }
746
747    #[cfg(feature = "ed25519")]
748    #[test]
749    fn test_full_sponsored_transaction() {
750        use crate::account::Ed25519Account;
751
752        let sender = Ed25519Account::generate();
753        let fee_payer = Ed25519Account::generate();
754        let recipient = AccountAddress::from_hex("0x123").unwrap();
755
756        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
757
758        let signed_txn = SponsoredTransactionBuilder::new()
759            .sender(sender.address())
760            .sequence_number(0)
761            .fee_payer(fee_payer.address())
762            .payload(payload.into())
763            .chain_id(ChainId::testnet())
764            .build_and_sign(&sender, &[], &fee_payer)
765            .unwrap();
766
767        // Verify the transaction structure
768        assert_eq!(signed_txn.raw_txn.sender, sender.address());
769        assert!(matches!(
770            signed_txn.authenticator,
771            TransactionAuthenticator::FeePayer { .. }
772        ));
773    }
774
775    #[cfg(feature = "ed25519")]
776    #[test]
777    fn test_sponsor_trait() {
778        use crate::account::Ed25519Account;
779
780        let sender = Ed25519Account::generate();
781        let sponsor = Ed25519Account::generate();
782        let recipient = AccountAddress::from_hex("0x123").unwrap();
783
784        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
785
786        // Use the Sponsor trait
787        let signed_txn = sponsor
788            .sponsor(&sender, 0, payload.into(), ChainId::testnet())
789            .unwrap();
790
791        assert_eq!(signed_txn.raw_txn.sender, sender.address());
792    }
793
794    #[cfg(feature = "ed25519")]
795    #[test]
796    fn test_sponsor_transaction_fn() {
797        use crate::account::Ed25519Account;
798
799        let sender = Ed25519Account::generate();
800        let fee_payer = Ed25519Account::generate();
801        let recipient = AccountAddress::from_hex("0x123").unwrap();
802
803        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
804
805        // Use the convenience function
806        let signed_txn =
807            sponsor_transaction(&sender, 0, &fee_payer, payload.into(), ChainId::testnet())
808                .unwrap();
809
810        assert_eq!(signed_txn.raw_txn.sender, sender.address());
811    }
812
813    #[cfg(feature = "ed25519")]
814    #[test]
815    fn test_partially_signed_flow() {
816        use crate::account::Ed25519Account;
817
818        let sender = Ed25519Account::generate();
819        let fee_payer = Ed25519Account::generate();
820        let recipient = AccountAddress::from_hex("0x123").unwrap();
821
822        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
823
824        // Build the transaction
825        let fee_payer_txn = SponsoredTransactionBuilder::new()
826            .sender(sender.address())
827            .sequence_number(0)
828            .fee_payer(fee_payer.address())
829            .payload(payload.into())
830            .chain_id(ChainId::testnet())
831            .build()
832            .unwrap();
833
834        // Create partially signed and collect signatures
835        let mut partially_signed = PartiallySigned::new(fee_payer_txn);
836
837        // Not complete yet
838        assert!(!partially_signed.is_complete());
839
840        // Sign as sender
841        partially_signed.sign_as_sender(&sender).unwrap();
842        assert!(!partially_signed.is_complete());
843
844        // Sign as fee payer
845        partially_signed.sign_as_fee_payer(&fee_payer).unwrap();
846        assert!(partially_signed.is_complete());
847
848        // Finalize
849        let signed_txn = partially_signed.finalize().unwrap();
850        assert_eq!(signed_txn.raw_txn.sender, sender.address());
851    }
852
853    #[test]
854    fn test_builder_missing_sequence_number() {
855        let recipient = AccountAddress::from_hex("0x123").unwrap();
856        let result = SponsoredTransactionBuilder::new()
857            .sender(AccountAddress::ONE)
858            .fee_payer(AccountAddress::from_hex("0x3").unwrap())
859            .payload(TransactionPayload::EntryFunction(
860                EntryFunction::apt_transfer(recipient, 1000).unwrap(),
861            ))
862            .chain_id(ChainId::testnet())
863            .build();
864
865        assert!(result.is_err());
866        assert!(result.unwrap_err().to_string().contains("sequence_number"));
867    }
868
869    #[test]
870    fn test_builder_missing_payload() {
871        let result = SponsoredTransactionBuilder::new()
872            .sender(AccountAddress::ONE)
873            .sequence_number(0)
874            .fee_payer(AccountAddress::from_hex("0x3").unwrap())
875            .chain_id(ChainId::testnet())
876            .build();
877
878        assert!(result.is_err());
879        assert!(result.unwrap_err().to_string().contains("payload"));
880    }
881
882    #[test]
883    fn test_builder_missing_chain_id() {
884        let recipient = AccountAddress::from_hex("0x123").unwrap();
885        let result = SponsoredTransactionBuilder::new()
886            .sender(AccountAddress::ONE)
887            .sequence_number(0)
888            .fee_payer(AccountAddress::from_hex("0x3").unwrap())
889            .payload(TransactionPayload::EntryFunction(
890                EntryFunction::apt_transfer(recipient, 1000).unwrap(),
891            ))
892            .build();
893
894        assert!(result.is_err());
895        assert!(result.unwrap_err().to_string().contains("chain_id"));
896    }
897
898    #[test]
899    fn test_builder_secondary_signers() {
900        let recipient = AccountAddress::from_hex("0x123").unwrap();
901        let secondary1 = AccountAddress::from_hex("0x4").unwrap();
902        let secondary2 = AccountAddress::from_hex("0x5").unwrap();
903
904        let fee_payer_txn = SponsoredTransactionBuilder::new()
905            .sender(AccountAddress::ONE)
906            .sequence_number(0)
907            .fee_payer(AccountAddress::from_hex("0x3").unwrap())
908            .secondary_signer(secondary1)
909            .secondary_signers(&[secondary2])
910            .payload(TransactionPayload::EntryFunction(
911                EntryFunction::apt_transfer(recipient, 1000).unwrap(),
912            ))
913            .chain_id(ChainId::testnet())
914            .build()
915            .unwrap();
916
917        assert_eq!(fee_payer_txn.secondary_signer_addresses.len(), 2);
918        assert_eq!(fee_payer_txn.secondary_signer_addresses[0], secondary1);
919        assert_eq!(fee_payer_txn.secondary_signer_addresses[1], secondary2);
920    }
921
922    #[test]
923    fn test_builder_expiration_timestamp() {
924        let recipient = AccountAddress::from_hex("0x123").unwrap();
925        let expiration = 1_234_567_890_u64;
926
927        let fee_payer_txn = SponsoredTransactionBuilder::new()
928            .sender(AccountAddress::ONE)
929            .sequence_number(0)
930            .fee_payer(AccountAddress::from_hex("0x3").unwrap())
931            .payload(TransactionPayload::EntryFunction(
932                EntryFunction::apt_transfer(recipient, 1000).unwrap(),
933            ))
934            .chain_id(ChainId::testnet())
935            .expiration_timestamp_secs(expiration)
936            .build()
937            .unwrap();
938
939        assert_eq!(fee_payer_txn.raw_txn.expiration_timestamp_secs, expiration);
940    }
941
942    #[test]
943    fn test_builder_expiration_from_now() {
944        let recipient = AccountAddress::from_hex("0x123").unwrap();
945
946        let fee_payer_txn = SponsoredTransactionBuilder::new()
947            .sender(AccountAddress::ONE)
948            .sequence_number(0)
949            .fee_payer(AccountAddress::from_hex("0x3").unwrap())
950            .payload(TransactionPayload::EntryFunction(
951                EntryFunction::apt_transfer(recipient, 1000).unwrap(),
952            ))
953            .chain_id(ChainId::testnet())
954            .expiration_from_now(60)
955            .build()
956            .unwrap();
957
958        // Expiration should be roughly now + 60 seconds
959        let now = SystemTime::now()
960            .duration_since(UNIX_EPOCH)
961            .unwrap()
962            .as_secs();
963        assert!(fee_payer_txn.raw_txn.expiration_timestamp_secs >= now);
964        assert!(fee_payer_txn.raw_txn.expiration_timestamp_secs <= now + 65);
965    }
966
967    #[test]
968    fn test_builder_default() {
969        let builder = SponsoredTransactionBuilder::default();
970        assert!(builder.sender_address.is_none());
971        assert!(builder.sequence_number.is_none());
972        assert!(builder.fee_payer_address.is_none());
973        assert!(builder.payload.is_none());
974        assert!(builder.chain_id.is_none());
975        // Default values are set via SponsoredTransactionBuilder::new()
976        // not via Default::default() which initializes to Rust defaults (0)
977        // Let's just check the builder is properly created
978    }
979
980    #[test]
981    fn test_builder_new_defaults() {
982        let builder = SponsoredTransactionBuilder::new();
983        assert!(builder.sender_address.is_none());
984        assert!(builder.sequence_number.is_none());
985        assert!(builder.fee_payer_address.is_none());
986        assert!(builder.payload.is_none());
987        assert!(builder.chain_id.is_none());
988        assert_eq!(builder.max_gas_amount, DEFAULT_MAX_GAS_AMOUNT);
989        assert_eq!(builder.gas_unit_price, DEFAULT_GAS_UNIT_PRICE);
990    }
991
992    #[test]
993    fn test_builder_debug() {
994        let builder = SponsoredTransactionBuilder::new().sender(AccountAddress::ONE);
995        let debug = format!("{builder:?}");
996        assert!(debug.contains("SponsoredTransactionBuilder"));
997    }
998
999    #[cfg(feature = "ed25519")]
1000    #[test]
1001    fn test_partially_signed_with_secondary_signers() {
1002        use crate::account::Ed25519Account;
1003
1004        let sender = Ed25519Account::generate();
1005        let secondary = Ed25519Account::generate();
1006        let fee_payer = Ed25519Account::generate();
1007        let recipient = AccountAddress::from_hex("0x123").unwrap();
1008
1009        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
1010
1011        // Build with secondary signer
1012        let fee_payer_txn = SponsoredTransactionBuilder::new()
1013            .sender(sender.address())
1014            .sequence_number(0)
1015            .secondary_signer(secondary.address())
1016            .fee_payer(fee_payer.address())
1017            .payload(payload.into())
1018            .chain_id(ChainId::testnet())
1019            .build()
1020            .unwrap();
1021
1022        let mut partially_signed = PartiallySigned::new(fee_payer_txn);
1023
1024        // Need all three signatures
1025        assert!(!partially_signed.is_complete());
1026
1027        partially_signed.sign_as_sender(&sender).unwrap();
1028        assert!(!partially_signed.is_complete());
1029
1030        partially_signed.sign_as_secondary(0, &secondary).unwrap();
1031        assert!(!partially_signed.is_complete());
1032
1033        partially_signed.sign_as_fee_payer(&fee_payer).unwrap();
1034        assert!(partially_signed.is_complete());
1035
1036        let signed = partially_signed.finalize().unwrap();
1037        assert_eq!(signed.raw_txn.sender, sender.address());
1038    }
1039
1040    #[cfg(feature = "ed25519")]
1041    #[test]
1042    fn test_partially_signed_secondary_index_out_of_bounds() {
1043        use crate::account::Ed25519Account;
1044
1045        let sender = Ed25519Account::generate();
1046        let fee_payer = Ed25519Account::generate();
1047        let secondary = Ed25519Account::generate();
1048        let recipient = AccountAddress::from_hex("0x123").unwrap();
1049
1050        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
1051
1052        // No secondary signers in the transaction
1053        let fee_payer_txn = SponsoredTransactionBuilder::new()
1054            .sender(sender.address())
1055            .sequence_number(0)
1056            .fee_payer(fee_payer.address())
1057            .payload(payload.into())
1058            .chain_id(ChainId::testnet())
1059            .build()
1060            .unwrap();
1061
1062        let mut partially_signed = PartiallySigned::new(fee_payer_txn);
1063
1064        // Try to sign as secondary at index 0 (out of bounds because no secondary signers)
1065        let result = partially_signed.sign_as_secondary(0, &secondary);
1066        assert!(result.is_err());
1067        assert!(result.unwrap_err().to_string().contains("out of bounds"));
1068    }
1069
1070    #[cfg(feature = "ed25519")]
1071    #[test]
1072    fn test_partially_signed_finalize_missing_secondary() {
1073        use crate::account::Ed25519Account;
1074
1075        let sender = Ed25519Account::generate();
1076        let fee_payer = Ed25519Account::generate();
1077        let recipient = AccountAddress::from_hex("0x123").unwrap();
1078
1079        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
1080
1081        // Build with secondary signer but don't sign it
1082        let fee_payer_txn = SponsoredTransactionBuilder::new()
1083            .sender(sender.address())
1084            .sequence_number(0)
1085            .secondary_signer(AccountAddress::from_hex("0x5").unwrap())
1086            .fee_payer(fee_payer.address())
1087            .payload(payload.into())
1088            .chain_id(ChainId::testnet())
1089            .build()
1090            .unwrap();
1091
1092        let mut partially_signed = PartiallySigned::new(fee_payer_txn);
1093
1094        // Sign sender and fee payer but not secondary
1095        partially_signed.sign_as_sender(&sender).unwrap();
1096        partially_signed.sign_as_fee_payer(&fee_payer).unwrap();
1097
1098        // Should fail because secondary is missing
1099        let result = partially_signed.finalize();
1100        assert!(result.is_err());
1101        assert!(result.unwrap_err().to_string().contains("secondary signer"));
1102    }
1103
1104    #[cfg(feature = "ed25519")]
1105    #[test]
1106    fn test_sponsor_with_gas() {
1107        use crate::account::Ed25519Account;
1108
1109        let sender = Ed25519Account::generate();
1110        let sponsor = Ed25519Account::generate();
1111        let recipient = AccountAddress::from_hex("0x123").unwrap();
1112
1113        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
1114
1115        let signed_txn = sponsor
1116            .sponsor_with_gas(&sender, 0, payload.into(), ChainId::testnet(), 50000, 200)
1117            .unwrap();
1118
1119        assert_eq!(signed_txn.raw_txn.sender, sender.address());
1120        assert_eq!(signed_txn.raw_txn.max_gas_amount, 50000);
1121        assert_eq!(signed_txn.raw_txn.gas_unit_price, 200);
1122    }
1123
1124    #[test]
1125    fn test_partially_signed_debug() {
1126        let recipient = AccountAddress::from_hex("0x123").unwrap();
1127        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
1128
1129        let fee_payer_txn = SponsoredTransactionBuilder::new()
1130            .sender(AccountAddress::ONE)
1131            .sequence_number(0)
1132            .fee_payer(AccountAddress::from_hex("0x3").unwrap())
1133            .payload(payload.into())
1134            .chain_id(ChainId::testnet())
1135            .build()
1136            .unwrap();
1137
1138        let partially_signed = PartiallySigned::new(fee_payer_txn);
1139        let debug = format!("{partially_signed:?}");
1140        assert!(debug.contains("PartiallySigned"));
1141    }
1142}