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