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