iota_analytics_indexer/handlers/
mod.rs1use 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 async fn read(&self) -> Result<Vec<S>>;
49 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
82struct 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
126struct 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 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 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}