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