Skip to main content

aptos_sdk/transaction/
payload.rs

1//! Transaction payloads.
2
3use crate::types::{MoveModuleId, TypeTag};
4use serde::{Deserialize, Serialize};
5
6/// The payload of a transaction, specifying what action to take.
7///
8/// Note: Variant indices must match Aptos core for BCS compatibility:
9/// - 0: Script
10/// - 1: `ModuleBundle` (deprecated)
11/// - 2: `EntryFunction`
12/// - 3: Multisig
13/// - 4: `Payload` (unified executable + extra-config format; used for
14///   orderless transactions)
15#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
16pub enum TransactionPayload {
17    /// Execute a script with bytecode (variant 0).
18    Script(Script),
19    /// Deprecated module bundle payload (variant 1).
20    /// This variant exists only for BCS compatibility.
21    #[doc(hidden)]
22    ModuleBundle(DeprecatedModuleBundle),
23    /// Call an entry function on a module (variant 2).
24    EntryFunction(EntryFunction),
25    /// Multisig transaction payload (variant 3).
26    Multisig(Multisig),
27    /// Unified payload format carrying an executable plus extra configuration
28    /// (variant 4).
29    ///
30    /// This is the format the chain uses for **orderless** transactions, where
31    /// replay protection is provided by a nonce (in the extra config) instead of
32    /// a sequence number. See [`TransactionPayloadInner`].
33    Payload(TransactionPayloadInner),
34}
35
36impl TransactionPayload {
37    /// Wraps an `EntryFunction` or `Script` payload as an orderless
38    /// [`Payload`](TransactionPayload::Payload) with the given replay-protection
39    /// `nonce`.
40    ///
41    /// The resulting payload must be sent in a [`RawTransaction`] whose
42    /// `sequence_number` is [`u64::MAX`], which is how the chain distinguishes
43    /// nonce-based (orderless) replay protection from sequence-number-based
44    /// replay protection.
45    ///
46    /// [`RawTransaction`]: crate::transaction::RawTransaction
47    ///
48    /// # Errors
49    ///
50    /// Returns [`AptosError::Transaction`](crate::error::AptosError::Transaction)
51    /// if the payload is not an `EntryFunction` or `Script` (multisig, module
52    /// bundle, and already-wrapped `Payload` variants are not supported as
53    /// orderless executables).
54    pub fn into_orderless(self, nonce: u64) -> crate::error::AptosResult<Self> {
55        let executable = match self {
56            TransactionPayload::EntryFunction(ef) => TransactionExecutable::EntryFunction(ef),
57            TransactionPayload::Script(s) => TransactionExecutable::Script(s),
58            other => {
59                return Err(crate::error::AptosError::Transaction(format!(
60                    "orderless transactions support only entry-function or script payloads, got {}",
61                    other.variant_name()
62                )));
63            }
64        };
65        Ok(TransactionPayload::Payload(TransactionPayloadInner::V1 {
66            executable,
67            extra_config: TransactionExtraConfig::V1 {
68                multisig_address: None,
69                replay_protection_nonce: Some(nonce),
70            },
71        }))
72    }
73
74    /// Returns a human-readable name for the payload variant (for diagnostics).
75    fn variant_name(&self) -> &'static str {
76        match self {
77            TransactionPayload::Script(_) => "Script",
78            TransactionPayload::ModuleBundle(_) => "ModuleBundle",
79            TransactionPayload::EntryFunction(_) => "EntryFunction",
80            TransactionPayload::Multisig(_) => "Multisig",
81            TransactionPayload::Payload(_) => "Payload",
82        }
83    }
84}
85
86/// The inner, versioned body of a [`TransactionPayload::Payload`].
87///
88/// Variant index must be 0 (`V1`) to match Aptos core for BCS compatibility.
89#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
90pub enum TransactionPayloadInner {
91    /// Version 1 (variant 0): an executable plus extra configuration.
92    V1 {
93        /// What to execute (entry function, script, or nothing).
94        executable: TransactionExecutable,
95        /// Extra configuration (multisig address, replay-protection nonce).
96        extra_config: TransactionExtraConfig,
97    },
98}
99
100/// The executable portion of a [`TransactionPayloadInner`].
101///
102/// Variant indices must match Aptos core for BCS compatibility:
103/// - 0: `Script`
104/// - 1: `EntryFunction`
105/// - 2: `Empty`
106#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
107pub enum TransactionExecutable {
108    /// Execute a script with bytecode (variant 0).
109    Script(Script),
110    /// Call an entry function on a module (variant 1).
111    EntryFunction(EntryFunction),
112    /// No executable (variant 2), e.g. a multisig transaction whose executable
113    /// is stored on chain.
114    Empty,
115}
116
117/// Extra configuration attached to a [`TransactionPayloadInner`].
118///
119/// Variant index must be 0 (`V1`) to match Aptos core for BCS compatibility.
120#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
121pub enum TransactionExtraConfig {
122    /// Version 1 (variant 0).
123    V1 {
124        /// The multisig account address, when this is a multisig transaction.
125        multisig_address: Option<crate::types::AccountAddress>,
126        /// The replay-protection nonce, present for orderless transactions.
127        replay_protection_nonce: Option<u64>,
128    },
129}
130
131/// Deprecated module bundle payload.
132/// This type exists only for BCS enum variant compatibility.
133#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
134pub struct DeprecatedModuleBundle {
135    #[doc(hidden)]
136    _private: (),
137}
138
139/// Multisig transaction payload.
140#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
141pub struct Multisig {
142    /// The multisig account address.
143    pub multisig_address: crate::types::AccountAddress,
144    /// The inner transaction payload (optional).
145    pub transaction_payload: Option<MultisigTransactionPayload>,
146}
147
148/// Inner payload for multisig transactions.
149#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
150pub enum MultisigTransactionPayload {
151    /// Entry function call.
152    EntryFunction(EntryFunction),
153}
154
155/// A script payload with inline bytecode.
156#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
157pub struct Script {
158    /// The Move bytecode to execute.
159    #[serde(with = "serde_bytes")]
160    pub code: Vec<u8>,
161    /// Type arguments for the script.
162    pub type_args: Vec<TypeTag>,
163    /// Arguments to the script.
164    pub args: Vec<ScriptArgument>,
165}
166
167impl Script {
168    /// Creates a new script payload.
169    pub fn new(code: Vec<u8>, type_args: Vec<TypeTag>, args: Vec<ScriptArgument>) -> Self {
170        Self {
171            code,
172            type_args,
173            args,
174        }
175    }
176}
177
178/// An argument to a script.
179///
180/// Variant order must match the chain's `ScriptTransactionArgumentVariants` for BCS compatibility with the
181/// chain and TS SDK. See: <https://github.com/aptos-labs/aptos-core/blob/main/third_party/move/move-core/types/src/transaction_argument.rs>
182#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
183pub enum ScriptArgument {
184    /// A u8 value (variant 0).
185    U8(u8),
186    /// A u64 value (variant 1).
187    U64(u64),
188    /// A u128 value (variant 2).
189    U128(u128),
190    /// An address value (variant 3).
191    Address(crate::types::AccountAddress),
192    /// A vector of u8 / bytes (variant 4).
193    U8Vector(#[serde(with = "serde_bytes")] Vec<u8>),
194    /// A boolean value (variant 5).
195    Bool(bool),
196    /// A u16 value (variant 6).
197    U16(u16),
198    /// A u32 value (variant 7).
199    U32(u32),
200    /// A u256 value as 32 bytes (variant 8).
201    U256([u8; 32]),
202    /// Pre-serialized BCS bytes (variant 9). Use for types not directly representable as other variants.
203    Serialized(#[serde(with = "serde_bytes")] Vec<u8>),
204    /// An i8 value (variant 10).
205    I8(i8),
206    /// An i16 value (variant 11).
207    I16(i16),
208    /// An i32 value (variant 12).
209    I32(i32),
210    /// An i64 value (variant 13).
211    I64(i64),
212    /// An i128 value (variant 14).
213    I128(i128),
214    /// An i256 value as 32 bytes (variant 15).
215    I256([u8; 32]),
216}
217
218/// An entry function call payload.
219///
220/// Entry functions are the most common type of transaction payload.
221/// They call a function marked as `entry` in a Move module.
222///
223/// # Example
224///
225/// ```rust
226/// use aptos_sdk::transaction::EntryFunction;
227/// use aptos_sdk::types::{MoveModuleId, TypeTag, AccountAddress};
228///
229/// // Create a coin transfer payload
230/// let module = MoveModuleId::from_str_strict("0x1::coin").unwrap();
231/// let recipient = AccountAddress::from_hex("0x123").unwrap();
232/// let entry_function = EntryFunction {
233///     module,
234///     function: "transfer".to_string(),
235///     type_args: vec![TypeTag::aptos_coin()],
236///     args: vec![
237///         aptos_bcs::to_bytes(&recipient).unwrap(),
238///         aptos_bcs::to_bytes(&1000u64).unwrap(),
239///     ],
240/// };
241/// ```
242#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
243pub struct EntryFunction {
244    /// The module containing the function.
245    pub module: MoveModuleId,
246    /// The function name.
247    pub function: String,
248    /// Type arguments for generic functions.
249    pub type_args: Vec<TypeTag>,
250    /// BCS-encoded arguments.
251    pub args: Vec<Vec<u8>>,
252}
253
254impl EntryFunction {
255    /// Creates a new entry function payload.
256    pub fn new(
257        module: MoveModuleId,
258        function: impl Into<String>,
259        type_args: Vec<TypeTag>,
260        args: Vec<Vec<u8>>,
261    ) -> Self {
262        Self {
263            module,
264            function: function.into(),
265            type_args,
266            args,
267        }
268    }
269
270    /// Creates an entry function from a function identifier string.
271    ///
272    /// # Arguments
273    ///
274    /// * `function_id` - Full function ID (e.g., "`0x1::coin::transfer`")
275    /// * `type_args` - Type arguments
276    /// * `args` - BCS-encoded arguments
277    ///
278    /// # Errors
279    ///
280    /// Returns an error if the function ID string is invalid.
281    pub fn from_function_id(
282        function_id: &str,
283        type_args: Vec<TypeTag>,
284        args: Vec<Vec<u8>>,
285    ) -> crate::error::AptosResult<Self> {
286        let func_id = crate::types::EntryFunctionId::from_str_strict(function_id)?;
287        Ok(Self {
288            module: func_id.module,
289            function: func_id.name.as_str().to_string(),
290            type_args,
291            args,
292        })
293    }
294
295    /// Creates a simple APT transfer payload.
296    ///
297    /// # Arguments
298    ///
299    /// * `recipient` - The recipient's address
300    /// * `amount` - Amount in octas (1 APT = 10^8 octas)
301    ///
302    /// # Errors
303    ///
304    /// Returns an error if the module ID is invalid or if BCS encoding of arguments fails.
305    pub fn apt_transfer(
306        recipient: crate::types::AccountAddress,
307        amount: u64,
308    ) -> crate::error::AptosResult<Self> {
309        let module = MoveModuleId::from_str_strict("0x1::aptos_account")?;
310        Ok(Self {
311            module,
312            function: "transfer".to_string(),
313            type_args: vec![],
314            args: vec![
315                aptos_bcs::to_bytes(&recipient).map_err(crate::error::AptosError::bcs)?,
316                aptos_bcs::to_bytes(&amount).map_err(crate::error::AptosError::bcs)?,
317            ],
318        })
319    }
320
321    /// Creates a coin transfer payload for any coin type.
322    ///
323    /// # Arguments
324    ///
325    /// * `coin_type` - The coin type tag
326    /// * `recipient` - The recipient's address
327    /// * `amount` - Amount in the coin's smallest unit
328    ///
329    /// # Errors
330    ///
331    /// Returns an error if the module ID is invalid or if BCS encoding of arguments fails.
332    pub fn coin_transfer(
333        coin_type: TypeTag,
334        recipient: crate::types::AccountAddress,
335        amount: u64,
336    ) -> crate::error::AptosResult<Self> {
337        let module = MoveModuleId::from_str_strict("0x1::coin")?;
338        Ok(Self {
339            module,
340            function: "transfer".to_string(),
341            type_args: vec![coin_type],
342            args: vec![
343                aptos_bcs::to_bytes(&recipient).map_err(crate::error::AptosError::bcs)?,
344                aptos_bcs::to_bytes(&amount).map_err(crate::error::AptosError::bcs)?,
345            ],
346        })
347    }
348}
349
350impl From<EntryFunction> for TransactionPayload {
351    fn from(entry_function: EntryFunction) -> Self {
352        TransactionPayload::EntryFunction(entry_function)
353    }
354}
355
356impl From<Script> for TransactionPayload {
357    fn from(script: Script) -> Self {
358        TransactionPayload::Script(script)
359    }
360}
361
362#[cfg(test)]
363mod tests {
364    use super::*;
365    use crate::types::AccountAddress;
366
367    #[test]
368    fn test_apt_transfer() {
369        let recipient = AccountAddress::from_hex("0x123").unwrap();
370        let entry_fn = EntryFunction::apt_transfer(recipient, 1000).unwrap();
371
372        assert_eq!(entry_fn.function, "transfer");
373        assert!(entry_fn.type_args.is_empty());
374        assert_eq!(entry_fn.args.len(), 2);
375    }
376
377    #[test]
378    fn test_from_function_id() {
379        let entry_fn = EntryFunction::from_function_id(
380            "0x1::coin::transfer",
381            vec![TypeTag::aptos_coin()],
382            vec![],
383        )
384        .unwrap();
385
386        assert_eq!(entry_fn.module.address, AccountAddress::ONE);
387        assert_eq!(entry_fn.module.name.as_str(), "coin");
388        assert_eq!(entry_fn.function, "transfer");
389    }
390
391    #[test]
392    fn test_entry_function_new() {
393        let module = MoveModuleId::from_str_strict("0x1::test_module").unwrap();
394        let entry_fn = EntryFunction::new(
395            module.clone(),
396            "test_function",
397            vec![TypeTag::U64],
398            vec![vec![1, 2, 3]],
399        );
400
401        assert_eq!(entry_fn.module, module);
402        assert_eq!(entry_fn.function, "test_function");
403        assert_eq!(entry_fn.type_args.len(), 1);
404        assert_eq!(entry_fn.args.len(), 1);
405    }
406
407    #[test]
408    fn test_coin_transfer() {
409        let recipient = AccountAddress::from_hex("0x456").unwrap();
410        let coin_type = TypeTag::aptos_coin();
411        let entry_fn = EntryFunction::coin_transfer(coin_type, recipient, 5000).unwrap();
412
413        assert_eq!(entry_fn.module.address, AccountAddress::ONE);
414        assert_eq!(entry_fn.module.name.as_str(), "coin");
415        assert_eq!(entry_fn.function, "transfer");
416        assert_eq!(entry_fn.type_args.len(), 1);
417        assert_eq!(entry_fn.args.len(), 2);
418    }
419
420    #[test]
421    fn test_into_orderless_wraps_entry_function() {
422        let ef = EntryFunction::apt_transfer(AccountAddress::ONE, 1).unwrap();
423        let payload = TransactionPayload::EntryFunction(ef.clone())
424            .into_orderless(0xdead_beef)
425            .unwrap();
426
427        match payload {
428            TransactionPayload::Payload(TransactionPayloadInner::V1 {
429                executable: TransactionExecutable::EntryFunction(inner),
430                extra_config:
431                    TransactionExtraConfig::V1 {
432                        multisig_address,
433                        replay_protection_nonce,
434                    },
435            }) => {
436                assert_eq!(inner, ef);
437                assert_eq!(multisig_address, None);
438                assert_eq!(replay_protection_nonce, Some(0xdead_beef));
439            }
440            other => panic!("expected orderless Payload, got {other:?}"),
441        }
442    }
443
444    #[test]
445    fn test_into_orderless_wraps_script() {
446        let script = Script::new(vec![0x00], vec![], vec![]);
447        let payload = TransactionPayload::Script(script.clone())
448            .into_orderless(7)
449            .unwrap();
450
451        assert!(matches!(
452            payload,
453            TransactionPayload::Payload(TransactionPayloadInner::V1 {
454                executable: TransactionExecutable::Script(_),
455                ..
456            })
457        ));
458    }
459
460    #[test]
461    fn test_into_orderless_rejects_multisig() {
462        let multisig = Multisig {
463            multisig_address: AccountAddress::ONE,
464            transaction_payload: None,
465        };
466        let err = TransactionPayload::Multisig(multisig)
467            .into_orderless(1)
468            .unwrap_err();
469        assert!(matches!(err, crate::error::AptosError::Transaction(_)));
470    }
471
472    #[test]
473    fn test_script_new() {
474        let code = vec![0x01, 0x02, 0x03];
475        let type_args = vec![TypeTag::U64];
476        let args = vec![ScriptArgument::U64(100)];
477        let script = Script::new(code.clone(), type_args.clone(), args);
478
479        assert_eq!(script.code, code);
480        assert_eq!(script.type_args.len(), 1);
481        assert_eq!(script.args.len(), 1);
482    }
483
484    #[test]
485    fn test_script_argument_variants() {
486        let u8_arg = ScriptArgument::U8(255);
487        let u64_arg = ScriptArgument::U64(18_446_744_073_709_551_615);
488        let u128_arg = ScriptArgument::U128(340_282_366_920_938_463_463_374_607_431_768_211_455);
489        let addr_arg = ScriptArgument::Address(AccountAddress::ONE);
490        let bytes_arg = ScriptArgument::U8Vector(vec![1, 2, 3]);
491        let bool_arg = ScriptArgument::Bool(true);
492        let u16_arg = ScriptArgument::U16(65535);
493        let u32_arg = ScriptArgument::U32(4_294_967_295);
494        let u256_arg = ScriptArgument::U256([0xff; 32]);
495        let serialized_arg = ScriptArgument::Serialized(vec![1, 2, 3]);
496        let i64_arg = ScriptArgument::I64(-1i64);
497
498        // Test BCS serialization roundtrip (order matches enum declaration)
499        let args = vec![
500            u8_arg.clone(),
501            u64_arg.clone(),
502            u128_arg.clone(),
503            addr_arg.clone(),
504            bytes_arg.clone(),
505            bool_arg.clone(),
506            u16_arg.clone(),
507            u32_arg.clone(),
508            u256_arg.clone(),
509            serialized_arg.clone(),
510            i64_arg.clone(),
511        ];
512
513        for arg in args {
514            let serialized = aptos_bcs::to_bytes(&arg).unwrap();
515            let deserialized: ScriptArgument = aptos_bcs::from_bytes(&serialized).unwrap();
516            assert_eq!(deserialized, arg);
517        }
518    }
519
520    #[test]
521    fn test_transaction_payload_from_entry_function() {
522        let entry_fn = EntryFunction::apt_transfer(AccountAddress::ONE, 100).unwrap();
523        let payload: TransactionPayload = entry_fn.into();
524
525        match payload {
526            TransactionPayload::EntryFunction(ef) => {
527                assert_eq!(ef.function, "transfer");
528            }
529            _ => panic!("Expected EntryFunction variant"),
530        }
531    }
532
533    #[test]
534    fn test_transaction_payload_from_script() {
535        let script = Script::new(vec![0x01], vec![], vec![]);
536        let payload: TransactionPayload = script.into();
537
538        match payload {
539            TransactionPayload::Script(s) => {
540                assert_eq!(s.code, vec![0x01]);
541            }
542            _ => panic!("Expected Script variant"),
543        }
544    }
545
546    #[test]
547    fn test_multisig_payload() {
548        let entry_fn = EntryFunction::apt_transfer(AccountAddress::ONE, 100).unwrap();
549        let multisig = Multisig {
550            multisig_address: AccountAddress::from_hex("0x999").unwrap(),
551            transaction_payload: Some(MultisigTransactionPayload::EntryFunction(entry_fn)),
552        };
553
554        let payload = TransactionPayload::Multisig(multisig.clone());
555        match payload {
556            TransactionPayload::Multisig(m) => {
557                assert_eq!(m.multisig_address, multisig.multisig_address);
558                assert!(m.transaction_payload.is_some());
559            }
560            _ => panic!("Expected Multisig variant"),
561        }
562    }
563
564    #[test]
565    fn test_entry_function_bcs_serialization() {
566        let entry_fn = EntryFunction::apt_transfer(AccountAddress::ONE, 1000).unwrap();
567        let serialized = aptos_bcs::to_bytes(&entry_fn).unwrap();
568        let deserialized: EntryFunction = aptos_bcs::from_bytes(&serialized).unwrap();
569
570        assert_eq!(entry_fn.module, deserialized.module);
571        assert_eq!(entry_fn.function, deserialized.function);
572        assert_eq!(entry_fn.type_args, deserialized.type_args);
573        assert_eq!(entry_fn.args, deserialized.args);
574    }
575
576    #[test]
577    fn test_transaction_payload_bcs_serialization() {
578        let entry_fn = EntryFunction::apt_transfer(AccountAddress::ONE, 1000).unwrap();
579        let payload = TransactionPayload::EntryFunction(entry_fn);
580
581        let serialized = aptos_bcs::to_bytes(&payload).unwrap();
582        let deserialized: TransactionPayload = aptos_bcs::from_bytes(&serialized).unwrap();
583
584        assert_eq!(payload, deserialized);
585    }
586
587    #[test]
588    fn test_from_function_id_invalid() {
589        // Missing module separator
590        let result = EntryFunction::from_function_id("0x1coin::transfer", vec![], vec![]);
591        assert!(result.is_err());
592
593        // Invalid address
594        let result = EntryFunction::from_function_id("invalid::module::function", vec![], vec![]);
595        assert!(result.is_err());
596    }
597}