Skip to main content

sui_json_rpc_types/
sui_move.rs

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