miden_objects/account/component/template/storage/
mod.rs

1use alloc::boxed::Box;
2use alloc::string::String;
3use alloc::vec::Vec;
4use core::ops::Range;
5
6use miden_core::utils::{ByteReader, ByteWriter, Deserializable, Serializable};
7use miden_core::{Felt, FieldElement};
8use miden_processor::DeserializationError;
9
10mod entry_content;
11pub use entry_content::*;
12
13use super::AccountComponentTemplateError;
14use crate::Word;
15use crate::account::StorageSlot;
16
17mod placeholder;
18pub use placeholder::{
19    PlaceholderTypeRequirement,
20    StorageValueName,
21    StorageValueNameError,
22    TemplateType,
23    TemplateTypeError,
24};
25
26mod init_storage_data;
27pub use init_storage_data::InitStorageData;
28
29#[cfg(feature = "std")]
30pub mod toml;
31
32/// Alias used for iterators that collect all placeholders and their types within a component
33/// template.
34pub type TemplateRequirementsIter<'a> =
35    Box<dyn Iterator<Item = (StorageValueName, PlaceholderTypeRequirement)> + 'a>;
36
37// IDENTIFIER
38// ================================================================================================
39
40/// An identifier for a storage entry field.
41///
42/// An identifier consists of a name that identifies the field, and an optional description.
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct FieldIdentifier {
45    /// A human-readable identifier for the template.
46    pub name: StorageValueName,
47    /// An optional description explaining the purpose of this template.
48    pub description: Option<String>,
49}
50
51impl FieldIdentifier {
52    /// Creates a new `FieldIdentifier` with the given name and no description.
53    pub fn with_name(name: StorageValueName) -> Self {
54        Self { name, description: None }
55    }
56
57    /// Creates a new `FieldIdentifier` with the given name and description.
58    pub fn with_description(name: StorageValueName, description: impl Into<String>) -> Self {
59        Self {
60            name,
61            description: Some(description.into()),
62        }
63    }
64
65    /// Returns the identifier name.
66    pub fn name(&self) -> &StorageValueName {
67        &self.name
68    }
69
70    /// Returns the identifier description.
71    pub fn description(&self) -> Option<&String> {
72        self.description.as_ref()
73    }
74}
75
76impl From<StorageValueName> for FieldIdentifier {
77    fn from(value: StorageValueName) -> Self {
78        FieldIdentifier::with_name(value)
79    }
80}
81
82impl Serializable for FieldIdentifier {
83    fn write_into<W: ByteWriter>(&self, target: &mut W) {
84        target.write(&self.name);
85        target.write(&self.description);
86    }
87}
88
89impl Deserializable for FieldIdentifier {
90    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
91        let name = StorageValueName::read_from(source)?;
92        let description = Option::<String>::read_from(source)?;
93        Ok(FieldIdentifier { name, description })
94    }
95}
96
97// STORAGE ENTRY
98// ================================================================================================
99
100/// Represents a single entry in the component's storage layout.
101///
102/// Each entry can describe:
103/// - A value slot with a single word.
104/// - A map slot with a key-value map that occupies one storage slot.
105/// - A multi-slot entry spanning multiple contiguous slots with multiple words (but not maps) that
106///   represent a single logical value.
107#[derive(Debug, Clone, PartialEq, Eq)]
108#[allow(clippy::large_enum_variant)]
109pub enum StorageEntry {
110    /// A value slot, which can contain one word.
111    Value {
112        /// The numeric index of this map slot in the component's storage.
113        slot: u8,
114        /// A description of a word, representing either a predefined value or a templated one.
115        word_entry: WordRepresentation,
116    },
117
118    /// A map slot, containing multiple key-value pairs. Keys and values are hex-encoded strings.
119    Map {
120        /// The numeric index of this map slot in the component's storage.
121        slot: u8,
122        /// A list of key-value pairs to initialize in this map slot.
123        map: MapRepresentation,
124    },
125
126    /// A multi-slot entry, representing a single logical value across multiple slots.
127    MultiSlot {
128        /// The indices of the slots that form this multi-slot entry.
129        slots: Range<u8>,
130        /// A description of the values.
131        word_entries: MultiWordRepresentation,
132    },
133}
134
135impl StorageEntry {
136    pub fn new_value(slot: u8, word_entry: impl Into<WordRepresentation>) -> Self {
137        StorageEntry::Value { slot, word_entry: word_entry.into() }
138    }
139
140    pub fn new_map(slot: u8, map: MapRepresentation) -> Self {
141        StorageEntry::Map { slot, map }
142    }
143
144    pub fn new_multislot(
145        identifier: FieldIdentifier,
146        slots: Range<u8>,
147        values: Vec<[FeltRepresentation; 4]>,
148    ) -> Self {
149        StorageEntry::MultiSlot {
150            slots,
151            word_entries: MultiWordRepresentation::Value { identifier, values },
152        }
153    }
154
155    pub fn name(&self) -> Option<&StorageValueName> {
156        match self {
157            StorageEntry::Value { word_entry, .. } => word_entry.name(),
158            StorageEntry::Map { map, .. } => Some(map.name()),
159            StorageEntry::MultiSlot { word_entries, .. } => match word_entries {
160                MultiWordRepresentation::Value { identifier, .. } => Some(&identifier.name),
161            },
162        }
163    }
164
165    /// Returns the slot indices that the storage entry covers.
166    pub fn slot_indices(&self) -> Range<u8> {
167        match self {
168            StorageEntry::MultiSlot { slots, .. } => slots.clone(),
169            StorageEntry::Value { slot, .. } | StorageEntry::Map { slot, .. } => *slot..*slot + 1,
170        }
171    }
172
173    /// Returns an iterator over all of the storage entries's value names, alongside their
174    /// expected type.
175    pub fn template_requirements(&self) -> TemplateRequirementsIter<'_> {
176        match self {
177            StorageEntry::Value { word_entry, .. } => {
178                word_entry.template_requirements(StorageValueName::empty())
179            },
180            StorageEntry::Map { map, .. } => map.template_requirements(),
181            StorageEntry::MultiSlot { word_entries, .. } => match word_entries {
182                MultiWordRepresentation::Value { identifier, values } => {
183                    Box::new(values.iter().flat_map(move |word| {
184                        word.iter()
185                            .flat_map(move |f| f.template_requirements(identifier.name.clone()))
186                    }))
187                },
188            },
189        }
190    }
191
192    /// Attempts to convert the storage entry into a list of [`StorageSlot`].
193    ///
194    /// - [`StorageEntry::Value`] would convert to a [`StorageSlot::Value`]
195    /// - [`StorageEntry::MultiSlot`] would convert to as many [`StorageSlot::Value`] as required by
196    ///   the defined type
197    /// - [`StorageEntry::Map`] would convert to a [`StorageSlot::Map`]
198    ///
199    /// Each of the entry's values could be templated. These values are replaced for values found
200    /// in `init_storage_data`, identified by its key.
201    pub fn try_build_storage_slots(
202        &self,
203        init_storage_data: &InitStorageData,
204    ) -> Result<Vec<StorageSlot>, AccountComponentTemplateError> {
205        match self {
206            StorageEntry::Value { word_entry, .. } => {
207                let slot =
208                    word_entry.try_build_word(init_storage_data, StorageValueName::empty())?;
209                Ok(vec![StorageSlot::Value(slot)])
210            },
211            StorageEntry::Map { map, .. } => {
212                let storage_map = map.try_build_map(init_storage_data)?;
213                Ok(vec![StorageSlot::Map(storage_map)])
214            },
215            StorageEntry::MultiSlot { word_entries, .. } => {
216                match word_entries {
217                    MultiWordRepresentation::Value { identifier, values } => {
218                        Ok(values
219                            .iter()
220                            .map(|word_repr| {
221                                let mut result = [Felt::ZERO; 4];
222
223                                for (index, felt_repr) in word_repr.iter().enumerate() {
224                                    result[index] = felt_repr.try_build_felt(
225                                        init_storage_data,
226                                        identifier.name.clone(),
227                                    )?;
228                                }
229                                // SAFETY: result is guaranteed to have all its 4 indices rewritten
230                                Ok(StorageSlot::Value(Word::from(result)))
231                            })
232                            .collect::<Result<Vec<StorageSlot>, _>>()?)
233                    },
234                }
235            },
236        }
237    }
238
239    /// Validates the storage entry for internal consistency.
240    pub(super) fn validate(&self) -> Result<(), AccountComponentTemplateError> {
241        match self {
242            StorageEntry::Map { map, .. } => map.validate(),
243            StorageEntry::MultiSlot { slots, word_entries, .. } => {
244                if slots.len() == 1 {
245                    return Err(AccountComponentTemplateError::MultiSlotSpansOneSlot);
246                }
247
248                if slots.len() != word_entries.num_words() {
249                    return Err(AccountComponentTemplateError::MultiSlotArityMismatch);
250                }
251
252                word_entries.validate()
253            },
254            StorageEntry::Value { word_entry, .. } => Ok(word_entry.validate()?),
255        }
256    }
257}
258
259// SERIALIZATION
260// ================================================================================================
261
262impl Serializable for StorageEntry {
263    fn write_into<W: ByteWriter>(&self, target: &mut W) {
264        match self {
265            StorageEntry::Value { slot, word_entry } => {
266                target.write_u8(0u8);
267                target.write_u8(*slot);
268                target.write(word_entry);
269            },
270            StorageEntry::Map { slot, map } => {
271                target.write_u8(1u8);
272                target.write_u8(*slot);
273                target.write(map);
274            },
275            StorageEntry::MultiSlot { word_entries, slots } => {
276                target.write_u8(2u8);
277                target.write(word_entries);
278                target.write(slots.start);
279                target.write(slots.end);
280            },
281        }
282    }
283}
284
285impl Deserializable for StorageEntry {
286    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
287        let variant_tag = source.read_u8()?;
288        match variant_tag {
289            0 => {
290                let slot = source.read_u8()?;
291                let word_entry: WordRepresentation = source.read()?;
292                Ok(StorageEntry::Value { slot, word_entry })
293            },
294            1 => {
295                let slot = source.read_u8()?;
296                let map: MapRepresentation = source.read()?;
297                Ok(StorageEntry::Map { slot, map })
298            },
299            2 => {
300                let word_entries: MultiWordRepresentation = source.read()?;
301                let slots_start: u8 = source.read()?;
302                let slots_end: u8 = source.read()?;
303                Ok(StorageEntry::MultiSlot {
304                    slots: slots_start..slots_end,
305                    word_entries,
306                })
307            },
308            _ => Err(DeserializationError::InvalidValue(format!(
309                "unknown variant tag '{variant_tag}' for StorageEntry"
310            ))),
311        }
312    }
313}
314
315// MAP ENTRY
316// ================================================================================================
317
318/// Key-value entry for storage maps.
319#[derive(Debug, Clone, PartialEq, Eq)]
320#[cfg_attr(feature = "std", derive(serde::Deserialize, serde::Serialize))]
321pub struct MapEntry {
322    key: WordRepresentation,
323    value: WordRepresentation,
324}
325
326impl MapEntry {
327    pub fn new(key: impl Into<WordRepresentation>, value: impl Into<WordRepresentation>) -> Self {
328        Self { key: key.into(), value: value.into() }
329    }
330
331    pub fn key(&self) -> &WordRepresentation {
332        &self.key
333    }
334
335    pub fn value(&self) -> &WordRepresentation {
336        &self.value
337    }
338
339    pub fn into_parts(self) -> (WordRepresentation, WordRepresentation) {
340        let MapEntry { key, value } = self;
341        (key, value)
342    }
343
344    pub fn template_requirements(
345        &self,
346        placeholder_prefix: StorageValueName,
347    ) -> TemplateRequirementsIter<'_> {
348        let key_iter = self.key.template_requirements(placeholder_prefix.clone());
349        let value_iter = self.value.template_requirements(placeholder_prefix);
350
351        Box::new(key_iter.chain(value_iter))
352    }
353}
354
355impl Serializable for MapEntry {
356    fn write_into<W: ByteWriter>(&self, target: &mut W) {
357        self.key.write_into(target);
358        self.value.write_into(target);
359    }
360}
361
362impl Deserializable for MapEntry {
363    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
364        let key = WordRepresentation::read_from(source)?;
365        let value = WordRepresentation::read_from(source)?;
366        Ok(MapEntry { key, value })
367    }
368}
369
370// TESTS
371// ================================================================================================
372
373#[cfg(test)]
374mod tests {
375    use alloc::collections::{BTreeMap, BTreeSet};
376    use alloc::string::ToString;
377    use core::error::Error;
378    use core::panic;
379
380    use miden_assembly::Assembler;
381    use miden_core::utils::{Deserializable, Serializable};
382    use miden_core::{EMPTY_WORD, Felt, Word};
383    use semver::Version;
384
385    use crate::account::component::FieldIdentifier;
386    use crate::account::component::template::storage::placeholder::TemplateType;
387    use crate::account::component::template::{
388        AccountComponentMetadata,
389        InitStorageData,
390        MapEntry,
391        MapRepresentation,
392        StorageValueName,
393    };
394    use crate::account::{
395        AccountComponent,
396        AccountComponentTemplate,
397        AccountType,
398        FeltRepresentation,
399        StorageEntry,
400        StorageSlot,
401        TemplateTypeError,
402        WordRepresentation,
403    };
404    use crate::errors::AccountComponentTemplateError;
405    use crate::testing::account_code::CODE;
406    use crate::{AccountError, word};
407
408    #[test]
409    fn test_storage_entry_serialization() {
410        let felt_array: [FeltRepresentation; 4] = [
411            FeltRepresentation::from(Felt::new(0xabc)),
412            FeltRepresentation::from(Felt::new(1218)),
413            FeltRepresentation::from(Felt::new(0xdba3)),
414            FeltRepresentation::new_template(
415                TemplateType::native_felt(),
416                StorageValueName::new("slot3").unwrap(),
417            )
418            .with_description("dummy description"),
419        ];
420
421        let test_word: Word = word!("0x000001");
422        let test_word = test_word.map(FeltRepresentation::from);
423
424        let map_representation = MapRepresentation::new_value(
425            vec![
426                MapEntry {
427                    key: WordRepresentation::new_template(
428                        TemplateType::native_word(),
429                        StorageValueName::new("foo").unwrap().into(),
430                    ),
431                    value: WordRepresentation::new_value(test_word.clone(), None),
432                },
433                MapEntry {
434                    key: WordRepresentation::new_value(test_word.clone(), None),
435                    value: WordRepresentation::new_template(
436                        TemplateType::native_word(),
437                        StorageValueName::new("bar").unwrap().into(),
438                    ),
439                },
440                MapEntry {
441                    key: WordRepresentation::new_template(
442                        TemplateType::native_word(),
443                        StorageValueName::new("baz").unwrap().into(),
444                    ),
445                    value: WordRepresentation::new_value(test_word, None),
446                },
447            ],
448            StorageValueName::new("map").unwrap(),
449        )
450        .with_description("a storage map description");
451
452        let storage = vec![
453            StorageEntry::new_value(0, felt_array.clone()),
454            StorageEntry::new_map(1, map_representation),
455            StorageEntry::new_multislot(
456                FieldIdentifier::with_description(
457                    StorageValueName::new("multi").unwrap(),
458                    "Multi slot entry",
459                ),
460                2..4,
461                vec![
462                    [
463                        FeltRepresentation::new_template(
464                            TemplateType::native_felt(),
465                            StorageValueName::new("test").unwrap(),
466                        ),
467                        FeltRepresentation::new_template(
468                            TemplateType::native_felt(),
469                            StorageValueName::new("test2").unwrap(),
470                        ),
471                        FeltRepresentation::new_template(
472                            TemplateType::native_felt(),
473                            StorageValueName::new("test3").unwrap(),
474                        ),
475                        FeltRepresentation::new_template(
476                            TemplateType::native_felt(),
477                            StorageValueName::new("test4").unwrap(),
478                        ),
479                    ],
480                    felt_array,
481                ],
482            ),
483            StorageEntry::new_value(
484                4,
485                WordRepresentation::new_template(
486                    TemplateType::native_word(),
487                    StorageValueName::new("single").unwrap().into(),
488                ),
489            ),
490        ];
491
492        let config = AccountComponentMetadata {
493            name: "Test Component".into(),
494            description: "This is a test component".into(),
495            version: Version::parse("1.0.0").unwrap(),
496            supported_types: BTreeSet::from([AccountType::FungibleFaucet]),
497            storage,
498        };
499        let toml = config.to_toml().unwrap();
500        let deserialized = AccountComponentMetadata::from_toml(&toml).unwrap();
501
502        assert_eq!(deserialized, config);
503    }
504
505    #[test]
506    pub fn toml_serde_roundtrip() {
507        let toml_text = r#"
508        name = "Test Component"
509        description = "This is a test component"
510        version = "1.0.1"
511        supported-types = ["FungibleFaucet", "RegularAccountImmutableCode"]
512
513        [[storage]]
514        name = "map_entry"
515        slot = 0
516        values = [
517            { key = "0x1", value = ["0x1","0x2","0x3","0"]},
518            { key = "0x3", value = "0x123" }, 
519            { key = { name = "map_key_template", description = "this tests that the default type is correctly set"}, value = "0x3" },
520        ]
521
522        [[storage]]
523        name = "token_metadata"
524        description = "Contains metadata about the token associated to the faucet account"
525        slot = 1
526        value = [
527            { type = "felt", name = "max_supply", description = "Maximum supply of the token in base units" }, # placeholder
528            { type = "token_symbol", value = "TST" }, # hardcoded non-felt type
529            { type = "u8", name = "decimals", description = "Number of decimal places" }, # placeholder
530            { value = "0" }, 
531        ]
532
533        [[storage]]
534        name = "default_recallable_height"
535        slot = 2
536        type = "word"
537        "#;
538
539        let component_metadata = AccountComponentMetadata::from_toml(toml_text).unwrap();
540        let requirements = component_metadata.get_placeholder_requirements();
541
542        assert_eq!(requirements.len(), 4);
543
544        let supply = requirements
545            .get(&StorageValueName::new("token_metadata.max_supply").unwrap())
546            .unwrap();
547        assert_eq!(supply.r#type.as_str(), "felt");
548
549        let decimals = requirements
550            .get(&StorageValueName::new("token_metadata.decimals").unwrap())
551            .unwrap();
552        assert_eq!(decimals.r#type.as_str(), "u8");
553
554        let default_recallable_height = requirements
555            .get(&StorageValueName::new("default_recallable_height").unwrap())
556            .unwrap();
557        assert_eq!(default_recallable_height.r#type.as_str(), "word");
558
559        let map_key_template = requirements
560            .get(&StorageValueName::new("map_entry.map_key_template").unwrap())
561            .unwrap();
562        assert_eq!(map_key_template.r#type.as_str(), "word");
563
564        let library = Assembler::default().assemble_library([CODE]).unwrap();
565        let template = AccountComponentTemplate::new(component_metadata, library);
566
567        let template_bytes = template.to_bytes();
568        let template_deserialized =
569            AccountComponentTemplate::read_from_bytes(&template_bytes).unwrap();
570        assert_eq!(template, template_deserialized);
571
572        // Fail to parse because 2800 > u8
573        let storage_placeholders = InitStorageData::new(
574            [
575                (
576                    StorageValueName::new("map_entry.map_key_template").unwrap(),
577                    "0x123".to_string(),
578                ),
579                (
580                    StorageValueName::new("token_metadata.max_supply").unwrap(),
581                    20_000u64.to_string(),
582                ),
583                (StorageValueName::new("token_metadata.decimals").unwrap(), "2800".into()),
584                (StorageValueName::new("default_recallable_height").unwrap(), "0".into()),
585            ],
586            BTreeMap::new(),
587        );
588
589        let component = AccountComponent::from_template(&template, &storage_placeholders);
590        assert_matches::assert_matches!(
591            component,
592            Err(AccountError::AccountComponentTemplateInstantiationError(
593                AccountComponentTemplateError::StorageValueParsingError(
594                    TemplateTypeError::ParseError { .. }
595                )
596            ))
597        );
598
599        // Instantiate successfully
600        let storage_placeholders = InitStorageData::new(
601            [
602                (
603                    StorageValueName::new("map_entry.map_key_template").unwrap(),
604                    "0x123".to_string(),
605                ),
606                (
607                    StorageValueName::new("token_metadata.max_supply").unwrap(),
608                    20_000u64.to_string(),
609                ),
610                (StorageValueName::new("token_metadata.decimals").unwrap(), "128".into()),
611                (StorageValueName::new("default_recallable_height").unwrap(), "0x0".into()),
612            ],
613            BTreeMap::new(),
614        );
615
616        let component = AccountComponent::from_template(&template, &storage_placeholders).unwrap();
617        assert_eq!(
618            component.supported_types(),
619            &[AccountType::FungibleFaucet, AccountType::RegularAccountImmutableCode]
620                .into_iter()
621                .collect()
622        );
623
624        let storage_map = component.storage_slots.first().unwrap();
625        match storage_map {
626            StorageSlot::Map(storage_map) => assert_eq!(storage_map.entries().count(), 3),
627            _ => panic!("should be map"),
628        }
629
630        let value_entry = component.storage_slots().get(2).unwrap();
631        match value_entry {
632            StorageSlot::Value(v) => {
633                assert_eq!(v, &EMPTY_WORD)
634            },
635            _ => panic!("should be value"),
636        }
637
638        let failed_instantiation =
639            AccountComponent::from_template(&template, &InitStorageData::default());
640
641        assert_matches::assert_matches!(
642            failed_instantiation,
643            Err(AccountError::AccountComponentTemplateInstantiationError(
644                AccountComponentTemplateError::PlaceholderValueNotProvided(_)
645            ))
646        );
647    }
648
649    #[test]
650    fn test_no_duplicate_slot_names() {
651        let toml_text = r#"
652        name = "Test Component"
653        description = "This is a test component"
654        version = "1.0.1"
655        supported-types = ["FungibleFaucet", "RegularAccountImmutableCode"]
656
657        [[storage]]
658        name = "test_duplicate"
659        slot = 0
660        type = "felt" # Felt is not a valid type for word slots
661        "#;
662
663        let err = AccountComponentMetadata::from_toml(toml_text).unwrap_err();
664        assert_matches::assert_matches!(err, AccountComponentTemplateError::InvalidType(_, _))
665    }
666
667    #[test]
668    fn parses_and_instantiates_ecdsa_template() {
669        let toml = r#"
670        name = "ecdsa_auth"
671        description = "Ecdsa authentication component, for verifying ECDSA K256 Keccak signatures."
672        version = "0.1.0"
673        supported-types = ["RegularAccountUpdatableCode", "RegularAccountImmutableCode", "FungibleFaucet", "NonFungibleFaucet"]
674
675        [[storage]]
676        name = "ecdsa_pubkey"
677        description = "ecdsa public key"
678        slot = 0
679        type = "auth::ecdsa_k256_keccak::pub_key"
680        "#;
681
682        let metadata = AccountComponentMetadata::from_toml(toml).unwrap();
683        assert_eq!(metadata.storage_entries().len(), 1);
684        assert_eq!(metadata.storage_entries()[0].name().unwrap().as_str(), "ecdsa_pubkey");
685
686        let library = Assembler::default().assemble_library([CODE]).unwrap();
687        let template = AccountComponentTemplate::new(metadata, library);
688
689        let init_storage = InitStorageData::new(
690            [(StorageValueName::new("ecdsa_pubkey").unwrap(), "0x1234".into())],
691            BTreeMap::new(),
692        );
693
694        let component = AccountComponent::from_template(&template, &init_storage).unwrap();
695        let slot = component.storage_slots().first().expect("missing storage slot");
696        match slot {
697            StorageSlot::Value(word) => {
698                let expected = Word::parse(
699                    "0x0000000000000000000000000000000000000000000000000000000000001234",
700                )
701                .unwrap();
702                assert_eq!(word, &expected);
703            },
704            _ => panic!("expected value storage slot"),
705        }
706    }
707
708    #[test]
709    fn map_template_can_build_from_entries() {
710        let map_name = StorageValueName::new("procedure_thresholds").unwrap();
711        let map_entry = StorageEntry::new_map(0, MapRepresentation::new_template(map_name.clone()));
712
713        let init_data = InitStorageData::from_toml(
714            r#"
715            procedure_thresholds = [
716                { key = "0x0000000000000000000000000000000000000000000000000000000000000001", value = "0x0000000000000000000000000000000000000000000000000000000000000010" },
717                { key = "0x0000000000000000000000000000000000000000000000000000000000000002", value = "0x0000000000000000000000000000000000000000000000000000000000000020" }
718            ]
719        "#,
720        )
721        .unwrap();
722
723        let entries = init_data.map_entries(&map_name).expect("map entries missing");
724        assert_eq!(entries.len(), 2);
725        assert_eq!(
726            entries[0],
727            (
728                Word::parse("0x0000000000000000000000000000000000000000000000000000000000000001",)
729                    .unwrap(),
730                Word::parse("0x0000000000000000000000000000000000000000000000000000000000000010",)
731                    .unwrap(),
732            )
733        );
734
735        let slots = map_entry.try_build_storage_slots(&init_data).unwrap();
736        assert_eq!(slots.len(), 1);
737
738        match &slots[0] {
739            StorageSlot::Map(storage_map) => {
740                assert_eq!(storage_map.num_entries(), 2);
741                let main_key = Word::parse(
742                    "0x0000000000000000000000000000000000000000000000000000000000000001",
743                )
744                .unwrap();
745                let main_value_expected = Word::parse(
746                    "0x0000000000000000000000000000000000000000000000000000000000000010",
747                )
748                .unwrap();
749                assert_eq!(storage_map.get(&main_key), main_value_expected);
750            },
751            _ => panic!("expected map storage slot"),
752        }
753    }
754
755    #[test]
756    fn map_template_requires_entries() {
757        let map_name = StorageValueName::new("procedure_thresholds").unwrap();
758        let map_entry = StorageEntry::new_map(0, MapRepresentation::new_template(map_name.clone()));
759
760        let result = map_entry.try_build_storage_slots(&InitStorageData::default());
761
762        assert_matches::assert_matches!(
763            result,
764            Err(AccountComponentTemplateError::PlaceholderValueNotProvided(name))
765                if name.as_str() == "procedure_thresholds"
766        );
767
768        // try with an empty list
769
770        let init_data = InitStorageData::from_toml(
771            r#"
772            procedure_thresholds = []
773        "#,
774        )
775        .unwrap();
776
777        let result = map_entry.try_build_storage_slots(&init_data).unwrap();
778
779        assert_eq!(result.len(), 1);
780        match &result[0] {
781            StorageSlot::Map(storage_map) => assert_eq!(storage_map.num_entries(), 0),
782            _ => panic!("expected map storage slot"),
783        }
784    }
785
786    #[test]
787    fn map_placeholder_requirement_is_reported() {
788        let targets = [AccountType::RegularAccountImmutableCode].into_iter().collect();
789        let map =
790            MapRepresentation::new_template(StorageValueName::new("procedure_thresholds").unwrap())
791                .with_description("Configures procedure thresholds");
792
793        let metadata = AccountComponentMetadata::new(
794            "test".into(),
795            "desc".into(),
796            Version::new(1, 0, 0),
797            targets,
798            vec![StorageEntry::new_map(0, map)],
799        )
800        .unwrap();
801
802        let requirements = metadata.get_placeholder_requirements();
803        let requirement = requirements
804            .get(&StorageValueName::new("procedure_thresholds").unwrap())
805            .expect("map placeholder should be reported");
806
807        assert_eq!(requirement.r#type.as_str(), "map");
808        assert_eq!(requirement.description.as_deref(), Some("Configures procedure thresholds"),);
809    }
810
811    #[test]
812    fn toml_template_map_roundtrip() {
813        let toml_text = r#"
814        name = "Test Component"
815        description = "Component with templated map"
816        version = "1.0.0"
817        supported-types = ["RegularAccountImmutableCode"]
818
819        [[storage]]
820        name = "my_map"
821        description = "Some description"
822        slot = 0
823        type = "map"
824        "#;
825
826        let metadata = AccountComponentMetadata::from_toml(toml_text).unwrap();
827        assert_eq!(metadata.storage_entries().len(), 1);
828        match metadata.storage_entries().first().unwrap() {
829            StorageEntry::Map { map, .. } => match map {
830                MapRepresentation::Template { identifier } => {
831                    assert_eq!(identifier.name.as_str(), "my_map");
832                    assert_eq!(identifier.description.as_deref(), Some("Some description"));
833                },
834                MapRepresentation::Value { .. } => panic!("expected template map"),
835            },
836            _ => panic!("expected map storage entry"),
837        }
838
839        let toml_roundtrip = metadata.to_toml().unwrap();
840        assert!(toml_roundtrip.contains("type = \"map\""));
841    }
842
843    #[test]
844    fn toml_map_with_empty_values_creates_value_map() {
845        // Test that when type = "map" and values = [] is specified,
846        // it creates a MapRepresentation::Value with empty entries,
847        // not a MapRepresentation::Template
848        let toml_text = r#"
849        name = "Test Component"
850        description = "Component with map having empty values"
851        version = "1.0.0"
852        supported-types = ["RegularAccountImmutableCode"]
853
854        [[storage]]
855        name = "executed_transactions"
856        description = "Map which stores executed transactions"
857        slot = 0
858        type = "map"
859        values = []
860        "#;
861
862        let metadata = AccountComponentMetadata::from_toml(toml_text).unwrap();
863        assert_eq!(metadata.storage_entries().len(), 1);
864        match metadata.storage_entries().first().unwrap() {
865            StorageEntry::Map { map, .. } => match map {
866                MapRepresentation::Value { identifier, entries } => {
867                    assert_eq!(identifier.name.as_str(), "executed_transactions");
868                    assert_eq!(
869                        identifier.description.as_deref(),
870                        Some("Map which stores executed transactions")
871                    );
872                    assert!(entries.is_empty(), "Expected empty entries for map with values = []");
873                },
874                MapRepresentation::Template { .. } => {
875                    panic!("expected value map with empty entries, not template map")
876                },
877            },
878            _ => panic!("expected map storage entry"),
879        }
880    }
881
882    #[test]
883    fn map_placeholder_populated_via_toml_array() {
884        let storage_entry = StorageEntry::new_map(
885            0,
886            MapRepresentation::new_template(StorageValueName::new("my_map").unwrap()),
887        );
888
889        let init_data = InitStorageData::from_toml(
890            r#"
891            my_map = [
892                { key = "0x0000000000000000000000000000000000000000000000000000000000000001", value = "0x0000000000000000000000000000000000000000000000000000000000000090" },
893                { key = "0x0000000000000000000000000000000000000000000000000000000000000002", value = ["1", "2", "3", "4"] }
894            ]
895            other_placeholder = "0xAB"
896        "#,
897        )
898        .unwrap();
899
900        assert_eq!(
901            init_data.get(&StorageValueName::new("other_placeholder").unwrap()).unwrap(),
902            "0xAB"
903        );
904
905        let slots = storage_entry.try_build_storage_slots(&init_data).unwrap();
906        assert_eq!(slots.len(), 1);
907        match &slots[0] {
908            StorageSlot::Map(storage_map) => {
909                assert_eq!(storage_map.num_entries(), 2);
910                let second_value = Word::from([
911                    Felt::new(1u64),
912                    Felt::new(2u64),
913                    Felt::new(3u64),
914                    Felt::new(4u64),
915                ]);
916                let second_key = Word::try_from(
917                    "0x0000000000000000000000000000000000000000000000000000000000000002",
918                )
919                .unwrap();
920                assert_eq!(storage_map.get(&second_key), second_value);
921            },
922            _ => panic!("expected map storage slot"),
923        }
924    }
925
926    #[test]
927    fn toml_map_type_with_values_is_invalid() {
928        let toml_text = r#"
929        name = "Invalid"
930        description = "Invalid map"
931        version = "1.0.0"
932        supported-types = ["RegularAccountImmutableCode"]
933
934        [[storage]]
935        name = "bad_map"
936        slot = 0
937        type = "map"
938        values = [ { key = "0x1", value = "0x2" } ]
939        "#;
940
941        let metadata = AccountComponentMetadata::from_toml(toml_text).unwrap();
942        match metadata.storage_entries().first().unwrap() {
943            StorageEntry::Map { map, .. } => match map {
944                MapRepresentation::Value { entries, .. } => {
945                    assert_eq!(entries.len(), 1);
946                },
947                _ => panic!("expected static map"),
948            },
949            _ => panic!("expected map storage entry"),
950        }
951    }
952
953    #[test]
954    fn toml_map_values_with_non_map_type_is_invalid() {
955        let toml_text = r#"
956        name = "Invalid"
957        description = "Invalid map"
958        version = "1.0.0"
959        supported-types = ["RegularAccountImmutableCode"]
960
961        [[storage]]
962        name = "bad_map"
963        slot = 0
964        type = "word"
965        values = [ { key = "0x1", value = "0x2" } ]
966        "#;
967
968        let result = AccountComponentMetadata::from_toml(toml_text);
969        assert_matches::assert_matches!(
970            result,
971            Err(AccountComponentTemplateError::TomlDeserializationError(_))
972        );
973    }
974
975    #[test]
976    fn toml_fail_multislot_arity_mismatch() {
977        let toml_text = r#"
978        name = "Test Component"
979        description = "Test multislot arity mismatch"
980        version = "1.0.1"
981        supported-types = ["FungibleFaucet"]
982
983        [[storage]]
984        name = "multislot_test"
985        slots = [0, 1]
986        values = [
987            [ "0x1", "0x2", "0x3", "0x4" ]
988        ]
989    "#;
990
991        let err = AccountComponentMetadata::from_toml(toml_text).unwrap_err();
992        assert_matches::assert_matches!(err, AccountComponentTemplateError::MultiSlotArityMismatch);
993    }
994
995    #[test]
996    fn toml_fail_multislot_duplicate_slot() {
997        let toml_text = r#"
998        name = "Test Component"
999        description = "Test multislot duplicate slot"
1000        version = "1.0.1"
1001        supported-types = ["FungibleFaucet"]
1002
1003        [[storage]]
1004        name = "multislot_duplicate"
1005        slots = [0, 1]
1006        values = [
1007            [ "0x1", "0x2", "0x3", "0x4" ],
1008            [ "0x5", "0x6", "0x7", "0x8" ]
1009        ]
1010
1011        [[storage]]
1012        name = "multislot_duplicate"
1013        slots = [1, 2]
1014        values = [
1015            [ "0x1", "0x2", "0x3", "0x4" ],
1016            [ "0x5", "0x6", "0x7", "0x8" ]
1017        ]
1018    "#;
1019
1020        let err = AccountComponentMetadata::from_toml(toml_text).unwrap_err();
1021        assert_matches::assert_matches!(err, AccountComponentTemplateError::DuplicateSlot(1));
1022    }
1023
1024    #[test]
1025    fn toml_fail_multislot_non_contiguous_slots() {
1026        let toml_text = r#"
1027        name = "Test Component"
1028        description = "Test multislot non contiguous"
1029        version = "1.0.1"
1030        supported-types = ["FungibleFaucet"]
1031
1032        [[storage]]
1033        name = "multislot_non_contiguous"
1034        slots = [0, 2]
1035        values = [
1036            [ "0x1", "0x2", "0x3", "0x4" ],
1037            [ "0x5", "0x6", "0x7", "0x8" ]
1038        ]
1039    "#;
1040
1041        let err = AccountComponentMetadata::from_toml(toml_text).unwrap_err();
1042        // validate inner serde error
1043        assert!(err.source().unwrap().to_string().contains("are not contiguous"));
1044    }
1045
1046    #[test]
1047    fn toml_fail_duplicate_storage_entry_names() {
1048        let toml_text = r#"
1049        name = "Test Component"
1050        description = "Component with duplicate storage entry names"
1051        version = "1.0.1"
1052        supported-types = ["FungibleFaucet"]
1053
1054        [[storage]]
1055        # placeholder
1056        name = "duplicate"
1057        slot = 0
1058        type = "word"
1059
1060        [[storage]]
1061        name = "duplicate"
1062        slot = 1
1063        value = [ "0x1", "0x1", "0x1", "0x1" ]
1064    "#;
1065
1066        let result = AccountComponentMetadata::from_toml(toml_text);
1067        assert_matches::assert_matches!(
1068            result.unwrap_err(),
1069            AccountComponentTemplateError::DuplicateEntryNames(_)
1070        );
1071    }
1072
1073    #[test]
1074    fn toml_fail_multislot_spans_one_slot() {
1075        let toml_text = r#"
1076        name = "Test Component"
1077        description = "Test multislot spans one slot"
1078        version = "1.0.1"
1079        supported-types = ["RegularAccountImmutableCode"]
1080
1081        [[storage]]
1082        name = "multislot_one_slot"
1083        slots = [0]
1084        values = [
1085            [ "0x1", "0x2", "0x3", "0x4" ],
1086        ]
1087    "#;
1088
1089        let result = AccountComponentMetadata::from_toml(toml_text);
1090        assert_matches::assert_matches!(
1091            result.unwrap_err(),
1092            AccountComponentTemplateError::MultiSlotSpansOneSlot
1093        );
1094    }
1095
1096    #[test]
1097    fn test_toml_multislot_success() {
1098        let toml_text = r#"
1099        name = "Test Component"
1100        description = "A multi-slot success scenario"
1101        version = "1.0.1"
1102        supported-types = ["FungibleFaucet"]
1103
1104        [[storage]]
1105        name = "multi_slot_example"
1106        slots = [0, 1, 2]
1107        values = [
1108            ["0x1", "0x2", "0x3", "0x4"],
1109            ["0x5", "0x6", "0x7", "0x8"],
1110            ["0x9", "0xa", "0xb", "0xc"]
1111        ]
1112    "#;
1113
1114        let metadata = AccountComponentMetadata::from_toml(toml_text).unwrap();
1115        match &metadata.storage_entries()[0] {
1116            StorageEntry::MultiSlot { slots, word_entries } => match word_entries {
1117                crate::account::component::template::MultiWordRepresentation::Value {
1118                    identifier,
1119                    values,
1120                } => {
1121                    assert_eq!(identifier.name.as_str(), "multi_slot_example");
1122                    assert_eq!(slots, &(0..3));
1123                    assert_eq!(values.len(), 3);
1124                },
1125            },
1126            _ => panic!("expected multislot"),
1127        }
1128    }
1129}