cainome_parser/abi/
parser.rs

1use starknet::core::types::contract::{AbiEntry, AbiEvent, SierraClass, TypedAbiEvent};
2use std::collections::HashMap;
3
4use crate::tokens::{Array, Composite, CompositeType, CoreBasic, Function, Token};
5use crate::{CainomeResult, Error};
6
7#[derive(Debug, Clone, PartialEq, Default)]
8pub struct TokenizedAbi {
9    /// All enums found in the contract ABI.
10    pub enums: Vec<Token>,
11    /// All structs found in the contract ABI.
12    pub structs: Vec<Token>,
13    /// Standalone functions in the contract ABI.
14    pub functions: Vec<Token>,
15    /// Fully qualified interface name mapped to all the defined functions in it.
16    pub interfaces: HashMap<String, Vec<Token>>,
17}
18
19pub struct AbiParser {}
20
21impl AbiParser {
22    /// Generates the [`Token`]s from the given ABI string.
23    ///
24    /// The `abi` can have two formats:
25    /// 1. Entire [`SierraClass`] json representation.
26    /// 2. The `abi` key from the [`SierraClass`], which is an array of [`AbiEntry`].
27    ///
28    /// # Arguments
29    ///
30    /// * `abi` - A string representing the ABI.
31    /// * `type_aliases` - Types to be renamed to avoid name clashing of generated types.
32    pub fn tokens_from_abi_string(
33        abi: &str,
34        type_aliases: &HashMap<String, String>,
35    ) -> CainomeResult<TokenizedAbi> {
36        let abi_entries = Self::parse_abi_string(abi)?;
37        let tokenized_abi =
38            AbiParser::collect_tokens(&abi_entries, type_aliases).expect("failed tokens parsing");
39
40        Ok(tokenized_abi)
41    }
42
43    /// Parses an ABI string to output a `Vec<AbiEntry>`.
44    ///
45    /// The `abi` can have two formats:
46    /// 1. Entire [`SierraClass`] json representation.
47    /// 2. The `abi` key from the [`SierraClass`], which is an array of AbiEntry.
48    ///
49    /// # Arguments
50    ///
51    /// * `abi` - A string representing the ABI.
52    pub fn parse_abi_string(abi: &str) -> CainomeResult<Vec<AbiEntry>> {
53        let entries = if let Ok(sierra) = serde_json::from_str::<SierraClass>(abi) {
54            sierra.abi
55        } else {
56            serde_json::from_str::<Vec<AbiEntry>>(abi).map_err(Error::SerdeJson)?
57        };
58
59        Ok(entries)
60    }
61
62    /// Parse all tokens in the ABI.
63    pub fn collect_tokens(
64        entries: &[AbiEntry],
65        type_aliases: &HashMap<String, String>,
66    ) -> CainomeResult<TokenizedAbi> {
67        let mut token_candidates: HashMap<String, Vec<Token>> = HashMap::new();
68
69        // Entry tokens are structs, enums and events (which are structs and enums).
70        for entry in entries {
71            Self::collect_entry_token(entry, &mut token_candidates)?;
72        }
73
74        let tokens = Self::filter_struct_enum_tokens(token_candidates);
75
76        let mut structs = vec![];
77        let mut enums = vec![];
78        // This is not memory efficient, but
79        // currently the focus is on search speed.
80        // To be optimized.
81        let mut all_composites: HashMap<String, Composite> = HashMap::new();
82
83        // Apply type aliases only on structs and enums.
84        for (_, mut t) in tokens {
85            for (type_path, alias) in type_aliases {
86                t.apply_alias(type_path, alias);
87            }
88
89            if let Token::Composite(ref c) = t {
90                all_composites.insert(c.type_path_no_generic(), c.clone());
91
92                match c.r#type {
93                    CompositeType::Struct => structs.push(t),
94                    CompositeType::Enum => enums.push(t),
95                    _ => (),
96                }
97            }
98        }
99
100        let mut functions = vec![];
101        let mut interfaces: HashMap<String, Vec<Token>> = HashMap::new();
102
103        for entry in entries {
104            Self::collect_entry_function(
105                entry,
106                &all_composites,
107                &mut functions,
108                &mut interfaces,
109                None,
110                type_aliases,
111            )?;
112        }
113
114        Ok(TokenizedAbi {
115            enums,
116            structs,
117            functions,
118            interfaces,
119        })
120    }
121
122    /// Collects the function from the ABI entry.
123    ///
124    /// # Arguments
125    ///
126    /// * `entry` - The ABI entry to collect functions from.
127    /// * `all_composites` - All known composites tokens.
128    /// * `functions` - The list of functions already collected.
129    /// * `interfaces` - The list of interfaces already collected.
130    /// * `interface_name` - The name of the interface (if any).
131    fn collect_entry_function(
132        entry: &AbiEntry,
133        all_composites: &HashMap<String, Composite>,
134        functions: &mut Vec<Token>,
135        interfaces: &mut HashMap<String, Vec<Token>>,
136        interface_name: Option<String>,
137        type_aliases: &HashMap<String, String>,
138    ) -> CainomeResult<()> {
139        /// Gets the existing token into known composite, if any.
140        /// Otherwise, return the parsed token.
141        fn get_existing_token_or_parsed(
142            type_path: &str,
143            all_composites: &HashMap<String, Composite>,
144        ) -> CainomeResult<Token> {
145            let parsed_token = Token::parse(type_path)?;
146
147            // If the token is an known struct or enum, we look up
148            // in existing one to get full info from there as the parsing
149            // of composites is already done before functions.
150            if let Token::Composite(ref c) = parsed_token {
151                match all_composites.get(&c.type_path_no_generic()) {
152                    Some(e) => Ok(Token::Composite(e.clone())),
153                    None => Ok(parsed_token),
154                }
155            } else {
156                Ok(parsed_token)
157            }
158        }
159
160        // TODO: optimize the search and data structures.
161        // HashMap would be more appropriate than vec.
162        match entry {
163            AbiEntry::Function(f) => {
164                let mut func = Function::new(&f.name, f.state_mutability.clone().into());
165
166                for i in &f.inputs {
167                    let mut token = get_existing_token_or_parsed(&i.r#type, all_composites)?;
168
169                    for (alias_type_path, alias) in type_aliases {
170                        token.apply_alias(alias_type_path, alias);
171                    }
172
173                    func.inputs.push((i.name.clone(), token));
174                }
175
176                for o in &f.outputs {
177                    let mut token = get_existing_token_or_parsed(&o.r#type, all_composites)?;
178
179                    for (alias_type_path, alias) in type_aliases {
180                        token.apply_alias(alias_type_path, alias);
181                    }
182
183                    func.outputs.push(token);
184                }
185
186                if let Some(name) = interface_name {
187                    interfaces
188                        .entry(name)
189                        .or_default()
190                        .push(Token::Function(func));
191                } else {
192                    functions.push(Token::Function(func));
193                }
194            }
195            AbiEntry::Interface(interface) => {
196                for entry in &interface.items {
197                    Self::collect_entry_function(
198                        entry,
199                        all_composites,
200                        functions,
201                        interfaces,
202                        Some(interface.name.clone()),
203                        type_aliases,
204                    )?;
205                }
206            }
207            _ => (),
208        }
209
210        Ok(())
211    }
212
213    /// Collects the token from the ABI entry.
214    ///
215    /// # Arguments
216    ///
217    /// * `entry` - The ABI entry to collect tokens from.
218    /// * `tokens` - The list of tokens already collected.
219    fn collect_entry_token(
220        entry: &AbiEntry,
221        tokens: &mut HashMap<String, Vec<Token>>,
222    ) -> CainomeResult<()> {
223        match entry {
224            AbiEntry::Struct(s) => {
225                if Array::parse(&s.name).is_ok() {
226                    // Spans can be found as a struct entry in the ABI. We don't want
227                    // them as Composite, they are considered as arrays.
228                    return Ok(());
229                };
230
231                let token: Token = s.try_into()?;
232                let entry = tokens.entry(token.type_path()).or_default();
233                entry.push(token);
234            }
235            AbiEntry::Enum(e) => {
236                // `bool` is a core basic enum, we want to skip it.
237                if CoreBasic::parse(&e.name).is_ok() {
238                    return Ok(());
239                };
240
241                let token: Token = e.try_into()?;
242                let entry = tokens.entry(token.type_path()).or_default();
243                entry.push(token);
244            }
245            AbiEntry::Event(ev) => {
246                let mut token: Token;
247                match ev {
248                    AbiEvent::Typed(typed_e) => match typed_e {
249                        TypedAbiEvent::Struct(s) => {
250                            // Some enums may be basics, we want to skip them.
251                            if CoreBasic::parse(&s.name).is_ok() {
252                                return Ok(());
253                            };
254
255                            token = s.try_into()?;
256                        }
257                        TypedAbiEvent::Enum(e) => {
258                            // Some enums may be basics, we want to skip them.
259                            if CoreBasic::parse(&e.name).is_ok() {
260                                return Ok(());
261                            };
262
263                            token = e.try_into()?;
264
265                            // All types inside an event enum are also events.
266                            // To ensure correctness of the tokens, we
267                            // set the boolean is_event to true for each variant
268                            // inner token (if any).
269
270                            // An other solution would have been to looks for the type
271                            // inside existing tokens, and clone it. More computation,
272                            // but less logic.
273
274                            // An enum if a composite, safe to expect here.
275                            if let Token::Composite(ref mut c) = token {
276                                for i in &mut c.inners {
277                                    if let Token::Composite(ref mut ic) = i.token {
278                                        ic.is_event = true;
279                                    }
280                                }
281                            }
282                        }
283                    },
284                    AbiEvent::Untyped(_) => {
285                        // Cairo 0.
286                        return Ok(());
287                    }
288                };
289
290                let entry = tokens.entry(token.type_path()).or_default();
291                entry.push(token);
292            }
293            AbiEntry::Interface(interface) => {
294                for entry in &interface.items {
295                    Self::collect_entry_token(entry, tokens)?;
296                }
297            }
298            _ => (),
299        };
300
301        Ok(())
302    }
303
304    fn filter_struct_enum_tokens(
305        token_candidates: HashMap<String, Vec<Token>>,
306    ) -> HashMap<String, Token> {
307        let tokens_filtered = Self::filter_token_candidates(token_candidates);
308
309        // Can be a very huge copy here. Need an other way to do that in the loop
310        // above here.
311        let filtered = tokens_filtered.clone();
312
313        // So now once it's filtered, we may actually iterate again on the tokens
314        // to resolve all structs/enums inners that may reference existing types.
315        Self::hydrate_composites(tokens_filtered, filtered)
316    }
317
318    /// ABI is a flat list of tokens that represents any types declared in cairo code.
319    /// We need therefore to filter them out and resolve generic types.
320    /// * `token_candidates` - A map of type name to a list of tokens that can be a type.
321    ///
322    fn filter_token_candidates(
323        token_candidates: HashMap<String, Vec<Token>>,
324    ) -> HashMap<String, Token> {
325        token_candidates
326            .into_iter()
327            .filter_map(|(name, tokens)| {
328                if tokens.is_empty() {
329                    return None;
330                }
331
332                if tokens.len() == 1 {
333                    // Only token with this type path -> we keep it without comparison.
334                    return Some((name, tokens[0].clone()));
335                }
336
337                if let Token::Composite(composite_0) = &tokens[0] {
338                    let unique_composite = composite_0.clone();
339                    let inners = composite_0
340                        .inners
341                        .iter()
342                        .map(|inner| {
343                            let inner_tokens = tokens
344                                .iter()
345                                .filter_map(|__t| {
346                                    __t.to_composite().ok().and_then(|comp| {
347                                        comp.inners
348                                            .iter()
349                                            .find(|__t_inner| __t_inner.name == inner.name)
350                                    })
351                                })
352                                .fold(HashMap::new(), |mut acc, __t_inner| {
353                                    let type_path = __t_inner.token.type_path();
354                                    let counter = acc
355                                        .entry(type_path.clone())
356                                        .or_insert((0, __t_inner.clone()));
357                                    counter.0 += 1;
358                                    acc
359                                });
360
361                            // Take the most abundant type path for each member, sorted by the usize counter in descending order.
362                            inner_tokens
363                                .into_iter()
364                                .max_by_key(|(_, (count, _))| *count)
365                                .map(|(_, (_, inner))| inner)
366                                .unwrap()
367                        })
368                        .collect();
369
370                    let mut unique_composite = unique_composite;
371                    unique_composite.inners = inners;
372
373                    return Some((name, Token::Composite(unique_composite)));
374                }
375
376                None
377            })
378            .collect()
379    }
380
381    fn hydrate_composites(
382        tokens_filtered: HashMap<String, Token>,
383        filtered: HashMap<String, Token>,
384    ) -> HashMap<String, Token> {
385        tokens_filtered
386            .into_iter()
387            .fold(HashMap::new(), |mut acc, (name, token)| {
388                acc.insert(name, Token::hydrate(token, &filtered, 10, 0));
389                acc
390            })
391    }
392}
393
394#[cfg(test)]
395mod tests {
396    use super::*;
397    use crate::tokens::{CompositeInner, CompositeInnerKind, CompositeType};
398
399    #[test]
400    fn test_filter_token_candidates_single_inner() {
401        let mut input: HashMap<String, Vec<Token>> = HashMap::new();
402        input.insert(
403            "dojo_starter::models::Direction".to_owned(),
404            vec![Token::Composite(Composite {
405                type_path: "dojo_starter::models::Direction".to_owned(),
406                inners: vec![
407                    CompositeInner {
408                        index: 0,
409                        name: "None".to_owned(),
410                        kind: CompositeInnerKind::NotUsed,
411                        token: Token::CoreBasic(CoreBasic {
412                            type_path: "()".to_owned(),
413                        }),
414                    },
415                    CompositeInner {
416                        index: 1,
417                        name: "North".to_owned(),
418                        kind: CompositeInnerKind::NotUsed,
419                        token: Token::CoreBasic(CoreBasic {
420                            type_path: "()".to_owned(),
421                        }),
422                    },
423                    CompositeInner {
424                        index: 2,
425                        name: "South".to_owned(),
426                        kind: CompositeInnerKind::NotUsed,
427                        token: Token::CoreBasic(CoreBasic {
428                            type_path: "()".to_owned(),
429                        }),
430                    },
431                    CompositeInner {
432                        index: 3,
433                        name: "West".to_owned(),
434                        kind: CompositeInnerKind::NotUsed,
435                        token: Token::CoreBasic(CoreBasic {
436                            type_path: "()".to_owned(),
437                        }),
438                    },
439                    CompositeInner {
440                        index: 4,
441                        name: "East".to_owned(),
442                        kind: CompositeInnerKind::NotUsed,
443                        token: Token::CoreBasic(CoreBasic {
444                            type_path: "()".to_owned(),
445                        }),
446                    },
447                ],
448                generic_args: vec![],
449                r#type: CompositeType::Enum,
450                is_event: false,
451                alias: None,
452            })],
453        );
454        input.insert(
455            "dojo_starter::models::DirectionsAvailable".to_owned(),
456            vec![Token::Composite(Composite {
457                type_path: "dojo_starter::models::DirectionsAvailable".to_owned(),
458                inners: vec![
459                    CompositeInner {
460                        index: 0,
461                        name: "player".to_owned(),
462                        kind: CompositeInnerKind::NotUsed,
463                        token: Token::CoreBasic(CoreBasic {
464                            type_path: "core::starknet::contract_address::ContractAddress"
465                                .to_owned(),
466                        }),
467                    },
468                    CompositeInner {
469                        index: 1,
470                        name: "directions".to_owned(),
471                        kind: CompositeInnerKind::NotUsed,
472                        token: Token::Array(Array {
473                            is_legacy: false,
474                            type_path: "core::array::Array::<dojo_starter::models::Direction>"
475                                .to_owned(),
476                            inner: Box::new(Token::Composite(Composite {
477                                type_path: "dojo_starter::models::Direction".to_owned(),
478                                inners: vec![],
479                                generic_args: vec![],
480                                r#type: CompositeType::Unknown,
481                                is_event: false,
482                                alias: None,
483                            })),
484                        }),
485                    },
486                ],
487                generic_args: vec![],
488                r#type: CompositeType::Struct,
489                is_event: false,
490                alias: None,
491            })],
492        );
493        let filtered = AbiParser::filter_token_candidates(input);
494        assert_eq!(2, filtered.len());
495        assert!(filtered.contains_key("dojo_starter::models::Direction"));
496        assert!(filtered.contains_key("dojo_starter::models::DirectionsAvailable"));
497    }
498
499    #[test]
500    fn test_filter_token_candidates_multiple_composites() {
501        let mut input = HashMap::new();
502
503        // First composite: Enum with multiple variants
504        input.insert(
505            "game::models::ItemType".to_owned(),
506            vec![
507                Token::Composite(Composite {
508                    type_path: "game::models::ItemType".to_owned(),
509                    inners: vec![
510                        CompositeInner {
511                            index: 0,
512                            name: "Weapon".to_owned(),
513                            kind: CompositeInnerKind::NotUsed,
514                            token: Token::CoreBasic(CoreBasic {
515                                type_path: "core::felt252".to_owned(),
516                            }),
517                        },
518                        CompositeInner {
519                            index: 1,
520                            name: "Armor".to_owned(),
521                            kind: CompositeInnerKind::NotUsed,
522                            token: Token::CoreBasic(CoreBasic {
523                                type_path: "core::felt252".to_owned(),
524                            }),
525                        },
526                    ],
527                    generic_args: vec![],
528                    r#type: CompositeType::Enum,
529                    is_event: false,
530                    alias: None,
531                }),
532                Token::Composite(Composite {
533                    type_path: "game::models::ItemType".to_owned(),
534                    inners: vec![
535                        CompositeInner {
536                            index: 0,
537                            name: "Weapon".to_owned(),
538                            kind: CompositeInnerKind::NotUsed,
539                            token: Token::CoreBasic(CoreBasic {
540                                type_path: "core::integer::u8".to_owned(),
541                            }),
542                        },
543                        CompositeInner {
544                            index: 1,
545                            name: "Armor".to_owned(),
546                            kind: CompositeInnerKind::NotUsed,
547                            token: Token::CoreBasic(CoreBasic {
548                                type_path: "core::integer::u8".to_owned(),
549                            }),
550                        },
551                    ],
552                    generic_args: vec![],
553                    r#type: CompositeType::Enum,
554                    is_event: false,
555                    alias: None,
556                }),
557                Token::Composite(Composite {
558                    type_path: "game::models::ItemType".to_owned(),
559                    inners: vec![
560                        CompositeInner {
561                            index: 0,
562                            name: "Weapon".to_owned(),
563                            kind: CompositeInnerKind::NotUsed,
564                            token: Token::CoreBasic(CoreBasic {
565                                type_path: "core::felt252".to_owned(),
566                            }),
567                        },
568                        CompositeInner {
569                            index: 1,
570                            name: "Armor".to_owned(),
571                            kind: CompositeInnerKind::NotUsed,
572                            token: Token::CoreBasic(CoreBasic {
573                                type_path: "core::felt252".to_owned(),
574                            }),
575                        },
576                    ],
577                    generic_args: vec![],
578                    r#type: CompositeType::Enum,
579                    is_event: false,
580                    alias: None,
581                }),
582            ],
583        );
584
585        // Second composite: Struct with different types for a member
586        input.insert(
587            "game::models::Player".to_owned(),
588            vec![
589                Token::Composite(Composite {
590                    type_path: "game::models::Player".to_owned(),
591                    inners: vec![
592                        CompositeInner {
593                            index: 0,
594                            name: "id".to_owned(),
595                            kind: CompositeInnerKind::NotUsed,
596                            token: Token::CoreBasic(CoreBasic {
597                                type_path: "core::integer::u64".to_owned(),
598                            }),
599                        },
600                        CompositeInner {
601                            index: 1,
602                            name: "name".to_owned(),
603                            kind: CompositeInnerKind::NotUsed,
604                            token: Token::CoreBasic(CoreBasic {
605                                type_path: "core::felt252".to_owned(),
606                            }),
607                        },
608                    ],
609                    generic_args: vec![],
610                    r#type: CompositeType::Struct,
611                    is_event: false,
612                    alias: None,
613                }),
614                Token::Composite(Composite {
615                    type_path: "game::models::Player".to_owned(),
616                    inners: vec![
617                        CompositeInner {
618                            index: 0,
619                            name: "id".to_owned(),
620                            kind: CompositeInnerKind::NotUsed,
621                            token: Token::CoreBasic(CoreBasic {
622                                type_path: "core::integer::u128".to_owned(),
623                            }),
624                        },
625                        CompositeInner {
626                            index: 1,
627                            name: "name".to_owned(),
628                            kind: CompositeInnerKind::NotUsed,
629                            token: Token::CoreBasic(CoreBasic {
630                                type_path: "core::felt252".to_owned(),
631                            }),
632                        },
633                    ],
634                    generic_args: vec![],
635                    r#type: CompositeType::Struct,
636                    is_event: false,
637                    alias: None,
638                }),
639                Token::Composite(Composite {
640                    type_path: "game::models::Player".to_owned(),
641                    inners: vec![
642                        CompositeInner {
643                            index: 0,
644                            name: "id".to_owned(),
645                            kind: CompositeInnerKind::NotUsed,
646                            token: Token::CoreBasic(CoreBasic {
647                                type_path: "core::integer::u64".to_owned(),
648                            }),
649                        },
650                        CompositeInner {
651                            index: 1,
652                            name: "name".to_owned(),
653                            kind: CompositeInnerKind::NotUsed,
654                            token: Token::CoreBasic(CoreBasic {
655                                type_path: "core::felt252".to_owned(),
656                            }),
657                        },
658                    ],
659                    generic_args: vec![],
660                    r#type: CompositeType::Struct,
661                    is_event: false,
662                    alias: None,
663                }),
664            ],
665        );
666
667        let filtered = AbiParser::filter_token_candidates(input);
668
669        assert_eq!(2, filtered.len());
670        assert!(filtered.contains_key("game::models::ItemType"));
671        assert!(filtered.contains_key("game::models::Player"));
672
673        // Check ItemType
674        let item_type = filtered
675            .get("game::models::ItemType")
676            .unwrap()
677            .to_composite()
678            .unwrap();
679        assert_eq!(item_type.inners.len(), 2);
680        assert_eq!(item_type.inners[0].name, "Weapon");
681        assert_eq!(item_type.inners[1].name, "Armor");
682        // The most abundant type should be chosen (felt252 in this case)
683        assert_eq!(item_type.inners[0].token.type_path(), "core::felt252");
684        assert_eq!(item_type.inners[1].token.type_path(), "core::felt252");
685
686        // Check Player
687        let player = filtered
688            .get("game::models::Player")
689            .unwrap()
690            .to_composite()
691            .unwrap();
692        assert_eq!(player.inners.len(), 2);
693        assert_eq!(player.inners[0].name, "id");
694        assert_eq!(player.inners[1].name, "name");
695        // The most abundant type should be chosen (u64 for id, felt252 for name)
696        assert_eq!(player.inners[0].token.type_path(), "core::integer::u64");
697        assert_eq!(player.inners[1].token.type_path(), "core::felt252");
698    }
699
700    #[test]
701    fn test_parse_abi_struct() {
702        let abi_json = r#"
703        [
704            {
705                "type": "struct",
706                "name": "package::StructOne",
707                "members": [
708                    {
709                        "name": "a",
710                        "type": "core::integer::u64"
711                    },
712                    {
713                        "name": "b",
714                        "type": "core::zeroable::NonZero"
715                    },
716                    {
717                        "name": "c",
718                        "type": "core::integer::u256"
719                    }
720                ]
721            }
722        ]
723        "#;
724
725        let result = AbiParser::tokens_from_abi_string(abi_json, &HashMap::new()).unwrap();
726
727        assert_eq!(result.structs.len(), 1);
728        assert_eq!(result.interfaces.len(), 0);
729        assert_eq!(result.functions.len(), 0);
730        assert_eq!(result.enums.len(), 0);
731
732        let s = result.structs[0].to_composite().unwrap();
733        assert_eq!(s.type_path, "package::StructOne");
734        assert_eq!(s.r#type, CompositeType::Struct);
735        assert_eq!(s.inners.len(), 3);
736        assert_eq!(s.inners[0].name, "a");
737        assert_eq!(s.inners[1].name, "b");
738        assert_eq!(s.inners[2].name, "c");
739    }
740
741    #[test]
742    fn test_dojo_starter_direction_available_abi() {
743        let abi = AbiParser::tokens_from_abi_string(
744            include_str!("../../test_data/dojo_starter-directions_available.abi.json"),
745            &HashMap::new(),
746        )
747        .unwrap();
748
749        assert_eq!(abi.structs.len(), 1);
750        let s = abi.structs[0].to_composite().unwrap();
751        if let Token::Array(a) = &s.inners[1].token {
752            let inner_array = a.inner.to_composite().unwrap();
753            assert_eq!(5, inner_array.inners.len());
754            // Check that copy was properly done
755            let src_enum = abi.enums[0].to_composite().unwrap();
756            assert_eq!(inner_array, src_enum);
757        } else {
758            panic!("Expected array");
759        }
760    }
761
762    #[test]
763    fn test_nested_tuple() {
764        let abi = AbiParser::tokens_from_abi_string(
765            include_str!("../../test_data/struct_tuple.abi.json"),
766            &HashMap::new(),
767        )
768        .unwrap();
769
770        assert_eq!(abi.structs.len(), 1);
771        let s = abi.structs[0].to_composite().unwrap();
772        if let Token::Array(a) = &s.inners[1].token {
773            if let Token::Tuple(t) = *a.inner.to_owned() {
774                let inner_array = t.inners[0].to_composite().unwrap();
775                assert_eq!(5, inner_array.inners.len());
776                // Check that copy was properly done
777                let src_enum = abi.enums[0].to_composite().unwrap();
778                assert_eq!(inner_array, src_enum);
779            } else {
780                panic!("Expected tuple");
781            }
782        } else {
783            panic!("Expected array");
784        }
785    }
786
787    #[test]
788    fn test_composite_generic_args_hydratation() {
789        let mut input: HashMap<String, Vec<Token>> = HashMap::new();
790        input.insert(
791            "tournament::ls15_components::models::tournament::GatedEntryType".to_owned(),
792            vec![Token::Composite(Composite {
793                type_path: "tournament::ls15_components::models::tournament::GatedEntryType"
794                    .to_owned(),
795                inners: vec![
796                    CompositeInner {
797                        index: 0,
798                        name: "criteria".to_owned(),
799                        kind: CompositeInnerKind::NotUsed,
800                        token: Token::Composite(Composite {
801                            type_path:
802                                "tournament::ls15_components::models::tournament::EntryCriteria"
803                                    .to_owned(),
804                            inners: vec![
805                                CompositeInner {
806                                    index: 0,
807                                    name: "token_id".to_owned(),
808                                    kind: CompositeInnerKind::NotUsed,
809                                    token: Token::CoreBasic(CoreBasic {
810                                        type_path: "core::integer::u128".to_owned(),
811                                    }),
812                                },
813                                CompositeInner {
814                                    index: 1,
815                                    name: "entry_count".to_owned(),
816                                    kind: CompositeInnerKind::NotUsed,
817                                    token: Token::CoreBasic(CoreBasic {
818                                        type_path: "core::integer::u64".to_owned(),
819                                    }),
820                                },
821                            ],
822                            generic_args: vec![],
823                            r#type: CompositeType::Struct,
824                            is_event: false,
825                            alias: None,
826                        }),
827                    },
828                    CompositeInner {
829                        index: 1,
830                        name: "uniform".to_owned(),
831                        kind: CompositeInnerKind::NotUsed,
832                        token: Token::CoreBasic(CoreBasic {
833                            type_path: "core::integer::u64".to_owned(),
834                        }),
835                    },
836                ],
837                generic_args: vec![],
838                r#type: CompositeType::Enum,
839                is_event: false,
840                alias: None,
841            })],
842        );
843
844        input.insert(
845            "tournament::ls15_components::models::tournament::GatedToken".to_owned(),
846            vec![Token::Composite(Composite {
847                type_path: "tournament::ls15_components::models::tournament::GatedToken".to_owned(),
848                inners: vec![
849                    CompositeInner {
850                        index: 0,
851                        name: "token".to_owned(),
852                        kind: CompositeInnerKind::NotUsed,
853                        token: Token::CoreBasic(CoreBasic {
854                            type_path: "core::starknet::contract_address::ContractAddress"
855                                .to_owned(),
856                        }),
857                    },
858                    CompositeInner {
859                        index: 1,
860                        name: "entry_type".to_owned(),
861                        kind: CompositeInnerKind::NotUsed,
862                        token: Token::Composite(Composite {
863                            type_path:
864                                "tournament::ls15_components::models::tournament::GatedEntryType"
865                                    .to_owned(),
866                            inners: vec![],
867                            generic_args: vec![],
868                            r#type: CompositeType::Unknown,
869                            is_event: false,
870                            alias: None,
871                        }),
872                    },
873                ],
874                generic_args: vec![],
875                r#type: CompositeType::Struct,
876                is_event: false,
877                alias: None,
878            })],
879        );
880        input.insert(
881            "tournament::ls15_components::models::tournament::GatedType".to_owned(),
882            vec![Token::Composite(
883Composite {
884    type_path: "tournament::ls15_components::models::tournament::GatedType".to_owned(),
885    inners: vec![
886        CompositeInner {
887            index: 0,
888            name: "token".to_owned(),
889            kind: CompositeInnerKind::NotUsed,
890            token: Token::Composite(
891                Composite {
892                    type_path: "tournament::ls15_components::models::tournament::GatedToken".to_owned(),
893                    inners: vec![
894                        CompositeInner {
895                            index: 0,
896                            name: "token".to_owned(),
897                            kind: CompositeInnerKind::NotUsed,
898                            token: Token::CoreBasic(
899                                CoreBasic {
900                                    type_path: "core::starknet::contract_address::ContractAddress".to_owned(),
901                                },
902                            ),
903                        },
904                        CompositeInner {
905                            index: 1,
906                            name: "entry_type".to_owned(),
907                            kind: CompositeInnerKind::NotUsed,
908                            token: Token::Composite(
909                                Composite {
910                                    type_path: "tournament::ls15_components::models::tournament::GatedEntryType".to_owned(),
911                                    inners: vec![],
912                                    generic_args: vec![],
913                                    r#type: CompositeType::Unknown,
914                                    is_event: false,
915                                    alias: None,
916                                },
917                            ),
918                        },
919                    ],
920                    generic_args: vec![],
921                    r#type: CompositeType::Struct,
922                    is_event: false,
923                    alias: None,
924                },
925            ),
926        },
927        CompositeInner {
928            index: 1,
929            name: "tournament".to_owned(),
930            kind: CompositeInnerKind::NotUsed,
931            token: Token::Array(
932                Array {
933                    type_path: "core::array::Span::<core::integer::u64>".to_owned(),
934                    inner: Box::new(Token::CoreBasic(
935                        CoreBasic {
936                            type_path: "core::integer::u64".to_owned(),
937                        },
938                    )),
939                    is_legacy: false,
940                },
941            ),
942        },
943        CompositeInner {
944            index: 2,
945            name: "address".to_owned(),
946            kind: CompositeInnerKind::NotUsed,
947            token: Token::Array(
948                Array {
949                    type_path: "core::array::Span::<core::starknet::contract_address::ContractAddress>".to_owned(),
950                    inner: Box::new(
951                        Token::CoreBasic(
952                        CoreBasic {
953                            type_path: "core::starknet::contract_address::ContractAddress".to_owned(),
954                        },
955                    )
956                    ),
957                    is_legacy: false,
958                },
959            ),
960        },
961    ],
962    generic_args: vec![],
963    r#type: CompositeType::Enum,
964    is_event: false,
965    alias: None,
966}            )],
967        );
968        input.insert(
969            "tournament::ls15_components::models::tournament::TournamentModelValue".to_owned(),
970            vec![Token::Composite(Composite {
971                type_path: "tournament::ls15_components::models::tournament::TournamentModelValue"
972                    .to_owned(),
973                inners: vec![CompositeInner {
974                    index: 0,
975                    name: "gated_type".to_owned(),
976                    kind: CompositeInnerKind::NotUsed,
977                    token: Token::Composite(Composite { type_path: "core::option::Option::<tournament::ls15_components::models::tournament::GatedType>".to_owned(), inners: vec![], generic_args: vec![
978                ("A".to_owned(), Token::Composite(Composite { type_path: "tournament::ls15_components::models::tournament::GatedType".to_owned(), inners: vec![], generic_args: vec![], r#type: CompositeType::Unknown, is_event: false, alias: None })),
979                    ], r#type: CompositeType::Unknown, is_event: false, alias: None }),
980                }],
981                generic_args: vec![],
982                r#type: CompositeType::Struct,
983                is_event: false,
984                alias: None,
985            })],
986        );
987
988        let filtered = AbiParser::filter_struct_enum_tokens(input);
989        let tmv = filtered
990            .get("tournament::ls15_components::models::tournament::TournamentModelValue")
991            .unwrap()
992            .to_composite()
993            .unwrap();
994        if let Token::Composite(c) = &tmv.inners[0].token {
995            if let Token::Composite(cc) = &c.generic_args[0].1 {
996                // Checking that inners are not empty ensures us that hydration was done, even for
997                // `generic_args`.
998                assert_ne!(0, cc.inners.len());
999            } else {
1000                panic!("Expected composite");
1001            }
1002        } else {
1003            panic!("Expected composite");
1004        }
1005    }
1006    #[test]
1007    fn test_deep_nested_hydration() {
1008        let mut input: HashMap<String, Vec<Token>> = HashMap::new();
1009        input.insert(
1010            "tournament::ls15_components::models::loot_survivor::Item".to_owned(),
1011            vec![Token::Composite(Composite {
1012                type_path: "tournament::ls15_components::models::loot_survivor::Item".to_owned(),
1013                inners: vec![
1014                    CompositeInner {
1015                        index: 0,
1016                        name: "id".to_owned(),
1017                        kind: CompositeInnerKind::NotUsed,
1018                        token: Token::CoreBasic(CoreBasic {
1019                            type_path: "core::integer::u8".to_owned(),
1020                        }),
1021                    },
1022                    CompositeInner {
1023                        index: 1,
1024                        name: "name".to_owned(),
1025                        kind: CompositeInnerKind::NotUsed,
1026                        token: Token::CoreBasic(CoreBasic {
1027                            type_path: "core::integer::u16".to_owned(),
1028                        }),
1029                    },
1030                ],
1031                generic_args: vec![],
1032                r#type: CompositeType::Struct,
1033                is_event: false,
1034                alias: None,
1035            })],
1036        );
1037        input.insert(
1038            "tournament::ls15_components::models::loot_survivor::Equipment".to_owned(),
1039            vec![Token::Composite(Composite {
1040                type_path: "tournament::ls15_components::models::loot_survivor::Equipment"
1041                    .to_owned(),
1042                inners: vec![
1043                    CompositeInner {
1044                        index: 0,
1045                        name: "weapon".to_owned(),
1046                        kind: CompositeInnerKind::NotUsed,
1047                        token: Token::Composite(Composite {
1048                            type_path: "tournament::ls15_components::models::loot_survivor::Item"
1049                                .to_owned(),
1050                            inners: vec![],
1051                            generic_args: vec![],
1052                            r#type: CompositeType::Unknown,
1053                            is_event: false,
1054                            alias: None,
1055                        }),
1056                    },
1057                    CompositeInner {
1058                        index: 1,
1059                        name: "chest".to_owned(),
1060                        kind: CompositeInnerKind::NotUsed,
1061                        token: Token::Composite(Composite {
1062                            type_path: "tournament::ls15_components::models::loot_survivor::Item"
1063                                .to_owned(),
1064                            inners: vec![],
1065                            generic_args: vec![],
1066                            r#type: CompositeType::Unknown,
1067                            is_event: false,
1068                            alias: None,
1069                        }),
1070                    },
1071                ],
1072                generic_args: vec![],
1073                r#type: CompositeType::Struct,
1074                is_event: false,
1075                alias: None,
1076            })],
1077        );
1078        input.insert(
1079            "tournament::ls15_components::models::loot_survivor::Adventurer".to_owned(),
1080            vec![Token::Composite(Composite {
1081                type_path: "tournament::ls15_components::models::loot_survivor::Adventurer"
1082                    .to_owned(),
1083                inners: vec![CompositeInner {
1084                    index: 0,
1085                    name: "equipment".to_owned(),
1086                    kind: CompositeInnerKind::NotUsed,
1087                    token: Token::Composite(Composite {
1088                        type_path: "tournament::ls15_components::models::loot_survivor::Equipment"
1089                            .to_owned(),
1090                        inners: vec![],
1091                        generic_args: vec![],
1092                        r#type: CompositeType::Unknown,
1093                        is_event: false,
1094                        alias: None,
1095                    }),
1096                }],
1097                generic_args: vec![],
1098                r#type: CompositeType::Struct,
1099                is_event: false,
1100                alias: None,
1101            })],
1102        );
1103        input.insert(
1104            "tournament::ls15_components::models::loot_survivor::AdventurerModel".to_owned(),
1105            vec![Token::Composite(Composite {
1106                type_path: "tournament::ls15_components::models::loot_survivor::AdventurerModel"
1107                    .to_owned(),
1108                inners: vec![
1109                    CompositeInner {
1110                        index: 0,
1111                        name: "adventurer_id".to_owned(),
1112                        kind: CompositeInnerKind::NotUsed,
1113                        token: Token::CoreBasic(CoreBasic {
1114                            type_path: "core::felt252".to_owned(),
1115                        }),
1116                    },
1117                    CompositeInner {
1118                        index: 1,
1119                        name: "adventurer".to_owned(),
1120                        kind: CompositeInnerKind::NotUsed,
1121                        token: Token::Composite(Composite {
1122                            type_path:
1123                                "tournament::ls15_components::models::loot_survivor::Adventurer"
1124                                    .to_owned(),
1125                            inners: vec![],
1126                            generic_args: vec![],
1127                            r#type: CompositeType::Unknown,
1128                            is_event: false,
1129                            alias: None,
1130                        }),
1131                    },
1132                ],
1133                generic_args: vec![],
1134                r#type: CompositeType::Struct,
1135                is_event: false,
1136                alias: None,
1137            })],
1138        );
1139
1140        let filtered = AbiParser::filter_struct_enum_tokens(input);
1141        fn check_token_inners(token: &Token) {
1142            // end of recursion, if token is composite and inners are empty, this means hydration
1143            // was not properly done.
1144            if let Token::Composite(c) = token {
1145                assert_ne!(0, c.inners.len());
1146                // deep dive into compsite,
1147                c.inners.iter().for_each(|i| check_token_inners(&i.token));
1148            }
1149        }
1150        filtered.iter().for_each(|(_, t)| check_token_inners(t));
1151    }
1152
1153    #[test]
1154    fn test_collect_tokens() {
1155        let sierra_abi = include_str!("../../test_data/cairo_ls_abi.json");
1156        let sierra = serde_json::from_str::<SierraClass>(sierra_abi).unwrap();
1157        let tokens = AbiParser::collect_tokens(&sierra.abi, &HashMap::new()).unwrap();
1158        assert_ne!(tokens.enums.len(), 0);
1159        assert_ne!(tokens.functions.len(), 0);
1160        assert_ne!(tokens.interfaces.len(), 0);
1161        assert_ne!(tokens.structs.len(), 0);
1162    }
1163}