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::{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 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: &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 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
129struct 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 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 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}