iota_json_rpc_types/
iota_move.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use std::{
6    collections::BTreeMap,
7    fmt,
8    fmt::{Display, Formatter, Write},
9};
10
11use colored::Colorize;
12use iota_macros::EnumVariantOrder;
13use iota_types::{
14    base_types::{IotaAddress, ObjectID},
15    iota_serde::IotaStructTag,
16};
17use itertools::Itertools;
18use move_binary_format::{
19    file_format::{Ability, AbilitySet, DatatypeTyParameter, Visibility},
20    normalized::{
21        Field as NormalizedField, Function as IotaNormalizedFunction, Module as NormalizedModule,
22        Struct as NormalizedStruct, Type as NormalizedType,
23    },
24};
25use move_core_types::{
26    annotated_value::{MoveStruct, MoveValue, MoveVariant},
27    identifier::Identifier,
28    language_storage::StructTag,
29};
30use schemars::JsonSchema;
31use serde::{Deserialize, Serialize};
32use serde_json::{Value, json};
33use serde_with::serde_as;
34use tracing::warn;
35
36pub type IotaMoveTypeParameterIndex = u16;
37
38#[cfg(test)]
39#[path = "unit_tests/iota_move_tests.rs"]
40mod iota_move_tests;
41
42#[derive(Serialize, Deserialize, Copy, Clone, Debug, JsonSchema, PartialEq)]
43pub enum IotaMoveAbility {
44    Copy,
45    Drop,
46    Store,
47    Key,
48}
49
50#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, PartialEq)]
51pub struct IotaMoveAbilitySet {
52    pub abilities: Vec<IotaMoveAbility>,
53}
54
55#[derive(Serialize, Deserialize, Copy, Clone, Debug, JsonSchema, PartialEq)]
56pub enum IotaMoveVisibility {
57    Private,
58    Public,
59    Friend,
60}
61
62#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, PartialEq)]
63#[serde(rename_all = "camelCase")]
64pub struct IotaMoveStructTypeParameter {
65    pub constraints: IotaMoveAbilitySet,
66    pub is_phantom: bool,
67}
68
69#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, PartialEq)]
70pub struct IotaMoveNormalizedField {
71    pub name: String,
72    #[serde(rename = "type")]
73    pub type_: IotaMoveNormalizedType,
74}
75
76#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, PartialEq)]
77#[serde(rename_all = "camelCase")]
78pub struct IotaMoveNormalizedStruct {
79    pub abilities: IotaMoveAbilitySet,
80    pub type_parameters: Vec<IotaMoveStructTypeParameter>,
81    pub fields: Vec<IotaMoveNormalizedField>,
82}
83
84#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, PartialEq)]
85pub enum IotaMoveNormalizedType {
86    Bool,
87    U8,
88    U16,
89    U32,
90    U64,
91    U128,
92    U256,
93    Address,
94    Signer,
95    #[serde(rename_all = "camelCase")]
96    Struct {
97        address: String,
98        module: String,
99        name: String,
100        type_arguments: Vec<IotaMoveNormalizedType>,
101    },
102    Vector(Box<IotaMoveNormalizedType>),
103    TypeParameter(IotaMoveTypeParameterIndex),
104    Reference(Box<IotaMoveNormalizedType>),
105    MutableReference(Box<IotaMoveNormalizedType>),
106}
107
108#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, PartialEq)]
109#[serde(rename_all = "camelCase")]
110pub struct IotaMoveNormalizedFunction {
111    pub visibility: IotaMoveVisibility,
112    pub is_entry: bool,
113    pub type_parameters: Vec<IotaMoveAbilitySet>,
114    pub parameters: Vec<IotaMoveNormalizedType>,
115    pub return_: Vec<IotaMoveNormalizedType>,
116}
117
118#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema)]
119pub struct IotaMoveModuleId {
120    address: String,
121    name: String,
122}
123
124#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema)]
125#[serde(rename_all = "camelCase")]
126pub struct IotaMoveNormalizedModule {
127    pub file_format_version: u32,
128    pub address: String,
129    pub name: String,
130    pub friends: Vec<IotaMoveModuleId>,
131    pub structs: BTreeMap<String, IotaMoveNormalizedStruct>,
132    pub exposed_functions: BTreeMap<String, IotaMoveNormalizedFunction>,
133}
134
135impl PartialEq for IotaMoveNormalizedModule {
136    fn eq(&self, other: &Self) -> bool {
137        self.file_format_version == other.file_format_version
138            && self.address == other.address
139            && self.name == other.name
140    }
141}
142
143impl From<NormalizedModule> for IotaMoveNormalizedModule {
144    fn from(module: NormalizedModule) -> Self {
145        Self {
146            file_format_version: module.file_format_version,
147            address: module.address.to_hex_literal(),
148            name: module.name.to_string(),
149            friends: module
150                .friends
151                .into_iter()
152                .map(|module_id| IotaMoveModuleId {
153                    address: module_id.address().to_hex_literal(),
154                    name: module_id.name().to_string(),
155                })
156                .collect::<Vec<IotaMoveModuleId>>(),
157            structs: module
158                .structs
159                .into_iter()
160                .map(|(name, struct_)| (name.to_string(), IotaMoveNormalizedStruct::from(struct_)))
161                .collect::<BTreeMap<String, IotaMoveNormalizedStruct>>(),
162            exposed_functions: module
163                .functions
164                .into_iter()
165                .filter_map(|(name, function)| {
166                    // TODO: Do we want to expose the private functions as well?
167                    (function.is_entry || function.visibility != Visibility::Private)
168                        .then(|| (name.to_string(), IotaMoveNormalizedFunction::from(function)))
169                })
170                .collect::<BTreeMap<String, IotaMoveNormalizedFunction>>(),
171        }
172    }
173}
174
175impl From<IotaNormalizedFunction> for IotaMoveNormalizedFunction {
176    fn from(function: IotaNormalizedFunction) -> Self {
177        Self {
178            visibility: match function.visibility {
179                Visibility::Private => IotaMoveVisibility::Private,
180                Visibility::Public => IotaMoveVisibility::Public,
181                Visibility::Friend => IotaMoveVisibility::Friend,
182            },
183            is_entry: function.is_entry,
184            type_parameters: function
185                .type_parameters
186                .into_iter()
187                .map(|a| a.into())
188                .collect::<Vec<IotaMoveAbilitySet>>(),
189            parameters: function
190                .parameters
191                .into_iter()
192                .map(IotaMoveNormalizedType::from)
193                .collect::<Vec<IotaMoveNormalizedType>>(),
194            return_: function
195                .return_
196                .into_iter()
197                .map(IotaMoveNormalizedType::from)
198                .collect::<Vec<IotaMoveNormalizedType>>(),
199        }
200    }
201}
202
203impl From<NormalizedStruct> for IotaMoveNormalizedStruct {
204    fn from(struct_: NormalizedStruct) -> Self {
205        Self {
206            abilities: struct_.abilities.into(),
207            type_parameters: struct_
208                .type_parameters
209                .into_iter()
210                .map(IotaMoveStructTypeParameter::from)
211                .collect::<Vec<IotaMoveStructTypeParameter>>(),
212            fields: struct_
213                .fields
214                .into_iter()
215                .map(IotaMoveNormalizedField::from)
216                .collect::<Vec<IotaMoveNormalizedField>>(),
217        }
218    }
219}
220
221impl From<DatatypeTyParameter> for IotaMoveStructTypeParameter {
222    fn from(type_parameter: DatatypeTyParameter) -> Self {
223        Self {
224            constraints: type_parameter.constraints.into(),
225            is_phantom: type_parameter.is_phantom,
226        }
227    }
228}
229
230impl From<NormalizedField> for IotaMoveNormalizedField {
231    fn from(normalized_field: NormalizedField) -> Self {
232        Self {
233            name: normalized_field.name.to_string(),
234            type_: IotaMoveNormalizedType::from(normalized_field.type_),
235        }
236    }
237}
238
239impl From<NormalizedType> for IotaMoveNormalizedType {
240    fn from(type_: NormalizedType) -> Self {
241        match type_ {
242            NormalizedType::Bool => IotaMoveNormalizedType::Bool,
243            NormalizedType::U8 => IotaMoveNormalizedType::U8,
244            NormalizedType::U16 => IotaMoveNormalizedType::U16,
245            NormalizedType::U32 => IotaMoveNormalizedType::U32,
246            NormalizedType::U64 => IotaMoveNormalizedType::U64,
247            NormalizedType::U128 => IotaMoveNormalizedType::U128,
248            NormalizedType::U256 => IotaMoveNormalizedType::U256,
249            NormalizedType::Address => IotaMoveNormalizedType::Address,
250            NormalizedType::Signer => IotaMoveNormalizedType::Signer,
251            NormalizedType::Struct {
252                address,
253                module,
254                name,
255                type_arguments,
256            } => IotaMoveNormalizedType::Struct {
257                address: address.to_hex_literal(),
258                module: module.to_string(),
259                name: name.to_string(),
260                type_arguments: type_arguments
261                    .into_iter()
262                    .map(IotaMoveNormalizedType::from)
263                    .collect::<Vec<IotaMoveNormalizedType>>(),
264            },
265            NormalizedType::Vector(v) => {
266                IotaMoveNormalizedType::Vector(Box::new(IotaMoveNormalizedType::from(*v)))
267            }
268            NormalizedType::TypeParameter(t) => IotaMoveNormalizedType::TypeParameter(t),
269            NormalizedType::Reference(r) => {
270                IotaMoveNormalizedType::Reference(Box::new(IotaMoveNormalizedType::from(*r)))
271            }
272            NormalizedType::MutableReference(mr) => IotaMoveNormalizedType::MutableReference(
273                Box::new(IotaMoveNormalizedType::from(*mr)),
274            ),
275        }
276    }
277}
278
279impl From<AbilitySet> for IotaMoveAbilitySet {
280    fn from(set: AbilitySet) -> IotaMoveAbilitySet {
281        Self {
282            abilities: set
283                .into_iter()
284                .map(|a| match a {
285                    Ability::Copy => IotaMoveAbility::Copy,
286                    Ability::Drop => IotaMoveAbility::Drop,
287                    Ability::Key => IotaMoveAbility::Key,
288                    Ability::Store => IotaMoveAbility::Store,
289                })
290                .collect::<Vec<IotaMoveAbility>>(),
291        }
292    }
293}
294
295#[derive(Serialize, Deserialize, Copy, Clone, Debug, JsonSchema, PartialEq)]
296pub enum ObjectValueKind {
297    ByImmutableReference,
298    ByMutableReference,
299    ByValue,
300}
301
302#[derive(Serialize, Deserialize, Copy, Clone, Debug, JsonSchema, PartialEq)]
303pub enum MoveFunctionArgType {
304    Pure,
305    Object(ObjectValueKind),
306}
307
308#[serde_as]
309#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone, Eq, PartialEq, EnumVariantOrder)]
310#[serde(untagged, rename = "MoveValue")]
311pub enum IotaMoveValue {
312    // u64 and u128 are converted to String to avoid overflow
313    Number(u32),
314    Bool(bool),
315    Address(IotaAddress),
316    Vector(Vec<IotaMoveValue>),
317    String(String),
318    UID { id: ObjectID },
319    Struct(IotaMoveStruct),
320    Option(Box<Option<IotaMoveValue>>),
321    Variant(IotaMoveVariant),
322}
323
324impl IotaMoveValue {
325    /// Extract values from MoveValue without type information in json format
326    pub fn to_json_value(self) -> Value {
327        match self {
328            IotaMoveValue::Struct(move_struct) => move_struct.to_json_value(),
329            IotaMoveValue::Vector(values) => IotaMoveStruct::Runtime(values).to_json_value(),
330            IotaMoveValue::Number(v) => json!(v),
331            IotaMoveValue::Bool(v) => json!(v),
332            IotaMoveValue::Address(v) => json!(v),
333            IotaMoveValue::String(v) => json!(v),
334            IotaMoveValue::UID { id } => json!({ "id": id }),
335            IotaMoveValue::Option(v) => json!(v),
336            IotaMoveValue::Variant(v) => v.to_json_value(),
337        }
338    }
339}
340
341impl Display for IotaMoveValue {
342    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
343        let mut writer = String::new();
344        match self {
345            IotaMoveValue::Number(value) => write!(writer, "{value}")?,
346            IotaMoveValue::Bool(value) => write!(writer, "{value}")?,
347            IotaMoveValue::Address(value) => write!(writer, "{value}")?,
348            IotaMoveValue::String(value) => write!(writer, "{value}")?,
349            IotaMoveValue::UID { id } => write!(writer, "{id}")?,
350            IotaMoveValue::Struct(value) => write!(writer, "{value}")?,
351            IotaMoveValue::Option(value) => write!(writer, "{value:?}")?,
352            IotaMoveValue::Vector(vec) => {
353                write!(
354                    writer,
355                    "{}",
356                    vec.iter().map(|value| format!("{value}")).join(",\n")
357                )?;
358            }
359            IotaMoveValue::Variant(value) => write!(writer, "{value}")?,
360        }
361        write!(f, "{}", writer.trim_end_matches('\n'))
362    }
363}
364
365impl From<MoveValue> for IotaMoveValue {
366    fn from(value: MoveValue) -> Self {
367        match value {
368            MoveValue::U8(value) => IotaMoveValue::Number(value.into()),
369            MoveValue::U16(value) => IotaMoveValue::Number(value.into()),
370            MoveValue::U32(value) => IotaMoveValue::Number(value),
371            MoveValue::U64(value) => IotaMoveValue::String(format!("{value}")),
372            MoveValue::U128(value) => IotaMoveValue::String(format!("{value}")),
373            MoveValue::U256(value) => IotaMoveValue::String(format!("{value}")),
374            MoveValue::Bool(value) => IotaMoveValue::Bool(value),
375            MoveValue::Vector(values) => {
376                IotaMoveValue::Vector(values.into_iter().map(|value| value.into()).collect())
377            }
378            MoveValue::Struct(value) => {
379                // Best effort IOTA core type conversion
380                let MoveStruct { type_, fields } = &value;
381                if let Some(value) = try_convert_type(type_, fields) {
382                    return value;
383                }
384                IotaMoveValue::Struct(value.into())
385            }
386            MoveValue::Signer(value) | MoveValue::Address(value) => {
387                IotaMoveValue::Address(IotaAddress::from(ObjectID::from(value)))
388            }
389            MoveValue::Variant(MoveVariant {
390                type_,
391                variant_name,
392                tag: _,
393                fields,
394            }) => IotaMoveValue::Variant(IotaMoveVariant {
395                type_: type_.clone(),
396                variant: variant_name.to_string(),
397                fields: fields
398                    .into_iter()
399                    .map(|(id, value)| (id.into_string(), value.into()))
400                    .collect::<BTreeMap<_, _>>(),
401            }),
402        }
403    }
404}
405
406fn to_bytearray(value: &[MoveValue]) -> Option<Vec<u8>> {
407    if value.iter().all(|value| matches!(value, MoveValue::U8(_))) {
408        let bytearray = value
409            .iter()
410            .flat_map(|value| {
411                if let MoveValue::U8(u8) = value {
412                    Some(*u8)
413                } else {
414                    None
415                }
416            })
417            .collect::<Vec<_>>();
418        Some(bytearray)
419    } else {
420        None
421    }
422}
423
424#[serde_as]
425#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone, Eq, PartialEq)]
426#[serde(rename = "MoveVariant")]
427pub struct IotaMoveVariant {
428    #[schemars(with = "String")]
429    #[serde(rename = "type")]
430    #[serde_as(as = "IotaStructTag")]
431    pub type_: StructTag,
432    pub variant: String,
433    pub fields: BTreeMap<String, IotaMoveValue>,
434}
435
436impl IotaMoveVariant {
437    pub fn to_json_value(self) -> Value {
438        // We only care about values here, assuming type information is known at the
439        // client side.
440        let fields = self
441            .fields
442            .into_iter()
443            .map(|(key, value)| (key, value.to_json_value()))
444            .collect::<BTreeMap<_, _>>();
445        json!({
446            "variant": self.variant,
447            "fields": fields,
448        })
449    }
450}
451
452impl Display for IotaMoveVariant {
453    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
454        let mut writer = String::new();
455        let IotaMoveVariant {
456            type_,
457            variant,
458            fields,
459        } = self;
460        writeln!(writer)?;
461        writeln!(writer, "  {}: {type_}", "type".bold().bright_black())?;
462        writeln!(writer, "  {}: {variant}", "variant".bold().bright_black())?;
463        for (name, value) in fields {
464            let value = format!("{}", value);
465            let value = if value.starts_with('\n') {
466                indent(&value, 2)
467            } else {
468                value
469            };
470            writeln!(writer, "  {}: {value}", name.bold().bright_black())?;
471        }
472
473        write!(f, "{}", writer.trim_end_matches('\n'))
474    }
475}
476
477#[serde_as]
478#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone, Eq, PartialEq, EnumVariantOrder)]
479#[serde(untagged, rename = "MoveStruct")]
480pub enum IotaMoveStruct {
481    Runtime(Vec<IotaMoveValue>),
482    WithTypes {
483        #[schemars(with = "String")]
484        #[serde(rename = "type")]
485        #[serde_as(as = "IotaStructTag")]
486        type_: StructTag,
487        fields: BTreeMap<String, IotaMoveValue>,
488    },
489    WithFields(BTreeMap<String, IotaMoveValue>),
490}
491
492impl IotaMoveStruct {
493    /// Extract values from MoveStruct without type information in json format
494    pub fn to_json_value(self) -> Value {
495        // Unwrap MoveStructs
496        match self {
497            IotaMoveStruct::Runtime(values) => {
498                let values = values
499                    .into_iter()
500                    .map(|value| value.to_json_value())
501                    .collect::<Vec<_>>();
502                json!(values)
503            }
504            // We only care about values here, assuming struct type information is known at the
505            // client side.
506            IotaMoveStruct::WithTypes { type_: _, fields } | IotaMoveStruct::WithFields(fields) => {
507                let fields = fields
508                    .into_iter()
509                    .map(|(key, value)| (key, value.to_json_value()))
510                    .collect::<BTreeMap<_, _>>();
511                json!(fields)
512            }
513        }
514    }
515
516    pub fn read_dynamic_field_value(&self, field_name: &str) -> Option<IotaMoveValue> {
517        match self {
518            IotaMoveStruct::WithFields(fields) => fields.get(field_name).cloned(),
519            IotaMoveStruct::WithTypes { type_: _, fields } => fields.get(field_name).cloned(),
520            _ => None,
521        }
522    }
523}
524
525impl Display for IotaMoveStruct {
526    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
527        let mut writer = String::new();
528        match self {
529            IotaMoveStruct::Runtime(_) => {}
530            IotaMoveStruct::WithFields(fields) => {
531                for (name, value) in fields {
532                    writeln!(writer, "{}: {value}", name.bold().bright_black())?;
533                }
534            }
535            IotaMoveStruct::WithTypes { type_, fields } => {
536                writeln!(writer)?;
537                writeln!(writer, "  {}: {type_}", "type".bold().bright_black())?;
538                for (name, value) in fields {
539                    let value = format!("{value}");
540                    let value = if value.starts_with('\n') {
541                        indent(&value, 2)
542                    } else {
543                        value
544                    };
545                    writeln!(writer, "  {}: {value}", name.bold().bright_black())?;
546                }
547            }
548        }
549        write!(f, "{}", writer.trim_end_matches('\n'))
550    }
551}
552
553fn indent<T: Display>(d: &T, indent: usize) -> String {
554    d.to_string()
555        .lines()
556        .map(|line| format!("{:indent$}{line}", ""))
557        .join("\n")
558}
559
560fn try_convert_type(
561    type_: &StructTag,
562    fields: &[(Identifier, MoveValue)],
563) -> Option<IotaMoveValue> {
564    let struct_name = format!(
565        "0x{}::{}::{}",
566        type_.address.short_str_lossless(),
567        type_.module,
568        type_.name
569    );
570    let mut values = fields
571        .iter()
572        .map(|(id, value)| (id.to_string(), value))
573        .collect::<BTreeMap<_, _>>();
574    match struct_name.as_str() {
575        "0x1::string::String" | "0x1::ascii::String" => {
576            if let Some(MoveValue::Vector(bytes)) = values.remove("bytes") {
577                return to_bytearray(bytes)
578                    .and_then(|bytes| String::from_utf8(bytes).ok())
579                    .map(IotaMoveValue::String);
580            }
581        }
582        "0x2::url::Url" => {
583            return values.remove("url").cloned().map(IotaMoveValue::from);
584        }
585        "0x2::object::ID" => {
586            return values.remove("bytes").cloned().map(IotaMoveValue::from);
587        }
588        "0x2::object::UID" => {
589            let id = values.remove("id").cloned().map(IotaMoveValue::from);
590            if let Some(IotaMoveValue::Address(address)) = id {
591                return Some(IotaMoveValue::UID {
592                    id: ObjectID::from(address),
593                });
594            }
595        }
596        "0x2::balance::Balance" => {
597            return values.remove("value").cloned().map(IotaMoveValue::from);
598        }
599        "0x1::option::Option" => {
600            if let Some(MoveValue::Vector(values)) = values.remove("vec") {
601                return Some(IotaMoveValue::Option(Box::new(
602                    // in Move option is modeled as vec of 1 element
603                    values.first().cloned().map(IotaMoveValue::from),
604                )));
605            }
606        }
607        _ => return None,
608    }
609    warn!(
610        fields =? fields,
611        "Failed to convert {struct_name} to IotaMoveValue"
612    );
613    None
614}
615
616impl From<MoveStruct> for IotaMoveStruct {
617    fn from(move_struct: MoveStruct) -> Self {
618        IotaMoveStruct::WithTypes {
619            type_: move_struct.type_,
620            fields: move_struct
621                .fields
622                .into_iter()
623                .map(|(id, value)| (id.into_string(), value.into()))
624                .collect(),
625        }
626    }
627}