1use 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#[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 pub object_type: Option<String>,
48 pub object_type_package: Option<Vec<u8>>,
49 pub object_type_module: Option<String>,
50 pub object_type_name: Option<String>,
52 pub serialized_object: Vec<u8>,
53 pub coin_type: Option<String>,
54 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(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(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(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(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(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(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 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 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}