iota_json_rpc_types/
iota_object.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    cmp::Ordering,
7    collections::BTreeMap,
8    fmt,
9    fmt::{Display, Formatter, Write},
10};
11
12use anyhow::{anyhow, bail};
13use colored::Colorize;
14use fastcrypto::encoding::Base64;
15use iota_protocol_config::ProtocolConfig;
16use iota_types::{
17    base_types::{
18        IotaAddress, ObjectDigest, ObjectID, ObjectInfo, ObjectRef, ObjectType, SequenceNumber,
19        TransactionDigest,
20    },
21    error::{
22        ExecutionError, IotaError, IotaObjectResponseError, IotaResult, UserInputError,
23        UserInputResult,
24    },
25    gas_coin::GasCoin,
26    iota_serde::{BigInt, IotaStructTag, SequenceNumber as AsSequenceNumber},
27    messages_checkpoint::CheckpointSequenceNumber,
28    move_package::{MovePackage, TypeOrigin, UpgradeInfo},
29    object::{Data, MoveObject, Object, ObjectInner, ObjectRead, Owner},
30};
31use move_bytecode_utils::module_cache::GetModule;
32use move_core_types::{
33    annotated_value::{MoveStructLayout, MoveValue},
34    identifier::Identifier,
35    language_storage::StructTag,
36};
37use schemars::JsonSchema;
38use serde::{Deserialize, Serialize};
39use serde_json::Value;
40use serde_with::{DisplayFromStr, serde_as};
41
42use crate::{IotaMoveStruct, IotaMoveValue, Page};
43
44#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone, PartialEq, Eq)]
45pub struct IotaObjectResponse {
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub data: Option<IotaObjectData>,
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub error: Option<IotaObjectResponseError>,
50}
51
52impl IotaObjectResponse {
53    pub fn new(data: Option<IotaObjectData>, error: Option<IotaObjectResponseError>) -> Self {
54        Self { data, error }
55    }
56
57    pub fn new_with_data(data: IotaObjectData) -> Self {
58        Self {
59            data: Some(data),
60            error: None,
61        }
62    }
63
64    pub fn new_with_error(error: IotaObjectResponseError) -> Self {
65        Self {
66            data: None,
67            error: Some(error),
68        }
69    }
70}
71
72impl Ord for IotaObjectResponse {
73    fn cmp(&self, other: &Self) -> Ordering {
74        match (&self.data, &other.data) {
75            (Some(data), Some(data_2)) => {
76                if data.object_id.cmp(&data_2.object_id).eq(&Ordering::Greater) {
77                    return Ordering::Greater;
78                } else if data.object_id.cmp(&data_2.object_id).eq(&Ordering::Less) {
79                    return Ordering::Less;
80                }
81                Ordering::Equal
82            }
83            // In this ordering those with data will come before IotaObjectResponses that are
84            // errors.
85            (Some(_), None) => Ordering::Less,
86            (None, Some(_)) => Ordering::Greater,
87            // IotaObjectResponses that are errors are just considered equal.
88            _ => Ordering::Equal,
89        }
90    }
91}
92
93impl PartialOrd for IotaObjectResponse {
94    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
95        Some(self.cmp(other))
96    }
97}
98
99impl IotaObjectResponse {
100    pub fn move_object_bcs(&self) -> Option<&Vec<u8>> {
101        match &self.data {
102            Some(IotaObjectData {
103                bcs: Some(IotaRawData::MoveObject(obj)),
104                ..
105            }) => Some(&obj.bcs_bytes),
106            _ => None,
107        }
108    }
109
110    pub fn owner(&self) -> Option<Owner> {
111        if let Some(data) = &self.data {
112            return data.owner;
113        }
114        None
115    }
116
117    pub fn object_id(&self) -> Result<ObjectID, anyhow::Error> {
118        Ok(match (&self.data, &self.error) {
119            (Some(obj_data), None) => obj_data.object_id,
120            (None, Some(IotaObjectResponseError::NotExists { object_id })) => *object_id,
121            (
122                None,
123                Some(IotaObjectResponseError::Deleted {
124                    object_id,
125                    version: _,
126                    digest: _,
127                }),
128            ) => *object_id,
129            _ => bail!(
130                "Could not get object_id, something went wrong with IotaObjectResponse construction."
131            ),
132        })
133    }
134
135    pub fn object_ref_if_exists(&self) -> Option<ObjectRef> {
136        match (&self.data, &self.error) {
137            (Some(obj_data), None) => Some(obj_data.object_ref()),
138            _ => None,
139        }
140    }
141}
142
143impl TryFrom<IotaObjectResponse> for ObjectInfo {
144    type Error = anyhow::Error;
145
146    fn try_from(value: IotaObjectResponse) -> Result<Self, Self::Error> {
147        let IotaObjectData {
148            object_id,
149            version,
150            digest,
151            type_,
152            owner,
153            previous_transaction,
154            ..
155        } = value.into_object()?;
156
157        Ok(ObjectInfo {
158            object_id,
159            version,
160            digest,
161            type_: type_.ok_or_else(|| anyhow!("Object type not found for object."))?,
162            owner: owner.ok_or_else(|| anyhow!("Owner not found for object."))?,
163            previous_transaction: previous_transaction
164                .ok_or_else(|| anyhow!("Transaction digest not found for object."))?,
165        })
166    }
167}
168
169#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, Eq, PartialEq)]
170pub struct DisplayFieldsResponse {
171    pub data: Option<BTreeMap<String, String>>,
172    pub error: Option<IotaObjectResponseError>,
173}
174
175#[serde_as]
176#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, Eq, PartialEq)]
177#[serde(rename_all = "camelCase", rename = "ObjectData")]
178pub struct IotaObjectData {
179    pub object_id: ObjectID,
180    /// Object version.
181    #[schemars(with = "AsSequenceNumber")]
182    #[serde_as(as = "AsSequenceNumber")]
183    pub version: SequenceNumber,
184    /// Base64 string representing the object digest
185    pub digest: ObjectDigest,
186    /// The type of the object. Default to be None unless
187    /// IotaObjectDataOptions.showType is set to true
188    #[schemars(with = "Option<String>")]
189    #[serde_as(as = "Option<DisplayFromStr>")]
190    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
191    pub type_: Option<ObjectType>,
192    // Default to be None because otherwise it will be repeated for the getOwnedObjects endpoint
193    /// The owner of this object. Default to be None unless
194    /// IotaObjectDataOptions.showOwner is set to true
195    #[serde(skip_serializing_if = "Option::is_none")]
196    pub owner: Option<Owner>,
197    /// The digest of the transaction that created or last mutated this object.
198    /// Default to be None unless IotaObjectDataOptions.
199    /// showPreviousTransaction is set to true
200    #[serde(skip_serializing_if = "Option::is_none")]
201    pub previous_transaction: Option<TransactionDigest>,
202    /// The amount of IOTA we would rebate if this object gets deleted.
203    /// This number is re-calculated each time the object is mutated based on
204    /// the present storage gas price.
205    #[schemars(with = "Option<BigInt<u64>>")]
206    #[serde_as(as = "Option<BigInt<u64>>")]
207    #[serde(skip_serializing_if = "Option::is_none")]
208    pub storage_rebate: Option<u64>,
209    /// The Display metadata for frontend UI rendering, default to be None
210    /// unless IotaObjectDataOptions.showContent is set to true This can also
211    /// be None if the struct type does not have Display defined
212    #[serde(skip_serializing_if = "Option::is_none")]
213    pub display: Option<DisplayFieldsResponse>,
214    /// Move object content or package content, default to be None unless
215    /// IotaObjectDataOptions.showContent is set to true
216    #[serde(skip_serializing_if = "Option::is_none")]
217    pub content: Option<IotaParsedData>,
218    /// Move object content or package content in BCS, default to be None unless
219    /// IotaObjectDataOptions.showBcs is set to true
220    #[serde(skip_serializing_if = "Option::is_none")]
221    pub bcs: Option<IotaRawData>,
222}
223
224impl IotaObjectData {
225    pub fn new(
226        object_ref: ObjectRef,
227        obj: Object,
228        layout: impl Into<Option<MoveStructLayout>>,
229        options: IotaObjectDataOptions,
230        display_fields: impl Into<Option<DisplayFieldsResponse>>,
231    ) -> anyhow::Result<Self> {
232        let layout = layout.into();
233        let display_fields = display_fields.into();
234        let show_display = options.show_display;
235        let IotaObjectDataOptions {
236            show_type,
237            show_owner,
238            show_previous_transaction,
239            show_content,
240            show_bcs,
241            show_storage_rebate,
242            ..
243        } = options;
244
245        let (object_id, version, digest) = object_ref;
246        let type_ = if show_type {
247            Some(Into::<ObjectType>::into(&obj))
248        } else {
249            None
250        };
251
252        let bcs: Option<IotaRawData> = if show_bcs {
253            let data = match obj.data.clone() {
254                Data::Move(m) => {
255                    let layout = layout.clone().ok_or_else(|| {
256                        anyhow!("Layout is required to convert Move object to json")
257                    })?;
258                    IotaRawData::try_from_object(m, layout)?
259                }
260                Data::Package(p) => IotaRawData::try_from_package(p)
261                    .map_err(|e| anyhow!("Error getting raw data from package: {e:#?}"))?,
262            };
263            Some(data)
264        } else {
265            None
266        };
267
268        let obj = obj.into_inner();
269
270        let content: Option<IotaParsedData> = if show_content {
271            let data = match obj.data {
272                Data::Move(m) => {
273                    let layout = layout.ok_or_else(|| {
274                        anyhow!("Layout is required to convert Move object to json")
275                    })?;
276                    IotaParsedData::try_from_object(m, layout)?
277                }
278                Data::Package(p) => IotaParsedData::try_from_package(p)?,
279            };
280            Some(data)
281        } else {
282            None
283        };
284
285        Ok(IotaObjectData {
286            object_id,
287            version,
288            digest,
289            type_,
290            owner: if show_owner { Some(obj.owner) } else { None },
291            storage_rebate: if show_storage_rebate {
292                Some(obj.storage_rebate)
293            } else {
294                None
295            },
296            previous_transaction: if show_previous_transaction {
297                Some(obj.previous_transaction)
298            } else {
299                None
300            },
301            content,
302            bcs,
303            display: if show_display { display_fields } else { None },
304        })
305    }
306
307    pub fn object_ref(&self) -> ObjectRef {
308        (self.object_id, self.version, self.digest)
309    }
310
311    pub fn object_type(&self) -> anyhow::Result<ObjectType> {
312        self.type_
313            .as_ref()
314            .ok_or_else(|| anyhow!("type is missing for object {:?}", self.object_id))
315            .cloned()
316    }
317
318    pub fn is_gas_coin(&self) -> bool {
319        match self.type_.as_ref() {
320            Some(ObjectType::Struct(ty)) if ty.is_gas_coin() => true,
321            Some(_) => false,
322            None => false,
323        }
324    }
325}
326
327impl Display for IotaObjectData {
328    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
329        let type_ = if let Some(type_) = &self.type_ {
330            type_.to_string()
331        } else {
332            "Unknown Type".into()
333        };
334        let mut writer = String::new();
335        writeln!(
336            writer,
337            "{}",
338            format!("----- {type_} ({}[{}]) -----", self.object_id, self.version).bold()
339        )?;
340        if let Some(owner) = self.owner {
341            writeln!(writer, "{}: {owner}", "Owner".bold().bright_black())?;
342        }
343
344        writeln!(
345            writer,
346            "{}: {}",
347            "Version".bold().bright_black(),
348            self.version
349        )?;
350        if let Some(storage_rebate) = self.storage_rebate {
351            writeln!(
352                writer,
353                "{}: {storage_rebate}",
354                "Storage Rebate".bold().bright_black(),
355            )?;
356        }
357
358        if let Some(previous_transaction) = self.previous_transaction {
359            writeln!(
360                writer,
361                "{}: {previous_transaction:?}",
362                "Previous Transaction".bold().bright_black(),
363            )?;
364        }
365        if let Some(content) = self.content.as_ref() {
366            writeln!(writer, "{}", "----- Data -----".bold())?;
367            write!(writer, "{content}")?;
368        }
369
370        write!(f, "{writer}")
371    }
372}
373
374impl TryFrom<&IotaObjectData> for GasCoin {
375    type Error = anyhow::Error;
376    fn try_from(object: &IotaObjectData) -> Result<Self, Self::Error> {
377        match &object
378            .content
379            .as_ref()
380            .ok_or_else(|| anyhow!("Expect object content to not be empty"))?
381        {
382            IotaParsedData::MoveObject(o) => {
383                if GasCoin::type_() == o.type_ {
384                    return GasCoin::try_from(&o.fields);
385                }
386            }
387            IotaParsedData::Package(_) => {}
388        }
389
390        bail!("Gas object type is not a gas coin: {:?}", object.type_)
391    }
392}
393
394impl TryFrom<&IotaMoveStruct> for GasCoin {
395    type Error = anyhow::Error;
396    fn try_from(move_struct: &IotaMoveStruct) -> Result<Self, Self::Error> {
397        match move_struct {
398            IotaMoveStruct::WithFields(fields) | IotaMoveStruct::WithTypes { type_: _, fields } => {
399                if let Some(IotaMoveValue::String(balance)) = fields.get("balance") {
400                    if let Ok(balance) = balance.parse::<u64>() {
401                        if let Some(IotaMoveValue::UID { id }) = fields.get("id") {
402                            return Ok(GasCoin::new(*id, balance));
403                        }
404                    }
405                }
406            }
407            _ => {}
408        }
409        bail!("Struct is not a gas coin: {move_struct:?}")
410    }
411}
412
413#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, Eq, PartialEq, Default)]
414#[serde(rename_all = "camelCase", rename = "ObjectDataOptions", default)]
415pub struct IotaObjectDataOptions {
416    /// Whether to show the type of the object. Default to be False
417    pub show_type: bool,
418    /// Whether to show the owner of the object. Default to be False
419    pub show_owner: bool,
420    /// Whether to show the previous transaction digest of the object. Default
421    /// to be False
422    pub show_previous_transaction: bool,
423    /// Whether to show the Display metadata of the object for frontend
424    /// rendering. Default to be False
425    pub show_display: bool,
426    /// Whether to show the content(i.e., package content or Move struct
427    /// content) of the object. Default to be False
428    pub show_content: bool,
429    /// Whether to show the content in BCS format. Default to be False
430    pub show_bcs: bool,
431    /// Whether to show the storage rebate of the object. Default to be False
432    pub show_storage_rebate: bool,
433}
434
435impl IotaObjectDataOptions {
436    pub fn new() -> Self {
437        Self::default()
438    }
439
440    /// return BCS data and all other metadata such as storage rebate
441    pub fn bcs_lossless() -> Self {
442        Self {
443            show_bcs: true,
444            show_type: true,
445            show_owner: true,
446            show_previous_transaction: true,
447            show_display: false,
448            show_content: false,
449            show_storage_rebate: true,
450        }
451    }
452
453    /// return full content except bcs
454    pub fn full_content() -> Self {
455        Self {
456            show_bcs: false,
457            show_type: true,
458            show_owner: true,
459            show_previous_transaction: true,
460            show_display: false,
461            show_content: true,
462            show_storage_rebate: true,
463        }
464    }
465
466    pub fn with_content(mut self) -> Self {
467        self.show_content = true;
468        self
469    }
470
471    pub fn with_owner(mut self) -> Self {
472        self.show_owner = true;
473        self
474    }
475
476    pub fn with_type(mut self) -> Self {
477        self.show_type = true;
478        self
479    }
480
481    pub fn with_display(mut self) -> Self {
482        self.show_display = true;
483        self
484    }
485
486    pub fn with_bcs(mut self) -> Self {
487        self.show_bcs = true;
488        self
489    }
490
491    pub fn with_previous_transaction(mut self) -> Self {
492        self.show_previous_transaction = true;
493        self
494    }
495
496    pub fn is_not_in_object_info(&self) -> bool {
497        self.show_bcs || self.show_content || self.show_display || self.show_storage_rebate
498    }
499}
500
501impl TryFrom<(ObjectRead, IotaObjectDataOptions)> for IotaObjectResponse {
502    type Error = anyhow::Error;
503
504    fn try_from(
505        (object_read, options): (ObjectRead, IotaObjectDataOptions),
506    ) -> Result<Self, Self::Error> {
507        match object_read {
508            ObjectRead::NotExists(id) => Ok(IotaObjectResponse::new_with_error(
509                IotaObjectResponseError::NotExists { object_id: id },
510            )),
511            ObjectRead::Exists(object_ref, o, layout) => Ok(IotaObjectResponse::new_with_data(
512                IotaObjectData::new(object_ref, o, layout, options, None)?,
513            )),
514            ObjectRead::Deleted((object_id, version, digest)) => Ok(
515                IotaObjectResponse::new_with_error(IotaObjectResponseError::Deleted {
516                    object_id,
517                    version,
518                    digest,
519                }),
520            ),
521        }
522    }
523}
524
525impl TryFrom<(ObjectInfo, IotaObjectDataOptions)> for IotaObjectResponse {
526    type Error = anyhow::Error;
527
528    fn try_from(
529        (object_info, options): (ObjectInfo, IotaObjectDataOptions),
530    ) -> Result<Self, Self::Error> {
531        let IotaObjectDataOptions {
532            show_type,
533            show_owner,
534            show_previous_transaction,
535            ..
536        } = options;
537
538        Ok(Self::new_with_data(IotaObjectData {
539            object_id: object_info.object_id,
540            version: object_info.version,
541            digest: object_info.digest,
542            type_: show_type.then_some(object_info.type_),
543            owner: show_owner.then_some(object_info.owner),
544            previous_transaction: show_previous_transaction
545                .then_some(object_info.previous_transaction),
546            storage_rebate: None,
547            display: None,
548            content: None,
549            bcs: None,
550        }))
551    }
552}
553
554impl IotaObjectResponse {
555    /// Returns a reference to the object if there is any, otherwise an Err if
556    /// the object does not exist or is deleted.
557    pub fn object(&self) -> Result<&IotaObjectData, IotaObjectResponseError> {
558        if let Some(data) = &self.data {
559            Ok(data)
560        } else if let Some(error) = &self.error {
561            Err(error.clone())
562        } else {
563            // We really shouldn't reach this code block since either data, or error field
564            // should always be filled.
565            Err(IotaObjectResponseError::Unknown)
566        }
567    }
568
569    /// Returns the object value if there is any, otherwise an Err if
570    /// the object does not exist or is deleted.
571    pub fn into_object(self) -> Result<IotaObjectData, IotaObjectResponseError> {
572        match self.object() {
573            Ok(data) => Ok(data.clone()),
574            Err(error) => Err(error),
575        }
576    }
577}
578
579impl TryInto<Object> for IotaObjectData {
580    type Error = anyhow::Error;
581
582    fn try_into(self) -> Result<Object, Self::Error> {
583        let protocol_config = ProtocolConfig::get_for_min_version();
584        let data = match self.bcs {
585            Some(IotaRawData::MoveObject(o)) => Data::Move({
586                MoveObject::new_from_execution(
587                    o.type_().clone().into(),
588                    o.version,
589                    o.bcs_bytes,
590                    &protocol_config,
591                )?
592            }),
593            Some(IotaRawData::Package(p)) => Data::Package(MovePackage::new(
594                p.id,
595                self.version,
596                p.module_map,
597                protocol_config.max_move_package_size(),
598                p.type_origin_table,
599                p.linkage_table,
600            )?),
601            _ => Err(anyhow!(
602                "BCS data is required to convert IotaObjectData to Object"
603            ))?,
604        };
605        Ok(ObjectInner {
606            data,
607            owner: self
608                .owner
609                .ok_or_else(|| anyhow!("Owner is required to convert IotaObjectData to Object"))?,
610            previous_transaction: self.previous_transaction.ok_or_else(|| {
611                anyhow!("previous_transaction is required to convert IotaObjectData to Object")
612            })?,
613            storage_rebate: self.storage_rebate.ok_or_else(|| {
614                anyhow!("storage_rebate is required to convert IotaObjectData to Object")
615            })?,
616        }
617        .into())
618    }
619}
620
621#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, Eq, PartialEq, Ord, PartialOrd)]
622#[serde(rename_all = "camelCase", rename = "ObjectRef")]
623pub struct IotaObjectRef {
624    /// Hex code as string representing the object id
625    pub object_id: ObjectID,
626    /// Object version.
627    pub version: SequenceNumber,
628    /// Base64 string representing the object digest
629    pub digest: ObjectDigest,
630}
631
632impl IotaObjectRef {
633    pub fn to_object_ref(&self) -> ObjectRef {
634        (self.object_id, self.version, self.digest)
635    }
636}
637
638impl Display for IotaObjectRef {
639    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
640        write!(
641            f,
642            "Object ID: {}, version: {}, digest: {}",
643            self.object_id, self.version, self.digest
644        )
645    }
646}
647
648impl From<ObjectRef> for IotaObjectRef {
649    fn from(oref: ObjectRef) -> Self {
650        Self {
651            object_id: oref.0,
652            version: oref.1,
653            digest: oref.2,
654        }
655    }
656}
657
658pub trait IotaData: Sized {
659    type ObjectType;
660    type PackageType;
661    fn try_from_object(object: MoveObject, layout: MoveStructLayout)
662    -> Result<Self, anyhow::Error>;
663    fn try_from_package(package: MovePackage) -> Result<Self, anyhow::Error>;
664    fn try_as_move(&self) -> Option<&Self::ObjectType>;
665    fn try_into_move(self) -> Option<Self::ObjectType>;
666    fn try_as_package(&self) -> Option<&Self::PackageType>;
667    fn type_(&self) -> Option<&StructTag>;
668}
669
670#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone, Eq, PartialEq)]
671#[serde(tag = "dataType", rename_all = "camelCase", rename = "RawData")]
672pub enum IotaRawData {
673    // Manually handle generic schema generation
674    MoveObject(IotaRawMoveObject),
675    Package(IotaRawMovePackage),
676}
677
678impl IotaData for IotaRawData {
679    type ObjectType = IotaRawMoveObject;
680    type PackageType = IotaRawMovePackage;
681
682    fn try_from_object(object: MoveObject, _: MoveStructLayout) -> Result<Self, anyhow::Error> {
683        Ok(Self::MoveObject(object.into()))
684    }
685
686    fn try_from_package(package: MovePackage) -> Result<Self, anyhow::Error> {
687        Ok(Self::Package(package.into()))
688    }
689
690    fn try_as_move(&self) -> Option<&Self::ObjectType> {
691        match self {
692            Self::MoveObject(o) => Some(o),
693            Self::Package(_) => None,
694        }
695    }
696
697    fn try_into_move(self) -> Option<Self::ObjectType> {
698        match self {
699            Self::MoveObject(o) => Some(o),
700            Self::Package(_) => None,
701        }
702    }
703
704    fn try_as_package(&self) -> Option<&Self::PackageType> {
705        match self {
706            Self::MoveObject(_) => None,
707            Self::Package(p) => Some(p),
708        }
709    }
710
711    fn type_(&self) -> Option<&StructTag> {
712        match self {
713            Self::MoveObject(o) => Some(&o.type_),
714            Self::Package(_) => None,
715        }
716    }
717}
718
719#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone, Eq, PartialEq)]
720#[serde(tag = "dataType", rename_all = "camelCase", rename = "Data")]
721pub enum IotaParsedData {
722    // Manually handle generic schema generation
723    MoveObject(IotaParsedMoveObject),
724    Package(IotaMovePackage),
725}
726
727impl IotaData for IotaParsedData {
728    type ObjectType = IotaParsedMoveObject;
729    type PackageType = IotaMovePackage;
730
731    fn try_from_object(
732        object: MoveObject,
733        layout: MoveStructLayout,
734    ) -> Result<Self, anyhow::Error> {
735        Ok(Self::MoveObject(IotaParsedMoveObject::try_from_layout(
736            object, layout,
737        )?))
738    }
739
740    fn try_from_package(package: MovePackage) -> Result<Self, anyhow::Error> {
741        let mut disassembled = BTreeMap::new();
742        for bytecode in package.serialized_module_map().values() {
743            // this function is only from JSON RPC - it is OK to deserialize with max Move
744            // binary version
745            let module = move_binary_format::CompiledModule::deserialize_with_defaults(bytecode)
746                .map_err(|error| IotaError::ModuleDeserializationFailure {
747                    error: error.to_string(),
748                })?;
749            let d = move_disassembler::disassembler::Disassembler::from_module(
750                &module,
751                move_ir_types::location::Spanned::unsafe_no_loc(()).loc,
752            )
753            .map_err(|e| IotaError::ObjectSerialization {
754                error: e.to_string(),
755            })?;
756            let bytecode_str = d
757                .disassemble()
758                .map_err(|e| IotaError::ObjectSerialization {
759                    error: e.to_string(),
760                })?;
761            disassembled.insert(module.name().to_string(), Value::String(bytecode_str));
762        }
763
764        Ok(Self::Package(IotaMovePackage { disassembled }))
765    }
766
767    fn try_as_move(&self) -> Option<&Self::ObjectType> {
768        match self {
769            Self::MoveObject(o) => Some(o),
770            Self::Package(_) => None,
771        }
772    }
773
774    fn try_into_move(self) -> Option<Self::ObjectType> {
775        match self {
776            Self::MoveObject(o) => Some(o),
777            Self::Package(_) => None,
778        }
779    }
780
781    fn try_as_package(&self) -> Option<&Self::PackageType> {
782        match self {
783            Self::MoveObject(_) => None,
784            Self::Package(p) => Some(p),
785        }
786    }
787
788    fn type_(&self) -> Option<&StructTag> {
789        match self {
790            Self::MoveObject(o) => Some(&o.type_),
791            Self::Package(_) => None,
792        }
793    }
794}
795
796impl Display for IotaParsedData {
797    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
798        let mut writer = String::new();
799        match self {
800            IotaParsedData::MoveObject(o) => {
801                writeln!(writer, "{}: {}", "type".bold().bright_black(), o.type_)?;
802                write!(writer, "{}", &o.fields)?;
803            }
804            IotaParsedData::Package(p) => {
805                write!(
806                    writer,
807                    "{}: {:?}",
808                    "Modules".bold().bright_black(),
809                    p.disassembled.keys()
810                )?;
811            }
812        }
813        write!(f, "{writer}")
814    }
815}
816
817impl IotaParsedData {
818    pub fn try_from_object_read(object_read: ObjectRead) -> Result<Self, anyhow::Error> {
819        match object_read {
820            ObjectRead::NotExists(id) => Err(anyhow::anyhow!("Object {id} does not exist")),
821            ObjectRead::Exists(_object_ref, o, layout) => {
822                let data = match o.into_inner().data {
823                    Data::Move(m) => {
824                        let layout = layout.ok_or_else(|| {
825                            anyhow!("Layout is required to convert Move object to json")
826                        })?;
827                        IotaParsedData::try_from_object(m, layout)?
828                    }
829                    Data::Package(p) => IotaParsedData::try_from_package(p)?,
830                };
831                Ok(data)
832            }
833            ObjectRead::Deleted((object_id, version, digest)) => Err(anyhow::anyhow!(
834                "Object {object_id} was deleted at version {version} with digest {digest}"
835            )),
836        }
837    }
838}
839
840pub trait IotaMoveObject: Sized {
841    fn try_from_layout(object: MoveObject, layout: MoveStructLayout)
842    -> Result<Self, anyhow::Error>;
843
844    fn try_from(o: MoveObject, resolver: &impl GetModule) -> Result<Self, anyhow::Error> {
845        let layout = o.get_layout(resolver)?;
846        Self::try_from_layout(o, layout)
847    }
848
849    fn type_(&self) -> &StructTag;
850}
851
852#[serde_as]
853#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone, Eq, PartialEq)]
854#[serde(rename = "MoveObject", rename_all = "camelCase")]
855pub struct IotaParsedMoveObject {
856    #[serde(rename = "type")]
857    #[serde_as(as = "IotaStructTag")]
858    #[schemars(with = "String")]
859    pub type_: StructTag,
860    pub fields: IotaMoveStruct,
861}
862
863impl IotaMoveObject for IotaParsedMoveObject {
864    fn try_from_layout(
865        object: MoveObject,
866        layout: MoveStructLayout,
867    ) -> Result<Self, anyhow::Error> {
868        let move_struct = object.to_move_struct(&layout)?.into();
869
870        Ok(
871            if let IotaMoveStruct::WithTypes { type_, fields } = move_struct {
872                IotaParsedMoveObject {
873                    type_,
874                    fields: IotaMoveStruct::WithFields(fields),
875                }
876            } else {
877                IotaParsedMoveObject {
878                    type_: object.type_().clone().into(),
879                    fields: move_struct,
880                }
881            },
882        )
883    }
884
885    fn type_(&self) -> &StructTag {
886        &self.type_
887    }
888}
889
890impl IotaParsedMoveObject {
891    pub fn try_from_object_read(object_read: ObjectRead) -> Result<Self, anyhow::Error> {
892        let parsed_data = IotaParsedData::try_from_object_read(object_read)?;
893        match parsed_data {
894            IotaParsedData::MoveObject(o) => Ok(o),
895            IotaParsedData::Package(_) => Err(anyhow::anyhow!("Object is not a Move object")),
896        }
897    }
898
899    pub fn read_dynamic_field_value(&self, field_name: &str) -> Option<IotaMoveValue> {
900        match &self.fields {
901            IotaMoveStruct::WithFields(fields) => fields.get(field_name).cloned(),
902            IotaMoveStruct::WithTypes { fields, .. } => fields.get(field_name).cloned(),
903            _ => None,
904        }
905    }
906}
907
908pub fn type_and_fields_from_move_event_data(
909    event_data: MoveValue,
910) -> IotaResult<(StructTag, serde_json::Value)> {
911    match event_data.into() {
912        IotaMoveValue::Struct(move_struct) => match &move_struct {
913            IotaMoveStruct::WithTypes { type_, .. } => {
914                Ok((type_.clone(), move_struct.clone().to_json_value()))
915            }
916            _ => Err(IotaError::ObjectDeserialization {
917                error: "Found non-type IotaMoveStruct in MoveValue event".to_string(),
918            }),
919        },
920        IotaMoveValue::Variant(v) => Ok((v.type_.clone(), v.clone().to_json_value())),
921        IotaMoveValue::Vector(_)
922        | IotaMoveValue::Number(_)
923        | IotaMoveValue::Bool(_)
924        | IotaMoveValue::Address(_)
925        | IotaMoveValue::String(_)
926        | IotaMoveValue::UID { .. }
927        | IotaMoveValue::Option(_) => Err(IotaError::ObjectDeserialization {
928            error: "Invalid MoveValue event type -- this should not be possible".to_string(),
929        }),
930    }
931}
932
933#[serde_as]
934#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone, Eq, PartialEq)]
935#[serde(rename = "RawMoveObject", rename_all = "camelCase")]
936pub struct IotaRawMoveObject {
937    #[schemars(with = "String")]
938    #[serde(rename = "type")]
939    #[serde_as(as = "IotaStructTag")]
940    pub type_: StructTag,
941    pub version: SequenceNumber,
942    #[serde_as(as = "Base64")]
943    #[schemars(with = "Base64")]
944    pub bcs_bytes: Vec<u8>,
945}
946
947impl From<MoveObject> for IotaRawMoveObject {
948    fn from(o: MoveObject) -> Self {
949        Self {
950            type_: o.type_().clone().into(),
951            version: o.version(),
952            bcs_bytes: o.into_contents(),
953        }
954    }
955}
956
957impl IotaMoveObject for IotaRawMoveObject {
958    fn try_from_layout(
959        object: MoveObject,
960        _layout: MoveStructLayout,
961    ) -> Result<Self, anyhow::Error> {
962        Ok(Self {
963            type_: object.type_().clone().into(),
964            version: object.version(),
965            bcs_bytes: object.into_contents(),
966        })
967    }
968
969    fn type_(&self) -> &StructTag {
970        &self.type_
971    }
972}
973
974impl IotaRawMoveObject {
975    pub fn deserialize<'a, T: Deserialize<'a>>(&'a self) -> Result<T, anyhow::Error> {
976        Ok(bcs::from_bytes(self.bcs_bytes.as_slice())?)
977    }
978}
979
980#[serde_as]
981#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone, Eq, PartialEq)]
982#[serde(rename = "RawMovePackage", rename_all = "camelCase")]
983pub struct IotaRawMovePackage {
984    pub id: ObjectID,
985    pub version: SequenceNumber,
986    #[schemars(with = "BTreeMap<String, Base64>")]
987    #[serde_as(as = "BTreeMap<_, Base64>")]
988    pub module_map: BTreeMap<String, Vec<u8>>,
989    pub type_origin_table: Vec<TypeOrigin>,
990    pub linkage_table: BTreeMap<ObjectID, UpgradeInfo>,
991}
992
993impl From<MovePackage> for IotaRawMovePackage {
994    fn from(p: MovePackage) -> Self {
995        Self {
996            id: p.id(),
997            version: p.version(),
998            module_map: p.serialized_module_map().clone(),
999            type_origin_table: p.type_origin_table().clone(),
1000            linkage_table: p.linkage_table().clone(),
1001        }
1002    }
1003}
1004
1005impl IotaRawMovePackage {
1006    pub fn to_move_package(
1007        &self,
1008        max_move_package_size: u64,
1009    ) -> Result<MovePackage, ExecutionError> {
1010        MovePackage::new(
1011            self.id,
1012            self.version,
1013            self.module_map.clone(),
1014            max_move_package_size,
1015            self.type_origin_table.clone(),
1016            self.linkage_table.clone(),
1017        )
1018    }
1019}
1020
1021#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone, PartialEq, Eq)]
1022#[serde(tag = "status", content = "details", rename = "ObjectRead")]
1023#[expect(clippy::large_enum_variant)]
1024pub enum IotaPastObjectResponse {
1025    /// The object exists and is found with this version
1026    VersionFound(IotaObjectData),
1027    /// The object does not exist
1028    ObjectNotExists(ObjectID),
1029    /// The object is found to be deleted with this version
1030    ObjectDeleted(IotaObjectRef),
1031    /// The object exists but not found with this version
1032    VersionNotFound(ObjectID, SequenceNumber),
1033    /// The asked object version is higher than the latest
1034    VersionTooHigh {
1035        object_id: ObjectID,
1036        asked_version: SequenceNumber,
1037        latest_version: SequenceNumber,
1038    },
1039}
1040
1041impl IotaPastObjectResponse {
1042    /// Returns a reference to the object if there is any, otherwise an Err
1043    pub fn object(&self) -> UserInputResult<&IotaObjectData> {
1044        match &self {
1045            Self::ObjectDeleted(oref) => Err(UserInputError::ObjectDeleted {
1046                object_ref: oref.to_object_ref(),
1047            }),
1048            Self::ObjectNotExists(id) => Err(UserInputError::ObjectNotFound {
1049                object_id: *id,
1050                version: None,
1051            }),
1052            Self::VersionFound(o) => Ok(o),
1053            Self::VersionNotFound(id, seq_num) => Err(UserInputError::ObjectNotFound {
1054                object_id: *id,
1055                version: Some(*seq_num),
1056            }),
1057            Self::VersionTooHigh {
1058                object_id,
1059                asked_version,
1060                latest_version,
1061            } => Err(UserInputError::ObjectSequenceNumberTooHigh {
1062                object_id: *object_id,
1063                asked_version: *asked_version,
1064                latest_version: *latest_version,
1065            }),
1066        }
1067    }
1068
1069    /// Returns the object value if there is any, otherwise an Err
1070    pub fn into_object(self) -> UserInputResult<IotaObjectData> {
1071        match self {
1072            Self::ObjectDeleted(oref) => Err(UserInputError::ObjectDeleted {
1073                object_ref: oref.to_object_ref(),
1074            }),
1075            Self::ObjectNotExists(id) => Err(UserInputError::ObjectNotFound {
1076                object_id: id,
1077                version: None,
1078            }),
1079            Self::VersionFound(o) => Ok(o),
1080            Self::VersionNotFound(object_id, version) => Err(UserInputError::ObjectNotFound {
1081                object_id,
1082                version: Some(version),
1083            }),
1084            Self::VersionTooHigh {
1085                object_id,
1086                asked_version,
1087                latest_version,
1088            } => Err(UserInputError::ObjectSequenceNumberTooHigh {
1089                object_id,
1090                asked_version,
1091                latest_version,
1092            }),
1093        }
1094    }
1095}
1096
1097#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone, Eq, PartialEq)]
1098#[serde(rename = "MovePackage", rename_all = "camelCase")]
1099pub struct IotaMovePackage {
1100    pub disassembled: BTreeMap<String, Value>,
1101}
1102
1103pub type QueryObjectsPage = Page<IotaObjectResponse, CheckpointedObjectID>;
1104pub type ObjectsPage = Page<IotaObjectResponse, ObjectID>;
1105
1106#[serde_as]
1107#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone, Copy, Eq, PartialEq)]
1108#[serde(rename_all = "camelCase")]
1109pub struct CheckpointedObjectID {
1110    pub object_id: ObjectID,
1111    #[schemars(with = "Option<BigInt<u64>>")]
1112    #[serde_as(as = "Option<BigInt<u64>>")]
1113    #[serde(skip_serializing_if = "Option::is_none")]
1114    pub at_checkpoint: Option<CheckpointSequenceNumber>,
1115}
1116
1117#[serde_as]
1118#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone, Eq, PartialEq)]
1119#[serde(rename = "GetPastObjectRequest", rename_all = "camelCase")]
1120pub struct IotaGetPastObjectRequest {
1121    /// the ID of the queried object
1122    pub object_id: ObjectID,
1123    /// the version of the queried object.
1124    #[schemars(with = "AsSequenceNumber")]
1125    #[serde_as(as = "AsSequenceNumber")]
1126    pub version: SequenceNumber,
1127}
1128
1129#[serde_as]
1130#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
1131pub enum IotaObjectDataFilter {
1132    MatchAll(Vec<IotaObjectDataFilter>),
1133    MatchAny(Vec<IotaObjectDataFilter>),
1134    MatchNone(Vec<IotaObjectDataFilter>),
1135    /// Query by type a specified Package.
1136    Package(ObjectID),
1137    /// Query by type a specified Move module.
1138    MoveModule {
1139        /// the Move package ID
1140        package: ObjectID,
1141        /// the module name
1142        #[schemars(with = "String")]
1143        #[serde_as(as = "DisplayFromStr")]
1144        module: Identifier,
1145    },
1146    /// Query by type
1147    StructType(
1148        #[schemars(with = "String")]
1149        #[serde_as(as = "IotaStructTag")]
1150        StructTag,
1151    ),
1152    AddressOwner(IotaAddress),
1153    ObjectOwner(ObjectID),
1154    ObjectId(ObjectID),
1155    // allow querying for multiple object ids
1156    ObjectIds(Vec<ObjectID>),
1157    Version(
1158        #[schemars(with = "BigInt<u64>")]
1159        #[serde_as(as = "BigInt<u64>")]
1160        u64,
1161    ),
1162}
1163
1164impl IotaObjectDataFilter {
1165    pub fn gas_coin() -> Self {
1166        Self::StructType(GasCoin::type_())
1167    }
1168
1169    pub fn and(self, other: Self) -> Self {
1170        Self::MatchAll(vec![self, other])
1171    }
1172    pub fn or(self, other: Self) -> Self {
1173        Self::MatchAny(vec![self, other])
1174    }
1175    pub fn not(self, other: Self) -> Self {
1176        Self::MatchNone(vec![self, other])
1177    }
1178
1179    pub fn matches(&self, object: &ObjectInfo) -> bool {
1180        match self {
1181            IotaObjectDataFilter::MatchAll(filters) => !filters.iter().any(|f| !f.matches(object)),
1182            IotaObjectDataFilter::MatchAny(filters) => filters.iter().any(|f| f.matches(object)),
1183            IotaObjectDataFilter::MatchNone(filters) => !filters.iter().any(|f| f.matches(object)),
1184            IotaObjectDataFilter::StructType(s) => {
1185                let obj_tag: StructTag = match &object.type_ {
1186                    ObjectType::Package => return false,
1187                    ObjectType::Struct(s) => s.clone().into(),
1188                };
1189                // If people do not provide type_params, we will match all type_params
1190                // e.g. `0x2::coin::Coin` can match `0x2::coin::Coin<0x2::iota::IOTA>`
1191                if !s.type_params.is_empty() && s.type_params != obj_tag.type_params {
1192                    false
1193                } else {
1194                    obj_tag.address == s.address
1195                        && obj_tag.module == s.module
1196                        && obj_tag.name == s.name
1197                }
1198            }
1199            IotaObjectDataFilter::MoveModule { package, module } => {
1200                matches!(&object.type_, ObjectType::Struct(s) if &ObjectID::from(s.address()) == package
1201                        && s.module() == module.as_ident_str())
1202            }
1203            IotaObjectDataFilter::Package(p) => {
1204                matches!(&object.type_, ObjectType::Struct(s) if &ObjectID::from(s.address()) == p)
1205            }
1206            IotaObjectDataFilter::AddressOwner(a) => {
1207                matches!(object.owner, Owner::AddressOwner(addr) if &addr == a)
1208            }
1209            IotaObjectDataFilter::ObjectOwner(o) => {
1210                matches!(object.owner, Owner::ObjectOwner(addr) if addr == IotaAddress::from(*o))
1211            }
1212            IotaObjectDataFilter::ObjectId(id) => &object.object_id == id,
1213            IotaObjectDataFilter::ObjectIds(ids) => ids.contains(&object.object_id),
1214            IotaObjectDataFilter::Version(v) => object.version.value() == *v,
1215        }
1216    }
1217}
1218
1219#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, Default)]
1220#[serde(rename_all = "camelCase", rename = "ObjectResponseQuery", default)]
1221pub struct IotaObjectResponseQuery {
1222    /// If None, no filter will be applied
1223    pub filter: Option<IotaObjectDataFilter>,
1224    /// config which fields to include in the response, by default only digest
1225    /// is included
1226    pub options: Option<IotaObjectDataOptions>,
1227}
1228
1229impl IotaObjectResponseQuery {
1230    pub fn new(
1231        filter: Option<IotaObjectDataFilter>,
1232        options: Option<IotaObjectDataOptions>,
1233    ) -> Self {
1234        Self { filter, options }
1235    }
1236
1237    pub fn new_with_filter(filter: IotaObjectDataFilter) -> Self {
1238        Self {
1239            filter: Some(filter),
1240            options: None,
1241        }
1242    }
1243
1244    pub fn new_with_options(options: IotaObjectDataOptions) -> Self {
1245        Self {
1246            filter: None,
1247            options: Some(options),
1248        }
1249    }
1250}