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#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
14pub enum TransactionPayload {
15    /// Execute a script with bytecode (variant 0).
16    Script(Script),
17    /// Deprecated module bundle payload (variant 1).
18    /// This variant exists only for BCS compatibility.
19    #[doc(hidden)]
20    ModuleBundle(DeprecatedModuleBundle),
21    /// Call an entry function on a module (variant 2).
22    EntryFunction(EntryFunction),
23    /// Multisig transaction payload (variant 3).
24    Multisig(Multisig),
25}
26
27/// Deprecated module bundle payload.
28/// This type exists only for BCS enum variant compatibility.
29#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
30pub struct DeprecatedModuleBundle {
31    #[doc(hidden)]
32    _private: (),
33}
34
35/// Multisig transaction payload.
36#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
37pub struct Multisig {
38    /// The multisig account address.
39    pub multisig_address: crate::types::AccountAddress,
40    /// The inner transaction payload (optional).
41    pub transaction_payload: Option<MultisigTransactionPayload>,
42}
43
44/// Inner payload for multisig transactions.
45#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
46pub enum MultisigTransactionPayload {
47    /// Entry function call.
48    EntryFunction(EntryFunction),
49}
50
51/// A script payload with inline bytecode.
52#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
53pub struct Script {
54    /// The Move bytecode to execute.
55    #[serde(with = "serde_bytes")]
56    pub code: Vec<u8>,
57    /// Type arguments for the script.
58    pub type_args: Vec<TypeTag>,
59    /// Arguments to the script.
60    pub args: Vec<ScriptArgument>,
61}
62
63impl Script {
64    /// Creates a new script payload.
65    pub fn new(code: Vec<u8>, type_args: Vec<TypeTag>, args: Vec<ScriptArgument>) -> Self {
66        Self {
67            code,
68            type_args,
69            args,
70        }
71    }
72}
73
74/// An argument to a script.
75///
76/// Variant order must match the chain's `ScriptTransactionArgumentVariants` for BCS compatibility with the
77/// chain and TS SDK. See: <https://github.com/aptos-labs/aptos-core/blob/main/third_party/move/move-core/types/src/transaction_argument.rs>
78#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
79pub enum ScriptArgument {
80    /// A u8 value (variant 0).
81    U8(u8),
82    /// A u64 value (variant 1).
83    U64(u64),
84    /// A u128 value (variant 2).
85    U128(u128),
86    /// An address value (variant 3).
87    Address(crate::types::AccountAddress),
88    /// A vector of u8 / bytes (variant 4).
89    U8Vector(#[serde(with = "serde_bytes")] Vec<u8>),
90    /// A boolean value (variant 5).
91    Bool(bool),
92    /// A u16 value (variant 6).
93    U16(u16),
94    /// A u32 value (variant 7).
95    U32(u32),
96    /// A u256 value as 32 bytes (variant 8).
97    U256([u8; 32]),
98    /// Pre-serialized BCS bytes (variant 9). Use for types not directly representable as other variants.
99    Serialized(#[serde(with = "serde_bytes")] Vec<u8>),
100    /// An i8 value (variant 10).
101    I8(i8),
102    /// An i16 value (variant 11).
103    I16(i16),
104    /// An i32 value (variant 12).
105    I32(i32),
106    /// An i64 value (variant 13).
107    I64(i64),
108    /// An i128 value (variant 14).
109    I128(i128),
110    /// An i256 value as 32 bytes (variant 15).
111    I256([u8; 32]),
112}
113
114/// An entry function call payload.
115///
116/// Entry functions are the most common type of transaction payload.
117/// They call a function marked as `entry` in a Move module.
118///
119/// # Example
120///
121/// ```rust
122/// use aptos_sdk::transaction::EntryFunction;
123/// use aptos_sdk::types::{MoveModuleId, TypeTag, AccountAddress};
124///
125/// // Create a coin transfer payload
126/// let module = MoveModuleId::from_str_strict("0x1::coin").unwrap();
127/// let recipient = AccountAddress::from_hex("0x123").unwrap();
128/// let entry_function = EntryFunction {
129///     module,
130///     function: "transfer".to_string(),
131///     type_args: vec![TypeTag::aptos_coin()],
132///     args: vec![
133///         aptos_bcs::to_bytes(&recipient).unwrap(),
134///         aptos_bcs::to_bytes(&1000u64).unwrap(),
135///     ],
136/// };
137/// ```
138#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
139pub struct EntryFunction {
140    /// The module containing the function.
141    pub module: MoveModuleId,
142    /// The function name.
143    pub function: String,
144    /// Type arguments for generic functions.
145    pub type_args: Vec<TypeTag>,
146    /// BCS-encoded arguments.
147    pub args: Vec<Vec<u8>>,
148}
149
150impl EntryFunction {
151    /// Creates a new entry function payload.
152    pub fn new(
153        module: MoveModuleId,
154        function: impl Into<String>,
155        type_args: Vec<TypeTag>,
156        args: Vec<Vec<u8>>,
157    ) -> Self {
158        Self {
159            module,
160            function: function.into(),
161            type_args,
162            args,
163        }
164    }
165
166    /// Creates an entry function from a function identifier string.
167    ///
168    /// # Arguments
169    ///
170    /// * `function_id` - Full function ID (e.g., "`0x1::coin::transfer`")
171    /// * `type_args` - Type arguments
172    /// * `args` - BCS-encoded arguments
173    ///
174    /// # Errors
175    ///
176    /// Returns an error if the function ID string is invalid.
177    pub fn from_function_id(
178        function_id: &str,
179        type_args: Vec<TypeTag>,
180        args: Vec<Vec<u8>>,
181    ) -> crate::error::AptosResult<Self> {
182        let func_id = crate::types::EntryFunctionId::from_str_strict(function_id)?;
183        Ok(Self {
184            module: func_id.module,
185            function: func_id.name.as_str().to_string(),
186            type_args,
187            args,
188        })
189    }
190
191    /// Creates a simple APT transfer payload.
192    ///
193    /// # Arguments
194    ///
195    /// * `recipient` - The recipient's address
196    /// * `amount` - Amount in octas (1 APT = 10^8 octas)
197    ///
198    /// # Errors
199    ///
200    /// Returns an error if the module ID is invalid or if BCS encoding of arguments fails.
201    pub fn apt_transfer(
202        recipient: crate::types::AccountAddress,
203        amount: u64,
204    ) -> crate::error::AptosResult<Self> {
205        let module = MoveModuleId::from_str_strict("0x1::aptos_account")?;
206        Ok(Self {
207            module,
208            function: "transfer".to_string(),
209            type_args: vec![],
210            args: vec![
211                aptos_bcs::to_bytes(&recipient).map_err(crate::error::AptosError::bcs)?,
212                aptos_bcs::to_bytes(&amount).map_err(crate::error::AptosError::bcs)?,
213            ],
214        })
215    }
216
217    /// Creates a coin transfer payload for any coin type.
218    ///
219    /// # Arguments
220    ///
221    /// * `coin_type` - The coin type tag
222    /// * `recipient` - The recipient's address
223    /// * `amount` - Amount in the coin's smallest unit
224    ///
225    /// # Errors
226    ///
227    /// Returns an error if the module ID is invalid or if BCS encoding of arguments fails.
228    pub fn coin_transfer(
229        coin_type: TypeTag,
230        recipient: crate::types::AccountAddress,
231        amount: u64,
232    ) -> crate::error::AptosResult<Self> {
233        let module = MoveModuleId::from_str_strict("0x1::coin")?;
234        Ok(Self {
235            module,
236            function: "transfer".to_string(),
237            type_args: vec![coin_type],
238            args: vec![
239                aptos_bcs::to_bytes(&recipient).map_err(crate::error::AptosError::bcs)?,
240                aptos_bcs::to_bytes(&amount).map_err(crate::error::AptosError::bcs)?,
241            ],
242        })
243    }
244}
245
246impl From<EntryFunction> for TransactionPayload {
247    fn from(entry_function: EntryFunction) -> Self {
248        TransactionPayload::EntryFunction(entry_function)
249    }
250}
251
252impl From<Script> for TransactionPayload {
253    fn from(script: Script) -> Self {
254        TransactionPayload::Script(script)
255    }
256}
257
258#[cfg(test)]
259mod tests {
260    use super::*;
261    use crate::types::AccountAddress;
262
263    #[test]
264    fn test_apt_transfer() {
265        let recipient = AccountAddress::from_hex("0x123").unwrap();
266        let entry_fn = EntryFunction::apt_transfer(recipient, 1000).unwrap();
267
268        assert_eq!(entry_fn.function, "transfer");
269        assert!(entry_fn.type_args.is_empty());
270        assert_eq!(entry_fn.args.len(), 2);
271    }
272
273    #[test]
274    fn test_from_function_id() {
275        let entry_fn = EntryFunction::from_function_id(
276            "0x1::coin::transfer",
277            vec![TypeTag::aptos_coin()],
278            vec![],
279        )
280        .unwrap();
281
282        assert_eq!(entry_fn.module.address, AccountAddress::ONE);
283        assert_eq!(entry_fn.module.name.as_str(), "coin");
284        assert_eq!(entry_fn.function, "transfer");
285    }
286
287    #[test]
288    fn test_entry_function_new() {
289        let module = MoveModuleId::from_str_strict("0x1::test_module").unwrap();
290        let entry_fn = EntryFunction::new(
291            module.clone(),
292            "test_function",
293            vec![TypeTag::U64],
294            vec![vec![1, 2, 3]],
295        );
296
297        assert_eq!(entry_fn.module, module);
298        assert_eq!(entry_fn.function, "test_function");
299        assert_eq!(entry_fn.type_args.len(), 1);
300        assert_eq!(entry_fn.args.len(), 1);
301    }
302
303    #[test]
304    fn test_coin_transfer() {
305        let recipient = AccountAddress::from_hex("0x456").unwrap();
306        let coin_type = TypeTag::aptos_coin();
307        let entry_fn = EntryFunction::coin_transfer(coin_type, recipient, 5000).unwrap();
308
309        assert_eq!(entry_fn.module.address, AccountAddress::ONE);
310        assert_eq!(entry_fn.module.name.as_str(), "coin");
311        assert_eq!(entry_fn.function, "transfer");
312        assert_eq!(entry_fn.type_args.len(), 1);
313        assert_eq!(entry_fn.args.len(), 2);
314    }
315
316    #[test]
317    fn test_script_new() {
318        let code = vec![0x01, 0x02, 0x03];
319        let type_args = vec![TypeTag::U64];
320        let args = vec![ScriptArgument::U64(100)];
321        let script = Script::new(code.clone(), type_args.clone(), args);
322
323        assert_eq!(script.code, code);
324        assert_eq!(script.type_args.len(), 1);
325        assert_eq!(script.args.len(), 1);
326    }
327
328    #[test]
329    fn test_script_argument_variants() {
330        let u8_arg = ScriptArgument::U8(255);
331        let u64_arg = ScriptArgument::U64(18_446_744_073_709_551_615);
332        let u128_arg = ScriptArgument::U128(340_282_366_920_938_463_463_374_607_431_768_211_455);
333        let addr_arg = ScriptArgument::Address(AccountAddress::ONE);
334        let bytes_arg = ScriptArgument::U8Vector(vec![1, 2, 3]);
335        let bool_arg = ScriptArgument::Bool(true);
336        let u16_arg = ScriptArgument::U16(65535);
337        let u32_arg = ScriptArgument::U32(4_294_967_295);
338        let u256_arg = ScriptArgument::U256([0xff; 32]);
339        let serialized_arg = ScriptArgument::Serialized(vec![1, 2, 3]);
340        let i64_arg = ScriptArgument::I64(-1i64);
341
342        // Test BCS serialization roundtrip (order matches enum declaration)
343        let args = vec![
344            u8_arg.clone(),
345            u64_arg.clone(),
346            u128_arg.clone(),
347            addr_arg.clone(),
348            bytes_arg.clone(),
349            bool_arg.clone(),
350            u16_arg.clone(),
351            u32_arg.clone(),
352            u256_arg.clone(),
353            serialized_arg.clone(),
354            i64_arg.clone(),
355        ];
356
357        for arg in args {
358            let serialized = aptos_bcs::to_bytes(&arg).unwrap();
359            let deserialized: ScriptArgument = aptos_bcs::from_bytes(&serialized).unwrap();
360            assert_eq!(deserialized, arg);
361        }
362    }
363
364    #[test]
365    fn test_transaction_payload_from_entry_function() {
366        let entry_fn = EntryFunction::apt_transfer(AccountAddress::ONE, 100).unwrap();
367        let payload: TransactionPayload = entry_fn.into();
368
369        match payload {
370            TransactionPayload::EntryFunction(ef) => {
371                assert_eq!(ef.function, "transfer");
372            }
373            _ => panic!("Expected EntryFunction variant"),
374        }
375    }
376
377    #[test]
378    fn test_transaction_payload_from_script() {
379        let script = Script::new(vec![0x01], vec![], vec![]);
380        let payload: TransactionPayload = script.into();
381
382        match payload {
383            TransactionPayload::Script(s) => {
384                assert_eq!(s.code, vec![0x01]);
385            }
386            _ => panic!("Expected Script variant"),
387        }
388    }
389
390    #[test]
391    fn test_multisig_payload() {
392        let entry_fn = EntryFunction::apt_transfer(AccountAddress::ONE, 100).unwrap();
393        let multisig = Multisig {
394            multisig_address: AccountAddress::from_hex("0x999").unwrap(),
395            transaction_payload: Some(MultisigTransactionPayload::EntryFunction(entry_fn)),
396        };
397
398        let payload = TransactionPayload::Multisig(multisig.clone());
399        match payload {
400            TransactionPayload::Multisig(m) => {
401                assert_eq!(m.multisig_address, multisig.multisig_address);
402                assert!(m.transaction_payload.is_some());
403            }
404            _ => panic!("Expected Multisig variant"),
405        }
406    }
407
408    #[test]
409    fn test_entry_function_bcs_serialization() {
410        let entry_fn = EntryFunction::apt_transfer(AccountAddress::ONE, 1000).unwrap();
411        let serialized = aptos_bcs::to_bytes(&entry_fn).unwrap();
412        let deserialized: EntryFunction = aptos_bcs::from_bytes(&serialized).unwrap();
413
414        assert_eq!(entry_fn.module, deserialized.module);
415        assert_eq!(entry_fn.function, deserialized.function);
416        assert_eq!(entry_fn.type_args, deserialized.type_args);
417        assert_eq!(entry_fn.args, deserialized.args);
418    }
419
420    #[test]
421    fn test_transaction_payload_bcs_serialization() {
422        let entry_fn = EntryFunction::apt_transfer(AccountAddress::ONE, 1000).unwrap();
423        let payload = TransactionPayload::EntryFunction(entry_fn);
424
425        let serialized = aptos_bcs::to_bytes(&payload).unwrap();
426        let deserialized: TransactionPayload = aptos_bcs::from_bytes(&serialized).unwrap();
427
428        assert_eq!(payload, deserialized);
429    }
430
431    #[test]
432    fn test_from_function_id_invalid() {
433        // Missing module separator
434        let result = EntryFunction::from_function_id("0x1coin::transfer", vec![], vec![]);
435        assert!(result.is_err());
436
437        // Invalid address
438        let result = EntryFunction::from_function_id("invalid::module::function", vec![], vec![]);
439        assert!(result.is_err());
440    }
441}