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