Skip to main content

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