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_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 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(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
76struct 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 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
123struct 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 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 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}