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