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