1use std::collections::{BTreeMap, BTreeSet};
6
7use anyhow::{Result, anyhow};
8use iota_data_ingestion_core::Worker;
9use iota_package_resolver::{PackageStore, Resolver};
10use iota_types::{
11 base_types::ObjectID,
12 effects::{TransactionEffects, TransactionEffectsAPI},
13 object::{Object, Owner, bounded_visitor::BoundedVisitor},
14 transaction::{TransactionData, TransactionDataAPI},
15};
16use move_core_types::{
17 annotated_value::{MoveStruct, MoveTypeLayout, MoveValue},
18 language_storage::{StructTag, TypeTag},
19};
20
21use crate::{
22 FileType,
23 tables::{InputObjectKind, ObjectStatus, OwnerType},
24};
25
26pub mod checkpoint_handler;
27pub mod df_handler;
28pub mod event_handler;
29pub mod move_call_handler;
30pub mod object_handler;
31pub mod package_handler;
32pub mod transaction_handler;
33pub mod transaction_objects_handler;
34pub mod wrapped_object_handler;
35
36const WRAPPED_INDEXING_DISALLOW_LIST: [&str; 4] = [
37 "0x1::string::String",
38 "0x1::ascii::String",
39 "0x2::url::Url",
40 "0x2::object::ID",
41];
42
43#[async_trait::async_trait]
44pub trait AnalyticsHandler<S>: Worker<Message = (), Error = anyhow::Error> {
45 async fn read(&self) -> Result<Vec<S>>;
49 fn file_type(&self) -> Result<FileType>;
52 fn name(&self) -> &str;
53}
54
55fn initial_shared_version(object: &Object) -> Option<u64> {
56 match object.owner {
57 Owner::Shared {
58 initial_shared_version,
59 } => Some(initial_shared_version.value()),
60 _ => None,
61 }
62}
63
64fn get_owner_type(object: &Object) -> OwnerType {
65 match object.owner {
66 Owner::AddressOwner(_) => OwnerType::AddressOwner,
67 Owner::ObjectOwner(_) => OwnerType::ObjectOwner,
68 Owner::Shared { .. } => OwnerType::Shared,
69 Owner::Immutable => OwnerType::Immutable,
70 }
71}
72
73fn get_owner_address(object: &Object) -> Option<String> {
74 match object.owner {
75 Owner::AddressOwner(address) => Some(address.to_string()),
76 Owner::ObjectOwner(address) => Some(address.to_string()),
77 Owner::Shared { .. } => None,
78 Owner::Immutable => None,
79 }
80}
81
82struct InputObjectTracker {
87 shared: BTreeSet<ObjectID>,
88 coins: BTreeSet<ObjectID>,
89 input: BTreeSet<ObjectID>,
90}
91
92impl InputObjectTracker {
93 fn new(txn_data: &TransactionData) -> Self {
94 let shared: BTreeSet<ObjectID> = txn_data
95 .shared_input_objects()
96 .iter()
97 .map(|shared_io| shared_io.id())
98 .collect();
99 let coins: BTreeSet<ObjectID> = txn_data.gas().iter().map(|obj_ref| obj_ref.0).collect();
100 let input: BTreeSet<ObjectID> = txn_data
101 .input_objects()
102 .expect("Input objects must be valid")
103 .iter()
104 .map(|io_kind| io_kind.object_id())
105 .collect();
106 Self {
107 shared,
108 coins,
109 input,
110 }
111 }
112
113 fn get_input_object_kind(&self, object_id: &ObjectID) -> Option<InputObjectKind> {
114 if self.coins.contains(object_id) {
115 Some(InputObjectKind::GasCoin)
116 } else if self.shared.contains(object_id) {
117 Some(InputObjectKind::SharedInput)
118 } else if self.input.contains(object_id) {
119 Some(InputObjectKind::Input)
120 } else {
121 None
122 }
123 }
124}
125
126struct ObjectStatusTracker {
130 created: BTreeSet<ObjectID>,
131 mutated: BTreeSet<ObjectID>,
132 deleted: BTreeSet<ObjectID>,
133}
134
135impl ObjectStatusTracker {
136 fn new(effects: &TransactionEffects) -> Self {
137 let created: BTreeSet<ObjectID> = effects
138 .created()
139 .iter()
140 .map(|(obj_ref, _)| obj_ref.0)
141 .collect();
142 let mutated: BTreeSet<ObjectID> = effects
143 .mutated()
144 .iter()
145 .chain(effects.unwrapped().iter())
146 .map(|(obj_ref, _)| obj_ref.0)
147 .collect();
148 let deleted: BTreeSet<ObjectID> = effects
149 .all_tombstones()
150 .into_iter()
151 .map(|(id, _)| id)
152 .collect();
153 Self {
154 created,
155 mutated,
156 deleted,
157 }
158 }
159
160 fn get_object_status(&self, object_id: &ObjectID) -> Option<ObjectStatus> {
161 if self.mutated.contains(object_id) {
162 Some(ObjectStatus::Mutated)
163 } else if self.deleted.contains(object_id) {
164 Some(ObjectStatus::Deleted)
165 } else if self.created.contains(object_id) {
166 Some(ObjectStatus::Created)
167 } else {
168 None
169 }
170 }
171}
172
173async fn get_move_struct<T: PackageStore>(
174 struct_tag: &StructTag,
175 contents: &[u8],
176 resolver: &Resolver<T>,
177) -> Result<MoveStruct> {
178 let move_struct = match resolver
179 .type_layout(TypeTag::Struct(Box::new(struct_tag.clone())))
180 .await?
181 {
182 MoveTypeLayout::Struct(move_struct_layout) => {
183 BoundedVisitor::deserialize_struct(contents, &move_struct_layout)
184 }
185 _ => Err(anyhow!("Object is not a move struct")),
186 }?;
187 Ok(move_struct)
188}
189
190#[derive(Debug, Default)]
191pub struct WrappedStruct {
192 object_id: Option<ObjectID>,
193 struct_tag: Option<StructTag>,
194}
195
196fn parse_struct(
197 path: &str,
198 move_struct: MoveStruct,
199 all_structs: &mut BTreeMap<String, WrappedStruct>,
200) {
201 let mut wrapped_struct = WrappedStruct {
202 struct_tag: Some(move_struct.type_),
203 ..Default::default()
204 };
205 for (k, v) in move_struct.fields {
206 parse_struct_field(
207 &format!("{}.{}", path, &k),
208 v,
209 &mut wrapped_struct,
210 all_structs,
211 );
212 }
213 all_structs.insert(path.to_string(), wrapped_struct);
214}
215
216fn parse_struct_field(
217 path: &str,
218 move_value: MoveValue,
219 curr_struct: &mut WrappedStruct,
220 all_structs: &mut BTreeMap<String, WrappedStruct>,
221) {
222 match move_value {
223 MoveValue::Struct(move_struct) => {
224 let values = move_struct
225 .fields
226 .iter()
227 .map(|(id, value)| (id.to_string(), value))
228 .collect::<BTreeMap<_, _>>();
229 let struct_name = format!(
230 "0x{}::{}::{}",
231 move_struct.type_.address.short_str_lossless(),
232 move_struct.type_.module,
233 move_struct.type_.name
234 );
235 if "0x2::object::UID" == struct_name {
236 if let Some(MoveValue::Struct(id_struct)) = values.get("id").cloned() {
237 let id_values = id_struct
238 .fields
239 .iter()
240 .map(|(id, value)| (id.to_string(), value))
241 .collect::<BTreeMap<_, _>>();
242 if let Some(MoveValue::Address(address) | MoveValue::Signer(address)) =
243 id_values.get("bytes").cloned()
244 {
245 curr_struct.object_id = Some(ObjectID::from_address(*address))
246 }
247 }
248 } else if "0x1::option::Option" == struct_name {
249 if let Some(MoveValue::Vector(vec_values)) = values.get("vec").cloned() {
251 if let Some(first_value) = vec_values.first() {
252 parse_struct_field(
253 &format!("{}[0]", path),
254 first_value.clone(),
255 curr_struct,
256 all_structs,
257 );
258 }
259 }
260 } else if !WRAPPED_INDEXING_DISALLOW_LIST.contains(&&*struct_name) {
261 parse_struct(path, move_struct, all_structs)
263 }
264 }
265 MoveValue::Variant(v) => {
266 for (k, field) in v.fields.iter() {
267 parse_struct_field(
268 &format!("{}.{}", path, k),
269 field.clone(),
270 curr_struct,
271 all_structs,
272 );
273 }
274 }
275 MoveValue::Vector(fields) => {
276 for (index, field) in fields.iter().enumerate() {
277 parse_struct_field(
278 &format!("{}[{}]", path, &index),
279 field.clone(),
280 curr_struct,
281 all_structs,
282 );
283 }
284 }
285 _ => {}
286 }
287}
288
289#[cfg(test)]
290mod tests {
291 use std::{collections::BTreeMap, str::FromStr};
292
293 use iota_types::base_types::ObjectID;
294 use move_core_types::{
295 account_address::AccountAddress,
296 annotated_value::{MoveStruct, MoveValue, MoveVariant},
297 identifier::Identifier,
298 language_storage::StructTag,
299 };
300
301 use crate::handlers::parse_struct;
302
303 #[tokio::test]
304 async fn test_wrapped_object_parsing() -> anyhow::Result<()> {
305 let uid_field = MoveValue::Struct(MoveStruct {
306 type_: StructTag::from_str("0x2::object::UID")?,
307 fields: vec![(
308 Identifier::from_str("id")?,
309 MoveValue::Struct(MoveStruct {
310 type_: StructTag::from_str("0x2::object::ID")?,
311 fields: vec![(
312 Identifier::from_str("bytes")?,
313 MoveValue::Signer(AccountAddress::from_hex_literal("0x300")?),
314 )],
315 }),
316 )],
317 });
318 let balance_field = MoveValue::Struct(MoveStruct {
319 type_: StructTag::from_str("0x2::balance::Balance")?,
320 fields: vec![(Identifier::from_str("value")?, MoveValue::U32(10))],
321 });
322 let move_struct = MoveStruct {
323 type_: StructTag::from_str("0x2::test::Test")?,
324 fields: vec![
325 (Identifier::from_str("id")?, uid_field),
326 (Identifier::from_str("principal")?, balance_field),
327 ],
328 };
329 let mut all_structs = BTreeMap::new();
330 parse_struct("$", move_struct, &mut all_structs);
331 assert_eq!(
332 all_structs.get("$").unwrap().object_id,
333 Some(ObjectID::from_hex_literal("0x300")?)
334 );
335 assert_eq!(
336 all_structs.get("$.principal").unwrap().struct_tag,
337 Some(StructTag::from_str("0x2::balance::Balance")?)
338 );
339 Ok(())
340 }
341
342 #[tokio::test]
343 async fn test_wrapped_object_parsing_within_enum() -> anyhow::Result<()> {
344 let uid_field = MoveValue::Struct(MoveStruct {
345 type_: StructTag::from_str("0x2::object::UID")?,
346 fields: vec![(
347 Identifier::from_str("id")?,
348 MoveValue::Struct(MoveStruct {
349 type_: StructTag::from_str("0x2::object::ID")?,
350 fields: vec![(
351 Identifier::from_str("bytes")?,
352 MoveValue::Signer(AccountAddress::from_hex_literal("0x300")?),
353 )],
354 }),
355 )],
356 });
357 let balance_field = MoveValue::Struct(MoveStruct {
358 type_: StructTag::from_str("0x2::balance::Balance")?,
359 fields: vec![(Identifier::from_str("value")?, MoveValue::U32(10))],
360 });
361 let move_enum = MoveVariant {
362 type_: StructTag::from_str("0x2::test::TestEnum")?,
363 variant_name: Identifier::from_str("TestVariant")?,
364 tag: 0,
365 fields: vec![
366 (Identifier::from_str("field0")?, MoveValue::U64(10)),
367 (Identifier::from_str("principal")?, balance_field),
368 ],
369 };
370 let move_struct = MoveStruct {
371 type_: StructTag::from_str("0x2::test::Test")?,
372 fields: vec![
373 (Identifier::from_str("id")?, uid_field),
374 (
375 Identifier::from_str("enum_field")?,
376 MoveValue::Variant(move_enum),
377 ),
378 ],
379 };
380 let mut all_structs = BTreeMap::new();
381 parse_struct("$", move_struct, &mut all_structs);
382 assert_eq!(
383 all_structs.get("$").unwrap().object_id,
384 Some(ObjectID::from_hex_literal("0x300")?)
385 );
386 assert_eq!(
387 all_structs
388 .get("$.enum_field.principal")
389 .unwrap()
390 .struct_tag,
391 Some(StructTag::from_str("0x2::balance::Balance")?)
392 );
393 Ok(())
394 }
395}