iota_analytics_indexer/handlers/
mod.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use 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    /// Read back rows which are ready to be persisted. This function
46    /// will be invoked by the analytics processor after every call to
47    /// process_checkpoint
48    async fn read(&self) -> Result<Vec<S>>;
49    /// Type of data being written by this processor i.e. checkpoint, object,
50    /// etc
51    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
82// Helper class to track input object kind.
83// Build sets of object ids for input, shared input and gas coin objects as
84// defined in the transaction data.
85// Input objects include coins and shared.
86struct 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
126// Helper class to track object status.
127// Build sets of object ids for created, mutated and deleted objects as reported
128// in the transaction effects.
129struct 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                // Option in iota move is implemented as vector of size 1
250                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                // Do not index most common struct types i.e. string, url, etc
262                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}