Skip to main content

aptos_sdk/transaction/
builder.rs

1//! Transaction builder.
2
3use crate::account::Account;
4use crate::crypto::{AnyPublicKey, AnySignature, MultiKeyPublicKey, MultiKeySignature};
5use crate::error::{AptosError, AptosResult};
6use crate::transaction::authenticator::{AccountAuthenticator, TransactionAuthenticator};
7use crate::transaction::payload::TransactionPayload;
8use crate::transaction::types::{
9    FeePayerRawTransaction, MultiAgentRawTransaction, RawTransaction, SignedTransaction,
10};
11use crate::types::{AccountAddress, ChainId};
12use std::time::{SystemTime, UNIX_EPOCH};
13
14/// Default maximum gas amount.
15pub const DEFAULT_MAX_GAS_AMOUNT: u64 = 2_000_000;
16/// Default gas unit price in octas.
17pub const DEFAULT_GAS_UNIT_PRICE: u64 = 100;
18/// Default transaction expiration time in seconds.
19pub const DEFAULT_EXPIRATION_SECONDS: u64 = 600; // 10 minutes
20
21/// A builder for constructing transactions.
22///
23/// # Example
24///
25/// ```rust,no_run
26/// use aptos_sdk::transaction::{TransactionBuilder, EntryFunction};
27/// use aptos_sdk::types::{AccountAddress, ChainId};
28///
29/// let payload = EntryFunction::apt_transfer(
30///     AccountAddress::from_hex("0x123").unwrap(),
31///     1000,
32/// ).unwrap();
33///
34/// let txn = TransactionBuilder::new()
35///     .sender(AccountAddress::ONE)
36///     .sequence_number(0)
37///     .payload(payload.into())
38///     .chain_id(ChainId::testnet())
39///     .build()
40///     .unwrap();
41/// ```
42#[derive(Debug, Clone)]
43pub struct TransactionBuilder {
44    sender: Option<AccountAddress>,
45    sequence_number: Option<u64>,
46    payload: Option<TransactionPayload>,
47    max_gas_amount: u64,
48    gas_unit_price: u64,
49    expiration_timestamp_secs: Option<u64>,
50    chain_id: Option<ChainId>,
51}
52
53impl Default for TransactionBuilder {
54    fn default() -> Self {
55        Self::new()
56    }
57}
58
59impl TransactionBuilder {
60    /// Creates a new transaction builder with default values.
61    #[must_use]
62    pub fn new() -> Self {
63        Self {
64            sender: None,
65            sequence_number: None,
66            payload: None,
67            max_gas_amount: DEFAULT_MAX_GAS_AMOUNT,
68            gas_unit_price: DEFAULT_GAS_UNIT_PRICE,
69            expiration_timestamp_secs: None,
70            chain_id: None,
71        }
72    }
73
74    /// Sets the sender address.
75    #[must_use]
76    pub fn sender(mut self, sender: AccountAddress) -> Self {
77        self.sender = Some(sender);
78        self
79    }
80
81    /// Sets the sequence number.
82    #[must_use]
83    pub fn sequence_number(mut self, sequence_number: u64) -> Self {
84        self.sequence_number = Some(sequence_number);
85        self
86    }
87
88    /// Sets the transaction payload.
89    #[must_use]
90    pub fn payload(mut self, payload: TransactionPayload) -> Self {
91        self.payload = Some(payload);
92        self
93    }
94
95    /// Sets the maximum gas amount.
96    #[must_use]
97    pub fn max_gas_amount(mut self, max_gas_amount: u64) -> Self {
98        self.max_gas_amount = max_gas_amount;
99        self
100    }
101
102    /// Sets the gas unit price in octas.
103    #[must_use]
104    pub fn gas_unit_price(mut self, gas_unit_price: u64) -> Self {
105        self.gas_unit_price = gas_unit_price;
106        self
107    }
108
109    /// Sets the expiration timestamp in seconds since Unix epoch.
110    #[must_use]
111    pub fn expiration_timestamp_secs(mut self, expiration_timestamp_secs: u64) -> Self {
112        self.expiration_timestamp_secs = Some(expiration_timestamp_secs);
113        self
114    }
115
116    /// Sets the expiration time relative to now.
117    ///
118    /// Uses saturating arithmetic to handle edge cases like system time going backwards.
119    #[must_use]
120    pub fn expiration_from_now(mut self, seconds: u64) -> Self {
121        let now = SystemTime::now()
122            .duration_since(UNIX_EPOCH)
123            .unwrap_or_default()
124            .as_secs();
125        self.expiration_timestamp_secs = Some(now.saturating_add(seconds));
126        self
127    }
128
129    /// Sets the chain ID.
130    #[must_use]
131    pub fn chain_id(mut self, chain_id: ChainId) -> Self {
132        self.chain_id = Some(chain_id);
133        self
134    }
135
136    /// Builds the raw transaction.
137    ///
138    /// # Errors
139    ///
140    /// Returns an error if any required field is missing:
141    /// - `sender` is required
142    /// - `sequence_number` is required
143    /// - `payload` is required
144    /// - `chain_id` is required
145    pub fn build(self) -> AptosResult<RawTransaction> {
146        let sender = self
147            .sender
148            .ok_or_else(|| AptosError::transaction("sender is required"))?;
149        let sequence_number = self
150            .sequence_number
151            .ok_or_else(|| AptosError::transaction("sequence_number is required"))?;
152        let payload = self
153            .payload
154            .ok_or_else(|| AptosError::transaction("payload is required"))?;
155        let chain_id = self
156            .chain_id
157            .ok_or_else(|| AptosError::transaction("chain_id is required"))?;
158
159        let expiration_timestamp_secs = self.expiration_timestamp_secs.unwrap_or_else(|| {
160            SystemTime::now()
161                .duration_since(UNIX_EPOCH)
162                .unwrap_or_default()
163                .as_secs()
164                .saturating_add(DEFAULT_EXPIRATION_SECONDS)
165        });
166
167        Ok(RawTransaction::new(
168            sender,
169            sequence_number,
170            payload,
171            self.max_gas_amount,
172            self.gas_unit_price,
173            expiration_timestamp_secs,
174            chain_id,
175        ))
176    }
177
178    /// Builds and signs the transaction with the given account.
179    ///
180    /// # Errors
181    ///
182    /// Returns an error if the transaction cannot be built or signed.
183    #[cfg(feature = "ed25519")]
184    pub fn build_and_sign<A: Account>(self, account: &A) -> AptosResult<SignedTransaction> {
185        let sender = self.sender.unwrap_or_else(|| account.address());
186        let raw_txn = Self {
187            sender: Some(sender),
188            ..self
189        }
190        .build()?;
191
192        sign_transaction(&raw_txn, account)
193    }
194}
195
196/// Signs a raw transaction with the given account.
197///
198/// # Errors
199///
200/// Returns an error if generating the signing message fails or if the account fails to sign.
201pub fn sign_transaction<A: Account>(
202    raw_txn: &RawTransaction,
203    account: &A,
204) -> AptosResult<SignedTransaction> {
205    let signing_message = raw_txn.signing_message()?;
206    let signature = account.sign(&signing_message)?;
207    let public_key = account.public_key_bytes();
208
209    let authenticator =
210        make_transaction_authenticator(account.signature_scheme(), public_key, signature)?;
211
212    Ok(SignedTransaction::new(raw_txn.clone(), authenticator))
213}
214
215/// Creates a transaction authenticator based on the signature scheme.
216///
217/// # Errors
218///
219/// Returns an error if the signature scheme is not recognized.
220fn make_transaction_authenticator(
221    scheme: u8,
222    public_key: Vec<u8>,
223    signature: Vec<u8>,
224) -> AptosResult<TransactionAuthenticator> {
225    match scheme {
226        crate::crypto::ED25519_SCHEME => {
227            Ok(TransactionAuthenticator::ed25519(public_key, signature))
228        }
229        crate::crypto::MULTI_ED25519_SCHEME => Ok(TransactionAuthenticator::multi_ed25519(
230            public_key, signature,
231        )),
232        crate::crypto::MULTI_KEY_SCHEME => {
233            let _ = MultiKeyPublicKey::from_bytes(&public_key)?;
234            let _ = MultiKeySignature::from_bytes(&signature)?;
235            Ok(TransactionAuthenticator::single_sender(
236                AccountAuthenticator::multi_key(public_key, signature),
237            ))
238        }
239        crate::crypto::SINGLE_KEY_SCHEME => {
240            let _ = AnyPublicKey::from_bcs_bytes(&public_key)?;
241            let _ = AnySignature::from_bcs_bytes(&signature)?;
242            Ok(TransactionAuthenticator::single_sender(
243                AccountAuthenticator::single_key(public_key, signature),
244            ))
245        }
246        #[cfg(feature = "keyless")]
247        crate::crypto::KEYLESS_SCHEME => {
248            // Keyless accounts use SingleSender variant with AccountAuthenticator::SingleKey
249            // The public key is the ephemeral Ed25519 key, and the signature is a BCS-serialized
250            // KeylessSignature struct containing the ephemeral signature and ZK proof
251            Ok(TransactionAuthenticator::single_sender(
252                AccountAuthenticator::keyless(public_key, signature),
253            ))
254        }
255        _ => Err(AptosError::InvalidSignature(format!(
256            "unknown signature scheme: {scheme}"
257        ))),
258    }
259}
260
261/// Creates an account authenticator based on the signature scheme.
262///
263/// # Errors
264///
265/// Returns an error if the signature scheme is not recognized.
266fn make_account_authenticator(
267    scheme: u8,
268    public_key: Vec<u8>,
269    signature: Vec<u8>,
270) -> AptosResult<AccountAuthenticator> {
271    match scheme {
272        crate::crypto::ED25519_SCHEME => Ok(AccountAuthenticator::ed25519(public_key, signature)),
273        crate::crypto::MULTI_ED25519_SCHEME => Ok(AccountAuthenticator::MultiEd25519 {
274            public_key,
275            signature,
276        }),
277        crate::crypto::SINGLE_KEY_SCHEME => {
278            let _ = AnyPublicKey::from_bcs_bytes(&public_key)?;
279            let _ = AnySignature::from_bcs_bytes(&signature)?;
280            Ok(AccountAuthenticator::single_key(public_key, signature))
281        }
282        crate::crypto::MULTI_KEY_SCHEME => {
283            let _ = MultiKeyPublicKey::from_bytes(&public_key)?;
284            let _ = MultiKeySignature::from_bytes(&signature)?;
285            Ok(AccountAuthenticator::multi_key(public_key, signature))
286        }
287        #[cfg(feature = "keyless")]
288        crate::crypto::KEYLESS_SCHEME => Ok(AccountAuthenticator::keyless(public_key, signature)),
289        _ => Err(AptosError::InvalidSignature(format!(
290            "unknown signature scheme: {scheme}"
291        ))),
292    }
293}
294
295/// Signs a multi-agent transaction.
296///
297/// # Errors
298///
299/// Returns an error if generating the signing message fails or if any signer fails to sign.
300pub fn sign_multi_agent_transaction<A: Account>(
301    multi_agent: &MultiAgentRawTransaction,
302    sender: &A,
303    secondary_signers: &[&dyn Account],
304) -> AptosResult<SignedTransaction> {
305    let signing_message = multi_agent.signing_message()?;
306
307    // Sign with sender
308    let sender_signature = sender.sign(&signing_message)?;
309    let sender_public_key = sender.public_key_bytes();
310    let sender_auth = make_account_authenticator(
311        sender.signature_scheme(),
312        sender_public_key,
313        sender_signature,
314    )?;
315
316    // Sign with secondary signers
317    let mut secondary_auths = Vec::with_capacity(secondary_signers.len());
318    for signer in secondary_signers {
319        let signature = signer.sign(&signing_message)?;
320        let public_key = signer.public_key_bytes();
321        secondary_auths.push(make_account_authenticator(
322            signer.signature_scheme(),
323            public_key,
324            signature,
325        )?);
326    }
327
328    let authenticator = TransactionAuthenticator::multi_agent(
329        sender_auth,
330        multi_agent.secondary_signer_addresses.clone(),
331        secondary_auths,
332    );
333
334    Ok(SignedTransaction::new(
335        multi_agent.raw_txn.clone(),
336        authenticator,
337    ))
338}
339
340/// Signs a fee payer transaction.
341///
342/// # Errors
343///
344/// Returns an error if generating the signing message fails or if any signer fails to sign.
345pub fn sign_fee_payer_transaction<A: Account>(
346    fee_payer_txn: &FeePayerRawTransaction,
347    sender: &A,
348    secondary_signers: &[&dyn Account],
349    fee_payer: &dyn Account,
350) -> AptosResult<SignedTransaction> {
351    let signing_message = fee_payer_txn.signing_message()?;
352
353    // Sign with sender
354    let sender_signature = sender.sign(&signing_message)?;
355    let sender_public_key = sender.public_key_bytes();
356    let sender_auth = make_account_authenticator(
357        sender.signature_scheme(),
358        sender_public_key,
359        sender_signature,
360    )?;
361
362    // Sign with secondary signers
363    let mut secondary_auths = Vec::with_capacity(secondary_signers.len());
364    for signer in secondary_signers {
365        let signature = signer.sign(&signing_message)?;
366        let public_key = signer.public_key_bytes();
367        secondary_auths.push(make_account_authenticator(
368            signer.signature_scheme(),
369            public_key,
370            signature,
371        )?);
372    }
373
374    // Sign with fee payer
375    let fee_payer_signature = fee_payer.sign(&signing_message)?;
376    let fee_payer_public_key = fee_payer.public_key_bytes();
377    let fee_payer_auth = make_account_authenticator(
378        fee_payer.signature_scheme(),
379        fee_payer_public_key,
380        fee_payer_signature,
381    )?;
382
383    let authenticator = TransactionAuthenticator::fee_payer(
384        sender_auth,
385        fee_payer_txn.secondary_signer_addresses.clone(),
386        secondary_auths,
387        fee_payer_txn.fee_payer_address,
388        fee_payer_auth,
389    );
390
391    Ok(SignedTransaction::new(
392        fee_payer_txn.raw_txn.clone(),
393        authenticator,
394    ))
395}
396
397/// Builds a signed transaction for simulation only (multi-agent).
398///
399/// Uses [`AccountAuthenticator::NoAccountAuthenticator`] for all signers so that
400/// the transaction can be sent to the simulate endpoint without real signatures.
401/// Use this to simulate a multi-agent transaction before collecting signatures.
402///
403/// # Example
404///
405/// ```rust,ignore
406/// let multi_agent = MultiAgentRawTransaction::new(raw_txn, secondary_addresses);
407/// let signed = build_simulation_signed_multi_agent(&multi_agent);
408/// let result = aptos.simulate_signed(&signed).await?;
409/// ```
410pub fn build_simulation_signed_multi_agent(
411    multi_agent: &MultiAgentRawTransaction,
412) -> SignedTransaction {
413    let sender_auth = AccountAuthenticator::no_account_authenticator();
414    let secondary_auths: Vec<AccountAuthenticator> = multi_agent
415        .secondary_signer_addresses
416        .iter()
417        .map(|_| AccountAuthenticator::no_account_authenticator())
418        .collect();
419    let authenticator = TransactionAuthenticator::multi_agent(
420        sender_auth,
421        multi_agent.secondary_signer_addresses.clone(),
422        secondary_auths,
423    );
424    SignedTransaction::new(multi_agent.raw_txn.clone(), authenticator)
425}
426
427/// Builds a signed transaction for simulation only (fee-payer / sponsored).
428///
429/// Uses [`AccountAuthenticator::NoAccountAuthenticator`] for all signers (sender,
430/// secondary signers, and fee payer) so that the transaction can be sent to the
431/// simulate endpoint without real signatures. Use this to simulate a fee-payer
432/// transaction before collecting signatures.
433///
434/// # Example
435///
436/// ```rust,ignore
437/// let fee_payer_txn = FeePayerRawTransaction::new_simple(raw_txn, fee_payer_address);
438/// let signed = build_simulation_signed_fee_payer(&fee_payer_txn);
439/// let result = aptos.simulate_signed(&signed).await?;
440/// ```
441pub fn build_simulation_signed_fee_payer(
442    fee_payer_txn: &FeePayerRawTransaction,
443) -> SignedTransaction {
444    let sender_auth = AccountAuthenticator::no_account_authenticator();
445    let secondary_auths: Vec<AccountAuthenticator> = fee_payer_txn
446        .secondary_signer_addresses
447        .iter()
448        .map(|_| AccountAuthenticator::no_account_authenticator())
449        .collect();
450    let fee_payer_auth = AccountAuthenticator::no_account_authenticator();
451    let authenticator = TransactionAuthenticator::fee_payer(
452        sender_auth,
453        fee_payer_txn.secondary_signer_addresses.clone(),
454        secondary_auths,
455        fee_payer_txn.fee_payer_address,
456        fee_payer_auth,
457    );
458    SignedTransaction::new(fee_payer_txn.raw_txn.clone(), authenticator)
459}
460
461#[cfg(test)]
462mod tests {
463    use super::*;
464    use crate::transaction::payload::EntryFunction;
465
466    #[test]
467    fn test_builder_missing_fields() {
468        let result = TransactionBuilder::new().build();
469        assert!(result.is_err());
470    }
471
472    #[test]
473    fn test_builder_complete() {
474        let recipient = AccountAddress::from_hex("0x123").unwrap();
475        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
476
477        let txn = TransactionBuilder::new()
478            .sender(AccountAddress::ONE)
479            .sequence_number(0)
480            .payload(payload.into())
481            .chain_id(ChainId::testnet())
482            .build()
483            .unwrap();
484
485        assert_eq!(txn.sender, AccountAddress::ONE);
486        assert_eq!(txn.sequence_number, 0);
487        assert_eq!(txn.max_gas_amount, DEFAULT_MAX_GAS_AMOUNT);
488        assert_eq!(txn.gas_unit_price, DEFAULT_GAS_UNIT_PRICE);
489    }
490
491    #[test]
492    fn test_builder_custom_gas() {
493        let recipient = AccountAddress::from_hex("0x123").unwrap();
494        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
495
496        let txn = TransactionBuilder::new()
497            .sender(AccountAddress::ONE)
498            .sequence_number(0)
499            .payload(payload.into())
500            .max_gas_amount(500_000)
501            .gas_unit_price(200)
502            .chain_id(ChainId::testnet())
503            .build()
504            .unwrap();
505
506        assert_eq!(txn.max_gas_amount, 500_000);
507        assert_eq!(txn.gas_unit_price, 200);
508    }
509
510    #[test]
511    fn test_builder_missing_sender() {
512        let recipient = AccountAddress::from_hex("0x123").unwrap();
513        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
514
515        let result = TransactionBuilder::new()
516            .sequence_number(0)
517            .payload(payload.into())
518            .chain_id(ChainId::testnet())
519            .build();
520
521        assert!(result.is_err());
522    }
523
524    #[test]
525    fn test_builder_missing_payload() {
526        let result = TransactionBuilder::new()
527            .sender(AccountAddress::ONE)
528            .sequence_number(0)
529            .chain_id(ChainId::testnet())
530            .build();
531
532        assert!(result.is_err());
533    }
534
535    #[test]
536    fn test_builder_missing_chain_id() {
537        let recipient = AccountAddress::from_hex("0x123").unwrap();
538        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
539
540        let result = TransactionBuilder::new()
541            .sender(AccountAddress::ONE)
542            .sequence_number(0)
543            .payload(payload.into())
544            .build();
545
546        assert!(result.is_err());
547    }
548
549    #[test]
550    fn test_builder_custom_expiration() {
551        let recipient = AccountAddress::from_hex("0x123").unwrap();
552        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
553        let custom_expiration = 9_999_999_999;
554
555        let txn = TransactionBuilder::new()
556            .sender(AccountAddress::ONE)
557            .sequence_number(0)
558            .payload(payload.into())
559            .expiration_timestamp_secs(custom_expiration)
560            .chain_id(ChainId::testnet())
561            .build()
562            .unwrap();
563
564        assert_eq!(txn.expiration_timestamp_secs, custom_expiration);
565    }
566
567    #[test]
568    fn test_default_expiration() {
569        // Default expiration should be set to about DEFAULT_EXPIRATION_SECONDS from now
570        let now = std::time::SystemTime::now()
571            .duration_since(std::time::UNIX_EPOCH)
572            .unwrap()
573            .as_secs();
574
575        let recipient = AccountAddress::from_hex("0x123").unwrap();
576        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
577
578        let txn = TransactionBuilder::new()
579            .sender(AccountAddress::ONE)
580            .sequence_number(0)
581            .payload(payload.into())
582            .chain_id(ChainId::testnet())
583            .build()
584            .unwrap();
585
586        // Should be approximately now + DEFAULT_EXPIRATION_SECONDS (give or take a second)
587        let expected_min = now + DEFAULT_EXPIRATION_SECONDS - 5;
588        let expected_max = now + DEFAULT_EXPIRATION_SECONDS + 5;
589        assert!(txn.expiration_timestamp_secs >= expected_min);
590        assert!(txn.expiration_timestamp_secs <= expected_max);
591    }
592
593    #[cfg(feature = "ed25519")]
594    #[test]
595    fn test_sign_transaction() {
596        use crate::account::Ed25519Account;
597
598        let account = Ed25519Account::generate();
599        let recipient = AccountAddress::from_hex("0x123").unwrap();
600        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
601
602        let txn = TransactionBuilder::new()
603            .sender(account.address())
604            .sequence_number(0)
605            .payload(payload.into())
606            .chain_id(ChainId::testnet())
607            .build()
608            .unwrap();
609
610        let signed = sign_transaction(&txn, &account).unwrap();
611        assert_eq!(signed.sender(), account.address());
612        signed.verify_signature().unwrap();
613    }
614
615    #[cfg(feature = "ed25519")]
616    #[test]
617    fn test_sign_transaction_with_ed25519_single_key_account() {
618        use crate::account::Ed25519SingleKeyAccount;
619        use crate::transaction::authenticator::{AccountAuthenticator, TransactionAuthenticator};
620
621        let account = Ed25519SingleKeyAccount::generate();
622        let recipient = AccountAddress::from_hex("0x123").unwrap();
623        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
624
625        let txn = TransactionBuilder::new()
626            .sender(account.address())
627            .sequence_number(0)
628            .payload(payload.into())
629            .chain_id(ChainId::testnet())
630            .build()
631            .unwrap();
632
633        let signed = sign_transaction(&txn, &account).unwrap();
634        match &signed.authenticator {
635            TransactionAuthenticator::SingleSender { sender } => {
636                assert!(matches!(sender, AccountAuthenticator::SingleKey { .. }));
637            }
638            _ => panic!("expected SingleSender authenticator"),
639        }
640        signed.verify_signature().unwrap();
641    }
642
643    #[cfg(all(feature = "ed25519", feature = "secp256k1"))]
644    #[test]
645    fn test_sign_transaction_with_mixed_multi_key_account() {
646        use crate::account::{AnyPrivateKey, MultiKeyAccount};
647        use crate::crypto::{Ed25519PrivateKey, Secp256k1PrivateKey};
648
649        let account = MultiKeyAccount::new(
650            vec![
651                AnyPrivateKey::ed25519(Ed25519PrivateKey::generate()),
652                AnyPrivateKey::secp256k1(Secp256k1PrivateKey::generate()),
653                AnyPrivateKey::ed25519(Ed25519PrivateKey::generate()),
654            ],
655            2,
656        )
657        .unwrap();
658        let recipient = AccountAddress::from_hex("0x123").unwrap();
659        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
660
661        let txn = TransactionBuilder::new()
662            .sender(account.address())
663            .sequence_number(0)
664            .payload(payload.into())
665            .chain_id(ChainId::testnet())
666            .build()
667            .unwrap();
668
669        let signed = sign_transaction(&txn, &account).unwrap();
670        assert_eq!(signed.sender(), account.address());
671        signed.verify_signature().unwrap();
672    }
673
674    #[cfg(feature = "ed25519")]
675    #[test]
676    fn test_sign_multi_agent_transaction() {
677        use crate::account::{Account, Ed25519Account};
678
679        let sender = Ed25519Account::generate();
680        let secondary = Ed25519Account::generate();
681        let recipient = AccountAddress::from_hex("0x123").unwrap();
682        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
683
684        let raw_txn = TransactionBuilder::new()
685            .sender(sender.address())
686            .sequence_number(0)
687            .payload(payload.into())
688            .chain_id(ChainId::testnet())
689            .build()
690            .unwrap();
691
692        let multi_agent = MultiAgentRawTransaction {
693            raw_txn,
694            secondary_signer_addresses: vec![secondary.address()],
695        };
696
697        let secondary_signers: Vec<&dyn Account> = vec![&secondary];
698        let signed =
699            sign_multi_agent_transaction(&multi_agent, &sender, &secondary_signers).unwrap();
700        assert_eq!(signed.sender(), sender.address());
701        signed.verify_signature().unwrap();
702    }
703
704    #[cfg(feature = "ed25519")]
705    #[test]
706    fn test_sign_multi_agent_transaction_with_single_key_accounts() {
707        use crate::account::{Account, Ed25519SingleKeyAccount};
708        use crate::transaction::authenticator::{AccountAuthenticator, TransactionAuthenticator};
709
710        let sender = Ed25519SingleKeyAccount::generate();
711        let secondary = Ed25519SingleKeyAccount::generate();
712        let recipient = AccountAddress::from_hex("0x123").unwrap();
713        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
714
715        let raw_txn = TransactionBuilder::new()
716            .sender(sender.address())
717            .sequence_number(0)
718            .payload(payload.into())
719            .chain_id(ChainId::testnet())
720            .build()
721            .unwrap();
722
723        let multi_agent = MultiAgentRawTransaction {
724            raw_txn,
725            secondary_signer_addresses: vec![secondary.address()],
726        };
727
728        let secondary_signers: Vec<&dyn Account> = vec![&secondary];
729        let signed =
730            sign_multi_agent_transaction(&multi_agent, &sender, &secondary_signers).unwrap();
731        match &signed.authenticator {
732            TransactionAuthenticator::MultiAgent {
733                sender,
734                secondary_signers,
735                ..
736            } => {
737                assert!(matches!(sender, AccountAuthenticator::SingleKey { .. }));
738                assert!(matches!(
739                    secondary_signers[0],
740                    AccountAuthenticator::SingleKey { .. }
741                ));
742            }
743            _ => panic!("expected MultiAgent authenticator"),
744        }
745        signed.verify_signature().unwrap();
746    }
747
748    #[cfg(feature = "ed25519")]
749    #[test]
750    fn test_multi_agent_verify_signature_rejects_secondary_signer_length_mismatch() {
751        use crate::account::{Account, Ed25519Account};
752        use crate::transaction::authenticator::{AccountAuthenticator, TransactionAuthenticator};
753
754        let sender = Ed25519Account::generate();
755        let secondary = Ed25519Account::generate();
756        let recipient = AccountAddress::from_hex("0x123").unwrap();
757        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
758
759        let raw_txn = TransactionBuilder::new()
760            .sender(sender.address())
761            .sequence_number(0)
762            .payload(payload.into())
763            .chain_id(ChainId::testnet())
764            .build()
765            .unwrap();
766
767        let multi_agent = MultiAgentRawTransaction {
768            raw_txn,
769            secondary_signer_addresses: vec![secondary.address()],
770        };
771
772        let secondary_signers: Vec<&dyn Account> = vec![&secondary];
773        let signed =
774            sign_multi_agent_transaction(&multi_agent, &sender, &secondary_signers).unwrap();
775        signed.verify_signature().unwrap();
776
777        let mut missing_secondary_signer = signed.clone();
778        if let TransactionAuthenticator::MultiAgent {
779            secondary_signers, ..
780        } = &mut missing_secondary_signer.authenticator
781        {
782            secondary_signers.clear();
783        } else {
784            panic!("expected MultiAgent authenticator");
785        }
786        assert!(missing_secondary_signer.verify_signature().is_err());
787
788        let mut extra_secondary_signer = signed;
789        if let TransactionAuthenticator::MultiAgent {
790            secondary_signers, ..
791        } = &mut extra_secondary_signer.authenticator
792        {
793            secondary_signers.push(AccountAuthenticator::NoAccountAuthenticator);
794        } else {
795            panic!("expected MultiAgent authenticator");
796        }
797        assert!(extra_secondary_signer.verify_signature().is_err());
798    }
799
800    #[cfg(feature = "ed25519")]
801    #[test]
802    fn test_sign_fee_payer_transaction() {
803        use crate::account::Ed25519Account;
804
805        let sender = Ed25519Account::generate();
806        let fee_payer = Ed25519Account::generate();
807        let recipient = AccountAddress::from_hex("0x123").unwrap();
808        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
809
810        let raw_txn = TransactionBuilder::new()
811            .sender(sender.address())
812            .sequence_number(0)
813            .payload(payload.into())
814            .chain_id(ChainId::testnet())
815            .build()
816            .unwrap();
817
818        let fee_payer_txn = FeePayerRawTransaction {
819            raw_txn,
820            secondary_signer_addresses: vec![],
821            fee_payer_address: fee_payer.address(),
822        };
823
824        let signed = sign_fee_payer_transaction(&fee_payer_txn, &sender, &[], &fee_payer).unwrap();
825        assert_eq!(signed.sender(), sender.address());
826        signed.verify_signature().unwrap();
827    }
828
829    #[cfg(feature = "ed25519")]
830    #[test]
831    fn test_sign_fee_payer_transaction_with_multi_key_accounts() {
832        use crate::account::{AnyPrivateKey, MultiKeyAccount};
833        use crate::crypto::Ed25519PrivateKey;
834        use crate::transaction::authenticator::{AccountAuthenticator, TransactionAuthenticator};
835
836        let sender = MultiKeyAccount::new(
837            vec![AnyPrivateKey::ed25519(Ed25519PrivateKey::generate())],
838            1,
839        )
840        .unwrap();
841        let fee_payer = MultiKeyAccount::new(
842            vec![AnyPrivateKey::ed25519(Ed25519PrivateKey::generate())],
843            1,
844        )
845        .unwrap();
846        let recipient = AccountAddress::from_hex("0x123").unwrap();
847        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
848
849        let raw_txn = TransactionBuilder::new()
850            .sender(sender.address())
851            .sequence_number(0)
852            .payload(payload.into())
853            .chain_id(ChainId::testnet())
854            .build()
855            .unwrap();
856
857        let fee_payer_txn = FeePayerRawTransaction::new_simple(raw_txn, fee_payer.address());
858        let signed = sign_fee_payer_transaction(&fee_payer_txn, &sender, &[], &fee_payer).unwrap();
859        match &signed.authenticator {
860            TransactionAuthenticator::FeePayer {
861                sender,
862                fee_payer_signer,
863                ..
864            } => {
865                assert!(matches!(sender, AccountAuthenticator::MultiKey { .. }));
866                assert!(matches!(
867                    fee_payer_signer,
868                    AccountAuthenticator::MultiKey { .. }
869                ));
870            }
871            _ => panic!("expected FeePayer authenticator"),
872        }
873        signed.verify_signature().unwrap();
874    }
875
876    #[cfg(feature = "ed25519")]
877    #[test]
878    fn test_fee_payer_verify_signature_rejects_secondary_signer_length_mismatch() {
879        use crate::account::{Account, Ed25519Account};
880        use crate::transaction::authenticator::{AccountAuthenticator, TransactionAuthenticator};
881
882        let sender = Ed25519Account::generate();
883        let secondary = Ed25519Account::generate();
884        let fee_payer = Ed25519Account::generate();
885        let recipient = AccountAddress::from_hex("0x123").unwrap();
886        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
887
888        let raw_txn = TransactionBuilder::new()
889            .sender(sender.address())
890            .sequence_number(0)
891            .payload(payload.into())
892            .chain_id(ChainId::testnet())
893            .build()
894            .unwrap();
895
896        let fee_payer_txn = FeePayerRawTransaction {
897            raw_txn,
898            secondary_signer_addresses: vec![secondary.address()],
899            fee_payer_address: fee_payer.address(),
900        };
901
902        let secondary_signers: Vec<&dyn Account> = vec![&secondary];
903        let signed =
904            sign_fee_payer_transaction(&fee_payer_txn, &sender, &secondary_signers, &fee_payer)
905                .unwrap();
906        signed.verify_signature().unwrap();
907
908        let mut missing_secondary_signer = signed.clone();
909        if let TransactionAuthenticator::FeePayer {
910            secondary_signers, ..
911        } = &mut missing_secondary_signer.authenticator
912        {
913            secondary_signers.clear();
914        } else {
915            panic!("expected FeePayer authenticator");
916        }
917        assert!(missing_secondary_signer.verify_signature().is_err());
918
919        let mut extra_secondary_signer = signed;
920        if let TransactionAuthenticator::FeePayer {
921            secondary_signers, ..
922        } = &mut extra_secondary_signer.authenticator
923        {
924            secondary_signers.push(AccountAuthenticator::NoAccountAuthenticator);
925        } else {
926            panic!("expected FeePayer authenticator");
927        }
928        assert!(extra_secondary_signer.verify_signature().is_err());
929    }
930
931    #[test]
932    fn test_build_simulation_signed_multi_agent() {
933        use crate::transaction::authenticator::{AccountAuthenticator, TransactionAuthenticator};
934
935        let recipient = AccountAddress::from_hex("0x123").unwrap();
936        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
937        let raw_txn = TransactionBuilder::new()
938            .sender(AccountAddress::ONE)
939            .sequence_number(0)
940            .payload(payload.into())
941            .chain_id(ChainId::testnet())
942            .build()
943            .unwrap();
944
945        let secondary_addr = AccountAddress::from_hex("0x456").unwrap();
946        let multi_agent = MultiAgentRawTransaction::new(raw_txn.clone(), vec![secondary_addr]);
947
948        let signed = build_simulation_signed_multi_agent(&multi_agent);
949        assert_eq!(signed.sender(), AccountAddress::ONE);
950        assert!(signed.to_bcs().is_ok());
951
952        match &signed.authenticator {
953            TransactionAuthenticator::MultiAgent {
954                sender,
955                secondary_signer_addresses,
956                secondary_signers,
957            } => {
958                assert!(matches!(
959                    sender,
960                    AccountAuthenticator::NoAccountAuthenticator
961                ));
962                assert_eq!(secondary_signer_addresses.len(), 1);
963                assert_eq!(secondary_signer_addresses[0], secondary_addr);
964                assert_eq!(secondary_signers.len(), 1);
965                assert!(matches!(
966                    &secondary_signers[0],
967                    AccountAuthenticator::NoAccountAuthenticator
968                ));
969            }
970            _ => panic!("expected MultiAgent authenticator"),
971        }
972    }
973
974    #[test]
975    fn test_build_simulation_signed_fee_payer() {
976        use crate::transaction::authenticator::{AccountAuthenticator, TransactionAuthenticator};
977
978        let recipient = AccountAddress::from_hex("0x123").unwrap();
979        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
980        let raw_txn = TransactionBuilder::new()
981            .sender(AccountAddress::ONE)
982            .sequence_number(0)
983            .payload(payload.into())
984            .chain_id(ChainId::testnet())
985            .build()
986            .unwrap();
987
988        let fee_payer_addr = AccountAddress::from_hex("0x789").unwrap();
989        let fee_payer_txn = FeePayerRawTransaction::new_simple(raw_txn.clone(), fee_payer_addr);
990
991        let signed = build_simulation_signed_fee_payer(&fee_payer_txn);
992        assert_eq!(signed.sender(), AccountAddress::ONE);
993        assert!(signed.to_bcs().is_ok());
994
995        match &signed.authenticator {
996            TransactionAuthenticator::FeePayer {
997                sender,
998                fee_payer_address,
999                fee_payer_signer,
1000                ..
1001            } => {
1002                assert!(matches!(
1003                    sender,
1004                    AccountAuthenticator::NoAccountAuthenticator
1005                ));
1006                assert_eq!(*fee_payer_address, fee_payer_addr);
1007                assert!(matches!(
1008                    fee_payer_signer,
1009                    AccountAuthenticator::NoAccountAuthenticator
1010                ));
1011            }
1012            _ => panic!("expected FeePayer authenticator"),
1013        }
1014    }
1015
1016    #[test]
1017    fn test_default_impl() {
1018        let builder = TransactionBuilder::default();
1019        // Verify defaults are set
1020        assert!(builder.sender.is_none());
1021        assert!(builder.sequence_number.is_none());
1022        assert!(builder.payload.is_none());
1023        assert_eq!(builder.max_gas_amount, DEFAULT_MAX_GAS_AMOUNT);
1024        assert_eq!(builder.gas_unit_price, DEFAULT_GAS_UNIT_PRICE);
1025    }
1026}