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