Skip to main content

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