Skip to main content

movement_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 movement_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::{MovementError, MovementResult};
41use crate::transaction::authenticator::{AccountAuthenticator, TransactionAuthenticator};
42use crate::transaction::builder::{
43    DEFAULT_EXPIRATION_SECONDS, DEFAULT_GAS_UNIT_PRICE, DEFAULT_MAX_GAS_AMOUNT,
44};
45use crate::transaction::payload::TransactionPayload;
46use crate::transaction::types::{FeePayerRawTransaction, RawTransaction, SignedTransaction};
47use crate::types::{AccountAddress, ChainId};
48use std::time::{SystemTime, UNIX_EPOCH};
49
50/// 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 movement_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) -> MovementResult<FeePayerRawTransaction> {
200        let sender = self
201            .sender_address
202            .ok_or_else(|| MovementError::transaction("sender is required"))?;
203        let sequence_number = self
204            .sequence_number
205            .ok_or_else(|| MovementError::transaction("sequence_number is required"))?;
206        let payload = self
207            .payload
208            .ok_or_else(|| MovementError::transaction("payload is required"))?;
209        let chain_id = self
210            .chain_id
211            .ok_or_else(|| MovementError::transaction("chain_id is required"))?;
212        let fee_payer_address = self
213            .fee_payer_address
214            .ok_or_else(|| MovementError::transaction("fee_payer is required"))?;
215
216        // SECURITY: Apply expiration offset only once (was previously doubled)
217        let expiration_timestamp_secs = self.expiration_timestamp_secs.unwrap_or_else(|| {
218            SystemTime::now()
219                .duration_since(UNIX_EPOCH)
220                .unwrap_or_default()
221                .as_secs()
222                .saturating_add(DEFAULT_EXPIRATION_SECONDS)
223        });
224
225        let raw_txn = RawTransaction::new(
226            sender,
227            sequence_number,
228            payload,
229            self.max_gas_amount,
230            self.gas_unit_price,
231            expiration_timestamp_secs,
232            chain_id,
233        );
234
235        Ok(FeePayerRawTransaction {
236            raw_txn,
237            secondary_signer_addresses: self.secondary_addresses,
238            fee_payer_address,
239        })
240    }
241
242    /// 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    ) -> MovementResult<SignedTransaction>
268    where
269        S: Account,
270        F: Account,
271    {
272        let fee_payer_txn = self.build()?;
273        sign_sponsored_transaction(&fee_payer_txn, sender, secondary_signers, fee_payer)
274    }
275}
276
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 movement_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) -> MovementResult<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) -> MovementResult<AccountAuthenticator> {
368    match scheme {
369        crate::crypto::ED25519_SCHEME => Ok(AccountAuthenticator::ed25519(public_key, signature)),
370        crate::crypto::MULTI_ED25519_SCHEME => Ok(AccountAuthenticator::MultiEd25519 {
371            public_key,
372            signature,
373        }),
374        crate::crypto::SINGLE_KEY_SCHEME => {
375            Ok(AccountAuthenticator::single_key(public_key, signature))
376        }
377        crate::crypto::MULTI_KEY_SCHEME => {
378            Ok(AccountAuthenticator::multi_key(public_key, signature))
379        }
380        _ => Err(MovementError::InvalidSignature(format!(
381            "unknown signature scheme: {scheme}"
382        ))),
383    }
384}
385
386/// 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) -> MovementResult<()> {
422        let signing_message = self.fee_payer_txn.signing_message()?;
423        let signature = sender.sign(&signing_message)?;
424        let public_key = sender.public_key_bytes();
425        self.sender_auth = Some(make_account_authenticator(
426            sender.signature_scheme(),
427            public_key,
428            signature,
429        )?);
430        Ok(())
431    }
432
433    /// 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>(
440        &mut self,
441        index: usize,
442        signer: &A,
443    ) -> MovementResult<()> {
444        if index >= self.secondary_auths.len() {
445            return Err(MovementError::transaction(format!(
446                "secondary signer index {} out of bounds (max {})",
447                index,
448                self.secondary_auths.len()
449            )));
450        }
451
452        let signing_message = self.fee_payer_txn.signing_message()?;
453        let signature = signer.sign(&signing_message)?;
454        let public_key = signer.public_key_bytes();
455        self.secondary_auths[index] = Some(make_account_authenticator(
456            signer.signature_scheme(),
457            public_key,
458            signature,
459        )?);
460        Ok(())
461    }
462
463    /// Signs as the fee payer.
464    ///
465    /// # Errors
466    ///
467    /// Returns an error if generating the signing message fails, if signing fails,
468    /// or if the signature scheme is not recognized.
469    pub fn sign_as_fee_payer<A: Account>(&mut self, fee_payer: &A) -> MovementResult<()> {
470        let signing_message = self.fee_payer_txn.signing_message()?;
471        let signature = fee_payer.sign(&signing_message)?;
472        let public_key = fee_payer.public_key_bytes();
473        self.fee_payer_auth = Some(make_account_authenticator(
474            fee_payer.signature_scheme(),
475            public_key,
476            signature,
477        )?);
478        Ok(())
479    }
480
481    /// Checks if all required signatures have been collected.
482    pub fn is_complete(&self) -> bool {
483        self.sender_auth.is_some()
484            && self.fee_payer_auth.is_some()
485            && self.secondary_auths.iter().all(Option::is_some)
486    }
487
488    /// Finalizes the transaction if all signatures are present.
489    ///
490    /// Returns an error if any signatures are missing.
491    ///
492    /// # Errors
493    ///
494    /// Returns an error if the sender signature, fee payer signature, or any secondary signer signature is missing.
495    pub fn finalize(self) -> MovementResult<SignedTransaction> {
496        let sender_auth = self
497            .sender_auth
498            .ok_or_else(|| MovementError::transaction("missing sender signature"))?;
499        let fee_payer_auth = self
500            .fee_payer_auth
501            .ok_or_else(|| MovementError::transaction("missing fee payer signature"))?;
502
503        let secondary_auths: Result<Vec<_>, _> = self
504            .secondary_auths
505            .into_iter()
506            .enumerate()
507            .map(|(i, auth)| {
508                auth.ok_or_else(|| {
509                    MovementError::transaction(format!("missing secondary signer {i} signature"))
510                })
511            })
512            .collect();
513        let secondary_auths = secondary_auths?;
514
515        let authenticator = TransactionAuthenticator::fee_payer(
516            sender_auth,
517            self.fee_payer_txn.secondary_signer_addresses.clone(),
518            secondary_auths,
519            self.fee_payer_txn.fee_payer_address,
520            fee_payer_auth,
521        );
522
523        Ok(SignedTransaction::new(
524            self.fee_payer_txn.raw_txn,
525            authenticator,
526        ))
527    }
528}
529
530/// Extension trait that adds sponsorship capabilities to accounts.
531///
532/// This trait provides convenient methods for an account to sponsor
533/// transactions for other users.
534pub trait Sponsor: Account + Sized {
535    /// Sponsors a transaction for another account.
536    ///
537    /// Creates and signs a sponsored transaction where `self` pays the gas fees.
538    ///
539    /// # Arguments
540    ///
541    /// * `sender` - The account initiating the transaction
542    /// * `sender_sequence_number` - The sender's current sequence number
543    /// * `payload` - The transaction payload
544    /// * `chain_id` - The target chain ID
545    ///
546    /// # Example
547    ///
548    /// ```rust,ignore
549    /// use movement_sdk::transaction::Sponsor;
550    ///
551    /// let signed_txn = sponsor_account.sponsor(
552    ///     &user_account,
553    ///     0,
554    ///     payload,
555    ///     ChainId::testnet(),
556    /// )?;
557    /// ```
558    ///
559    /// # Errors
560    ///
561    /// Returns an error if building the transaction fails or if any signer fails to sign.
562    fn sponsor<S: Account>(
563        &self,
564        sender: &S,
565        sender_sequence_number: u64,
566        payload: TransactionPayload,
567        chain_id: ChainId,
568    ) -> MovementResult<SignedTransaction> {
569        SponsoredTransactionBuilder::new()
570            .sender(sender.address())
571            .sequence_number(sender_sequence_number)
572            .fee_payer(self.address())
573            .payload(payload)
574            .chain_id(chain_id)
575            .build_and_sign(sender, &[], self)
576    }
577
578    /// Sponsors a transaction with custom gas settings.
579    ///
580    /// # Errors
581    ///
582    /// Returns an error if building the transaction fails or if any signer fails to sign.
583    fn sponsor_with_gas<S: Account>(
584        &self,
585        sender: &S,
586        sender_sequence_number: u64,
587        payload: TransactionPayload,
588        chain_id: ChainId,
589        max_gas_amount: u64,
590        gas_unit_price: u64,
591    ) -> MovementResult<SignedTransaction> {
592        SponsoredTransactionBuilder::new()
593            .sender(sender.address())
594            .sequence_number(sender_sequence_number)
595            .fee_payer(self.address())
596            .payload(payload)
597            .chain_id(chain_id)
598            .max_gas_amount(max_gas_amount)
599            .gas_unit_price(gas_unit_price)
600            .build_and_sign(sender, &[], self)
601    }
602}
603
604// Implement Sponsor for all Account types that are Sized
605impl<A: Account + Sized> Sponsor for A {}
606
607/// Creates a simple sponsored transaction with minimal configuration.
608///
609/// This is a convenience function for the common case of sponsoring a
610/// simple transaction without secondary signers.
611///
612/// # Example
613///
614/// ```rust,ignore
615/// use movement_sdk::transaction::sponsor_transaction;
616///
617/// let signed = sponsor_transaction(
618///     &sender_account,
619///     sender_sequence_number,
620///     &sponsor_account,
621///     payload,
622///     ChainId::testnet(),
623/// )?;
624/// ```
625///
626/// # Errors
627///
628/// Returns an error if building the transaction fails or if any signer fails to sign.
629pub fn sponsor_transaction<S, F>(
630    sender: &S,
631    sender_sequence_number: u64,
632    fee_payer: &F,
633    payload: TransactionPayload,
634    chain_id: ChainId,
635) -> MovementResult<SignedTransaction>
636where
637    S: Account,
638    F: Account,
639{
640    SponsoredTransactionBuilder::new()
641        .sender(sender.address())
642        .sequence_number(sender_sequence_number)
643        .fee_payer(fee_payer.address())
644        .payload(payload)
645        .chain_id(chain_id)
646        .build_and_sign(sender, &[], fee_payer)
647}
648
649#[cfg(test)]
650mod tests {
651    use super::*;
652    use crate::transaction::payload::EntryFunction;
653
654    #[test]
655    fn test_builder_missing_sender() {
656        let recipient = AccountAddress::from_hex("0x123").unwrap();
657        let result = SponsoredTransactionBuilder::new()
658            .sequence_number(0)
659            .fee_payer(AccountAddress::ONE)
660            .payload(TransactionPayload::EntryFunction(
661                EntryFunction::apt_transfer(recipient, 1000).unwrap(),
662            ))
663            .chain_id(ChainId::testnet())
664            .build();
665
666        assert!(result.is_err());
667        assert!(result.unwrap_err().to_string().contains("sender"));
668    }
669
670    #[test]
671    fn test_builder_missing_fee_payer() {
672        let recipient = AccountAddress::from_hex("0x123").unwrap();
673        let result = SponsoredTransactionBuilder::new()
674            .sender(AccountAddress::ONE)
675            .sequence_number(0)
676            .payload(TransactionPayload::EntryFunction(
677                EntryFunction::apt_transfer(recipient, 1000).unwrap(),
678            ))
679            .chain_id(ChainId::testnet())
680            .build();
681
682        assert!(result.is_err());
683        assert!(result.unwrap_err().to_string().contains("fee_payer"));
684    }
685
686    #[test]
687    fn test_builder_complete() {
688        let recipient = AccountAddress::from_hex("0x123").unwrap();
689        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
690
691        let fee_payer_txn = SponsoredTransactionBuilder::new()
692            .sender(AccountAddress::ONE)
693            .sequence_number(5)
694            .fee_payer(AccountAddress::from_hex("0x3").unwrap())
695            .payload(payload.into())
696            .chain_id(ChainId::testnet())
697            .max_gas_amount(100_000)
698            .gas_unit_price(150)
699            .build()
700            .unwrap();
701
702        assert_eq!(fee_payer_txn.raw_txn.sender, AccountAddress::ONE);
703        assert_eq!(fee_payer_txn.raw_txn.sequence_number, 5);
704        assert_eq!(fee_payer_txn.raw_txn.max_gas_amount, 100_000);
705        assert_eq!(fee_payer_txn.raw_txn.gas_unit_price, 150);
706        assert_eq!(
707            fee_payer_txn.fee_payer_address,
708            AccountAddress::from_hex("0x3").unwrap()
709        );
710    }
711
712    #[test]
713    fn test_partially_signed_completion_check() {
714        let recipient = AccountAddress::from_hex("0x123").unwrap();
715        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
716
717        let fee_payer_txn = SponsoredTransactionBuilder::new()
718            .sender(AccountAddress::ONE)
719            .sequence_number(0)
720            .fee_payer(AccountAddress::from_hex("0x3").unwrap())
721            .payload(payload.into())
722            .chain_id(ChainId::testnet())
723            .build()
724            .unwrap();
725
726        let partially_signed = PartiallySigned::new(fee_payer_txn);
727        assert!(!partially_signed.is_complete());
728    }
729
730    #[test]
731    fn test_partially_signed_finalize_incomplete() {
732        let recipient = AccountAddress::from_hex("0x123").unwrap();
733        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
734
735        let fee_payer_txn = SponsoredTransactionBuilder::new()
736            .sender(AccountAddress::ONE)
737            .sequence_number(0)
738            .fee_payer(AccountAddress::from_hex("0x3").unwrap())
739            .payload(payload.into())
740            .chain_id(ChainId::testnet())
741            .build()
742            .unwrap();
743
744        let partially_signed = PartiallySigned::new(fee_payer_txn);
745        let result = partially_signed.finalize();
746
747        assert!(result.is_err());
748        assert!(result.unwrap_err().to_string().contains("missing"));
749    }
750
751    #[cfg(feature = "ed25519")]
752    #[test]
753    fn test_full_sponsored_transaction() {
754        use crate::account::Ed25519Account;
755
756        let sender = Ed25519Account::generate();
757        let fee_payer = Ed25519Account::generate();
758        let recipient = AccountAddress::from_hex("0x123").unwrap();
759
760        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
761
762        let signed_txn = SponsoredTransactionBuilder::new()
763            .sender(sender.address())
764            .sequence_number(0)
765            .fee_payer(fee_payer.address())
766            .payload(payload.into())
767            .chain_id(ChainId::testnet())
768            .build_and_sign(&sender, &[], &fee_payer)
769            .unwrap();
770
771        // Verify the transaction structure
772        assert_eq!(signed_txn.raw_txn.sender, sender.address());
773        assert!(matches!(
774            signed_txn.authenticator,
775            TransactionAuthenticator::FeePayer { .. }
776        ));
777    }
778
779    #[cfg(feature = "ed25519")]
780    #[test]
781    fn test_sponsor_trait() {
782        use crate::account::Ed25519Account;
783
784        let sender = Ed25519Account::generate();
785        let sponsor = Ed25519Account::generate();
786        let recipient = AccountAddress::from_hex("0x123").unwrap();
787
788        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
789
790        // Use the Sponsor trait
791        let signed_txn = sponsor
792            .sponsor(&sender, 0, payload.into(), ChainId::testnet())
793            .unwrap();
794
795        assert_eq!(signed_txn.raw_txn.sender, sender.address());
796    }
797
798    #[cfg(feature = "ed25519")]
799    #[test]
800    fn test_sponsor_transaction_fn() {
801        use crate::account::Ed25519Account;
802
803        let sender = Ed25519Account::generate();
804        let fee_payer = Ed25519Account::generate();
805        let recipient = AccountAddress::from_hex("0x123").unwrap();
806
807        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
808
809        // Use the convenience function
810        let signed_txn =
811            sponsor_transaction(&sender, 0, &fee_payer, payload.into(), ChainId::testnet())
812                .unwrap();
813
814        assert_eq!(signed_txn.raw_txn.sender, sender.address());
815    }
816
817    #[cfg(feature = "ed25519")]
818    #[test]
819    fn test_partially_signed_flow() {
820        use crate::account::Ed25519Account;
821
822        let sender = Ed25519Account::generate();
823        let fee_payer = Ed25519Account::generate();
824        let recipient = AccountAddress::from_hex("0x123").unwrap();
825
826        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
827
828        // Build the transaction
829        let fee_payer_txn = SponsoredTransactionBuilder::new()
830            .sender(sender.address())
831            .sequence_number(0)
832            .fee_payer(fee_payer.address())
833            .payload(payload.into())
834            .chain_id(ChainId::testnet())
835            .build()
836            .unwrap();
837
838        // Create partially signed and collect signatures
839        let mut partially_signed = PartiallySigned::new(fee_payer_txn);
840
841        // Not complete yet
842        assert!(!partially_signed.is_complete());
843
844        // Sign as sender
845        partially_signed.sign_as_sender(&sender).unwrap();
846        assert!(!partially_signed.is_complete());
847
848        // Sign as fee payer
849        partially_signed.sign_as_fee_payer(&fee_payer).unwrap();
850        assert!(partially_signed.is_complete());
851
852        // Finalize
853        let signed_txn = partially_signed.finalize().unwrap();
854        assert_eq!(signed_txn.raw_txn.sender, sender.address());
855    }
856
857    #[test]
858    fn test_builder_missing_sequence_number() {
859        let recipient = AccountAddress::from_hex("0x123").unwrap();
860        let result = SponsoredTransactionBuilder::new()
861            .sender(AccountAddress::ONE)
862            .fee_payer(AccountAddress::from_hex("0x3").unwrap())
863            .payload(TransactionPayload::EntryFunction(
864                EntryFunction::apt_transfer(recipient, 1000).unwrap(),
865            ))
866            .chain_id(ChainId::testnet())
867            .build();
868
869        assert!(result.is_err());
870        assert!(result.unwrap_err().to_string().contains("sequence_number"));
871    }
872
873    #[test]
874    fn test_builder_missing_payload() {
875        let result = SponsoredTransactionBuilder::new()
876            .sender(AccountAddress::ONE)
877            .sequence_number(0)
878            .fee_payer(AccountAddress::from_hex("0x3").unwrap())
879            .chain_id(ChainId::testnet())
880            .build();
881
882        assert!(result.is_err());
883        assert!(result.unwrap_err().to_string().contains("payload"));
884    }
885
886    #[test]
887    fn test_builder_missing_chain_id() {
888        let recipient = AccountAddress::from_hex("0x123").unwrap();
889        let result = SponsoredTransactionBuilder::new()
890            .sender(AccountAddress::ONE)
891            .sequence_number(0)
892            .fee_payer(AccountAddress::from_hex("0x3").unwrap())
893            .payload(TransactionPayload::EntryFunction(
894                EntryFunction::apt_transfer(recipient, 1000).unwrap(),
895            ))
896            .build();
897
898        assert!(result.is_err());
899        assert!(result.unwrap_err().to_string().contains("chain_id"));
900    }
901
902    #[test]
903    fn test_builder_secondary_signers() {
904        let recipient = AccountAddress::from_hex("0x123").unwrap();
905        let secondary1 = AccountAddress::from_hex("0x4").unwrap();
906        let secondary2 = AccountAddress::from_hex("0x5").unwrap();
907
908        let fee_payer_txn = SponsoredTransactionBuilder::new()
909            .sender(AccountAddress::ONE)
910            .sequence_number(0)
911            .fee_payer(AccountAddress::from_hex("0x3").unwrap())
912            .secondary_signer(secondary1)
913            .secondary_signers(&[secondary2])
914            .payload(TransactionPayload::EntryFunction(
915                EntryFunction::apt_transfer(recipient, 1000).unwrap(),
916            ))
917            .chain_id(ChainId::testnet())
918            .build()
919            .unwrap();
920
921        assert_eq!(fee_payer_txn.secondary_signer_addresses.len(), 2);
922        assert_eq!(fee_payer_txn.secondary_signer_addresses[0], secondary1);
923        assert_eq!(fee_payer_txn.secondary_signer_addresses[1], secondary2);
924    }
925
926    #[test]
927    fn test_builder_expiration_timestamp() {
928        let recipient = AccountAddress::from_hex("0x123").unwrap();
929        let expiration = 1_234_567_890_u64;
930
931        let fee_payer_txn = SponsoredTransactionBuilder::new()
932            .sender(AccountAddress::ONE)
933            .sequence_number(0)
934            .fee_payer(AccountAddress::from_hex("0x3").unwrap())
935            .payload(TransactionPayload::EntryFunction(
936                EntryFunction::apt_transfer(recipient, 1000).unwrap(),
937            ))
938            .chain_id(ChainId::testnet())
939            .expiration_timestamp_secs(expiration)
940            .build()
941            .unwrap();
942
943        assert_eq!(fee_payer_txn.raw_txn.expiration_timestamp_secs, expiration);
944    }
945
946    #[test]
947    fn test_builder_expiration_from_now() {
948        let recipient = AccountAddress::from_hex("0x123").unwrap();
949
950        let fee_payer_txn = SponsoredTransactionBuilder::new()
951            .sender(AccountAddress::ONE)
952            .sequence_number(0)
953            .fee_payer(AccountAddress::from_hex("0x3").unwrap())
954            .payload(TransactionPayload::EntryFunction(
955                EntryFunction::apt_transfer(recipient, 1000).unwrap(),
956            ))
957            .chain_id(ChainId::testnet())
958            .expiration_from_now(60)
959            .build()
960            .unwrap();
961
962        // Expiration should be roughly now + 60 seconds
963        let now = SystemTime::now()
964            .duration_since(UNIX_EPOCH)
965            .unwrap()
966            .as_secs();
967        assert!(fee_payer_txn.raw_txn.expiration_timestamp_secs >= now);
968        assert!(fee_payer_txn.raw_txn.expiration_timestamp_secs <= now + 65);
969    }
970
971    #[test]
972    fn test_builder_default() {
973        let builder = SponsoredTransactionBuilder::default();
974        assert!(builder.sender_address.is_none());
975        assert!(builder.sequence_number.is_none());
976        assert!(builder.fee_payer_address.is_none());
977        assert!(builder.payload.is_none());
978        assert!(builder.chain_id.is_none());
979        // Default values are set via SponsoredTransactionBuilder::new()
980        // not via Default::default() which initializes to Rust defaults (0)
981        // Let's just check the builder is properly created
982    }
983
984    #[test]
985    fn test_builder_new_defaults() {
986        let builder = SponsoredTransactionBuilder::new();
987        assert!(builder.sender_address.is_none());
988        assert!(builder.sequence_number.is_none());
989        assert!(builder.fee_payer_address.is_none());
990        assert!(builder.payload.is_none());
991        assert!(builder.chain_id.is_none());
992        assert_eq!(builder.max_gas_amount, DEFAULT_MAX_GAS_AMOUNT);
993        assert_eq!(builder.gas_unit_price, DEFAULT_GAS_UNIT_PRICE);
994    }
995
996    #[test]
997    fn test_builder_debug() {
998        let builder = SponsoredTransactionBuilder::new().sender(AccountAddress::ONE);
999        let debug = format!("{builder:?}");
1000        assert!(debug.contains("SponsoredTransactionBuilder"));
1001    }
1002
1003    #[cfg(feature = "ed25519")]
1004    #[test]
1005    fn test_partially_signed_with_secondary_signers() {
1006        use crate::account::Ed25519Account;
1007
1008        let sender = Ed25519Account::generate();
1009        let secondary = Ed25519Account::generate();
1010        let fee_payer = Ed25519Account::generate();
1011        let recipient = AccountAddress::from_hex("0x123").unwrap();
1012
1013        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
1014
1015        // Build with secondary signer
1016        let fee_payer_txn = SponsoredTransactionBuilder::new()
1017            .sender(sender.address())
1018            .sequence_number(0)
1019            .secondary_signer(secondary.address())
1020            .fee_payer(fee_payer.address())
1021            .payload(payload.into())
1022            .chain_id(ChainId::testnet())
1023            .build()
1024            .unwrap();
1025
1026        let mut partially_signed = PartiallySigned::new(fee_payer_txn);
1027
1028        // Need all three signatures
1029        assert!(!partially_signed.is_complete());
1030
1031        partially_signed.sign_as_sender(&sender).unwrap();
1032        assert!(!partially_signed.is_complete());
1033
1034        partially_signed.sign_as_secondary(0, &secondary).unwrap();
1035        assert!(!partially_signed.is_complete());
1036
1037        partially_signed.sign_as_fee_payer(&fee_payer).unwrap();
1038        assert!(partially_signed.is_complete());
1039
1040        let signed = partially_signed.finalize().unwrap();
1041        assert_eq!(signed.raw_txn.sender, sender.address());
1042    }
1043
1044    #[cfg(feature = "ed25519")]
1045    #[test]
1046    fn test_partially_signed_secondary_index_out_of_bounds() {
1047        use crate::account::Ed25519Account;
1048
1049        let sender = Ed25519Account::generate();
1050        let fee_payer = Ed25519Account::generate();
1051        let secondary = Ed25519Account::generate();
1052        let recipient = AccountAddress::from_hex("0x123").unwrap();
1053
1054        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
1055
1056        // No secondary signers in the transaction
1057        let fee_payer_txn = SponsoredTransactionBuilder::new()
1058            .sender(sender.address())
1059            .sequence_number(0)
1060            .fee_payer(fee_payer.address())
1061            .payload(payload.into())
1062            .chain_id(ChainId::testnet())
1063            .build()
1064            .unwrap();
1065
1066        let mut partially_signed = PartiallySigned::new(fee_payer_txn);
1067
1068        // Try to sign as secondary at index 0 (out of bounds because no secondary signers)
1069        let result = partially_signed.sign_as_secondary(0, &secondary);
1070        assert!(result.is_err());
1071        assert!(result.unwrap_err().to_string().contains("out of bounds"));
1072    }
1073
1074    #[cfg(feature = "ed25519")]
1075    #[test]
1076    fn test_partially_signed_finalize_missing_secondary() {
1077        use crate::account::Ed25519Account;
1078
1079        let sender = Ed25519Account::generate();
1080        let fee_payer = Ed25519Account::generate();
1081        let recipient = AccountAddress::from_hex("0x123").unwrap();
1082
1083        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
1084
1085        // Build with secondary signer but don't sign it
1086        let fee_payer_txn = SponsoredTransactionBuilder::new()
1087            .sender(sender.address())
1088            .sequence_number(0)
1089            .secondary_signer(AccountAddress::from_hex("0x5").unwrap())
1090            .fee_payer(fee_payer.address())
1091            .payload(payload.into())
1092            .chain_id(ChainId::testnet())
1093            .build()
1094            .unwrap();
1095
1096        let mut partially_signed = PartiallySigned::new(fee_payer_txn);
1097
1098        // Sign sender and fee payer but not secondary
1099        partially_signed.sign_as_sender(&sender).unwrap();
1100        partially_signed.sign_as_fee_payer(&fee_payer).unwrap();
1101
1102        // Should fail because secondary is missing
1103        let result = partially_signed.finalize();
1104        assert!(result.is_err());
1105        assert!(result.unwrap_err().to_string().contains("secondary signer"));
1106    }
1107
1108    #[cfg(feature = "ed25519")]
1109    #[test]
1110    fn test_sponsor_with_gas() {
1111        use crate::account::Ed25519Account;
1112
1113        let sender = Ed25519Account::generate();
1114        let sponsor = Ed25519Account::generate();
1115        let recipient = AccountAddress::from_hex("0x123").unwrap();
1116
1117        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
1118
1119        let signed_txn = sponsor
1120            .sponsor_with_gas(&sender, 0, payload.into(), ChainId::testnet(), 50000, 200)
1121            .unwrap();
1122
1123        assert_eq!(signed_txn.raw_txn.sender, sender.address());
1124        assert_eq!(signed_txn.raw_txn.max_gas_amount, 50000);
1125        assert_eq!(signed_txn.raw_txn.gas_unit_price, 200);
1126    }
1127
1128    #[test]
1129    fn test_partially_signed_debug() {
1130        let recipient = AccountAddress::from_hex("0x123").unwrap();
1131        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
1132
1133        let fee_payer_txn = SponsoredTransactionBuilder::new()
1134            .sender(AccountAddress::ONE)
1135            .sequence_number(0)
1136            .fee_payer(AccountAddress::from_hex("0x3").unwrap())
1137            .payload(payload.into())
1138            .chain_id(ChainId::testnet())
1139            .build()
1140            .unwrap();
1141
1142        let partially_signed = PartiallySigned::new(fee_payer_txn);
1143        let debug = format!("{partially_signed:?}");
1144        assert!(debug.contains("PartiallySigned"));
1145    }
1146}