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