1use move_core_types::{ident_str, identifier::IdentStr, language_storage::StructTag};
5use serde::{Deserialize, Serialize};
6
7use crate::{
8 IOTA_FRAMEWORK_ADDRESS,
9 base_types::{ObjectDigest, ObjectID, SequenceNumber},
10 transaction::{Argument, CallArg, Command, ObjectArg},
11 type_input::TypeName,
12};
13
14pub const CALL_ARG_MODULE_NAME: &IdentStr = ident_str!("ptb_call_arg");
19pub const CALL_ARG_STRUCT_NAME: &IdentStr = ident_str!("CallArg");
20pub const OBJECT_ARG_STRUCT_NAME: &IdentStr = ident_str!("ObjectArg");
21pub const OBJECT_REF_STRUCT_NAME: &IdentStr = ident_str!("ObjectRef");
22
23pub const COMMAND_MODULE_NAME: &IdentStr = ident_str!("ptb_command");
24pub const COMMAND_STRUCT_NAME: &IdentStr = ident_str!("Command");
25pub const ARGUMENT_STRUCT_NAME: &IdentStr = ident_str!("Argument");
26pub const PROGRAMMABLE_MOVE_CALL_STRUCT_NAME: &IdentStr = ident_str!("ProgrammableMoveCall");
27pub const TRANSFER_OBJECTS_DATA_STRUCT_NAME: &IdentStr = ident_str!("TransferObjectsData");
28pub const SPLIT_COINS_DATA_STRUCT_NAME: &IdentStr = ident_str!("SplitCoinsData");
29pub const MERGE_COINS_DATA_STRUCT_NAME: &IdentStr = ident_str!("MergeCoinsData");
30pub const PUBLISH_DATA_STRUCT_NAME: &IdentStr = ident_str!("PublishData");
31pub const MAKE_MOVE_VEC_DATA_STRUCT_NAME: &IdentStr = ident_str!("MakeMoveVecData");
32pub const UPGRADE_DATA_STRUCT_NAME: &IdentStr = ident_str!("UpgradeData");
33
34#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
43pub struct MoveProgrammableMoveCall {
44 pub package: ObjectID,
45 pub module: String,
46 pub function: String,
47 pub type_arguments: Vec<TypeName>,
48 pub arguments: Vec<Argument>,
49}
50
51#[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(Option<TypeName>, Vec<Argument>),
67 Upgrade(Vec<Vec<u8>>, Vec<ObjectID>, ObjectID, Argument),
68}
69
70impl From<&Command> for MoveCommand {
71 fn from(cmd: &Command) -> Self {
72 match cmd {
73 Command::MoveCall(m) => MoveCommand::MoveCall(Box::new(MoveProgrammableMoveCall {
74 package: m.package,
75 module: m.module.clone(),
76 function: m.function.clone(),
77 type_arguments: m.type_arguments.iter().map(TypeName::from).collect(),
78 arguments: m.arguments.clone(),
79 })),
80 Command::TransferObjects(objects, recipient) => {
81 MoveCommand::TransferObjects(objects.clone(), *recipient)
82 }
83 Command::SplitCoins(coin, amounts) => MoveCommand::SplitCoins(*coin, amounts.clone()),
84 Command::MergeCoins(target_coin, source_coins) => {
85 MoveCommand::MergeCoins(*target_coin, source_coins.clone())
86 }
87 Command::Publish(modules, dependencies) => {
88 MoveCommand::Publish(modules.clone(), dependencies.clone())
89 }
90 Command::MakeMoveVec(type_arg, elements) => {
91 MoveCommand::MakeMoveVec(type_arg.as_ref().map(TypeName::from), elements.clone())
92 }
93 Command::Upgrade(modules, dependencies, package, upgrade_ticket) => {
94 MoveCommand::Upgrade(
95 modules.clone(),
96 dependencies.clone(),
97 *package,
98 *upgrade_ticket,
99 )
100 }
101 }
102 }
103}
104
105impl MoveCommand {
106 pub fn type_() -> StructTag {
107 StructTag {
108 address: IOTA_FRAMEWORK_ADDRESS,
109 module: COMMAND_MODULE_NAME.to_owned(),
110 name: COMMAND_STRUCT_NAME.to_owned(),
111 type_params: vec![],
112 }
113 }
114}
115
116#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
123pub enum MoveObjectArg {
124 ImmOrOwnedObject((ObjectID, SequenceNumber, ObjectDigest)),
125 SharedObject {
126 id: ObjectID,
127 initial_shared_version: SequenceNumber,
128 mutable: bool,
129 },
130 Receiving((ObjectID, SequenceNumber, ObjectDigest)),
131}
132
133impl From<&ObjectArg> for MoveObjectArg {
134 fn from(obj: &ObjectArg) -> Self {
135 match obj {
136 ObjectArg::ImmOrOwnedObject(r) => MoveObjectArg::ImmOrOwnedObject(*r),
137 ObjectArg::SharedObject {
138 id,
139 initial_shared_version,
140 mutable,
141 } => MoveObjectArg::SharedObject {
142 id: *id,
143 initial_shared_version: *initial_shared_version,
144 mutable: *mutable,
145 },
146 ObjectArg::Receiving(r) => MoveObjectArg::Receiving(*r),
147 }
148 }
149}
150
151impl MoveObjectArg {
152 pub fn type_() -> StructTag {
153 StructTag {
154 address: IOTA_FRAMEWORK_ADDRESS,
155 module: CALL_ARG_MODULE_NAME.to_owned(),
156 name: OBJECT_ARG_STRUCT_NAME.to_owned(),
157 type_params: vec![],
158 }
159 }
160}
161
162#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
165pub enum MoveCallArg {
166 Pure(Vec<u8>),
167 Object(MoveObjectArg),
168}
169
170impl From<&CallArg> for MoveCallArg {
171 fn from(arg: &CallArg) -> Self {
172 match arg {
173 CallArg::Pure(bytes) => MoveCallArg::Pure(bytes.clone()),
174 CallArg::Object(obj_arg) => MoveCallArg::Object(MoveObjectArg::from(obj_arg)),
175 }
176 }
177}
178
179impl MoveCallArg {
180 pub fn type_() -> StructTag {
181 StructTag {
182 address: IOTA_FRAMEWORK_ADDRESS,
183 module: CALL_ARG_MODULE_NAME.to_owned(),
184 name: CALL_ARG_STRUCT_NAME.to_owned(),
185 type_params: vec![],
186 }
187 }
188}
189
190#[cfg(test)]
191mod tests {
192 use move_core_types::account_address::AccountAddress;
193
194 use super::*;
195 use crate::{
196 base_types::{ObjectDigest, ObjectID, SequenceNumber},
197 transaction::{Argument, CallArg, Command, ObjectArg, ProgrammableMoveCall},
198 type_input::{StructInput, TypeInput},
199 };
200
201 fn obj_id() -> ObjectID {
204 ObjectID::from_hex_literal("0x0000000000000000000000000000000000000001").unwrap()
205 }
206
207 fn obj_ref() -> (ObjectID, SequenceNumber, ObjectDigest) {
208 (
209 obj_id(),
210 SequenceNumber::from(1),
211 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 obj_arg = ObjectArg::ImmOrOwnedObject(obj_ref());
266 let converted = MoveCallArg::from(&CallArg::Object(obj_arg));
267 assert_eq!(
268 converted,
269 MoveCallArg::Object(MoveObjectArg::ImmOrOwnedObject(obj_ref()))
270 );
271 }
272
273 #[test]
274 fn call_arg_from_call_arg() {
275 let call_arg = CallArg::Pure(vec![99]);
276 let converted = MoveCallArg::from(&call_arg);
277 assert!(matches!(converted, MoveCallArg::Pure(_)));
278 }
279
280 #[test]
283 fn object_arg_bcs_compatible_imm_or_owned() {
284 let tx_arg = ObjectArg::ImmOrOwnedObject(obj_ref());
285 let ctx_arg = MoveObjectArg::from(&tx_arg);
286 assert_eq!(
287 bcs::to_bytes(&tx_arg).unwrap(),
288 bcs::to_bytes(&ctx_arg).unwrap()
289 );
290 }
291
292 #[test]
293 fn object_arg_bcs_compatible_shared() {
294 let tx_arg = ObjectArg::SharedObject {
295 id: obj_id(),
296 initial_shared_version: SequenceNumber::from(5),
297 mutable: true,
298 };
299 let ctx_arg = MoveObjectArg::from(&tx_arg);
300 assert_eq!(
301 bcs::to_bytes(&tx_arg).unwrap(),
302 bcs::to_bytes(&ctx_arg).unwrap()
303 );
304 }
305
306 #[test]
307 fn object_arg_bcs_compatible_receiving() {
308 let tx_arg = ObjectArg::Receiving(obj_ref());
309 let ctx_arg = MoveObjectArg::from(&tx_arg);
310 assert_eq!(
311 bcs::to_bytes(&tx_arg).unwrap(),
312 bcs::to_bytes(&ctx_arg).unwrap()
313 );
314 }
315
316 fn sample_move_call() -> MoveCommand {
319 MoveCommand::MoveCall(Box::new(MoveProgrammableMoveCall {
320 package: obj_id(),
321 module: "my_module".to_string(),
322 function: "my_func".to_string(),
323 type_arguments: vec![TypeName {
324 name: "u64".to_string(),
325 }],
326 arguments: vec![Argument::GasCoin, Argument::Input(0)],
327 }))
328 }
329
330 #[test]
331 fn command_move_call_round_trip() {
332 assert_eq!(round_trip(&sample_move_call()), sample_move_call());
333 }
334
335 #[test]
336 fn command_transfer_objects_round_trip() {
337 let cmd = MoveCommand::TransferObjects(
338 vec![Argument::Input(0), Argument::Result(1)],
339 Argument::Input(2),
340 );
341 assert_eq!(round_trip(&cmd), cmd);
342 }
343
344 #[test]
345 fn command_split_coins_round_trip() {
346 let cmd = MoveCommand::SplitCoins(Argument::GasCoin, vec![Argument::Input(0)]);
347 assert_eq!(round_trip(&cmd), cmd);
348 }
349
350 #[test]
351 fn command_merge_coins_round_trip() {
352 let cmd = MoveCommand::MergeCoins(
353 Argument::GasCoin,
354 vec![Argument::Input(0), Argument::Input(1)],
355 );
356 assert_eq!(round_trip(&cmd), cmd);
357 }
358
359 #[test]
360 fn command_publish_round_trip() {
361 let cmd = MoveCommand::Publish(vec![vec![1, 2, 3]], vec![obj_id()]);
362 assert_eq!(round_trip(&cmd), cmd);
363 }
364
365 #[test]
366 fn command_make_move_vec_with_type_round_trip() {
367 let cmd = MoveCommand::MakeMoveVec(
368 Some(TypeName {
369 name: "0x2::coin::Coin<u64>".to_string(),
370 }),
371 vec![Argument::Input(0)],
372 );
373 assert_eq!(round_trip(&cmd), cmd);
374 }
375
376 #[test]
377 fn command_make_move_vec_no_type_round_trip() {
378 let cmd = MoveCommand::MakeMoveVec(None, vec![Argument::Result(0)]);
379 assert_eq!(round_trip(&cmd), cmd);
380 }
381
382 #[test]
383 fn command_upgrade_round_trip() {
384 let cmd = MoveCommand::Upgrade(
385 vec![vec![0xde, 0xad]],
386 vec![obj_id()],
387 obj_id(),
388 Argument::Result(0),
389 );
390 assert_eq!(round_trip(&cmd), cmd);
391 }
392
393 #[test]
398 fn command_from_move_call_primitive_type_input() {
399 let cases = [
400 (TypeInput::Bool, "bool"),
401 (TypeInput::U8, "u8"),
402 (TypeInput::U64, "u64"),
403 (TypeInput::U128, "u128"),
404 (TypeInput::U16, "u16"),
405 (TypeInput::U32, "u32"),
406 (TypeInput::U256, "u256"),
407 (TypeInput::Address, "address"),
408 ];
409 for (type_input, expected_name) in cases {
410 let cmd = Command::MoveCall(Box::new(ProgrammableMoveCall {
411 package: obj_id(),
412 module: "m".to_string(),
413 function: "f".to_string(),
414 type_arguments: vec![type_input],
415 arguments: vec![],
416 }));
417 let MoveCommand::MoveCall(call) = MoveCommand::from(&cmd) else {
418 panic!("expected MoveCall");
419 };
420 assert_eq!(
421 call.type_arguments,
422 vec![TypeName {
423 name: expected_name.to_string()
424 }],
425 "failed for {expected_name}"
426 );
427 }
428 }
429
430 #[test]
432 fn command_from_move_call_struct_type_input() {
433 let type_input = TypeInput::Struct(Box::new(StructInput {
434 address: AccountAddress::from_hex_literal("0x2").unwrap(),
435 module: "coin".to_string(),
436 name: "Coin".to_string(),
437 type_params: vec![TypeInput::U64],
438 }));
439 let expected = TypeName::from(&type_input);
440
441 let cmd = Command::MoveCall(Box::new(ProgrammableMoveCall {
442 package: obj_id(),
443 module: "m".to_string(),
444 function: "f".to_string(),
445 type_arguments: vec![type_input],
446 arguments: vec![],
447 }));
448 let MoveCommand::MoveCall(call) = MoveCommand::from(&cmd) else {
449 panic!("expected MoveCall");
450 };
451 assert_eq!(call.type_arguments, vec![expected]);
452 }
453
454 #[test]
455 fn command_from_make_move_vec_type_input_becomes_type_name() {
456 let type_input = TypeInput::Bool;
457 let expected = TypeName::from(&type_input);
458 let cmd = Command::MakeMoveVec(Some(type_input), vec![Argument::Input(0)]);
459 let MoveCommand::MakeMoveVec(name, _) = MoveCommand::from(&cmd) else {
460 panic!("expected MakeMoveVec");
461 };
462 assert_eq!(name, Some(expected));
463 }
464
465 #[test]
466 fn command_from_make_move_vec_none_type() {
467 let cmd = Command::MakeMoveVec(None, vec![]);
468 let MoveCommand::MakeMoveVec(name, elements) = MoveCommand::from(&cmd) else {
469 panic!("expected MakeMoveVec");
470 };
471 assert!(name.is_none());
472 assert!(elements.is_empty());
473 }
474
475 #[test]
476 fn command_from_command() {
477 let cmd = Command::MoveCall(Box::new(ProgrammableMoveCall {
478 package: obj_id(),
479 module: "m".to_string(),
480 function: "f".to_string(),
481 type_arguments: vec![TypeInput::U8],
482 arguments: vec![],
483 }));
484 let converted = MoveCommand::from(&cmd);
485 assert!(matches!(converted, MoveCommand::MoveCall(_)));
486 }
487}