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, bail};
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        _ => bail!("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(&format!("{path}.{k}"), v, &mut wrapped_struct, all_structs);
207    }
208    all_structs.insert(path.to_string(), wrapped_struct);
209}
210
211fn parse_struct_field(
212    path: &str,
213    move_value: MoveValue,
214    curr_struct: &mut WrappedStruct,
215    all_structs: &mut BTreeMap<String, WrappedStruct>,
216) {
217    match move_value {
218        MoveValue::Struct(move_struct) => {
219            let values = move_struct
220                .fields
221                .iter()
222                .map(|(id, value)| (id.to_string(), value))
223                .collect::<BTreeMap<_, _>>();
224            let struct_name = format!(
225                "0x{}::{}::{}",
226                move_struct.type_.address.short_str_lossless(),
227                move_struct.type_.module,
228                move_struct.type_.name
229            );
230            if "0x2::object::UID" == struct_name {
231                if let Some(MoveValue::Struct(id_struct)) = values.get("id").cloned() {
232                    let id_values = id_struct
233                        .fields
234                        .iter()
235                        .map(|(id, value)| (id.to_string(), value))
236                        .collect::<BTreeMap<_, _>>();
237                    if let Some(MoveValue::Address(address) | MoveValue::Signer(address)) =
238                        id_values.get("bytes").cloned()
239                    {
240                        curr_struct.object_id = Some(ObjectID::from_address(*address))
241                    }
242                }
243            } else if "0x1::option::Option" == struct_name {
244                // Option in iota move is implemented as vector of size 1
245                if let Some(MoveValue::Vector(vec_values)) = values.get("vec").cloned() {
246                    if let Some(first_value) = vec_values.first() {
247                        parse_struct_field(
248                            &format!("{path}[0]"),
249                            first_value.clone(),
250                            curr_struct,
251                            all_structs,
252                        );
253                    }
254                }
255            } else if !WRAPPED_INDEXING_DISALLOW_LIST.contains(&&*struct_name) {
256                // Do not index most common struct types i.e. string, url, etc
257                parse_struct(path, move_struct, all_structs)
258            }
259        }
260        MoveValue::Variant(v) => {
261            for (k, field) in v.fields.iter() {
262                parse_struct_field(
263                    &format!("{path}.{k}"),
264                    field.clone(),
265                    curr_struct,
266                    all_structs,
267                );
268            }
269        }
270        MoveValue::Vector(fields) => {
271            for (index, field) in fields.iter().enumerate() {
272                parse_struct_field(
273                    &format!("{path}[{index}]"),
274                    field.clone(),
275                    curr_struct,
276                    all_structs,
277                );
278            }
279        }
280        _ => {}
281    }
282}
283
284#[cfg(test)]
285mod tests {
286    use std::{collections::BTreeMap, str::FromStr};
287
288    use iota_types::base_types::ObjectID;
289    use move_core_types::{
290        account_address::AccountAddress,
291        annotated_value::{MoveStruct, MoveValue, MoveVariant},
292        identifier::Identifier,
293        language_storage::StructTag,
294    };
295
296    use crate::handlers::parse_struct;
297
298    #[tokio::test]
299    async fn test_wrapped_object_parsing() -> anyhow::Result<()> {
300        let uid_field = MoveValue::Struct(MoveStruct {
301            type_: StructTag::from_str("0x2::object::UID")?,
302            fields: vec![(
303                Identifier::from_str("id")?,
304                MoveValue::Struct(MoveStruct {
305                    type_: StructTag::from_str("0x2::object::ID")?,
306                    fields: vec![(
307                        Identifier::from_str("bytes")?,
308                        MoveValue::Signer(AccountAddress::from_hex_literal("0x300")?),
309                    )],
310                }),
311            )],
312        });
313        let balance_field = MoveValue::Struct(MoveStruct {
314            type_: StructTag::from_str("0x2::balance::Balance")?,
315            fields: vec![(Identifier::from_str("value")?, MoveValue::U32(10))],
316        });
317        let move_struct = MoveStruct {
318            type_: StructTag::from_str("0x2::test::Test")?,
319            fields: vec![
320                (Identifier::from_str("id")?, uid_field),
321                (Identifier::from_str("principal")?, balance_field),
322            ],
323        };
324        let mut all_structs = BTreeMap::new();
325        parse_struct("$", move_struct, &mut all_structs);
326        assert_eq!(
327            all_structs.get("$").unwrap().object_id,
328            Some(ObjectID::from_hex_literal("0x300")?)
329        );
330        assert_eq!(
331            all_structs.get("$.principal").unwrap().struct_tag,
332            Some(StructTag::from_str("0x2::balance::Balance")?)
333        );
334        Ok(())
335    }
336
337    #[tokio::test]
338    async fn test_wrapped_object_parsing_within_enum() -> anyhow::Result<()> {
339        let uid_field = MoveValue::Struct(MoveStruct {
340            type_: StructTag::from_str("0x2::object::UID")?,
341            fields: vec![(
342                Identifier::from_str("id")?,
343                MoveValue::Struct(MoveStruct {
344                    type_: StructTag::from_str("0x2::object::ID")?,
345                    fields: vec![(
346                        Identifier::from_str("bytes")?,
347                        MoveValue::Signer(AccountAddress::from_hex_literal("0x300")?),
348                    )],
349                }),
350            )],
351        });
352        let balance_field = MoveValue::Struct(MoveStruct {
353            type_: StructTag::from_str("0x2::balance::Balance")?,
354            fields: vec![(Identifier::from_str("value")?, MoveValue::U32(10))],
355        });
356        let move_enum = MoveVariant {
357            type_: StructTag::from_str("0x2::test::TestEnum")?,
358            variant_name: Identifier::from_str("TestVariant")?,
359            tag: 0,
360            fields: vec![
361                (Identifier::from_str("field0")?, MoveValue::U64(10)),
362                (Identifier::from_str("principal")?, balance_field),
363            ],
364        };
365        let move_struct = MoveStruct {
366            type_: StructTag::from_str("0x2::test::Test")?,
367            fields: vec![
368                (Identifier::from_str("id")?, uid_field),
369                (
370                    Identifier::from_str("enum_field")?,
371                    MoveValue::Variant(move_enum),
372                ),
373            ],
374        };
375        let mut all_structs = BTreeMap::new();
376        parse_struct("$", move_struct, &mut all_structs);
377        assert_eq!(
378            all_structs.get("$").unwrap().object_id,
379            Some(ObjectID::from_hex_literal("0x300")?)
380        );
381        assert_eq!(
382            all_structs
383                .get("$.enum_field.principal")
384                .unwrap()
385                .struct_tag,
386            Some(StructTag::from_str("0x2::balance::Balance")?)
387        );
388        Ok(())
389    }
390}