Skip to main content

aptos_sdk/transaction/
builder.rs

1//! Transaction builder.
2
3use crate::account::Account;
4use crate::error::{AptosError, AptosResult};
5use crate::transaction::authenticator::{AccountAuthenticator, TransactionAuthenticator};
6use crate::transaction::payload::TransactionPayload;
7use crate::transaction::types::{
8    FeePayerRawTransaction, MultiAgentRawTransaction, RawTransaction, SignedTransaction,
9};
10use crate::types::{AccountAddress, ChainId};
11use std::time::{SystemTime, UNIX_EPOCH};
12
13/// Default maximum gas amount.
14pub const DEFAULT_MAX_GAS_AMOUNT: u64 = 200_000;
15/// Default gas unit price in octas.
16pub const DEFAULT_GAS_UNIT_PRICE: u64 = 100;
17/// Default transaction expiration time in seconds.
18pub const DEFAULT_EXPIRATION_SECONDS: u64 = 600; // 10 minutes
19
20/// A builder for constructing transactions.
21///
22/// # Example
23///
24/// ```rust,no_run
25/// use aptos_sdk::transaction::{TransactionBuilder, EntryFunction};
26/// use aptos_sdk::types::{AccountAddress, ChainId};
27///
28/// let payload = EntryFunction::apt_transfer(
29///     AccountAddress::from_hex("0x123").unwrap(),
30///     1000,
31/// ).unwrap();
32///
33/// let txn = TransactionBuilder::new()
34///     .sender(AccountAddress::ONE)
35///     .sequence_number(0)
36///     .payload(payload.into())
37///     .chain_id(ChainId::testnet())
38///     .build()
39///     .unwrap();
40/// ```
41#[derive(Debug, Clone)]
42pub struct TransactionBuilder {
43    sender: Option<AccountAddress>,
44    sequence_number: Option<u64>,
45    payload: Option<TransactionPayload>,
46    max_gas_amount: u64,
47    gas_unit_price: u64,
48    expiration_timestamp_secs: Option<u64>,
49    chain_id: Option<ChainId>,
50}
51
52impl Default for TransactionBuilder {
53    fn default() -> Self {
54        Self::new()
55    }
56}
57
58impl TransactionBuilder {
59    /// Creates a new transaction builder with default values.
60    #[must_use]
61    pub fn new() -> Self {
62        Self {
63            sender: None,
64            sequence_number: None,
65            payload: None,
66            max_gas_amount: DEFAULT_MAX_GAS_AMOUNT,
67            gas_unit_price: DEFAULT_GAS_UNIT_PRICE,
68            expiration_timestamp_secs: None,
69            chain_id: None,
70        }
71    }
72
73    /// Sets the sender address.
74    #[must_use]
75    pub fn sender(mut self, sender: AccountAddress) -> Self {
76        self.sender = Some(sender);
77        self
78    }
79
80    /// Sets the sequence number.
81    #[must_use]
82    pub fn sequence_number(mut self, sequence_number: u64) -> Self {
83        self.sequence_number = Some(sequence_number);
84        self
85    }
86
87    /// Sets the transaction payload.
88    #[must_use]
89    pub fn payload(mut self, payload: TransactionPayload) -> Self {
90        self.payload = Some(payload);
91        self
92    }
93
94    /// Sets the maximum gas amount.
95    #[must_use]
96    pub fn max_gas_amount(mut self, max_gas_amount: u64) -> Self {
97        self.max_gas_amount = max_gas_amount;
98        self
99    }
100
101    /// Sets the gas unit price in octas.
102    #[must_use]
103    pub fn gas_unit_price(mut self, gas_unit_price: u64) -> Self {
104        self.gas_unit_price = gas_unit_price;
105        self
106    }
107
108    /// Sets the expiration timestamp in seconds since Unix epoch.
109    #[must_use]
110    pub fn expiration_timestamp_secs(mut self, expiration_timestamp_secs: u64) -> Self {
111        self.expiration_timestamp_secs = Some(expiration_timestamp_secs);
112        self
113    }
114
115    /// Sets the expiration time relative to now.
116    ///
117    /// Uses saturating arithmetic to handle edge cases like system time going backwards.
118    #[must_use]
119    pub fn expiration_from_now(mut self, seconds: u64) -> Self {
120        let now = SystemTime::now()
121            .duration_since(UNIX_EPOCH)
122            .unwrap_or_default()
123            .as_secs();
124        self.expiration_timestamp_secs = Some(now.saturating_add(seconds));
125        self
126    }
127
128    /// Sets the chain ID.
129    #[must_use]
130    pub fn chain_id(mut self, chain_id: ChainId) -> Self {
131        self.chain_id = Some(chain_id);
132        self
133    }
134
135    /// Builds the raw transaction.
136    ///
137    /// # Errors
138    ///
139    /// Returns an error if any required field is missing:
140    /// - `sender` is required
141    /// - `sequence_number` is required
142    /// - `payload` is required
143    /// - `chain_id` is required
144    pub fn build(self) -> AptosResult<RawTransaction> {
145        let sender = self
146            .sender
147            .ok_or_else(|| AptosError::transaction("sender is required"))?;
148        let sequence_number = self
149            .sequence_number
150            .ok_or_else(|| AptosError::transaction("sequence_number is required"))?;
151        let payload = self
152            .payload
153            .ok_or_else(|| AptosError::transaction("payload is required"))?;
154        let chain_id = self
155            .chain_id
156            .ok_or_else(|| AptosError::transaction("chain_id is required"))?;
157
158        let expiration_timestamp_secs = self.expiration_timestamp_secs.unwrap_or_else(|| {
159            SystemTime::now()
160                .duration_since(UNIX_EPOCH)
161                .unwrap_or_default()
162                .as_secs()
163                .saturating_add(DEFAULT_EXPIRATION_SECONDS)
164        });
165
166        Ok(RawTransaction::new(
167            sender,
168            sequence_number,
169            payload,
170            self.max_gas_amount,
171            self.gas_unit_price,
172            expiration_timestamp_secs,
173            chain_id,
174        ))
175    }
176
177    /// Builds and signs the transaction with the given account.
178    ///
179    /// # Errors
180    ///
181    /// Returns an error if the transaction cannot be built or signed.
182    #[cfg(feature = "ed25519")]
183    pub fn build_and_sign<A: Account>(self, account: &A) -> AptosResult<SignedTransaction> {
184        let sender = self.sender.unwrap_or_else(|| account.address());
185        let raw_txn = Self {
186            sender: Some(sender),
187            ..self
188        }
189        .build()?;
190
191        sign_transaction(&raw_txn, account)
192    }
193}
194
195/// Signs a raw transaction with the given account.
196///
197/// # Errors
198///
199/// Returns an error if generating the signing message fails or if the account fails to sign.
200pub fn sign_transaction<A: Account>(
201    raw_txn: &RawTransaction,
202    account: &A,
203) -> AptosResult<SignedTransaction> {
204    let signing_message = raw_txn.signing_message()?;
205    let signature = account.sign(&signing_message)?;
206    let public_key = account.public_key_bytes();
207
208    let authenticator =
209        make_transaction_authenticator(account.signature_scheme(), public_key, signature)?;
210
211    Ok(SignedTransaction::new(raw_txn.clone(), authenticator))
212}
213
214/// Creates a transaction authenticator based on the signature scheme.
215///
216/// # Errors
217///
218/// Returns an error if the signature scheme is not recognized.
219fn make_transaction_authenticator(
220    scheme: u8,
221    public_key: Vec<u8>,
222    signature: Vec<u8>,
223) -> AptosResult<TransactionAuthenticator> {
224    match scheme {
225        crate::crypto::ED25519_SCHEME => {
226            Ok(TransactionAuthenticator::ed25519(public_key, signature))
227        }
228        crate::crypto::MULTI_ED25519_SCHEME => Ok(TransactionAuthenticator::multi_ed25519(
229            public_key, signature,
230        )),
231        crate::crypto::MULTI_KEY_SCHEME => {
232            // Multi-key uses SingleSender variant with AccountAuthenticator::MultiKey
233            Ok(TransactionAuthenticator::single_sender(
234                AccountAuthenticator::multi_key(public_key, signature),
235            ))
236        }
237        crate::crypto::SINGLE_KEY_SCHEME => {
238            // Single-key scheme is used by Secp256k1, Secp256r1, and Ed25519SingleKey accounts
239            // Uses SingleSender variant with AccountAuthenticator::SingleKey
240            Ok(TransactionAuthenticator::single_sender(
241                AccountAuthenticator::single_key(public_key, signature),
242            ))
243        }
244        #[cfg(feature = "keyless")]
245        crate::crypto::KEYLESS_SCHEME => {
246            // Keyless accounts use SingleSender variant with AccountAuthenticator::SingleKey
247            // The public key is the ephemeral Ed25519 key, and the signature is a BCS-serialized
248            // KeylessSignature struct containing the ephemeral signature and ZK proof
249            Ok(TransactionAuthenticator::single_sender(
250                AccountAuthenticator::keyless(public_key, signature),
251            ))
252        }
253        _ => Err(AptosError::InvalidSignature(format!(
254            "unknown signature scheme: {scheme}"
255        ))),
256    }
257}
258
259/// Creates an account authenticator based on the signature scheme.
260///
261/// # Errors
262///
263/// Returns an error if the signature scheme is not recognized.
264fn make_account_authenticator(
265    scheme: u8,
266    public_key: Vec<u8>,
267    signature: Vec<u8>,
268) -> AptosResult<AccountAuthenticator> {
269    match scheme {
270        crate::crypto::ED25519_SCHEME => Ok(AccountAuthenticator::ed25519(public_key, signature)),
271        crate::crypto::MULTI_ED25519_SCHEME => Ok(AccountAuthenticator::MultiEd25519 {
272            public_key,
273            signature,
274        }),
275        crate::crypto::SINGLE_KEY_SCHEME => {
276            Ok(AccountAuthenticator::single_key(public_key, signature))
277        }
278        crate::crypto::MULTI_KEY_SCHEME => {
279            Ok(AccountAuthenticator::multi_key(public_key, signature))
280        }
281        #[cfg(feature = "keyless")]
282        crate::crypto::KEYLESS_SCHEME => Ok(AccountAuthenticator::keyless(public_key, signature)),
283        _ => Err(AptosError::InvalidSignature(format!(
284            "unknown signature scheme: {scheme}"
285        ))),
286    }
287}
288
289/// Signs a multi-agent transaction.
290///
291/// # Errors
292///
293/// Returns an error if generating the signing message fails or if any signer fails to sign.
294pub fn sign_multi_agent_transaction<A: Account>(
295    multi_agent: &MultiAgentRawTransaction,
296    sender: &A,
297    secondary_signers: &[&dyn Account],
298) -> AptosResult<SignedTransaction> {
299    let signing_message = multi_agent.signing_message()?;
300
301    // Sign with sender
302    let sender_signature = sender.sign(&signing_message)?;
303    let sender_public_key = sender.public_key_bytes();
304    let sender_auth = make_account_authenticator(
305        sender.signature_scheme(),
306        sender_public_key,
307        sender_signature,
308    )?;
309
310    // Sign with secondary signers
311    let mut secondary_auths = Vec::with_capacity(secondary_signers.len());
312    for signer in secondary_signers {
313        let signature = signer.sign(&signing_message)?;
314        let public_key = signer.public_key_bytes();
315        secondary_auths.push(make_account_authenticator(
316            signer.signature_scheme(),
317            public_key,
318            signature,
319        )?);
320    }
321
322    let authenticator = TransactionAuthenticator::multi_agent(
323        sender_auth,
324        multi_agent.secondary_signer_addresses.clone(),
325        secondary_auths,
326    );
327
328    Ok(SignedTransaction::new(
329        multi_agent.raw_txn.clone(),
330        authenticator,
331    ))
332}
333
334/// Signs a fee payer transaction.
335///
336/// # Errors
337///
338/// Returns an error if generating the signing message fails or if any signer fails to sign.
339pub fn sign_fee_payer_transaction<A: Account>(
340    fee_payer_txn: &FeePayerRawTransaction,
341    sender: &A,
342    secondary_signers: &[&dyn Account],
343    fee_payer: &dyn Account,
344) -> AptosResult<SignedTransaction> {
345    let signing_message = fee_payer_txn.signing_message()?;
346
347    // Sign with sender
348    let sender_signature = sender.sign(&signing_message)?;
349    let sender_public_key = sender.public_key_bytes();
350    let sender_auth = make_account_authenticator(
351        sender.signature_scheme(),
352        sender_public_key,
353        sender_signature,
354    )?;
355
356    // Sign with secondary signers
357    let mut secondary_auths = Vec::with_capacity(secondary_signers.len());
358    for signer in secondary_signers {
359        let signature = signer.sign(&signing_message)?;
360        let public_key = signer.public_key_bytes();
361        secondary_auths.push(make_account_authenticator(
362            signer.signature_scheme(),
363            public_key,
364            signature,
365        )?);
366    }
367
368    // Sign with fee payer
369    let fee_payer_signature = fee_payer.sign(&signing_message)?;
370    let fee_payer_public_key = fee_payer.public_key_bytes();
371    let fee_payer_auth = make_account_authenticator(
372        fee_payer.signature_scheme(),
373        fee_payer_public_key,
374        fee_payer_signature,
375    )?;
376
377    let authenticator = TransactionAuthenticator::fee_payer(
378        sender_auth,
379        fee_payer_txn.secondary_signer_addresses.clone(),
380        secondary_auths,
381        fee_payer_txn.fee_payer_address,
382        fee_payer_auth,
383    );
384
385    Ok(SignedTransaction::new(
386        fee_payer_txn.raw_txn.clone(),
387        authenticator,
388    ))
389}
390
391#[cfg(test)]
392mod tests {
393    use super::*;
394    use crate::transaction::payload::EntryFunction;
395
396    #[test]
397    fn test_builder_missing_fields() {
398        let result = TransactionBuilder::new().build();
399        assert!(result.is_err());
400    }
401
402    #[test]
403    fn test_builder_complete() {
404        let recipient = AccountAddress::from_hex("0x123").unwrap();
405        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
406
407        let txn = TransactionBuilder::new()
408            .sender(AccountAddress::ONE)
409            .sequence_number(0)
410            .payload(payload.into())
411            .chain_id(ChainId::testnet())
412            .build()
413            .unwrap();
414
415        assert_eq!(txn.sender, AccountAddress::ONE);
416        assert_eq!(txn.sequence_number, 0);
417        assert_eq!(txn.max_gas_amount, DEFAULT_MAX_GAS_AMOUNT);
418        assert_eq!(txn.gas_unit_price, DEFAULT_GAS_UNIT_PRICE);
419    }
420
421    #[test]
422    fn test_builder_custom_gas() {
423        let recipient = AccountAddress::from_hex("0x123").unwrap();
424        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
425
426        let txn = TransactionBuilder::new()
427            .sender(AccountAddress::ONE)
428            .sequence_number(0)
429            .payload(payload.into())
430            .max_gas_amount(500_000)
431            .gas_unit_price(200)
432            .chain_id(ChainId::testnet())
433            .build()
434            .unwrap();
435
436        assert_eq!(txn.max_gas_amount, 500_000);
437        assert_eq!(txn.gas_unit_price, 200);
438    }
439
440    #[test]
441    fn test_builder_missing_sender() {
442        let recipient = AccountAddress::from_hex("0x123").unwrap();
443        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
444
445        let result = TransactionBuilder::new()
446            .sequence_number(0)
447            .payload(payload.into())
448            .chain_id(ChainId::testnet())
449            .build();
450
451        assert!(result.is_err());
452    }
453
454    #[test]
455    fn test_builder_missing_payload() {
456        let result = TransactionBuilder::new()
457            .sender(AccountAddress::ONE)
458            .sequence_number(0)
459            .chain_id(ChainId::testnet())
460            .build();
461
462        assert!(result.is_err());
463    }
464
465    #[test]
466    fn test_builder_missing_chain_id() {
467        let recipient = AccountAddress::from_hex("0x123").unwrap();
468        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
469
470        let result = TransactionBuilder::new()
471            .sender(AccountAddress::ONE)
472            .sequence_number(0)
473            .payload(payload.into())
474            .build();
475
476        assert!(result.is_err());
477    }
478
479    #[test]
480    fn test_builder_custom_expiration() {
481        let recipient = AccountAddress::from_hex("0x123").unwrap();
482        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
483        let custom_expiration = 9_999_999_999;
484
485        let txn = TransactionBuilder::new()
486            .sender(AccountAddress::ONE)
487            .sequence_number(0)
488            .payload(payload.into())
489            .expiration_timestamp_secs(custom_expiration)
490            .chain_id(ChainId::testnet())
491            .build()
492            .unwrap();
493
494        assert_eq!(txn.expiration_timestamp_secs, custom_expiration);
495    }
496
497    #[test]
498    fn test_default_expiration() {
499        // Default expiration should be set to about DEFAULT_EXPIRATION_SECONDS from now
500        let now = std::time::SystemTime::now()
501            .duration_since(std::time::UNIX_EPOCH)
502            .unwrap()
503            .as_secs();
504
505        let recipient = AccountAddress::from_hex("0x123").unwrap();
506        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
507
508        let txn = TransactionBuilder::new()
509            .sender(AccountAddress::ONE)
510            .sequence_number(0)
511            .payload(payload.into())
512            .chain_id(ChainId::testnet())
513            .build()
514            .unwrap();
515
516        // Should be approximately now + DEFAULT_EXPIRATION_SECONDS (give or take a second)
517        let expected_min = now + DEFAULT_EXPIRATION_SECONDS - 5;
518        let expected_max = now + DEFAULT_EXPIRATION_SECONDS + 5;
519        assert!(txn.expiration_timestamp_secs >= expected_min);
520        assert!(txn.expiration_timestamp_secs <= expected_max);
521    }
522
523    #[cfg(feature = "ed25519")]
524    #[test]
525    fn test_sign_transaction() {
526        use crate::account::Ed25519Account;
527
528        let account = Ed25519Account::generate();
529        let recipient = AccountAddress::from_hex("0x123").unwrap();
530        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
531
532        let txn = TransactionBuilder::new()
533            .sender(account.address())
534            .sequence_number(0)
535            .payload(payload.into())
536            .chain_id(ChainId::testnet())
537            .build()
538            .unwrap();
539
540        let signed = sign_transaction(&txn, &account).unwrap();
541        assert_eq!(signed.sender(), account.address());
542    }
543
544    #[cfg(feature = "ed25519")]
545    #[test]
546    fn test_sign_multi_agent_transaction() {
547        use crate::account::{Account, Ed25519Account};
548
549        let sender = Ed25519Account::generate();
550        let secondary = Ed25519Account::generate();
551        let recipient = AccountAddress::from_hex("0x123").unwrap();
552        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
553
554        let raw_txn = TransactionBuilder::new()
555            .sender(sender.address())
556            .sequence_number(0)
557            .payload(payload.into())
558            .chain_id(ChainId::testnet())
559            .build()
560            .unwrap();
561
562        let multi_agent = MultiAgentRawTransaction {
563            raw_txn,
564            secondary_signer_addresses: vec![secondary.address()],
565        };
566
567        let secondary_signers: Vec<&dyn Account> = vec![&secondary];
568        let signed =
569            sign_multi_agent_transaction(&multi_agent, &sender, &secondary_signers).unwrap();
570        assert_eq!(signed.sender(), sender.address());
571    }
572
573    #[cfg(feature = "ed25519")]
574    #[test]
575    fn test_sign_fee_payer_transaction() {
576        use crate::account::Ed25519Account;
577
578        let sender = Ed25519Account::generate();
579        let fee_payer = Ed25519Account::generate();
580        let recipient = AccountAddress::from_hex("0x123").unwrap();
581        let payload = EntryFunction::apt_transfer(recipient, 1000).unwrap();
582
583        let raw_txn = TransactionBuilder::new()
584            .sender(sender.address())
585            .sequence_number(0)
586            .payload(payload.into())
587            .chain_id(ChainId::testnet())
588            .build()
589            .unwrap();
590
591        let fee_payer_txn = FeePayerRawTransaction {
592            raw_txn,
593            secondary_signer_addresses: vec![],
594            fee_payer_address: fee_payer.address(),
595        };
596
597        let signed = sign_fee_payer_transaction(&fee_payer_txn, &sender, &[], &fee_payer).unwrap();
598        assert_eq!(signed.sender(), sender.address());
599    }
600
601    #[test]
602    fn test_default_impl() {
603        let builder = TransactionBuilder::default();
604        // Verify defaults are set
605        assert!(builder.sender.is_none());
606        assert!(builder.sequence_number.is_none());
607        assert!(builder.payload.is_none());
608        assert_eq!(builder.max_gas_amount, DEFAULT_MAX_GAS_AMOUNT);
609        assert_eq!(builder.gas_unit_price, DEFAULT_GAS_UNIT_PRICE);
610    }
611}