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        _ => Err(AptosError::InvalidSignature(format!(
247            "unknown signature scheme: {scheme}"
248        ))),
249    }
250}
251
252/// Creates an account authenticator based on the signature scheme.
253///
254/// # Errors
255///
256/// Returns an error if the signature scheme is not recognized.
257fn make_account_authenticator(
258    scheme: u8,
259    public_key: Vec<u8>,
260    signature: Vec<u8>,
261) -> AptosResult<AccountAuthenticator> {
262    match scheme {
263        crate::crypto::ED25519_SCHEME => Ok(AccountAuthenticator::ed25519(public_key, signature)),
264        crate::crypto::MULTI_ED25519_SCHEME => Ok(AccountAuthenticator::MultiEd25519 {
265            public_key,
266            signature,
267        }),
268        crate::crypto::SINGLE_KEY_SCHEME => {
269            let _ = AnyPublicKey::from_bcs_bytes(&public_key)?;
270            let _ = AnySignature::from_bcs_bytes(&signature)?;
271            Ok(AccountAuthenticator::single_key(public_key, signature))
272        }
273        crate::crypto::MULTI_KEY_SCHEME => {
274            let _ = MultiKeyPublicKey::from_bytes(&public_key)?;
275            let _ = MultiKeySignature::from_bytes(&signature)?;
276            Ok(AccountAuthenticator::multi_key(public_key, signature))
277        }
278        _ => Err(AptosError::InvalidSignature(format!(
279            "unknown signature scheme: {scheme}"
280        ))),
281    }
282}
283
284/// Signs a multi-agent transaction.
285///
286/// # Errors
287///
288/// Returns an error if generating the signing message fails or if any signer fails to sign.
289pub fn sign_multi_agent_transaction<A: Account>(
290    multi_agent: &MultiAgentRawTransaction,
291    sender: &A,
292    secondary_signers: &[&dyn Account],
293) -> AptosResult<SignedTransaction> {
294    let signing_message = multi_agent.signing_message()?;
295
296    // Sign with sender
297    let sender_signature = sender.sign(&signing_message)?;
298    let sender_public_key = sender.public_key_bytes();
299    let sender_auth = make_account_authenticator(
300        sender.signature_scheme(),
301        sender_public_key,
302        sender_signature,
303    )?;
304
305    // Sign with secondary signers
306    let mut secondary_auths = Vec::with_capacity(secondary_signers.len());
307    for signer in secondary_signers {
308        let signature = signer.sign(&signing_message)?;
309        let public_key = signer.public_key_bytes();
310        secondary_auths.push(make_account_authenticator(
311            signer.signature_scheme(),
312            public_key,
313            signature,
314        )?);
315    }
316
317    let authenticator = TransactionAuthenticator::multi_agent(
318        sender_auth,
319        multi_agent.secondary_signer_addresses.clone(),
320        secondary_auths,
321    );
322
323    Ok(SignedTransaction::new(
324        multi_agent.raw_txn.clone(),
325        authenticator,
326    ))
327}
328
329/// Signs a fee payer transaction.
330///
331/// # Errors
332///
333/// Returns an error if generating the signing message fails or if any signer fails to sign.
334pub fn sign_fee_payer_transaction<A: Account>(
335    fee_payer_txn: &FeePayerRawTransaction,
336    sender: &A,
337    secondary_signers: &[&dyn Account],
338    fee_payer: &dyn Account,
339) -> AptosResult<SignedTransaction> {
340    let signing_message = fee_payer_txn.signing_message()?;
341
342    // Sign with sender
343    let sender_signature = sender.sign(&signing_message)?;
344    let sender_public_key = sender.public_key_bytes();
345    let sender_auth = make_account_authenticator(
346        sender.signature_scheme(),
347        sender_public_key,
348        sender_signature,
349    )?;
350
351    // Sign with secondary signers
352    let mut secondary_auths = Vec::with_capacity(secondary_signers.len());
353    for signer in secondary_signers {
354        let signature = signer.sign(&signing_message)?;
355        let public_key = signer.public_key_bytes();
356        secondary_auths.push(make_account_authenticator(
357            signer.signature_scheme(),
358            public_key,
359            signature,
360        )?);
361    }
362
363    // Sign with fee payer
364    let fee_payer_signature = fee_payer.sign(&signing_message)?;
365    let fee_payer_public_key = fee_payer.public_key_bytes();
366    let fee_payer_auth = make_account_authenticator(
367        fee_payer.signature_scheme(),
368        fee_payer_public_key,
369        fee_payer_signature,
370    )?;
371
372    let authenticator = TransactionAuthenticator::fee_payer(
373        sender_auth,
374        fee_payer_txn.secondary_signer_addresses.clone(),
375        secondary_auths,
376        fee_payer_txn.fee_payer_address,
377        fee_payer_auth,
378    );
379
380    Ok(SignedTransaction::new(
381        fee_payer_txn.raw_txn.clone(),
382        authenticator,
383    ))
384}
385
386/// Builds a signed transaction for simulation only (multi-agent).
387///
388/// Uses [`AccountAuthenticator::NoAccountAuthenticator`] for all signers so that
389/// the transaction can be sent to the simulate endpoint without real signatures.
390/// Use this to simulate a multi-agent transaction before collecting signatures.
391///
392/// # Example
393///
394/// ```rust,ignore
395/// let multi_agent = MultiAgentRawTransaction::new(raw_txn, secondary_addresses);
396/// let signed = build_simulation_signed_multi_agent(&multi_agent);
397/// let result = aptos.simulate_signed(&signed).await?;
398/// ```
399pub fn build_simulation_signed_multi_agent(
400    multi_agent: &MultiAgentRawTransaction,
401) -> SignedTransaction {
402    let sender_auth = AccountAuthenticator::no_account_authenticator();
403    let secondary_auths: Vec<AccountAuthenticator> = multi_agent
404        .secondary_signer_addresses
405        .iter()
406        .map(|_| AccountAuthenticator::no_account_authenticator())
407        .collect();
408    let authenticator = TransactionAuthenticator::multi_agent(
409        sender_auth,
410        multi_agent.secondary_signer_addresses.clone(),
411        secondary_auths,
412    );
413    SignedTransaction::new(multi_agent.raw_txn.clone(), authenticator)
414}
415
416/// Builds a signed transaction for simulation only (fee-payer / sponsored).
417///
418/// Uses [`AccountAuthenticator::NoAccountAuthenticator`] for all signers (sender,
419/// secondary signers, and fee payer) so that the transaction can be sent to the
420/// simulate endpoint without real signatures. Use this to simulate a fee-payer
421/// transaction before collecting signatures.
422///
423/// # Example
424///
425/// ```rust,ignore
426/// let fee_payer_txn = FeePayerRawTransaction::new_simple(raw_txn, fee_payer_address);
427/// let signed = build_simulation_signed_fee_payer(&fee_payer_txn);
428/// let result = aptos.simulate_signed(&signed).await?;
429/// ```
430pub fn build_simulation_signed_fee_payer(
431    fee_payer_txn: &FeePayerRawTransaction,
432) -> SignedTransaction {
433    let sender_auth = AccountAuthenticator::no_account_authenticator();
434    let secondary_auths: Vec<AccountAuthenticator> = fee_payer_txn
435        .secondary_signer_addresses
436        .iter()
437        .map(|_| AccountAuthenticator::no_account_authenticator())
438        .collect();
439    let fee_payer_auth = AccountAuthenticator::no_account_authenticator();
440    let authenticator = TransactionAuthenticator::fee_payer(
441        sender_auth,
442        fee_payer_txn.secondary_signer_addresses.clone(),
443        secondary_auths,
444        fee_payer_txn.fee_payer_address,
445        fee_payer_auth,
446    );
447    SignedTransaction::new(fee_payer_txn.raw_txn.clone(), authenticator)
448}
449
450#[cfg(test)]
451mod tests {
452    use super::*;
453    use crate::transaction::payload::EntryFunction;
454
455    #[test]
456    fn test_builder_missing_fields() {
457        let result = TransactionBuilder::new().build();
458        assert!(result.is_err());
459    }
460
461    #[test]
462    fn test_builder_complete() {
463        let recipient = AccountAddress::from_hex("0x123").unwrap();
464        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
465
466        let txn = TransactionBuilder::new()
467            .sender(AccountAddress::ONE)
468            .sequence_number(0)
469            .payload(payload.into())
470            .chain_id(ChainId::testnet())
471            .build()
472            .unwrap();
473
474        assert_eq!(txn.sender, AccountAddress::ONE);
475        assert_eq!(txn.sequence_number, 0);
476        assert_eq!(txn.max_gas_amount, DEFAULT_MAX_GAS_AMOUNT);
477        assert_eq!(txn.gas_unit_price, DEFAULT_GAS_UNIT_PRICE);
478    }
479
480    #[test]
481    fn test_builder_custom_gas() {
482        let recipient = AccountAddress::from_hex("0x123").unwrap();
483        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
484
485        let txn = TransactionBuilder::new()
486            .sender(AccountAddress::ONE)
487            .sequence_number(0)
488            .payload(payload.into())
489            .max_gas_amount(500_000)
490            .gas_unit_price(200)
491            .chain_id(ChainId::testnet())
492            .build()
493            .unwrap();
494
495        assert_eq!(txn.max_gas_amount, 500_000);
496        assert_eq!(txn.gas_unit_price, 200);
497    }
498
499    #[test]
500    fn test_builder_missing_sender() {
501        let recipient = AccountAddress::from_hex("0x123").unwrap();
502        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
503
504        let result = TransactionBuilder::new()
505            .sequence_number(0)
506            .payload(payload.into())
507            .chain_id(ChainId::testnet())
508            .build();
509
510        assert!(result.is_err());
511    }
512
513    #[test]
514    fn test_builder_missing_payload() {
515        let result = TransactionBuilder::new()
516            .sender(AccountAddress::ONE)
517            .sequence_number(0)
518            .chain_id(ChainId::testnet())
519            .build();
520
521        assert!(result.is_err());
522    }
523
524    #[test]
525    fn test_builder_missing_chain_id() {
526        let recipient = AccountAddress::from_hex("0x123").unwrap();
527        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
528
529        let result = TransactionBuilder::new()
530            .sender(AccountAddress::ONE)
531            .sequence_number(0)
532            .payload(payload.into())
533            .build();
534
535        assert!(result.is_err());
536    }
537
538    #[test]
539    fn test_builder_custom_expiration() {
540        let recipient = AccountAddress::from_hex("0x123").unwrap();
541        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
542        let custom_expiration = 9_999_999_999;
543
544        let txn = TransactionBuilder::new()
545            .sender(AccountAddress::ONE)
546            .sequence_number(0)
547            .payload(payload.into())
548            .expiration_timestamp_secs(custom_expiration)
549            .chain_id(ChainId::testnet())
550            .build()
551            .unwrap();
552
553        assert_eq!(txn.expiration_timestamp_secs, custom_expiration);
554    }
555
556    #[test]
557    fn test_default_expiration() {
558        // Default expiration should be set to about DEFAULT_EXPIRATION_SECONDS from now
559        let now = std::time::SystemTime::now()
560            .duration_since(std::time::UNIX_EPOCH)
561            .unwrap()
562            .as_secs();
563
564        let recipient = AccountAddress::from_hex("0x123").unwrap();
565        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
566
567        let txn = TransactionBuilder::new()
568            .sender(AccountAddress::ONE)
569            .sequence_number(0)
570            .payload(payload.into())
571            .chain_id(ChainId::testnet())
572            .build()
573            .unwrap();
574
575        // Should be approximately now + DEFAULT_EXPIRATION_SECONDS (give or take a second)
576        let expected_min = now + DEFAULT_EXPIRATION_SECONDS - 5;
577        let expected_max = now + DEFAULT_EXPIRATION_SECONDS + 5;
578        assert!(txn.expiration_timestamp_secs >= expected_min);
579        assert!(txn.expiration_timestamp_secs <= expected_max);
580    }
581
582    #[cfg(feature = "ed25519")]
583    #[test]
584    fn test_sign_transaction() {
585        use crate::account::Ed25519Account;
586
587        let account = Ed25519Account::generate();
588        let recipient = AccountAddress::from_hex("0x123").unwrap();
589        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
590
591        let txn = TransactionBuilder::new()
592            .sender(account.address())
593            .sequence_number(0)
594            .payload(payload.into())
595            .chain_id(ChainId::testnet())
596            .build()
597            .unwrap();
598
599        let signed = sign_transaction(&txn, &account).unwrap();
600        assert_eq!(signed.sender(), account.address());
601        signed.verify_signature().unwrap();
602    }
603
604    #[cfg(feature = "ed25519")]
605    #[test]
606    fn test_sign_transaction_with_ed25519_single_key_account() {
607        use crate::account::Ed25519SingleKeyAccount;
608        use crate::transaction::authenticator::{AccountAuthenticator, TransactionAuthenticator};
609
610        let account = Ed25519SingleKeyAccount::generate();
611        let recipient = AccountAddress::from_hex("0x123").unwrap();
612        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
613
614        let txn = TransactionBuilder::new()
615            .sender(account.address())
616            .sequence_number(0)
617            .payload(payload.into())
618            .chain_id(ChainId::testnet())
619            .build()
620            .unwrap();
621
622        let signed = sign_transaction(&txn, &account).unwrap();
623        match &signed.authenticator {
624            TransactionAuthenticator::SingleSender { sender } => {
625                assert!(matches!(sender, AccountAuthenticator::SingleKey { .. }));
626            }
627            _ => panic!("expected SingleSender authenticator"),
628        }
629        signed.verify_signature().unwrap();
630    }
631
632    #[cfg(all(feature = "ed25519", feature = "secp256k1"))]
633    #[test]
634    fn test_sign_transaction_with_mixed_multi_key_account() {
635        use crate::account::{AnyPrivateKey, MultiKeyAccount};
636        use crate::crypto::{Ed25519PrivateKey, Secp256k1PrivateKey};
637
638        let account = MultiKeyAccount::new(
639            vec![
640                AnyPrivateKey::ed25519(Ed25519PrivateKey::generate()),
641                AnyPrivateKey::secp256k1(Secp256k1PrivateKey::generate()),
642                AnyPrivateKey::ed25519(Ed25519PrivateKey::generate()),
643            ],
644            2,
645        )
646        .unwrap();
647        let recipient = AccountAddress::from_hex("0x123").unwrap();
648        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
649
650        let txn = TransactionBuilder::new()
651            .sender(account.address())
652            .sequence_number(0)
653            .payload(payload.into())
654            .chain_id(ChainId::testnet())
655            .build()
656            .unwrap();
657
658        let signed = sign_transaction(&txn, &account).unwrap();
659        assert_eq!(signed.sender(), account.address());
660        signed.verify_signature().unwrap();
661    }
662
663    #[cfg(feature = "ed25519")]
664    #[test]
665    fn test_sign_multi_agent_transaction() {
666        use crate::account::{Account, Ed25519Account};
667
668        let sender = Ed25519Account::generate();
669        let secondary = Ed25519Account::generate();
670        let recipient = AccountAddress::from_hex("0x123").unwrap();
671        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
672
673        let raw_txn = TransactionBuilder::new()
674            .sender(sender.address())
675            .sequence_number(0)
676            .payload(payload.into())
677            .chain_id(ChainId::testnet())
678            .build()
679            .unwrap();
680
681        let multi_agent = MultiAgentRawTransaction {
682            raw_txn,
683            secondary_signer_addresses: vec![secondary.address()],
684        };
685
686        let secondary_signers: Vec<&dyn Account> = vec![&secondary];
687        let signed =
688            sign_multi_agent_transaction(&multi_agent, &sender, &secondary_signers).unwrap();
689        assert_eq!(signed.sender(), sender.address());
690        signed.verify_signature().unwrap();
691    }
692
693    #[cfg(feature = "ed25519")]
694    #[test]
695    fn test_sign_multi_agent_transaction_with_single_key_accounts() {
696        use crate::account::{Account, Ed25519SingleKeyAccount};
697        use crate::transaction::authenticator::{AccountAuthenticator, TransactionAuthenticator};
698
699        let sender = Ed25519SingleKeyAccount::generate();
700        let secondary = Ed25519SingleKeyAccount::generate();
701        let recipient = AccountAddress::from_hex("0x123").unwrap();
702        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
703
704        let raw_txn = TransactionBuilder::new()
705            .sender(sender.address())
706            .sequence_number(0)
707            .payload(payload.into())
708            .chain_id(ChainId::testnet())
709            .build()
710            .unwrap();
711
712        let multi_agent = MultiAgentRawTransaction {
713            raw_txn,
714            secondary_signer_addresses: vec![secondary.address()],
715        };
716
717        let secondary_signers: Vec<&dyn Account> = vec![&secondary];
718        let signed =
719            sign_multi_agent_transaction(&multi_agent, &sender, &secondary_signers).unwrap();
720        match &signed.authenticator {
721            TransactionAuthenticator::MultiAgent {
722                sender,
723                secondary_signers,
724                ..
725            } => {
726                assert!(matches!(sender, AccountAuthenticator::SingleKey { .. }));
727                assert!(matches!(
728                    secondary_signers[0],
729                    AccountAuthenticator::SingleKey { .. }
730                ));
731            }
732            _ => panic!("expected MultiAgent authenticator"),
733        }
734        signed.verify_signature().unwrap();
735    }
736
737    #[cfg(feature = "ed25519")]
738    #[test]
739    fn test_multi_agent_verify_signature_rejects_secondary_signer_length_mismatch() {
740        use crate::account::{Account, Ed25519Account};
741        use crate::transaction::authenticator::{AccountAuthenticator, TransactionAuthenticator};
742
743        let sender = Ed25519Account::generate();
744        let secondary = Ed25519Account::generate();
745        let recipient = AccountAddress::from_hex("0x123").unwrap();
746        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
747
748        let raw_txn = TransactionBuilder::new()
749            .sender(sender.address())
750            .sequence_number(0)
751            .payload(payload.into())
752            .chain_id(ChainId::testnet())
753            .build()
754            .unwrap();
755
756        let multi_agent = MultiAgentRawTransaction {
757            raw_txn,
758            secondary_signer_addresses: vec![secondary.address()],
759        };
760
761        let secondary_signers: Vec<&dyn Account> = vec![&secondary];
762        let signed =
763            sign_multi_agent_transaction(&multi_agent, &sender, &secondary_signers).unwrap();
764        signed.verify_signature().unwrap();
765
766        let mut missing_secondary_signer = signed.clone();
767        if let TransactionAuthenticator::MultiAgent {
768            secondary_signers, ..
769        } = &mut missing_secondary_signer.authenticator
770        {
771            secondary_signers.clear();
772        } else {
773            panic!("expected MultiAgent authenticator");
774        }
775        assert!(missing_secondary_signer.verify_signature().is_err());
776
777        let mut extra_secondary_signer = signed;
778        if let TransactionAuthenticator::MultiAgent {
779            secondary_signers, ..
780        } = &mut extra_secondary_signer.authenticator
781        {
782            secondary_signers.push(AccountAuthenticator::NoAccountAuthenticator);
783        } else {
784            panic!("expected MultiAgent authenticator");
785        }
786        assert!(extra_secondary_signer.verify_signature().is_err());
787    }
788
789    #[cfg(feature = "ed25519")]
790    #[test]
791    fn test_sign_fee_payer_transaction() {
792        use crate::account::Ed25519Account;
793
794        let sender = Ed25519Account::generate();
795        let fee_payer = Ed25519Account::generate();
796        let recipient = AccountAddress::from_hex("0x123").unwrap();
797        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
798
799        let raw_txn = TransactionBuilder::new()
800            .sender(sender.address())
801            .sequence_number(0)
802            .payload(payload.into())
803            .chain_id(ChainId::testnet())
804            .build()
805            .unwrap();
806
807        let fee_payer_txn = FeePayerRawTransaction {
808            raw_txn,
809            secondary_signer_addresses: vec![],
810            fee_payer_address: fee_payer.address(),
811        };
812
813        let signed = sign_fee_payer_transaction(&fee_payer_txn, &sender, &[], &fee_payer).unwrap();
814        assert_eq!(signed.sender(), sender.address());
815        signed.verify_signature().unwrap();
816    }
817
818    #[cfg(feature = "ed25519")]
819    #[test]
820    fn test_sign_fee_payer_transaction_with_multi_key_accounts() {
821        use crate::account::{AnyPrivateKey, MultiKeyAccount};
822        use crate::crypto::Ed25519PrivateKey;
823        use crate::transaction::authenticator::{AccountAuthenticator, TransactionAuthenticator};
824
825        let sender = MultiKeyAccount::new(
826            vec![AnyPrivateKey::ed25519(Ed25519PrivateKey::generate())],
827            1,
828        )
829        .unwrap();
830        let fee_payer = MultiKeyAccount::new(
831            vec![AnyPrivateKey::ed25519(Ed25519PrivateKey::generate())],
832            1,
833        )
834        .unwrap();
835        let recipient = AccountAddress::from_hex("0x123").unwrap();
836        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
837
838        let raw_txn = TransactionBuilder::new()
839            .sender(sender.address())
840            .sequence_number(0)
841            .payload(payload.into())
842            .chain_id(ChainId::testnet())
843            .build()
844            .unwrap();
845
846        let fee_payer_txn = FeePayerRawTransaction::new_simple(raw_txn, fee_payer.address());
847        let signed = sign_fee_payer_transaction(&fee_payer_txn, &sender, &[], &fee_payer).unwrap();
848        match &signed.authenticator {
849            TransactionAuthenticator::FeePayer {
850                sender,
851                fee_payer_signer,
852                ..
853            } => {
854                assert!(matches!(sender, AccountAuthenticator::MultiKey { .. }));
855                assert!(matches!(
856                    fee_payer_signer,
857                    AccountAuthenticator::MultiKey { .. }
858                ));
859            }
860            _ => panic!("expected FeePayer authenticator"),
861        }
862        signed.verify_signature().unwrap();
863    }
864
865    #[cfg(feature = "ed25519")]
866    #[test]
867    fn test_fee_payer_verify_signature_rejects_secondary_signer_length_mismatch() {
868        use crate::account::{Account, Ed25519Account};
869        use crate::transaction::authenticator::{AccountAuthenticator, TransactionAuthenticator};
870
871        let sender = Ed25519Account::generate();
872        let secondary = Ed25519Account::generate();
873        let fee_payer = Ed25519Account::generate();
874        let recipient = AccountAddress::from_hex("0x123").unwrap();
875        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
876
877        let raw_txn = TransactionBuilder::new()
878            .sender(sender.address())
879            .sequence_number(0)
880            .payload(payload.into())
881            .chain_id(ChainId::testnet())
882            .build()
883            .unwrap();
884
885        let fee_payer_txn = FeePayerRawTransaction {
886            raw_txn,
887            secondary_signer_addresses: vec![secondary.address()],
888            fee_payer_address: fee_payer.address(),
889        };
890
891        let secondary_signers: Vec<&dyn Account> = vec![&secondary];
892        let signed =
893            sign_fee_payer_transaction(&fee_payer_txn, &sender, &secondary_signers, &fee_payer)
894                .unwrap();
895        signed.verify_signature().unwrap();
896
897        let mut missing_secondary_signer = signed.clone();
898        if let TransactionAuthenticator::FeePayer {
899            secondary_signers, ..
900        } = &mut missing_secondary_signer.authenticator
901        {
902            secondary_signers.clear();
903        } else {
904            panic!("expected FeePayer authenticator");
905        }
906        assert!(missing_secondary_signer.verify_signature().is_err());
907
908        let mut extra_secondary_signer = signed;
909        if let TransactionAuthenticator::FeePayer {
910            secondary_signers, ..
911        } = &mut extra_secondary_signer.authenticator
912        {
913            secondary_signers.push(AccountAuthenticator::NoAccountAuthenticator);
914        } else {
915            panic!("expected FeePayer authenticator");
916        }
917        assert!(extra_secondary_signer.verify_signature().is_err());
918    }
919
920    #[test]
921    fn test_build_simulation_signed_multi_agent() {
922        use crate::transaction::authenticator::{AccountAuthenticator, TransactionAuthenticator};
923
924        let recipient = AccountAddress::from_hex("0x123").unwrap();
925        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
926        let raw_txn = TransactionBuilder::new()
927            .sender(AccountAddress::ONE)
928            .sequence_number(0)
929            .payload(payload.into())
930            .chain_id(ChainId::testnet())
931            .build()
932            .unwrap();
933
934        let secondary_addr = AccountAddress::from_hex("0x456").unwrap();
935        let multi_agent = MultiAgentRawTransaction::new(raw_txn.clone(), vec![secondary_addr]);
936
937        let signed = build_simulation_signed_multi_agent(&multi_agent);
938        assert_eq!(signed.sender(), AccountAddress::ONE);
939        assert!(signed.to_bcs().is_ok());
940
941        match &signed.authenticator {
942            TransactionAuthenticator::MultiAgent {
943                sender,
944                secondary_signer_addresses,
945                secondary_signers,
946            } => {
947                assert!(matches!(
948                    sender,
949                    AccountAuthenticator::NoAccountAuthenticator
950                ));
951                assert_eq!(secondary_signer_addresses.len(), 1);
952                assert_eq!(secondary_signer_addresses[0], secondary_addr);
953                assert_eq!(secondary_signers.len(), 1);
954                assert!(matches!(
955                    &secondary_signers[0],
956                    AccountAuthenticator::NoAccountAuthenticator
957                ));
958            }
959            _ => panic!("expected MultiAgent authenticator"),
960        }
961    }
962
963    #[test]
964    fn test_build_simulation_signed_fee_payer() {
965        use crate::transaction::authenticator::{AccountAuthenticator, TransactionAuthenticator};
966
967        let recipient = AccountAddress::from_hex("0x123").unwrap();
968        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
969        let raw_txn = TransactionBuilder::new()
970            .sender(AccountAddress::ONE)
971            .sequence_number(0)
972            .payload(payload.into())
973            .chain_id(ChainId::testnet())
974            .build()
975            .unwrap();
976
977        let fee_payer_addr = AccountAddress::from_hex("0x789").unwrap();
978        let fee_payer_txn = FeePayerRawTransaction::new_simple(raw_txn.clone(), fee_payer_addr);
979
980        let signed = build_simulation_signed_fee_payer(&fee_payer_txn);
981        assert_eq!(signed.sender(), AccountAddress::ONE);
982        assert!(signed.to_bcs().is_ok());
983
984        match &signed.authenticator {
985            TransactionAuthenticator::FeePayer {
986                sender,
987                fee_payer_address,
988                fee_payer_signer,
989                ..
990            } => {
991                assert!(matches!(
992                    sender,
993                    AccountAuthenticator::NoAccountAuthenticator
994                ));
995                assert_eq!(*fee_payer_address, fee_payer_addr);
996                assert!(matches!(
997                    fee_payer_signer,
998                    AccountAuthenticator::NoAccountAuthenticator
999                ));
1000            }
1001            _ => panic!("expected FeePayer authenticator"),
1002        }
1003    }
1004
1005    #[test]
1006    fn test_default_impl() {
1007        let builder = TransactionBuilder::default();
1008        // Verify defaults are set
1009        assert!(builder.sender.is_none());
1010        assert!(builder.sequence_number.is_none());
1011        assert!(builder.payload.is_none());
1012        assert_eq!(builder.max_gas_amount, DEFAULT_MAX_GAS_AMOUNT);
1013        assert_eq!(builder.gas_unit_price, DEFAULT_GAS_UNIT_PRICE);
1014    }
1015}