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::{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    /// Read back rows which are ready to be persisted. This function
44    /// will be invoked by the analytics processor after every call to
45    /// process_checkpoint
46    async fn read(&self) -> Result<Vec<S>>;
47    /// Type of data being written by this processor i.e. checkpoint, object,
48    /// etc
49    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
74// Helper class to track input object kind.
75// Build sets of object ids for input, shared input and gas coin objects as
76// defined in the transaction data.
77// Input objects include coins and shared.
78struct 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        // All input objects (transaction + authenticators) are collected here, just
98        // like the shared objects previously.
99        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
125// Helper class to track object status.
126// Build sets of object ids for created, mutated and deleted objects as reported
127// in the transaction effects.
128struct 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                // Option in iota move is implemented as vector of size 1
244                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                // Do not index most common struct types i.e. string, url, etc
256                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}