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