1use iota_sdk_types::{Argument, Command, ObjectId, ObjectReference, TypeTag, Version};
5use move_core_types::{ident_str, identifier::IdentStr, language_storage::StructTag};
6use serde::{Deserialize, Serialize};
7use serde_with::serde_as;
8
9use crate::{IOTA_FRAMEWORK_ADDRESS, iota_serde::TypeName, transaction::CallArg};
10
11pub const CALL_ARG_MODULE_NAME: &IdentStr = ident_str!("ptb_call_arg");
16pub const CALL_ARG_STRUCT_NAME: &IdentStr = ident_str!("CallArg");
17pub const OBJECT_ARG_STRUCT_NAME: &IdentStr = ident_str!("ObjectArg");
18pub const OBJECT_REF_STRUCT_NAME: &IdentStr = ident_str!("ObjectRef");
19
20pub const COMMAND_MODULE_NAME: &IdentStr = ident_str!("ptb_command");
21pub const COMMAND_STRUCT_NAME: &IdentStr = ident_str!("Command");
22pub const ARGUMENT_STRUCT_NAME: &IdentStr = ident_str!("Argument");
23pub const PROGRAMMABLE_MOVE_CALL_STRUCT_NAME: &IdentStr = ident_str!("ProgrammableMoveCall");
24pub const TRANSFER_OBJECTS_DATA_STRUCT_NAME: &IdentStr = ident_str!("TransferObjectsData");
25pub const SPLIT_COINS_DATA_STRUCT_NAME: &IdentStr = ident_str!("SplitCoinsData");
26pub const MERGE_COINS_DATA_STRUCT_NAME: &IdentStr = ident_str!("MergeCoinsData");
27pub const PUBLISH_DATA_STRUCT_NAME: &IdentStr = ident_str!("PublishData");
28pub const MAKE_MOVE_VEC_DATA_STRUCT_NAME: &IdentStr = ident_str!("MakeMoveVecData");
29pub const UPGRADE_DATA_STRUCT_NAME: &IdentStr = ident_str!("UpgradeData");
30
31#[serde_as]
40#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
41pub struct MoveProgrammableMoveCall {
42 pub package: ObjectId,
43 pub module: String,
44 pub function: String,
45 #[serde_as(as = "Vec<TypeName>")]
46 pub type_arguments: Vec<TypeTag>,
47 pub arguments: Vec<Argument>,
48}
49
50#[serde_as]
59#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
60pub enum MoveCommand {
61 MoveCall(Box<MoveProgrammableMoveCall>),
62 TransferObjects(Vec<Argument>, Argument),
63 SplitCoins(Argument, Vec<Argument>),
64 MergeCoins(Argument, Vec<Argument>),
65 Publish(Vec<Vec<u8>>, Vec<ObjectId>),
66 MakeMoveVec(
67 #[serde_as(as = "Option<TypeName>")] Option<TypeTag>,
68 Vec<Argument>,
69 ),
70 Upgrade(Vec<Vec<u8>>, Vec<ObjectId>, ObjectId, Argument),
71}
72
73impl From<&Command> for MoveCommand {
74 fn from(cmd: &Command) -> Self {
75 match cmd {
76 Command::MoveCall(cmd) => MoveCommand::MoveCall(Box::new(MoveProgrammableMoveCall {
77 package: cmd.package,
78 module: cmd.module.to_string(),
79 function: cmd.function.to_string(),
80 type_arguments: cmd.type_arguments.clone(),
81 arguments: cmd.arguments.clone(),
82 })),
83 Command::TransferObjects(cmd) => {
84 MoveCommand::TransferObjects(cmd.objects.clone(), cmd.address)
85 }
86 Command::SplitCoins(cmd) => MoveCommand::SplitCoins(cmd.coin, cmd.amounts.clone()),
87 Command::MergeCoins(cmd) => {
88 MoveCommand::MergeCoins(cmd.coin, cmd.coins_to_merge.clone())
89 }
90 Command::Publish(cmd) => {
91 MoveCommand::Publish(cmd.modules.clone(), cmd.dependencies.clone())
92 }
93 Command::MakeMoveVector(cmd) => {
94 MoveCommand::MakeMoveVec(cmd.type_.clone(), cmd.elements.clone())
95 }
96 Command::Upgrade(cmd) => MoveCommand::Upgrade(
97 cmd.modules.clone(),
98 cmd.dependencies.clone(),
99 cmd.package,
100 cmd.ticket,
101 ),
102 _ => unimplemented!("a new Command enum variant was added and needs to be handled"),
103 }
104 }
105}
106
107impl MoveCommand {
108 pub fn type_() -> StructTag {
109 StructTag {
110 address: IOTA_FRAMEWORK_ADDRESS,
111 module: COMMAND_MODULE_NAME.to_owned(),
112 name: COMMAND_STRUCT_NAME.to_owned(),
113 type_params: vec![],
114 }
115 }
116}
117
118#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
125pub enum MoveObjectArg {
126 ImmOrOwnedObject(ObjectReference),
127 SharedObject {
128 id: ObjectId,
129 initial_shared_version: Version,
130 mutable: bool,
131 },
132 Receiving(ObjectReference),
133}
134
135impl MoveObjectArg {
136 pub fn type_() -> StructTag {
137 StructTag {
138 address: IOTA_FRAMEWORK_ADDRESS,
139 module: CALL_ARG_MODULE_NAME.to_owned(),
140 name: OBJECT_ARG_STRUCT_NAME.to_owned(),
141 type_params: vec![],
142 }
143 }
144}
145
146#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
149pub enum MoveCallArg {
150 Pure(Vec<u8>),
151 Object(MoveObjectArg),
152}
153
154impl From<&CallArg> for MoveCallArg {
155 fn from(arg: &CallArg) -> Self {
156 match arg {
157 CallArg::Pure(bytes) => MoveCallArg::Pure(bytes.clone()),
158 CallArg::ImmutableOrOwned(obj_arg) => {
159 MoveCallArg::Object(MoveObjectArg::ImmOrOwnedObject(*obj_arg))
160 }
161 CallArg::Shared(obj_arg) => MoveCallArg::Object(MoveObjectArg::SharedObject {
162 id: obj_arg.object_id,
163 initial_shared_version: obj_arg.initial_shared_version,
164 mutable: obj_arg.mutable,
165 }),
166 CallArg::Receiving(obj_arg) => MoveCallArg::Object(MoveObjectArg::Receiving(*obj_arg)),
167 _ => unimplemented!("a new CallArg enum variant was added and needs to be handled"),
168 }
169 }
170}
171
172impl MoveCallArg {
173 pub fn type_() -> StructTag {
174 StructTag {
175 address: IOTA_FRAMEWORK_ADDRESS,
176 module: CALL_ARG_MODULE_NAME.to_owned(),
177 name: CALL_ARG_STRUCT_NAME.to_owned(),
178 type_params: vec![],
179 }
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use std::str::FromStr;
186
187 use iota_sdk_types::{
188 Address, Identifier, ObjectDigest, ObjectReference, SharedObjectReference, StructTag,
189 TypeTag,
190 };
191
192 use super::*;
193 use crate::transaction::CallArg;
194
195 fn obj_id() -> ObjectId {
198 ObjectId::from_prefixed_short_hex("0x0000000000000000000000000000000000000001").unwrap()
199 }
200
201 fn obj_ref() -> ObjectReference {
202 ObjectReference {
203 object_id: obj_id(),
204 version: Version::from(1),
205 digest: ObjectDigest::new([1u8; 32]),
206 }
207 }
208
209 fn round_trip<T>(value: &T) -> T
211 where
212 T: serde::Serialize + for<'de> serde::Deserialize<'de> + PartialEq + std::fmt::Debug,
213 {
214 let bytes = bcs::to_bytes(value).unwrap();
215 bcs::from_bytes(&bytes).unwrap()
216 }
217
218 #[test]
221 fn call_arg_pure_round_trip() {
222 let arg = MoveCallArg::Pure(vec![1, 2, 3]);
223 assert_eq!(round_trip(&arg), arg);
224 }
225
226 #[test]
227 fn call_arg_imm_or_owned_round_trip() {
228 let arg = MoveCallArg::Object(MoveObjectArg::ImmOrOwnedObject(obj_ref()));
229 assert_eq!(round_trip(&arg), arg);
230 }
231
232 #[test]
233 fn call_arg_shared_object_round_trip() {
234 let arg = MoveCallArg::Object(MoveObjectArg::SharedObject {
235 id: obj_id(),
236 initial_shared_version: Version::from(5),
237 mutable: true,
238 });
239 assert_eq!(round_trip(&arg), arg);
240 }
241
242 #[test]
243 fn call_arg_receiving_round_trip() {
244 let arg = MoveCallArg::Object(MoveObjectArg::Receiving(obj_ref()));
245 assert_eq!(round_trip(&arg), arg);
246 }
247
248 #[test]
251 fn call_arg_from_pure() {
252 let data = vec![10, 20, 30];
253 let converted = MoveCallArg::from(&CallArg::Pure(data.clone()));
254 assert_eq!(converted, MoveCallArg::Pure(data));
255 }
256
257 #[test]
258 fn call_arg_from_object() {
259 let converted = MoveCallArg::from(&CallArg::ImmutableOrOwned(obj_ref()));
260 assert_eq!(
261 converted,
262 MoveCallArg::Object(MoveObjectArg::ImmOrOwnedObject(obj_ref()))
263 );
264 }
265
266 #[test]
267 fn call_arg_from_call_arg() {
268 let call_arg = CallArg::Pure(vec![99]);
269 let converted = MoveCallArg::from(&call_arg);
270 assert!(matches!(converted, MoveCallArg::Pure(_)));
271 }
272
273 #[test]
276 fn call_arg_bcs_compatible_imm_or_owned() {
277 let tx_arg = CallArg::ImmutableOrOwned(obj_ref());
278 let ctx_arg = MoveCallArg::from(&tx_arg);
279 assert_eq!(
280 bcs::to_bytes(&tx_arg).unwrap(),
281 bcs::to_bytes(&ctx_arg).unwrap()
282 );
283 }
284
285 #[test]
286 fn call_arg_bcs_compatible_shared() {
287 let tx_arg = CallArg::Shared(SharedObjectReference::new(obj_id(), Version::from(5), true));
288 let ctx_arg = MoveCallArg::from(&tx_arg);
289 assert_eq!(
290 bcs::to_bytes(&tx_arg).unwrap(),
291 bcs::to_bytes(&ctx_arg).unwrap()
292 );
293 }
294
295 #[test]
296 fn call_arg_bcs_compatible_receiving() {
297 let tx_arg = CallArg::Receiving(obj_ref());
298 let ctx_arg = MoveCallArg::from(&tx_arg);
299 assert_eq!(
300 bcs::to_bytes(&tx_arg).unwrap(),
301 bcs::to_bytes(&ctx_arg).unwrap()
302 );
303 }
304
305 fn sample_move_call() -> MoveCommand {
308 MoveCommand::MoveCall(Box::new(MoveProgrammableMoveCall {
309 package: obj_id(),
310 module: "my_module".to_string(),
311 function: "my_func".to_string(),
312 type_arguments: vec![TypeTag::U64],
313 arguments: vec![Argument::Gas, Argument::Input(0)],
314 }))
315 }
316
317 #[test]
318 fn command_move_call_round_trip() {
319 assert_eq!(round_trip(&sample_move_call()), sample_move_call());
320 }
321
322 #[test]
323 fn command_transfer_objects_round_trip() {
324 let cmd = MoveCommand::TransferObjects(
325 vec![Argument::Input(0), Argument::Result(1)],
326 Argument::Input(2),
327 );
328 assert_eq!(round_trip(&cmd), cmd);
329 }
330
331 #[test]
332 fn command_split_coins_round_trip() {
333 let cmd = MoveCommand::SplitCoins(Argument::Gas, vec![Argument::Input(0)]);
334 assert_eq!(round_trip(&cmd), cmd);
335 }
336
337 #[test]
338 fn command_merge_coins_round_trip() {
339 let cmd =
340 MoveCommand::MergeCoins(Argument::Gas, vec![Argument::Input(0), Argument::Input(1)]);
341 assert_eq!(round_trip(&cmd), cmd);
342 }
343
344 #[test]
345 fn command_publish_round_trip() {
346 let cmd = MoveCommand::Publish(vec![vec![1, 2, 3]], vec![obj_id()]);
347 assert_eq!(round_trip(&cmd), cmd);
348 }
349
350 #[test]
351 fn command_make_move_vec_with_type_round_trip() {
352 let cmd = MoveCommand::MakeMoveVec(
353 Some(TypeTag::from_str("0x2::coin::Coin<u64>").unwrap()),
354 vec![Argument::Input(0)],
355 );
356 assert_eq!(round_trip(&cmd), cmd);
357 }
358
359 #[test]
360 fn command_make_move_vec_no_type_round_trip() {
361 let cmd = MoveCommand::MakeMoveVec(None, vec![Argument::Result(0)]);
362 assert_eq!(round_trip(&cmd), cmd);
363 }
364
365 #[test]
366 fn command_upgrade_round_trip() {
367 let cmd = MoveCommand::Upgrade(
368 vec![vec![0xde, 0xad]],
369 vec![obj_id()],
370 obj_id(),
371 Argument::Result(0),
372 );
373 assert_eq!(round_trip(&cmd), cmd);
374 }
375
376 #[test]
381 fn command_from_move_call_primitive_type_tag() {
382 let cases = [
383 (TypeTag::Bool, "bool"),
384 (TypeTag::U8, "u8"),
385 (TypeTag::U64, "u64"),
386 (TypeTag::U128, "u128"),
387 (TypeTag::U16, "u16"),
388 (TypeTag::U32, "u32"),
389 (TypeTag::U256, "u256"),
390 (TypeTag::Address, "address"),
391 ];
392 for (type_tag, expected_name) in cases {
393 let cmd = Command::new_move_call(
394 obj_id(),
395 Identifier::new_unchecked("m"),
396 Identifier::new_unchecked("f"),
397 vec![type_tag],
398 vec![],
399 );
400 let MoveCommand::MoveCall(call) = MoveCommand::from(&cmd) else {
401 panic!("expected MoveCall");
402 };
403 assert_eq!(
404 call.type_arguments,
405 vec![TypeTag::from_str(expected_name).unwrap()],
406 "failed for {expected_name}"
407 );
408 }
409 }
410
411 #[test]
413 fn command_from_move_call_struct_type_tag() {
414 let expected = TypeTag::Struct(Box::new(StructTag::new(
415 Address::FRAMEWORK,
416 "coin",
417 "Coin",
418 vec![TypeTag::U64],
419 )));
420
421 let cmd = Command::new_move_call(
422 obj_id(),
423 Identifier::new_unchecked("m"),
424 Identifier::new_unchecked("f"),
425 vec![expected.clone()],
426 vec![],
427 );
428 let MoveCommand::MoveCall(call) = MoveCommand::from(&cmd) else {
429 panic!("expected MoveCall");
430 };
431 assert_eq!(call.type_arguments, vec![expected]);
432 }
433
434 #[test]
435 fn command_from_make_move_vec_type_tag_becomes_type_name() {
436 let expected = TypeTag::Bool;
437 let cmd = Command::new_make_move_vector(Some(expected.clone()), vec![Argument::Input(0)]);
438 let MoveCommand::MakeMoveVec(name, _) = MoveCommand::from(&cmd) else {
439 panic!("expected MakeMoveVec");
440 };
441 assert_eq!(name, Some(expected));
442 }
443
444 #[test]
445 fn command_from_make_move_vec_none_type() {
446 let cmd = Command::new_make_move_vector(None, vec![]);
447 let MoveCommand::MakeMoveVec(name, elements) = MoveCommand::from(&cmd) else {
448 panic!("expected MakeMoveVec");
449 };
450 assert!(name.is_none());
451 assert!(elements.is_empty());
452 }
453
454 #[test]
455 fn command_from_command() {
456 let cmd = Command::new_move_call(
457 obj_id(),
458 Identifier::new_unchecked("m"),
459 Identifier::new_unchecked("f"),
460 vec![TypeTag::U8],
461 vec![],
462 );
463 let converted = MoveCommand::from(&cmd);
464 assert!(matches!(converted, MoveCommand::MoveCall(_)));
465 }
466}