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