iota_analytics_indexer/handlers/
mod.rs1use std::collections::{BTreeMap, BTreeSet};
6
7use anyhow::{Result, bail};
8use iota_data_ingestion_core::Worker;
9use iota_package_resolver::{PackageStore, Resolver};
10use iota_types::{
11 base_types::{ObjectID, StructTag, TypeTag},
12 effects::{TransactionEffects, TransactionEffectsAPI},
13 iota_sdk_types_conversions::struct_tag_core_to_sdk,
14 object::{Object, Owner, bounded_visitor::BoundedVisitor},
15 transaction::{SenderSignedData, TransactionDataAPI},
16};
17use move_core_types::annotated_value::{MoveStruct, MoveTypeLayout, MoveValue};
18
19use crate::{
20 FileType,
21 tables::{InputObjectKind, ObjectStatus, OwnerType},
22};
23
24pub mod checkpoint_handler;
25pub mod df_handler;
26pub mod event_handler;
27pub mod move_call_handler;
28pub mod object_handler;
29pub mod package_handler;
30pub mod transaction_handler;
31pub mod transaction_objects_handler;
32pub mod wrapped_object_handler;
33
34const WRAPPED_INDEXING_DISALLOW_LIST: [&str; 4] = [
35 "0x1::string::String",
36 "0x1::ascii::String",
37 "0x2::url::Url",
38 "0x2::object::ID",
39];
40
41#[async_trait::async_trait]
42pub trait AnalyticsHandler<S>: Worker<Message = (), Error = anyhow::Error> {
43 async fn read(&self) -> Result<Vec<S>>;
47 fn file_type(&self) -> Result<FileType>;
50 fn name(&self) -> &str;
51}
52
53fn initial_shared_version(object: &Object) -> Option<u64> {
54 match object.owner {
55 Owner::Shared {
56 initial_shared_version,
57 } => Some(initial_shared_version.as_u64()),
58 _ => None,
59 }
60}
61
62fn get_owner_type(object: &Object) -> OwnerType {
63 match object.owner {
64 Owner::AddressOwner(_) => OwnerType::AddressOwner,
65 Owner::ObjectOwner(_) => OwnerType::ObjectOwner,
66 Owner::Shared { .. } => OwnerType::Shared,
67 Owner::Immutable => OwnerType::Immutable,
68 }
69}
70
71fn get_owner_address(object: &Object) -> Option<String> {
72 match object.owner {
73 Owner::AddressOwner(address) => Some(address.to_string()),
74 Owner::ObjectOwner(address) => Some(address.to_string()),
75 Owner::Shared { .. } => None,
76 Owner::Immutable => None,
77 }
78}
79
80struct InputObjectTracker {
85 shared: BTreeSet<ObjectID>,
86 coins: BTreeSet<ObjectID>,
87 input: BTreeSet<ObjectID>,
88}
89
90impl InputObjectTracker {
91 fn new(txn: &SenderSignedData) -> Self {
92 let shared: BTreeSet<ObjectID> = txn
93 .shared_input_objects()
94 .into_iter()
95 .map(|shared_io| shared_io.id())
96 .collect();
97 let tx_data = txn.transaction_data();
98 let coins: BTreeSet<ObjectID> = tx_data
99 .gas()
100 .iter()
101 .map(|obj_ref| obj_ref.object_id)
102 .collect();
103 let input: BTreeSet<ObjectID> = txn
106 .input_objects()
107 .expect("input objects must be valid")
108 .into_iter()
109 .map(|io_kind| io_kind.object_id())
110 .collect();
111 Self {
112 shared,
113 coins,
114 input,
115 }
116 }
117
118 fn get_input_object_kind(&self, object_id: &ObjectID) -> Option<InputObjectKind> {
119 if self.coins.contains(object_id) {
120 Some(InputObjectKind::GasCoin)
121 } else if self.shared.contains(object_id) {
122 Some(InputObjectKind::SharedInput)
123 } else if self.input.contains(object_id) {
124 Some(InputObjectKind::Input)
125 } else {
126 None
127 }
128 }
129}
130
131struct ObjectStatusTracker {
135 created: BTreeSet<ObjectID>,
136 mutated: BTreeSet<ObjectID>,
137 deleted: BTreeSet<ObjectID>,
138}
139
140impl ObjectStatusTracker {
141 fn new(effects: &TransactionEffects) -> Self {
142 let created: BTreeSet<ObjectID> = effects
143 .created()
144 .iter()
145 .map(|(obj_ref, _)| obj_ref.object_id)
146 .collect();
147 let mutated: BTreeSet<ObjectID> = effects
148 .mutated()
149 .iter()
150 .chain(effects.unwrapped().iter())
151 .map(|(obj_ref, _)| obj_ref.object_id)
152 .collect();
153 let deleted: BTreeSet<ObjectID> = effects
154 .all_tombstones()
155 .into_iter()
156 .map(|(id, _)| id)
157 .collect();
158 Self {
159 created,
160 mutated,
161 deleted,
162 }
163 }
164
165 fn get_object_status(&self, object_id: &ObjectID) -> Option<ObjectStatus> {
166 if self.mutated.contains(object_id) {
167 Some(ObjectStatus::Mutated)
168 } else if self.deleted.contains(object_id) {
169 Some(ObjectStatus::Deleted)
170 } else if self.created.contains(object_id) {
171 Some(ObjectStatus::Created)
172 } else {
173 None
174 }
175 }
176}
177
178async fn get_move_struct<T: PackageStore>(
179 struct_tag: &StructTag,
180 contents: &[u8],
181 resolver: &Resolver<T>,
182) -> Result<MoveStruct> {
183 let move_struct = match resolver
184 .type_layout(TypeTag::Struct(Box::new(struct_tag.clone())))
185 .await?
186 {
187 MoveTypeLayout::Struct(move_struct_layout) => {
188 BoundedVisitor::deserialize_struct(contents, &move_struct_layout)
189 }
190 _ => bail!("object is not a move struct"),
191 }?;
192 Ok(move_struct)
193}
194
195#[derive(Debug, Default)]
196pub struct WrappedStruct {
197 object_id: Option<ObjectID>,
198 struct_tag: Option<StructTag>,
199}
200
201fn parse_struct(
202 path: &str,
203 move_struct: MoveStruct,
204 all_structs: &mut BTreeMap<String, WrappedStruct>,
205) {
206 let mut wrapped_struct = WrappedStruct {
207 struct_tag: Some(struct_tag_core_to_sdk(&move_struct.type_)),
208 ..Default::default()
209 };
210 for (k, v) in move_struct.fields {
211 parse_struct_field(&format!("{path}.{k}"), v, &mut wrapped_struct, all_structs);
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::new(address.into_bytes()))
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!("{path}[0]"),
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, StructTag};
294 use move_core_types::{
295 account_address::AccountAddress,
296 annotated_value::{MoveStruct, MoveValue, MoveVariant},
297 identifier::Identifier,
298 language_storage::StructTag as MoveStructTag,
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_: MoveStructTag::from_str("0x2::object::UID")?,
307 fields: vec![(
308 Identifier::from_str("id")?,
309 MoveValue::Struct(MoveStruct {
310 type_: MoveStructTag::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_: MoveStructTag::from_str("0x2::balance::Balance")?,
320 fields: vec![(Identifier::from_str("value")?, MoveValue::U32(10))],
321 });
322 let move_struct = MoveStruct {
323 type_: MoveStructTag::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_short_hex("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_: MoveStructTag::from_str("0x2::object::UID")?,
346 fields: vec![(
347 Identifier::from_str("id")?,
348 MoveValue::Struct(MoveStruct {
349 type_: MoveStructTag::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_: MoveStructTag::from_str("0x2::balance::Balance")?,
359 fields: vec![(Identifier::from_str("value")?, MoveValue::U32(10))],
360 });
361 let move_enum = MoveVariant {
362 type_: MoveStructTag::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_: MoveStructTag::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_short_hex("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}