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::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/// A builder for constructing sponsored (fee payer) transactions.
52///
53/// This provides a fluent API for creating transactions where a fee payer
54/// account pays the gas fees on behalf of the sender.
55///
56/// # Example
57///
58/// ```rust,ignore
59/// use aptos_sdk::transaction::{SponsoredTransactionBuilder, EntryFunction};
60///
61/// // Build the fee payer transaction structure
62/// let fee_payer_txn = SponsoredTransactionBuilder::new()
63///     .sender(user_account.address())
64///     .sequence_number(0)
65///     .fee_payer(sponsor_account.address())
66///     .payload(payload)
67///     .chain_id(ChainId::testnet())
68///     .build()?;
69///
70/// // Then sign it
71/// let signed = sign_sponsored_transaction(
72///     &fee_payer_txn,
73///     &user_account,
74///     &[],  // no secondary signers
75///     &sponsor_account,
76/// )?;
77/// ```
78#[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    /// Creates a new sponsored transaction builder with default values.
93    #[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    /// Sets the sender address.
109    #[must_use]
110    pub fn sender(mut self, address: AccountAddress) -> Self {
111        self.sender_address = Some(address);
112        self
113    }
114
115    /// Sets the sender's sequence number.
116    #[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    /// Adds a secondary signer address to the transaction.
123    ///
124    /// Secondary signers are additional accounts that must sign the transaction.
125    /// This is useful for multi-party transactions.
126    #[must_use]
127    pub fn secondary_signer(mut self, address: AccountAddress) -> Self {
128        self.secondary_addresses.push(address);
129        self
130    }
131
132    /// Adds multiple secondary signer addresses to the transaction.
133    #[must_use]
134    pub fn secondary_signers(mut self, addresses: &[AccountAddress]) -> Self {
135        self.secondary_addresses.extend(addresses);
136        self
137    }
138
139    /// Sets the fee payer address.
140    #[must_use]
141    pub fn fee_payer(mut self, address: AccountAddress) -> Self {
142        self.fee_payer_address = Some(address);
143        self
144    }
145
146    /// Sets the transaction payload.
147    #[must_use]
148    pub fn payload(mut self, payload: TransactionPayload) -> Self {
149        self.payload = Some(payload);
150        self
151    }
152
153    /// Sets the maximum gas amount.
154    #[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    /// Sets the gas unit price in octas.
161    #[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    /// Sets the expiration timestamp in seconds since Unix epoch.
168    #[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    /// Sets the expiration time relative to now.
175    ///
176    /// Uses saturating arithmetic to handle edge cases like system time going backwards.
177    #[must_use]
178    pub fn expiration_from_now(mut self, seconds: u64) -> Self {
179        let now = SystemTime::now()
180            .duration_since(UNIX_EPOCH)
181            .unwrap_or_default()
182            .as_secs();
183        self.expiration_timestamp_secs = Some(now.saturating_add(seconds));
184        self
185    }
186
187    /// Sets the chain ID.
188    #[must_use]
189    pub fn chain_id(mut self, chain_id: ChainId) -> Self {
190        self.chain_id = Some(chain_id);
191        self
192    }
193
194    /// Builds the raw fee payer transaction (unsigned).
195    ///
196    /// This returns a `FeePayerRawTransaction` that can be signed later
197    /// by the sender, secondary signers, and fee payer.
198    ///
199    /// # Errors
200    ///
201    /// Returns an error if `sender`, `sequence_number`, `payload`, `chain_id`, or `fee_payer` is not set.
202    pub fn build(self) -> AptosResult<FeePayerRawTransaction> {
203        let sender = self
204            .sender_address
205            .ok_or_else(|| AptosError::transaction("sender is required"))?;
206        let sequence_number = self
207            .sequence_number
208            .ok_or_else(|| AptosError::transaction("sequence_number is required"))?;
209        let payload = self
210            .payload
211            .ok_or_else(|| AptosError::transaction("payload is required"))?;
212        let chain_id = self
213            .chain_id
214            .ok_or_else(|| AptosError::transaction("chain_id is required"))?;
215        let fee_payer_address = self
216            .fee_payer_address
217            .ok_or_else(|| AptosError::transaction("fee_payer is required"))?;
218
219        // SECURITY: Apply expiration offset only once (was previously doubled)
220        let expiration_timestamp_secs = self.expiration_timestamp_secs.unwrap_or_else(|| {
221            SystemTime::now()
222                .duration_since(UNIX_EPOCH)
223                .unwrap_or_default()
224                .as_secs()
225                .saturating_add(DEFAULT_EXPIRATION_SECONDS)
226        });
227
228        let raw_txn = RawTransaction::new(
229            sender,
230            sequence_number,
231            payload,
232            self.max_gas_amount,
233            self.gas_unit_price,
234            expiration_timestamp_secs,
235            chain_id,
236        );
237
238        Ok(FeePayerRawTransaction {
239            raw_txn,
240            secondary_signer_addresses: self.secondary_addresses,
241            fee_payer_address,
242        })
243    }
244
245    /// Builds and signs the transaction with all provided accounts.
246    ///
247    /// This is a convenience method that builds the transaction and signs it
248    /// in one step.
249    ///
250    /// # Example
251    ///
252    /// ```rust,ignore
253    /// let signed = SponsoredTransactionBuilder::new()
254    ///     .sender(user.address())
255    ///     .sequence_number(0)
256    ///     .fee_payer(sponsor.address())
257    ///     .payload(payload)
258    ///     .chain_id(ChainId::testnet())
259    ///     .build_and_sign(&user, &[], &sponsor)?;
260    /// ```
261    ///
262    /// # Errors
263    ///
264    /// Returns an error if building the transaction fails or if any signer fails to sign.
265    pub fn build_and_sign<S, F>(
266        self,
267        sender: &S,
268        secondary_signers: &[&dyn Account],
269        fee_payer: &F,
270    ) -> AptosResult<SignedTransaction>
271    where
272        S: Account,
273        F: Account,
274    {
275        let fee_payer_txn = self.build()?;
276        sign_sponsored_transaction(&fee_payer_txn, sender, secondary_signers, fee_payer)
277    }
278}
279
280/// Signs a sponsored (fee payer) transaction with all required signatures.
281///
282/// # Arguments
283///
284/// * `fee_payer_txn` - The unsigned fee payer transaction
285/// * `sender` - The sender account
286/// * `secondary_signers` - Additional signers (if any)
287/// * `fee_payer` - The account paying gas fees
288///
289/// # Example
290///
291/// ```rust,ignore
292/// use aptos_sdk::transaction::sign_sponsored_transaction;
293///
294/// let signed_txn = sign_sponsored_transaction(
295///     &fee_payer_txn,
296///     &sender_account,
297///     &[],  // No secondary signers
298///     &fee_payer_account,
299/// )?;
300/// ```
301///
302/// # Errors
303///
304/// Returns an error if generating the signing message fails or if any signer fails to sign.
305pub fn sign_sponsored_transaction<S, F>(
306    fee_payer_txn: &FeePayerRawTransaction,
307    sender: &S,
308    secondary_signers: &[&dyn Account],
309    fee_payer: &F,
310) -> AptosResult<SignedTransaction>
311where
312    S: Account,
313    F: Account,
314{
315    let signing_message = fee_payer_txn.signing_message()?;
316
317    // Sign with sender
318    let sender_signature = sender.sign(&signing_message)?;
319    let sender_public_key = sender.public_key_bytes();
320    let sender_auth = make_account_authenticator(
321        sender.signature_scheme(),
322        sender_public_key,
323        sender_signature,
324    )?;
325
326    // Sign with secondary signers
327    let mut secondary_auths = Vec::with_capacity(secondary_signers.len());
328    for signer in secondary_signers {
329        let signature = signer.sign(&signing_message)?;
330        let public_key = signer.public_key_bytes();
331        secondary_auths.push(make_account_authenticator(
332            signer.signature_scheme(),
333            public_key,
334            signature,
335        )?);
336    }
337
338    // Sign with fee payer
339    let fee_payer_signature = fee_payer.sign(&signing_message)?;
340    let fee_payer_public_key = fee_payer.public_key_bytes();
341    let fee_payer_auth = make_account_authenticator(
342        fee_payer.signature_scheme(),
343        fee_payer_public_key,
344        fee_payer_signature,
345    )?;
346
347    let authenticator = TransactionAuthenticator::fee_payer(
348        sender_auth,
349        fee_payer_txn.secondary_signer_addresses.clone(),
350        secondary_auths,
351        fee_payer_txn.fee_payer_address,
352        fee_payer_auth,
353    );
354
355    Ok(SignedTransaction::new(
356        fee_payer_txn.raw_txn.clone(),
357        authenticator,
358    ))
359}
360
361/// Creates an account authenticator from signature components.
362///
363/// # Errors
364///
365/// Returns an error if the signature scheme is not recognized.
366fn make_account_authenticator(
367    scheme: u8,
368    public_key: Vec<u8>,
369    signature: Vec<u8>,
370) -> AptosResult<AccountAuthenticator> {
371    match scheme {
372        crate::crypto::ED25519_SCHEME => Ok(AccountAuthenticator::ed25519(public_key, signature)),
373        crate::crypto::MULTI_ED25519_SCHEME => Ok(AccountAuthenticator::MultiEd25519 {
374            public_key,
375            signature,
376        }),
377        crate::crypto::SINGLE_KEY_SCHEME => {
378            let _ = AnyPublicKey::from_bcs_bytes(&public_key)?;
379            let _ = AnySignature::from_bcs_bytes(&signature)?;
380            Ok(AccountAuthenticator::single_key(public_key, signature))
381        }
382        crate::crypto::MULTI_KEY_SCHEME => {
383            let _ = MultiKeyPublicKey::from_bytes(&public_key)?;
384            let _ = MultiKeySignature::from_bytes(&signature)?;
385            Ok(AccountAuthenticator::multi_key(public_key, signature))
386        }
387        _ => Err(AptosError::InvalidSignature(format!(
388            "unknown signature scheme: {scheme}"
389        ))),
390    }
391}
392
393/// A partially signed sponsored transaction.
394///
395/// This represents a sponsored transaction that has been signed by some but
396/// not all required signers. It can be passed between parties for signature
397/// collection.
398#[derive(Debug, Clone)]
399pub struct PartiallySigned {
400    /// The underlying fee payer transaction.
401    pub fee_payer_txn: FeePayerRawTransaction,
402    /// Sender's signature (if signed).
403    pub sender_auth: Option<AccountAuthenticator>,
404    /// Secondary signer signatures.
405    pub secondary_auths: Vec<Option<AccountAuthenticator>>,
406    /// Fee payer's signature (if signed).
407    pub fee_payer_auth: Option<AccountAuthenticator>,
408}
409
410impl PartiallySigned {
411    /// Creates a new partially signed transaction.
412    pub fn new(fee_payer_txn: FeePayerRawTransaction) -> Self {
413        let num_secondary = fee_payer_txn.secondary_signer_addresses.len();
414        Self {
415            fee_payer_txn,
416            sender_auth: None,
417            secondary_auths: vec![None; num_secondary],
418            fee_payer_auth: None,
419        }
420    }
421
422    /// Signs as the sender.
423    ///
424    /// # Errors
425    ///
426    /// Returns an error if generating the signing message fails, if signing fails,
427    /// or if the signature scheme is not recognized.
428    pub fn sign_as_sender<A: Account>(&mut self, sender: &A) -> AptosResult<()> {
429        let signing_message = self.fee_payer_txn.signing_message()?;
430        let signature = sender.sign(&signing_message)?;
431        let public_key = sender.public_key_bytes();
432        self.sender_auth = Some(make_account_authenticator(
433            sender.signature_scheme(),
434            public_key,
435            signature,
436        )?);
437        Ok(())
438    }
439
440    /// Signs as a secondary signer at the given index.
441    ///
442    /// # Errors
443    ///
444    /// Returns an error if the index is out of bounds, if generating the signing message fails,
445    /// if signing fails, or if the signature scheme is not recognized.
446    pub fn sign_as_secondary<A: Account>(&mut self, index: usize, signer: &A) -> AptosResult<()> {
447        if index >= self.secondary_auths.len() {
448            return Err(AptosError::transaction(format!(
449                "secondary signer index {} out of bounds (max {})",
450                index,
451                self.secondary_auths.len()
452            )));
453        }
454
455        let signing_message = self.fee_payer_txn.signing_message()?;
456        let signature = signer.sign(&signing_message)?;
457        let public_key = signer.public_key_bytes();
458        self.secondary_auths[index] = Some(make_account_authenticator(
459            signer.signature_scheme(),
460            public_key,
461            signature,
462        )?);
463        Ok(())
464    }
465
466    /// Signs as the fee payer.
467    ///
468    /// # Errors
469    ///
470    /// Returns an error if generating the signing message fails, if signing fails,
471    /// or if the signature scheme is not recognized.
472    pub fn sign_as_fee_payer<A: Account>(&mut self, fee_payer: &A) -> AptosResult<()> {
473        let signing_message = self.fee_payer_txn.signing_message()?;
474        let signature = fee_payer.sign(&signing_message)?;
475        let public_key = fee_payer.public_key_bytes();
476        self.fee_payer_auth = Some(make_account_authenticator(
477            fee_payer.signature_scheme(),
478            public_key,
479            signature,
480        )?);
481        Ok(())
482    }
483
484    /// Checks if all required signatures have been collected.
485    pub fn is_complete(&self) -> bool {
486        self.sender_auth.is_some()
487            && self.fee_payer_auth.is_some()
488            && self.secondary_auths.iter().all(Option::is_some)
489    }
490
491    /// Finalizes the transaction if all signatures are present.
492    ///
493    /// Returns an error if any signatures are missing.
494    ///
495    /// # Errors
496    ///
497    /// Returns an error if the sender signature, fee payer signature, or any secondary signer signature is missing.
498    pub fn finalize(self) -> AptosResult<SignedTransaction> {
499        let sender_auth = self
500            .sender_auth
501            .ok_or_else(|| AptosError::transaction("missing sender signature"))?;
502        let fee_payer_auth = self
503            .fee_payer_auth
504            .ok_or_else(|| AptosError::transaction("missing fee payer signature"))?;
505
506        let secondary_auths: Result<Vec<_>, _> = self
507            .secondary_auths
508            .into_iter()
509            .enumerate()
510            .map(|(i, auth)| {
511                auth.ok_or_else(|| {
512                    AptosError::transaction(format!("missing secondary signer {i} signature"))
513                })
514            })
515            .collect();
516        let secondary_auths = secondary_auths?;
517
518        let authenticator = TransactionAuthenticator::fee_payer(
519            sender_auth,
520            self.fee_payer_txn.secondary_signer_addresses.clone(),
521            secondary_auths,
522            self.fee_payer_txn.fee_payer_address,
523            fee_payer_auth,
524        );
525
526        Ok(SignedTransaction::new(
527            self.fee_payer_txn.raw_txn,
528            authenticator,
529        ))
530    }
531}
532
533/// Extension trait that adds sponsorship capabilities to accounts.
534///
535/// This trait provides convenient methods for an account to sponsor
536/// transactions for other users.
537pub trait Sponsor: Account + Sized {
538    /// Sponsors a transaction for another account.
539    ///
540    /// Creates and signs a sponsored transaction where `self` pays the gas fees.
541    ///
542    /// # Arguments
543    ///
544    /// * `sender` - The account initiating the transaction
545    /// * `sender_sequence_number` - The sender's current sequence number
546    /// * `payload` - The transaction payload
547    /// * `chain_id` - The target chain ID
548    ///
549    /// # Example
550    ///
551    /// ```rust,ignore
552    /// use aptos_sdk::transaction::Sponsor;
553    ///
554    /// let signed_txn = sponsor_account.sponsor(
555    ///     &user_account,
556    ///     0,
557    ///     payload,
558    ///     ChainId::testnet(),
559    /// )?;
560    /// ```
561    ///
562    /// # Errors
563    ///
564    /// Returns an error if building the transaction fails or if any signer fails to sign.
565    fn sponsor<S: Account>(
566        &self,
567        sender: &S,
568        sender_sequence_number: u64,
569        payload: TransactionPayload,
570        chain_id: ChainId,
571    ) -> AptosResult<SignedTransaction> {
572        SponsoredTransactionBuilder::new()
573            .sender(sender.address())
574            .sequence_number(sender_sequence_number)
575            .fee_payer(self.address())
576            .payload(payload)
577            .chain_id(chain_id)
578            .build_and_sign(sender, &[], self)
579    }
580
581    /// Sponsors a transaction with custom gas settings.
582    ///
583    /// # Errors
584    ///
585    /// Returns an error if building the transaction fails or if any signer fails to sign.
586    fn sponsor_with_gas<S: Account>(
587        &self,
588        sender: &S,
589        sender_sequence_number: u64,
590        payload: TransactionPayload,
591        chain_id: ChainId,
592        max_gas_amount: u64,
593        gas_unit_price: u64,
594    ) -> AptosResult<SignedTransaction> {
595        SponsoredTransactionBuilder::new()
596            .sender(sender.address())
597            .sequence_number(sender_sequence_number)
598            .fee_payer(self.address())
599            .payload(payload)
600            .chain_id(chain_id)
601            .max_gas_amount(max_gas_amount)
602            .gas_unit_price(gas_unit_price)
603            .build_and_sign(sender, &[], self)
604    }
605}
606
607// Implement Sponsor for all Account types that are Sized
608impl<A: Account + Sized> Sponsor for A {}
609
610/// Creates a simple sponsored transaction with minimal configuration.
611///
612/// This is a convenience function for the common case of sponsoring a
613/// simple transaction without secondary signers.
614///
615/// # Example
616///
617/// ```rust,ignore
618/// use aptos_sdk::transaction::sponsor_transaction;
619///
620/// let signed = sponsor_transaction(
621///     &sender_account,
622///     sender_sequence_number,
623///     &sponsor_account,
624///     payload,
625///     ChainId::testnet(),
626/// )?;
627/// ```
628///
629/// # Errors
630///
631/// Returns an error if building the transaction fails or if any signer fails to sign.
632pub fn sponsor_transaction<S, F>(
633    sender: &S,
634    sender_sequence_number: u64,
635    fee_payer: &F,
636    payload: TransactionPayload,
637    chain_id: ChainId,
638) -> AptosResult<SignedTransaction>
639where
640    S: Account,
641    F: Account,
642{
643    SponsoredTransactionBuilder::new()
644        .sender(sender.address())
645        .sequence_number(sender_sequence_number)
646        .fee_payer(fee_payer.address())
647        .payload(payload)
648        .chain_id(chain_id)
649        .build_and_sign(sender, &[], fee_payer)
650}
651
652#[cfg(test)]
653mod tests {
654    use super::*;
655    use crate::transaction::payload::EntryFunction;
656
657    #[test]
658    fn test_builder_missing_sender() {
659        let recipient = AccountAddress::from_hex("0x123").unwrap();
660        let result = SponsoredTransactionBuilder::new()
661            .sequence_number(0)
662            .fee_payer(AccountAddress::ONE)
663            .payload(TransactionPayload::EntryFunction(
664                EntryFunction::apt_transfer(recipient, 1000).unwrap(),
665            ))
666            .chain_id(ChainId::testnet())
667            .build();
668
669        assert!(result.is_err());
670        assert!(result.unwrap_err().to_string().contains("sender"));
671    }
672
673    #[test]
674    fn test_builder_missing_fee_payer() {
675        let recipient = AccountAddress::from_hex("0x123").unwrap();
676        let result = SponsoredTransactionBuilder::new()
677            .sender(AccountAddress::ONE)
678            .sequence_number(0)
679            .payload(TransactionPayload::EntryFunction(
680                EntryFunction::apt_transfer(recipient, 1000).unwrap(),
681            ))
682            .chain_id(ChainId::testnet())
683            .build();
684
685        assert!(result.is_err());
686        assert!(result.unwrap_err().to_string().contains("fee_payer"));
687    }
688
689    #[test]
690    fn test_builder_complete() {
691        let recipient = AccountAddress::from_hex("0x123").unwrap();
692        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
693
694        let fee_payer_txn = SponsoredTransactionBuilder::new()
695            .sender(AccountAddress::ONE)
696            .sequence_number(5)
697            .fee_payer(AccountAddress::from_hex("0x3").unwrap())
698            .payload(payload.into())
699            .chain_id(ChainId::testnet())
700            .max_gas_amount(100_000)
701            .gas_unit_price(150)
702            .build()
703            .unwrap();
704
705        assert_eq!(fee_payer_txn.raw_txn.sender, AccountAddress::ONE);
706        assert_eq!(fee_payer_txn.raw_txn.sequence_number, 5);
707        assert_eq!(fee_payer_txn.raw_txn.max_gas_amount, 100_000);
708        assert_eq!(fee_payer_txn.raw_txn.gas_unit_price, 150);
709        assert_eq!(
710            fee_payer_txn.fee_payer_address,
711            AccountAddress::from_hex("0x3").unwrap()
712        );
713    }
714
715    #[test]
716    fn test_partially_signed_completion_check() {
717        let recipient = AccountAddress::from_hex("0x123").unwrap();
718        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
719
720        let fee_payer_txn = SponsoredTransactionBuilder::new()
721            .sender(AccountAddress::ONE)
722            .sequence_number(0)
723            .fee_payer(AccountAddress::from_hex("0x3").unwrap())
724            .payload(payload.into())
725            .chain_id(ChainId::testnet())
726            .build()
727            .unwrap();
728
729        let partially_signed = PartiallySigned::new(fee_payer_txn);
730        assert!(!partially_signed.is_complete());
731    }
732
733    #[test]
734    fn test_partially_signed_finalize_incomplete() {
735        let recipient = AccountAddress::from_hex("0x123").unwrap();
736        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
737
738        let fee_payer_txn = SponsoredTransactionBuilder::new()
739            .sender(AccountAddress::ONE)
740            .sequence_number(0)
741            .fee_payer(AccountAddress::from_hex("0x3").unwrap())
742            .payload(payload.into())
743            .chain_id(ChainId::testnet())
744            .build()
745            .unwrap();
746
747        let partially_signed = PartiallySigned::new(fee_payer_txn);
748        let result = partially_signed.finalize();
749
750        assert!(result.is_err());
751        assert!(result.unwrap_err().to_string().contains("missing"));
752    }
753
754    #[cfg(feature = "ed25519")]
755    #[test]
756    fn test_full_sponsored_transaction() {
757        use crate::account::Ed25519Account;
758
759        let sender = Ed25519Account::generate();
760        let fee_payer = Ed25519Account::generate();
761        let recipient = AccountAddress::from_hex("0x123").unwrap();
762
763        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
764
765        let signed_txn = SponsoredTransactionBuilder::new()
766            .sender(sender.address())
767            .sequence_number(0)
768            .fee_payer(fee_payer.address())
769            .payload(payload.into())
770            .chain_id(ChainId::testnet())
771            .build_and_sign(&sender, &[], &fee_payer)
772            .unwrap();
773
774        // Verify the transaction structure
775        assert_eq!(signed_txn.raw_txn.sender, sender.address());
776        assert!(matches!(
777            signed_txn.authenticator,
778            TransactionAuthenticator::FeePayer { .. }
779        ));
780    }
781
782    #[cfg(feature = "ed25519")]
783    #[test]
784    fn test_sponsor_trait() {
785        use crate::account::Ed25519Account;
786
787        let sender = Ed25519Account::generate();
788        let sponsor = Ed25519Account::generate();
789        let recipient = AccountAddress::from_hex("0x123").unwrap();
790
791        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
792
793        // Use the Sponsor trait
794        let signed_txn = sponsor
795            .sponsor(&sender, 0, payload.into(), ChainId::testnet())
796            .unwrap();
797
798        assert_eq!(signed_txn.raw_txn.sender, sender.address());
799    }
800
801    #[cfg(feature = "ed25519")]
802    #[test]
803    fn test_sponsor_transaction_fn() {
804        use crate::account::Ed25519Account;
805
806        let sender = Ed25519Account::generate();
807        let fee_payer = Ed25519Account::generate();
808        let recipient = AccountAddress::from_hex("0x123").unwrap();
809
810        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
811
812        // Use the convenience function
813        let signed_txn =
814            sponsor_transaction(&sender, 0, &fee_payer, payload.into(), ChainId::testnet())
815                .unwrap();
816
817        assert_eq!(signed_txn.raw_txn.sender, sender.address());
818    }
819
820    #[cfg(feature = "ed25519")]
821    #[test]
822    fn test_partially_signed_flow() {
823        use crate::account::Ed25519Account;
824
825        let sender = Ed25519Account::generate();
826        let fee_payer = Ed25519Account::generate();
827        let recipient = AccountAddress::from_hex("0x123").unwrap();
828
829        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
830
831        // Build the transaction
832        let fee_payer_txn = SponsoredTransactionBuilder::new()
833            .sender(sender.address())
834            .sequence_number(0)
835            .fee_payer(fee_payer.address())
836            .payload(payload.into())
837            .chain_id(ChainId::testnet())
838            .build()
839            .unwrap();
840
841        // Create partially signed and collect signatures
842        let mut partially_signed = PartiallySigned::new(fee_payer_txn);
843
844        // Not complete yet
845        assert!(!partially_signed.is_complete());
846
847        // Sign as sender
848        partially_signed.sign_as_sender(&sender).unwrap();
849        assert!(!partially_signed.is_complete());
850
851        // Sign as fee payer
852        partially_signed.sign_as_fee_payer(&fee_payer).unwrap();
853        assert!(partially_signed.is_complete());
854
855        // Finalize
856        let signed_txn = partially_signed.finalize().unwrap();
857        assert_eq!(signed_txn.raw_txn.sender, sender.address());
858    }
859
860    #[test]
861    fn test_builder_missing_sequence_number() {
862        let recipient = AccountAddress::from_hex("0x123").unwrap();
863        let result = SponsoredTransactionBuilder::new()
864            .sender(AccountAddress::ONE)
865            .fee_payer(AccountAddress::from_hex("0x3").unwrap())
866            .payload(TransactionPayload::EntryFunction(
867                EntryFunction::apt_transfer(recipient, 1000).unwrap(),
868            ))
869            .chain_id(ChainId::testnet())
870            .build();
871
872        assert!(result.is_err());
873        assert!(result.unwrap_err().to_string().contains("sequence_number"));
874    }
875
876    #[test]
877    fn test_builder_missing_payload() {
878        let result = SponsoredTransactionBuilder::new()
879            .sender(AccountAddress::ONE)
880            .sequence_number(0)
881            .fee_payer(AccountAddress::from_hex("0x3").unwrap())
882            .chain_id(ChainId::testnet())
883            .build();
884
885        assert!(result.is_err());
886        assert!(result.unwrap_err().to_string().contains("payload"));
887    }
888
889    #[test]
890    fn test_builder_missing_chain_id() {
891        let recipient = AccountAddress::from_hex("0x123").unwrap();
892        let result = SponsoredTransactionBuilder::new()
893            .sender(AccountAddress::ONE)
894            .sequence_number(0)
895            .fee_payer(AccountAddress::from_hex("0x3").unwrap())
896            .payload(TransactionPayload::EntryFunction(
897                EntryFunction::apt_transfer(recipient, 1000).unwrap(),
898            ))
899            .build();
900
901        assert!(result.is_err());
902        assert!(result.unwrap_err().to_string().contains("chain_id"));
903    }
904
905    #[test]
906    fn test_builder_secondary_signers() {
907        let recipient = AccountAddress::from_hex("0x123").unwrap();
908        let secondary1 = AccountAddress::from_hex("0x4").unwrap();
909        let secondary2 = AccountAddress::from_hex("0x5").unwrap();
910
911        let fee_payer_txn = SponsoredTransactionBuilder::new()
912            .sender(AccountAddress::ONE)
913            .sequence_number(0)
914            .fee_payer(AccountAddress::from_hex("0x3").unwrap())
915            .secondary_signer(secondary1)
916            .secondary_signers(&[secondary2])
917            .payload(TransactionPayload::EntryFunction(
918                EntryFunction::apt_transfer(recipient, 1000).unwrap(),
919            ))
920            .chain_id(ChainId::testnet())
921            .build()
922            .unwrap();
923
924        assert_eq!(fee_payer_txn.secondary_signer_addresses.len(), 2);
925        assert_eq!(fee_payer_txn.secondary_signer_addresses[0], secondary1);
926        assert_eq!(fee_payer_txn.secondary_signer_addresses[1], secondary2);
927    }
928
929    #[test]
930    fn test_builder_expiration_timestamp() {
931        let recipient = AccountAddress::from_hex("0x123").unwrap();
932        let expiration = 1_234_567_890_u64;
933
934        let fee_payer_txn = SponsoredTransactionBuilder::new()
935            .sender(AccountAddress::ONE)
936            .sequence_number(0)
937            .fee_payer(AccountAddress::from_hex("0x3").unwrap())
938            .payload(TransactionPayload::EntryFunction(
939                EntryFunction::apt_transfer(recipient, 1000).unwrap(),
940            ))
941            .chain_id(ChainId::testnet())
942            .expiration_timestamp_secs(expiration)
943            .build()
944            .unwrap();
945
946        assert_eq!(fee_payer_txn.raw_txn.expiration_timestamp_secs, expiration);
947    }
948
949    #[test]
950    fn test_builder_expiration_from_now() {
951        let recipient = AccountAddress::from_hex("0x123").unwrap();
952
953        let fee_payer_txn = SponsoredTransactionBuilder::new()
954            .sender(AccountAddress::ONE)
955            .sequence_number(0)
956            .fee_payer(AccountAddress::from_hex("0x3").unwrap())
957            .payload(TransactionPayload::EntryFunction(
958                EntryFunction::apt_transfer(recipient, 1000).unwrap(),
959            ))
960            .chain_id(ChainId::testnet())
961            .expiration_from_now(60)
962            .build()
963            .unwrap();
964
965        // Expiration should be roughly now + 60 seconds
966        let now = SystemTime::now()
967            .duration_since(UNIX_EPOCH)
968            .unwrap()
969            .as_secs();
970        assert!(fee_payer_txn.raw_txn.expiration_timestamp_secs >= now);
971        assert!(fee_payer_txn.raw_txn.expiration_timestamp_secs <= now + 65);
972    }
973
974    #[test]
975    fn test_builder_default() {
976        let builder = SponsoredTransactionBuilder::default();
977        assert!(builder.sender_address.is_none());
978        assert!(builder.sequence_number.is_none());
979        assert!(builder.fee_payer_address.is_none());
980        assert!(builder.payload.is_none());
981        assert!(builder.chain_id.is_none());
982        // Default values are set via SponsoredTransactionBuilder::new()
983        // not via Default::default() which initializes to Rust defaults (0)
984        // Let's just check the builder is properly created
985    }
986
987    #[test]
988    fn test_builder_new_defaults() {
989        let builder = SponsoredTransactionBuilder::new();
990        assert!(builder.sender_address.is_none());
991        assert!(builder.sequence_number.is_none());
992        assert!(builder.fee_payer_address.is_none());
993        assert!(builder.payload.is_none());
994        assert!(builder.chain_id.is_none());
995        assert_eq!(builder.max_gas_amount, DEFAULT_MAX_GAS_AMOUNT);
996        assert_eq!(builder.gas_unit_price, DEFAULT_GAS_UNIT_PRICE);
997    }
998
999    #[test]
1000    fn test_builder_debug() {
1001        let builder = SponsoredTransactionBuilder::new().sender(AccountAddress::ONE);
1002        let debug = format!("{builder:?}");
1003        assert!(debug.contains("SponsoredTransactionBuilder"));
1004    }
1005
1006    #[cfg(feature = "ed25519")]
1007    #[test]
1008    fn test_partially_signed_with_secondary_signers() {
1009        use crate::account::Ed25519Account;
1010
1011        let sender = Ed25519Account::generate();
1012        let secondary = Ed25519Account::generate();
1013        let fee_payer = Ed25519Account::generate();
1014        let recipient = AccountAddress::from_hex("0x123").unwrap();
1015
1016        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
1017
1018        // Build with secondary signer
1019        let fee_payer_txn = SponsoredTransactionBuilder::new()
1020            .sender(sender.address())
1021            .sequence_number(0)
1022            .secondary_signer(secondary.address())
1023            .fee_payer(fee_payer.address())
1024            .payload(payload.into())
1025            .chain_id(ChainId::testnet())
1026            .build()
1027            .unwrap();
1028
1029        let mut partially_signed = PartiallySigned::new(fee_payer_txn);
1030
1031        // Need all three signatures
1032        assert!(!partially_signed.is_complete());
1033
1034        partially_signed.sign_as_sender(&sender).unwrap();
1035        assert!(!partially_signed.is_complete());
1036
1037        partially_signed.sign_as_secondary(0, &secondary).unwrap();
1038        assert!(!partially_signed.is_complete());
1039
1040        partially_signed.sign_as_fee_payer(&fee_payer).unwrap();
1041        assert!(partially_signed.is_complete());
1042
1043        let signed = partially_signed.finalize().unwrap();
1044        assert_eq!(signed.raw_txn.sender, sender.address());
1045    }
1046
1047    #[cfg(feature = "ed25519")]
1048    #[test]
1049    fn test_partially_signed_flow_with_single_key_accounts() {
1050        use crate::account::Ed25519SingleKeyAccount;
1051        use crate::transaction::authenticator::{AccountAuthenticator, TransactionAuthenticator};
1052
1053        let sender = Ed25519SingleKeyAccount::generate();
1054        let fee_payer = Ed25519SingleKeyAccount::generate();
1055        let recipient = AccountAddress::from_hex("0x123").unwrap();
1056        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
1057
1058        let fee_payer_txn = SponsoredTransactionBuilder::new()
1059            .sender(sender.address())
1060            .sequence_number(0)
1061            .fee_payer(fee_payer.address())
1062            .payload(payload.into())
1063            .chain_id(ChainId::testnet())
1064            .build()
1065            .unwrap();
1066
1067        let mut partially_signed = PartiallySigned::new(fee_payer_txn);
1068        partially_signed.sign_as_sender(&sender).unwrap();
1069        partially_signed.sign_as_fee_payer(&fee_payer).unwrap();
1070        let signed = partially_signed.finalize().unwrap();
1071        match &signed.authenticator {
1072            TransactionAuthenticator::FeePayer {
1073                sender,
1074                fee_payer_signer,
1075                ..
1076            } => {
1077                assert!(matches!(sender, AccountAuthenticator::SingleKey { .. }));
1078                assert!(matches!(
1079                    fee_payer_signer,
1080                    AccountAuthenticator::SingleKey { .. }
1081                ));
1082            }
1083            _ => panic!("expected FeePayer authenticator"),
1084        }
1085        signed.verify_signature().unwrap();
1086    }
1087
1088    #[cfg(feature = "ed25519")]
1089    #[test]
1090    fn test_partially_signed_flow_with_multi_key_accounts() {
1091        use crate::account::{AnyPrivateKey, MultiKeyAccount};
1092        use crate::crypto::Ed25519PrivateKey;
1093        use crate::transaction::authenticator::{AccountAuthenticator, TransactionAuthenticator};
1094
1095        let sender = MultiKeyAccount::new(
1096            vec![AnyPrivateKey::ed25519(Ed25519PrivateKey::generate())],
1097            1,
1098        )
1099        .unwrap();
1100        let fee_payer = MultiKeyAccount::new(
1101            vec![AnyPrivateKey::ed25519(Ed25519PrivateKey::generate())],
1102            1,
1103        )
1104        .unwrap();
1105        let recipient = AccountAddress::from_hex("0x123").unwrap();
1106        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
1107
1108        let fee_payer_txn = SponsoredTransactionBuilder::new()
1109            .sender(sender.address())
1110            .sequence_number(0)
1111            .fee_payer(fee_payer.address())
1112            .payload(payload.into())
1113            .chain_id(ChainId::testnet())
1114            .build()
1115            .unwrap();
1116
1117        let mut partially_signed = PartiallySigned::new(fee_payer_txn);
1118        partially_signed.sign_as_sender(&sender).unwrap();
1119        partially_signed.sign_as_fee_payer(&fee_payer).unwrap();
1120        let signed = partially_signed.finalize().unwrap();
1121        match &signed.authenticator {
1122            TransactionAuthenticator::FeePayer {
1123                sender,
1124                fee_payer_signer,
1125                ..
1126            } => {
1127                assert!(matches!(sender, AccountAuthenticator::MultiKey { .. }));
1128                assert!(matches!(
1129                    fee_payer_signer,
1130                    AccountAuthenticator::MultiKey { .. }
1131                ));
1132            }
1133            _ => panic!("expected FeePayer authenticator"),
1134        }
1135        signed.verify_signature().unwrap();
1136    }
1137
1138    #[cfg(feature = "ed25519")]
1139    #[test]
1140    fn test_partially_signed_secondary_index_out_of_bounds() {
1141        use crate::account::Ed25519Account;
1142
1143        let sender = Ed25519Account::generate();
1144        let fee_payer = Ed25519Account::generate();
1145        let secondary = Ed25519Account::generate();
1146        let recipient = AccountAddress::from_hex("0x123").unwrap();
1147
1148        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
1149
1150        // No secondary signers in the transaction
1151        let fee_payer_txn = SponsoredTransactionBuilder::new()
1152            .sender(sender.address())
1153            .sequence_number(0)
1154            .fee_payer(fee_payer.address())
1155            .payload(payload.into())
1156            .chain_id(ChainId::testnet())
1157            .build()
1158            .unwrap();
1159
1160        let mut partially_signed = PartiallySigned::new(fee_payer_txn);
1161
1162        // Try to sign as secondary at index 0 (out of bounds because no secondary signers)
1163        let result = partially_signed.sign_as_secondary(0, &secondary);
1164        assert!(result.is_err());
1165        assert!(result.unwrap_err().to_string().contains("out of bounds"));
1166    }
1167
1168    #[cfg(feature = "ed25519")]
1169    #[test]
1170    fn test_partially_signed_finalize_missing_secondary() {
1171        use crate::account::Ed25519Account;
1172
1173        let sender = Ed25519Account::generate();
1174        let fee_payer = Ed25519Account::generate();
1175        let recipient = AccountAddress::from_hex("0x123").unwrap();
1176
1177        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
1178
1179        // Build with secondary signer but don't sign it
1180        let fee_payer_txn = SponsoredTransactionBuilder::new()
1181            .sender(sender.address())
1182            .sequence_number(0)
1183            .secondary_signer(AccountAddress::from_hex("0x5").unwrap())
1184            .fee_payer(fee_payer.address())
1185            .payload(payload.into())
1186            .chain_id(ChainId::testnet())
1187            .build()
1188            .unwrap();
1189
1190        let mut partially_signed = PartiallySigned::new(fee_payer_txn);
1191
1192        // Sign sender and fee payer but not secondary
1193        partially_signed.sign_as_sender(&sender).unwrap();
1194        partially_signed.sign_as_fee_payer(&fee_payer).unwrap();
1195
1196        // Should fail because secondary is missing
1197        let result = partially_signed.finalize();
1198        assert!(result.is_err());
1199        assert!(result.unwrap_err().to_string().contains("secondary signer"));
1200    }
1201
1202    #[cfg(feature = "ed25519")]
1203    #[test]
1204    fn test_sponsor_with_gas() {
1205        use crate::account::Ed25519Account;
1206
1207        let sender = Ed25519Account::generate();
1208        let sponsor = Ed25519Account::generate();
1209        let recipient = AccountAddress::from_hex("0x123").unwrap();
1210
1211        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
1212
1213        let signed_txn = sponsor
1214            .sponsor_with_gas(&sender, 0, payload.into(), ChainId::testnet(), 50000, 200)
1215            .unwrap();
1216
1217        assert_eq!(signed_txn.raw_txn.sender, sender.address());
1218        assert_eq!(signed_txn.raw_txn.max_gas_amount, 50000);
1219        assert_eq!(signed_txn.raw_txn.gas_unit_price, 200);
1220    }
1221
1222    #[test]
1223    fn test_partially_signed_debug() {
1224        let recipient = AccountAddress::from_hex("0x123").unwrap();
1225        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
1226
1227        let fee_payer_txn = SponsoredTransactionBuilder::new()
1228            .sender(AccountAddress::ONE)
1229            .sequence_number(0)
1230            .fee_payer(AccountAddress::from_hex("0x3").unwrap())
1231            .payload(payload.into())
1232            .chain_id(ChainId::testnet())
1233            .build()
1234            .unwrap();
1235
1236        let partially_signed = PartiallySigned::new(fee_payer_txn);
1237        let debug = format!("{partially_signed:?}");
1238        assert!(debug.contains("PartiallySigned"));
1239    }
1240}