iota_indexer/models/
objects.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use std::sync::Arc;
6
7use diesel::prelude::*;
8use iota_json_rpc::coin_api::parse_to_struct_tag;
9use iota_json_rpc_types::{Balance, Coin as IotaCoin};
10use iota_package_resolver::{PackageStore, Resolver};
11use iota_types::{
12    base_types::{ObjectID, ObjectRef, SequenceNumber},
13    digests::ObjectDigest,
14    dynamic_field::{DynamicFieldType, Field},
15    object::{Object, ObjectRead, PastObjectRead},
16};
17use move_core_types::annotated_value::MoveTypeLayout;
18use serde::de::DeserializeOwned;
19
20use crate::{
21    errors::IndexerError,
22    schema::{objects, objects_history, objects_snapshot},
23    types::{IndexedDeletedObject, IndexedObject, ObjectStatus, owner_to_owner_info},
24};
25
26#[derive(Queryable)]
27pub struct ObjectRefColumn {
28    pub object_id: Vec<u8>,
29    pub object_version: i64,
30    pub object_digest: Vec<u8>,
31}
32
33// NOTE: please add updating statement like below in pg_indexer_store.rs,
34// if new columns are added here:
35// objects::epoch.eq(excluded(objects::epoch))
36#[derive(Queryable, Insertable, Debug, Identifiable, Clone, QueryableByName)]
37#[diesel(table_name = objects, primary_key(object_id))]
38pub struct StoredObject {
39    pub object_id: Vec<u8>,
40    pub object_version: i64,
41    pub object_digest: Vec<u8>,
42    pub owner_type: i16,
43    pub owner_id: Option<Vec<u8>>,
44    /// The full type of this object, including package id, module, name and
45    /// type parameters. This and following three fields will be None if the
46    /// object is a Package
47    pub object_type: Option<String>,
48    pub object_type_package: Option<Vec<u8>>,
49    pub object_type_module: Option<String>,
50    /// Name of the object type, e.g., "Coin", without type parameters.
51    pub object_type_name: Option<String>,
52    pub serialized_object: Vec<u8>,
53    pub coin_type: Option<String>,
54    // TODO deal with overflow
55    pub coin_balance: Option<i64>,
56    pub df_kind: Option<i16>,
57}
58
59#[derive(Queryable, Insertable, Selectable, Debug, Identifiable, Clone, QueryableByName)]
60#[diesel(table_name = objects_snapshot, primary_key(object_id))]
61pub struct StoredObjectSnapshot {
62    pub object_id: Vec<u8>,
63    pub object_version: i64,
64    pub object_status: i16,
65    pub object_digest: Option<Vec<u8>>,
66    pub checkpoint_sequence_number: i64,
67    pub owner_type: Option<i16>,
68    pub owner_id: Option<Vec<u8>>,
69    pub object_type: Option<String>,
70    pub object_type_package: Option<Vec<u8>>,
71    pub object_type_module: Option<String>,
72    pub object_type_name: Option<String>,
73    pub serialized_object: Option<Vec<u8>>,
74    pub coin_type: Option<String>,
75    pub coin_balance: Option<i64>,
76    pub df_kind: Option<i16>,
77}
78
79impl From<IndexedObject> for StoredObjectSnapshot {
80    fn from(o: IndexedObject) -> Self {
81        let IndexedObject {
82            checkpoint_sequence_number,
83            object,
84            df_kind,
85        } = o;
86        let (owner_type, owner_id) = owner_to_owner_info(&object.owner);
87        let coin_type = object
88            .coin_type_maybe()
89            .map(|t| t.to_canonical_string(/* with_prefix */ true));
90        let coin_balance = if coin_type.is_some() {
91            Some(object.get_coin_value_unsafe())
92        } else {
93            None
94        };
95
96        Self {
97            object_id: object.id().to_vec(),
98            object_version: object.version().value() as i64,
99            object_status: ObjectStatus::Active as i16,
100            object_digest: Some(object.digest().into_inner().to_vec()),
101            checkpoint_sequence_number: checkpoint_sequence_number as i64,
102            owner_type: Some(owner_type as i16),
103            owner_id: owner_id.map(|id| id.to_vec()),
104            object_type: object
105                .type_()
106                .map(|t| t.to_canonical_string(/* with_prefix */ true)),
107            object_type_package: object.type_().map(|t| t.address().to_vec()),
108            object_type_module: object.type_().map(|t| t.module().to_string()),
109            object_type_name: object.type_().map(|t| t.name().to_string()),
110            serialized_object: Some(bcs::to_bytes(&object).unwrap()),
111            coin_type,
112            coin_balance: coin_balance.map(|b| b as i64),
113            df_kind: df_kind.map(|k| match k {
114                DynamicFieldType::DynamicField => 0,
115                DynamicFieldType::DynamicObject => 1,
116            }),
117        }
118    }
119}
120
121impl From<IndexedDeletedObject> for StoredObjectSnapshot {
122    fn from(o: IndexedDeletedObject) -> Self {
123        Self {
124            object_id: o.object_id.to_vec(),
125            object_version: o.object_version as i64,
126            object_status: ObjectStatus::WrappedOrDeleted as i16,
127            object_digest: None,
128            checkpoint_sequence_number: o.checkpoint_sequence_number as i64,
129            owner_type: None,
130            owner_id: None,
131            object_type: None,
132            object_type_package: None,
133            object_type_module: None,
134            object_type_name: None,
135            serialized_object: None,
136            coin_type: None,
137            coin_balance: None,
138            df_kind: None,
139        }
140    }
141}
142
143#[derive(Queryable, Insertable, Selectable, Debug, Identifiable, Clone, QueryableByName)]
144#[diesel(table_name = objects_history, primary_key(object_id, object_version, checkpoint_sequence_number))]
145pub struct StoredHistoryObject {
146    pub object_id: Vec<u8>,
147    pub object_version: i64,
148    pub object_status: i16,
149    pub object_digest: Option<Vec<u8>>,
150    pub checkpoint_sequence_number: i64,
151    pub owner_type: Option<i16>,
152    pub owner_id: Option<Vec<u8>>,
153    pub object_type: Option<String>,
154    pub object_type_package: Option<Vec<u8>>,
155    pub object_type_module: Option<String>,
156    pub object_type_name: Option<String>,
157    pub serialized_object: Option<Vec<u8>>,
158    pub coin_type: Option<String>,
159    pub coin_balance: Option<i64>,
160    pub df_kind: Option<i16>,
161}
162
163impl StoredHistoryObject {
164    pub async fn try_into_past_object_read(
165        self,
166        package_resolver: Arc<Resolver<impl PackageStore>>,
167    ) -> Result<PastObjectRead, IndexerError> {
168        let object_status = ObjectStatus::try_from(self.object_status).map_err(|_| {
169            IndexerError::PersistentStorageDataCorruption(format!(
170                "Object {} has an invalid object status: {}",
171                ObjectID::from_bytes(self.object_id.clone()).unwrap(),
172                self.object_status
173            ))
174        })?;
175
176        if let ObjectStatus::WrappedOrDeleted = object_status {
177            let object_ref = (
178                ObjectID::from_bytes(self.object_id.clone())?,
179                SequenceNumber::from_u64(self.object_version as u64),
180                ObjectDigest::OBJECT_DIGEST_DELETED,
181            );
182            return Ok(PastObjectRead::ObjectDeleted(object_ref));
183        }
184
185        let object: Object = self.try_into()?;
186        let object_ref = object.compute_object_reference();
187
188        let Some(move_object) = object.data.try_as_move().cloned() else {
189            return Ok(PastObjectRead::VersionFound(object_ref, object, None));
190        };
191
192        let move_type_layout = package_resolver
193            .type_layout(move_object.type_().clone().into())
194            .await
195            .map_err(|e| {
196                IndexerError::ResolveMoveStruct(format!(
197                    "failed to convert into object read for obj {}:{}, type: {}. error: {e}",
198                    object.id(),
199                    object.version(),
200                    move_object.type_(),
201                ))
202            })?;
203
204        let move_struct_layout = match move_type_layout {
205            MoveTypeLayout::Struct(s) => Ok(s),
206            _ => Err(IndexerError::ResolveMoveStruct(
207                "MoveTypeLayout is not a Struct".to_string(),
208            )),
209        }?;
210
211        Ok(PastObjectRead::VersionFound(
212            object_ref,
213            object,
214            Some(*move_struct_layout),
215        ))
216    }
217}
218
219impl TryFrom<StoredHistoryObject> for Object {
220    type Error = IndexerError;
221
222    fn try_from(o: StoredHistoryObject) -> Result<Self, Self::Error> {
223        let serialized_object = o.serialized_object.ok_or_else(|| {
224            IndexerError::Serde(format!(
225                "Failed to deserialize object: {:?}, error: object is None",
226                o.object_id
227            ))
228        })?;
229
230        bcs::from_bytes(&serialized_object).map_err(|e| {
231            IndexerError::Serde(format!(
232                "Failed to deserialize object: {:?}, error: {e}",
233                o.object_id
234            ))
235        })
236    }
237}
238
239impl From<IndexedObject> for StoredHistoryObject {
240    fn from(o: IndexedObject) -> Self {
241        let IndexedObject {
242            checkpoint_sequence_number,
243            object,
244            df_kind,
245        } = o;
246        let (owner_type, owner_id) = owner_to_owner_info(&object.owner);
247        let coin_type = object
248            .coin_type_maybe()
249            .map(|t| t.to_canonical_string(/* with_prefix */ true));
250        let coin_balance = if coin_type.is_some() {
251            Some(object.get_coin_value_unsafe())
252        } else {
253            None
254        };
255
256        Self {
257            object_id: object.id().to_vec(),
258            object_version: object.version().value() as i64,
259            object_status: ObjectStatus::Active as i16,
260            object_digest: Some(object.digest().into_inner().to_vec()),
261            checkpoint_sequence_number: checkpoint_sequence_number as i64,
262            owner_type: Some(owner_type as i16),
263            owner_id: owner_id.map(|id| id.to_vec()),
264            object_type: object
265                .type_()
266                .map(|t| t.to_canonical_string(/* with_prefix */ true)),
267            object_type_package: object.type_().map(|t| t.address().to_vec()),
268            object_type_module: object.type_().map(|t| t.module().to_string()),
269            object_type_name: object.type_().map(|t| t.name().to_string()),
270            serialized_object: Some(bcs::to_bytes(&object).unwrap()),
271            coin_type,
272            coin_balance: coin_balance.map(|b| b as i64),
273            df_kind: df_kind.map(|k| match k {
274                DynamicFieldType::DynamicField => 0,
275                DynamicFieldType::DynamicObject => 1,
276            }),
277        }
278    }
279}
280
281impl From<IndexedDeletedObject> for StoredHistoryObject {
282    fn from(o: IndexedDeletedObject) -> Self {
283        Self {
284            object_id: o.object_id.to_vec(),
285            object_version: o.object_version as i64,
286            object_status: ObjectStatus::WrappedOrDeleted as i16,
287            object_digest: None,
288            checkpoint_sequence_number: o.checkpoint_sequence_number as i64,
289            owner_type: None,
290            owner_id: None,
291            object_type: None,
292            object_type_package: None,
293            object_type_module: None,
294            object_type_name: None,
295            serialized_object: None,
296            coin_type: None,
297            coin_balance: None,
298            df_kind: None,
299        }
300    }
301}
302
303#[derive(Queryable, Insertable, Debug, Identifiable, Clone, QueryableByName)]
304#[diesel(table_name = objects, primary_key(object_id))]
305pub struct StoredDeletedObject {
306    pub object_id: Vec<u8>,
307    pub object_version: i64,
308}
309
310impl From<IndexedDeletedObject> for StoredDeletedObject {
311    fn from(o: IndexedDeletedObject) -> Self {
312        Self {
313            object_id: o.object_id.to_vec(),
314            object_version: o.object_version as i64,
315        }
316    }
317}
318
319#[derive(Queryable, Insertable, Debug, Identifiable, Clone, QueryableByName)]
320#[diesel(table_name = objects_history, primary_key(object_id, object_version, checkpoint_sequence_number))]
321pub(crate) struct StoredDeletedHistoryObject {
322    pub object_id: Vec<u8>,
323    pub object_version: i64,
324    pub object_status: i16,
325    pub checkpoint_sequence_number: i64,
326}
327
328impl From<IndexedObject> for StoredObject {
329    fn from(o: IndexedObject) -> Self {
330        let IndexedObject {
331            checkpoint_sequence_number: _,
332            object,
333            df_kind,
334        } = o;
335        let (owner_type, owner_id) = owner_to_owner_info(&object.owner);
336        let coin_type = object
337            .coin_type_maybe()
338            .map(|t| t.to_canonical_string(/* with_prefix */ true));
339        let coin_balance = if coin_type.is_some() {
340            Some(object.get_coin_value_unsafe())
341        } else {
342            None
343        };
344        Self {
345            object_id: object.id().to_vec(),
346            object_version: object.version().value() as i64,
347            object_digest: object.digest().into_inner().to_vec(),
348            owner_type: owner_type as i16,
349            owner_id: owner_id.map(|id| id.to_vec()),
350            object_type: object
351                .type_()
352                .map(|t| t.to_canonical_string(/* with_prefix */ true)),
353            object_type_package: object.type_().map(|t| t.address().to_vec()),
354            object_type_module: object.type_().map(|t| t.module().to_string()),
355            object_type_name: object.type_().map(|t| t.name().to_string()),
356            serialized_object: bcs::to_bytes(&object).unwrap(),
357            coin_type,
358            coin_balance: coin_balance.map(|b| b as i64),
359            df_kind: df_kind.map(|k| match k {
360                DynamicFieldType::DynamicField => 0,
361                DynamicFieldType::DynamicObject => 1,
362            }),
363        }
364    }
365}
366
367impl TryFrom<StoredObject> for Object {
368    type Error = IndexerError;
369
370    fn try_from(o: StoredObject) -> Result<Self, Self::Error> {
371        bcs::from_bytes(&o.serialized_object).map_err(|e| {
372            IndexerError::Serde(format!(
373                "Failed to deserialize object: {:?}, error: {}",
374                o.object_id, e
375            ))
376        })
377    }
378}
379
380impl StoredObject {
381    pub async fn try_into_object_read(
382        self,
383        package_resolver: Arc<Resolver<impl PackageStore>>,
384    ) -> Result<ObjectRead, IndexerError> {
385        let oref = self.get_object_ref()?;
386        let object: iota_types::object::Object = self.try_into()?;
387
388        let Some(move_object) = object.data.try_as_move().cloned() else {
389            return Ok(ObjectRead::Exists(oref, object, None));
390        };
391
392        let move_type_layout = package_resolver
393            .type_layout(move_object.type_().clone().into())
394            .await
395            .map_err(|e| {
396                IndexerError::ResolveMoveStruct(format!(
397                    "Failed to convert into object read for obj {}:{}, type: {}. Error: {e}",
398                    object.id(),
399                    object.version(),
400                    move_object.type_(),
401                ))
402            })?;
403        let move_struct_layout = match move_type_layout {
404            MoveTypeLayout::Struct(s) => Ok(s),
405            _ => Err(IndexerError::ResolveMoveStruct(
406                "MoveTypeLayout is not a Struct".to_string(),
407            )),
408        }?;
409
410        Ok(ObjectRead::Exists(oref, object, Some(*move_struct_layout)))
411    }
412
413    pub fn get_object_ref(&self) -> Result<ObjectRef, IndexerError> {
414        let object_id = ObjectID::from_bytes(self.object_id.clone()).map_err(|_| {
415            IndexerError::Serde(format!("Can't convert {:?} to object_id", self.object_id))
416        })?;
417        let object_digest =
418            ObjectDigest::try_from(self.object_digest.as_slice()).map_err(|_| {
419                IndexerError::Serde(format!(
420                    "Can't convert {:?} to object_digest",
421                    self.object_digest
422                ))
423            })?;
424        Ok((
425            object_id,
426            (self.object_version as u64).into(),
427            object_digest,
428        ))
429    }
430
431    pub fn to_dynamic_field<K, V>(&self) -> Option<Field<K, V>>
432    where
433        K: DeserializeOwned,
434        V: DeserializeOwned,
435    {
436        let object: Object = bcs::from_bytes(&self.serialized_object).ok()?;
437
438        let object = object.data.try_as_move()?;
439        let ty = object.type_();
440
441        if !ty.is_dynamic_field() {
442            return None;
443        }
444
445        bcs::from_bytes(object.contents()).ok()
446    }
447}
448
449impl TryFrom<StoredObject> for IotaCoin {
450    type Error = IndexerError;
451
452    fn try_from(o: StoredObject) -> Result<Self, Self::Error> {
453        let object: Object = o.clone().try_into()?;
454        let (coin_object_id, version, digest) = o.get_object_ref()?;
455        let coin_type_canonical =
456            o.coin_type
457                .ok_or(IndexerError::PersistentStorageDataCorruption(format!(
458                    "Object {} is supposed to be a coin but has an empty coin_type column",
459                    coin_object_id,
460                )))?;
461        let coin_type = parse_to_struct_tag(coin_type_canonical.as_str())
462            .map_err(|_| {
463                IndexerError::PersistentStorageDataCorruption(format!(
464                    "The type of object {} cannot be parsed as a struct tag",
465                    coin_object_id,
466                ))
467            })?
468            .to_string();
469        let balance = o
470            .coin_balance
471            .ok_or(IndexerError::PersistentStorageDataCorruption(format!(
472                "Object {} is supposed to be a coin but has an empty coin_balance column",
473                coin_object_id,
474            )))?;
475        Ok(IotaCoin {
476            coin_type,
477            coin_object_id,
478            version,
479            digest,
480            balance: balance as u64,
481            previous_transaction: object.previous_transaction,
482        })
483    }
484}
485
486#[derive(QueryableByName)]
487pub struct CoinBalance {
488    #[diesel(sql_type = diesel::sql_types::Text)]
489    pub coin_type: String,
490    #[diesel(sql_type = diesel::sql_types::BigInt)]
491    pub coin_num: i64,
492    #[diesel(sql_type = diesel::sql_types::BigInt)]
493    pub coin_balance: i64,
494}
495
496impl TryFrom<CoinBalance> for Balance {
497    type Error = IndexerError;
498
499    fn try_from(c: CoinBalance) -> Result<Self, Self::Error> {
500        let coin_type = parse_to_struct_tag(c.coin_type.as_str())
501            .map_err(|_| {
502                IndexerError::PersistentStorageDataCorruption(
503                    "The type of coin balance cannot be parsed as a struct tag".to_string(),
504                )
505            })?
506            .to_string();
507        Ok(Self {
508            coin_type,
509            coin_object_count: c.coin_num as usize,
510            // TODO: deal with overflow
511            total_balance: c.coin_balance as u128,
512        })
513    }
514}
515
516#[cfg(test)]
517mod tests {
518    use iota_types::{
519        Identifier, TypeTag,
520        coin::Coin,
521        digests::TransactionDigest,
522        gas_coin::{GAS, GasCoin},
523        object::{Data, MoveObject, ObjectInner, Owner},
524    };
525    use move_core_types::{account_address::AccountAddress, language_storage::StructTag};
526
527    use super::*;
528
529    #[test]
530    fn test_canonical_string_of_object_type_for_coin() {
531        let test_obj = Object::new_gas_for_testing();
532        let indexed_obj = IndexedObject::from_object(1, test_obj, None);
533
534        let stored_obj = StoredObject::from(indexed_obj);
535
536        match stored_obj.object_type {
537            Some(t) => {
538                assert_eq!(
539                    t,
540                    "0x0000000000000000000000000000000000000000000000000000000000000002::coin::Coin<0x0000000000000000000000000000000000000000000000000000000000000002::iota::IOTA>"
541                );
542            }
543            None => {
544                panic!("object_type should not be none");
545            }
546        }
547    }
548
549    #[test]
550    fn test_convert_stored_obj_to_iota_coin() {
551        let test_obj = Object::new_gas_for_testing();
552        let indexed_obj = IndexedObject::from_object(1, test_obj, None);
553
554        let stored_obj = StoredObject::from(indexed_obj);
555
556        let iota_coin = IotaCoin::try_from(stored_obj).unwrap();
557        assert_eq!(iota_coin.coin_type, "0x2::iota::IOTA");
558    }
559
560    #[test]
561    fn test_output_format_coin_balance() {
562        let test_obj = Object::new_gas_for_testing();
563        let indexed_obj = IndexedObject::from_object(1, test_obj, None);
564
565        let stored_obj = StoredObject::from(indexed_obj);
566        let test_balance = CoinBalance {
567            coin_type: stored_obj.coin_type.unwrap(),
568            coin_num: 1,
569            coin_balance: 100,
570        };
571        let balance = Balance::try_from(test_balance).unwrap();
572        assert_eq!(balance.coin_type, "0x2::iota::IOTA");
573    }
574
575    #[test]
576    fn test_vec_of_coin_iota_conversion() {
577        // 0xe7::vec_coin::VecCoin<vector<0x2::coin::Coin<0x2::iota::IOTA>>>
578        let vec_coins_type = TypeTag::Vector(Box::new(
579            Coin::type_(TypeTag::Struct(Box::new(GAS::type_()))).into(),
580        ));
581        let object_type = StructTag {
582            address: AccountAddress::from_hex_literal("0xe7").unwrap(),
583            module: Identifier::new("vec_coin").unwrap(),
584            name: Identifier::new("VecCoin").unwrap(),
585            type_params: vec![vec_coins_type],
586        };
587
588        let id = ObjectID::ZERO;
589        let gas = 10;
590
591        let contents = bcs::to_bytes(&vec![GasCoin::new(id, gas)]).unwrap();
592        let data = Data::Move(
593            {
594                MoveObject::new_from_execution_with_limit(
595                    object_type.into(),
596                    1.into(),
597                    contents,
598                    256,
599                )
600            }
601            .unwrap(),
602        );
603
604        let owner = AccountAddress::from_hex_literal("0x1").unwrap();
605
606        let object = ObjectInner {
607            owner: Owner::AddressOwner(owner.into()),
608            data,
609            previous_transaction: TransactionDigest::genesis_marker(),
610            storage_rebate: 0,
611        }
612        .into();
613
614        let indexed_obj = IndexedObject::from_object(1, object, None);
615
616        let stored_obj = StoredObject::from(indexed_obj);
617
618        match stored_obj.object_type {
619            Some(t) => {
620                assert_eq!(
621                    t,
622                    "0x00000000000000000000000000000000000000000000000000000000000000e7::vec_coin::VecCoin<vector<0x0000000000000000000000000000000000000000000000000000000000000002::coin::Coin<0x0000000000000000000000000000000000000000000000000000000000000002::iota::IOTA>>>"
623                );
624            }
625            None => {
626                panic!("object_type should not be none");
627            }
628        }
629    }
630}