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#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
76pub enum ScriptArgument {
77    /// A u8 value.
78    U8(u8),
79    /// A u16 value.
80    U16(u16),
81    /// A u32 value.
82    U32(u32),
83    /// A u64 value.
84    U64(u64),
85    /// A u128 value.
86    U128(u128),
87    /// A u256 value (as bytes).
88    U256([u8; 32]),
89    /// An address value.
90    Address(crate::types::AccountAddress),
91    /// A vector of u8 (bytes).
92    U8Vector(#[serde(with = "serde_bytes")] Vec<u8>),
93    /// A boolean value.
94    Bool(bool),
95}
96
97/// An entry function call payload.
98///
99/// Entry functions are the most common type of transaction payload.
100/// They call a function marked as `entry` in a Move module.
101///
102/// # Example
103///
104/// ```rust
105/// use aptos_sdk::transaction::EntryFunction;
106/// use aptos_sdk::types::{MoveModuleId, TypeTag, AccountAddress};
107///
108/// // Create a coin transfer payload
109/// let module = MoveModuleId::from_str_strict("0x1::coin").unwrap();
110/// let recipient = AccountAddress::from_hex("0x123").unwrap();
111/// let entry_function = EntryFunction {
112///     module,
113///     function: "transfer".to_string(),
114///     type_args: vec![TypeTag::aptos_coin()],
115///     args: vec![
116///         aptos_bcs::to_bytes(&recipient).unwrap(),
117///         aptos_bcs::to_bytes(&1000u64).unwrap(),
118///     ],
119/// };
120/// ```
121#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
122pub struct EntryFunction {
123    /// The module containing the function.
124    pub module: MoveModuleId,
125    /// The function name.
126    pub function: String,
127    /// Type arguments for generic functions.
128    pub type_args: Vec<TypeTag>,
129    /// BCS-encoded arguments.
130    pub args: Vec<Vec<u8>>,
131}
132
133impl EntryFunction {
134    /// Creates a new entry function payload.
135    pub fn new(
136        module: MoveModuleId,
137        function: impl Into<String>,
138        type_args: Vec<TypeTag>,
139        args: Vec<Vec<u8>>,
140    ) -> Self {
141        Self {
142            module,
143            function: function.into(),
144            type_args,
145            args,
146        }
147    }
148
149    /// Creates an entry function from a function identifier string.
150    ///
151    /// # Arguments
152    ///
153    /// * `function_id` - Full function ID (e.g., "`0x1::coin::transfer`")
154    /// * `type_args` - Type arguments
155    /// * `args` - BCS-encoded arguments
156    ///
157    /// # Errors
158    ///
159    /// Returns an error if the function ID string is invalid.
160    pub fn from_function_id(
161        function_id: &str,
162        type_args: Vec<TypeTag>,
163        args: Vec<Vec<u8>>,
164    ) -> crate::error::AptosResult<Self> {
165        let func_id = crate::types::EntryFunctionId::from_str_strict(function_id)?;
166        Ok(Self {
167            module: func_id.module,
168            function: func_id.name.as_str().to_string(),
169            type_args,
170            args,
171        })
172    }
173
174    /// Creates a simple APT transfer payload.
175    ///
176    /// # Arguments
177    ///
178    /// * `recipient` - The recipient's address
179    /// * `amount` - Amount in octas (1 APT = 10^8 octas)
180    ///
181    /// # Errors
182    ///
183    /// Returns an error if the module ID is invalid or if BCS encoding of arguments fails.
184    pub fn apt_transfer(
185        recipient: crate::types::AccountAddress,
186        amount: u64,
187    ) -> crate::error::AptosResult<Self> {
188        let module = MoveModuleId::from_str_strict("0x1::aptos_account")?;
189        Ok(Self {
190            module,
191            function: "transfer".to_string(),
192            type_args: vec![],
193            args: vec![
194                aptos_bcs::to_bytes(&recipient).map_err(crate::error::AptosError::bcs)?,
195                aptos_bcs::to_bytes(&amount).map_err(crate::error::AptosError::bcs)?,
196            ],
197        })
198    }
199
200    /// Creates a coin transfer payload for any coin type.
201    ///
202    /// # Arguments
203    ///
204    /// * `coin_type` - The coin type tag
205    /// * `recipient` - The recipient's address
206    /// * `amount` - Amount in the coin's smallest unit
207    ///
208    /// # Errors
209    ///
210    /// Returns an error if the module ID is invalid or if BCS encoding of arguments fails.
211    pub fn coin_transfer(
212        coin_type: TypeTag,
213        recipient: crate::types::AccountAddress,
214        amount: u64,
215    ) -> crate::error::AptosResult<Self> {
216        let module = MoveModuleId::from_str_strict("0x1::coin")?;
217        Ok(Self {
218            module,
219            function: "transfer".to_string(),
220            type_args: vec![coin_type],
221            args: vec![
222                aptos_bcs::to_bytes(&recipient).map_err(crate::error::AptosError::bcs)?,
223                aptos_bcs::to_bytes(&amount).map_err(crate::error::AptosError::bcs)?,
224            ],
225        })
226    }
227}
228
229impl From<EntryFunction> for TransactionPayload {
230    fn from(entry_function: EntryFunction) -> Self {
231        TransactionPayload::EntryFunction(entry_function)
232    }
233}
234
235impl From<Script> for TransactionPayload {
236    fn from(script: Script) -> Self {
237        TransactionPayload::Script(script)
238    }
239}
240
241#[cfg(test)]
242mod tests {
243    use super::*;
244    use crate::types::AccountAddress;
245
246    #[test]
247    fn test_apt_transfer() {
248        let recipient = AccountAddress::from_hex("0x123").unwrap();
249        let entry_fn = EntryFunction::apt_transfer(recipient, 1000).unwrap();
250
251        assert_eq!(entry_fn.function, "transfer");
252        assert!(entry_fn.type_args.is_empty());
253        assert_eq!(entry_fn.args.len(), 2);
254    }
255
256    #[test]
257    fn test_from_function_id() {
258        let entry_fn = EntryFunction::from_function_id(
259            "0x1::coin::transfer",
260            vec![TypeTag::aptos_coin()],
261            vec![],
262        )
263        .unwrap();
264
265        assert_eq!(entry_fn.module.address, AccountAddress::ONE);
266        assert_eq!(entry_fn.module.name.as_str(), "coin");
267        assert_eq!(entry_fn.function, "transfer");
268    }
269
270    #[test]
271    fn test_entry_function_new() {
272        let module = MoveModuleId::from_str_strict("0x1::test_module").unwrap();
273        let entry_fn = EntryFunction::new(
274            module.clone(),
275            "test_function",
276            vec![TypeTag::U64],
277            vec![vec![1, 2, 3]],
278        );
279
280        assert_eq!(entry_fn.module, module);
281        assert_eq!(entry_fn.function, "test_function");
282        assert_eq!(entry_fn.type_args.len(), 1);
283        assert_eq!(entry_fn.args.len(), 1);
284    }
285
286    #[test]
287    fn test_coin_transfer() {
288        let recipient = AccountAddress::from_hex("0x456").unwrap();
289        let coin_type = TypeTag::aptos_coin();
290        let entry_fn = EntryFunction::coin_transfer(coin_type, recipient, 5000).unwrap();
291
292        assert_eq!(entry_fn.module.address, AccountAddress::ONE);
293        assert_eq!(entry_fn.module.name.as_str(), "coin");
294        assert_eq!(entry_fn.function, "transfer");
295        assert_eq!(entry_fn.type_args.len(), 1);
296        assert_eq!(entry_fn.args.len(), 2);
297    }
298
299    #[test]
300    fn test_script_new() {
301        let code = vec![0x01, 0x02, 0x03];
302        let type_args = vec![TypeTag::U64];
303        let args = vec![ScriptArgument::U64(100)];
304        let script = Script::new(code.clone(), type_args.clone(), args);
305
306        assert_eq!(script.code, code);
307        assert_eq!(script.type_args.len(), 1);
308        assert_eq!(script.args.len(), 1);
309    }
310
311    #[test]
312    fn test_script_argument_variants() {
313        let u8_arg = ScriptArgument::U8(255);
314        let u16_arg = ScriptArgument::U16(65535);
315        let u32_arg = ScriptArgument::U32(4_294_967_295);
316        let u64_arg = ScriptArgument::U64(18_446_744_073_709_551_615);
317        let u128_arg = ScriptArgument::U128(340_282_366_920_938_463_463_374_607_431_768_211_455);
318        let bool_arg = ScriptArgument::Bool(true);
319        let addr_arg = ScriptArgument::Address(AccountAddress::ONE);
320        let bytes_arg = ScriptArgument::U8Vector(vec![1, 2, 3]);
321        let u256_arg = ScriptArgument::U256([0xff; 32]);
322
323        // Test BCS serialization roundtrip
324        let args = vec![
325            u8_arg.clone(),
326            u16_arg.clone(),
327            u32_arg.clone(),
328            u64_arg.clone(),
329            u128_arg.clone(),
330            bool_arg.clone(),
331            addr_arg.clone(),
332            bytes_arg.clone(),
333            u256_arg.clone(),
334        ];
335
336        for arg in args {
337            let serialized = aptos_bcs::to_bytes(&arg).unwrap();
338            let deserialized: ScriptArgument = aptos_bcs::from_bytes(&serialized).unwrap();
339            assert_eq!(deserialized, arg);
340        }
341    }
342
343    #[test]
344    fn test_transaction_payload_from_entry_function() {
345        let entry_fn = EntryFunction::apt_transfer(AccountAddress::ONE, 100).unwrap();
346        let payload: TransactionPayload = entry_fn.into();
347
348        match payload {
349            TransactionPayload::EntryFunction(ef) => {
350                assert_eq!(ef.function, "transfer");
351            }
352            _ => panic!("Expected EntryFunction variant"),
353        }
354    }
355
356    #[test]
357    fn test_transaction_payload_from_script() {
358        let script = Script::new(vec![0x01], vec![], vec![]);
359        let payload: TransactionPayload = script.into();
360
361        match payload {
362            TransactionPayload::Script(s) => {
363                assert_eq!(s.code, vec![0x01]);
364            }
365            _ => panic!("Expected Script variant"),
366        }
367    }
368
369    #[test]
370    fn test_multisig_payload() {
371        let entry_fn = EntryFunction::apt_transfer(AccountAddress::ONE, 100).unwrap();
372        let multisig = Multisig {
373            multisig_address: AccountAddress::from_hex("0x999").unwrap(),
374            transaction_payload: Some(MultisigTransactionPayload::EntryFunction(entry_fn)),
375        };
376
377        let payload = TransactionPayload::Multisig(multisig.clone());
378        match payload {
379            TransactionPayload::Multisig(m) => {
380                assert_eq!(m.multisig_address, multisig.multisig_address);
381                assert!(m.transaction_payload.is_some());
382            }
383            _ => panic!("Expected Multisig variant"),
384        }
385    }
386
387    #[test]
388    fn test_entry_function_bcs_serialization() {
389        let entry_fn = EntryFunction::apt_transfer(AccountAddress::ONE, 1000).unwrap();
390        let serialized = aptos_bcs::to_bytes(&entry_fn).unwrap();
391        let deserialized: EntryFunction = aptos_bcs::from_bytes(&serialized).unwrap();
392
393        assert_eq!(entry_fn.module, deserialized.module);
394        assert_eq!(entry_fn.function, deserialized.function);
395        assert_eq!(entry_fn.type_args, deserialized.type_args);
396        assert_eq!(entry_fn.args, deserialized.args);
397    }
398
399    #[test]
400    fn test_transaction_payload_bcs_serialization() {
401        let entry_fn = EntryFunction::apt_transfer(AccountAddress::ONE, 1000).unwrap();
402        let payload = TransactionPayload::EntryFunction(entry_fn);
403
404        let serialized = aptos_bcs::to_bytes(&payload).unwrap();
405        let deserialized: TransactionPayload = aptos_bcs::from_bytes(&serialized).unwrap();
406
407        assert_eq!(payload, deserialized);
408    }
409
410    #[test]
411    fn test_from_function_id_invalid() {
412        // Missing module separator
413        let result = EntryFunction::from_function_id("0x1coin::transfer", vec![], vec![]);
414        assert!(result.is_err());
415
416        // Invalid address
417        let result = EntryFunction::from_function_id("invalid::module::function", vec![], vec![]);
418        assert!(result.is_err());
419    }
420}