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

1use alloc::{boxed::Box, string::String, vec::Vec};
2use core::ops::Range;
3
4use vm_core::{
5    Felt, FieldElement,
6    utils::{ByteReader, ByteWriter, Deserializable, Serializable},
7};
8use vm_processor::DeserializationError;
9
10mod entry_content;
11pub use entry_content::*;
12
13use super::AccountComponentTemplateError;
14use crate::account::StorageSlot;
15
16mod placeholder;
17pub use placeholder::{
18    PlaceholderTypeRequirement, StorageValueName, StorageValueNameError, TemplateType,
19    TemplateTypeError,
20};
21
22mod init_storage_data;
23pub use init_storage_data::InitStorageData;
24
25#[cfg(feature = "std")]
26pub mod toml;
27
28/// Alias used for iterators that collect all placeholders and their types within a component
29/// template.
30pub type TemplateRequirementsIter<'a> =
31    Box<dyn Iterator<Item = (StorageValueName, PlaceholderTypeRequirement)> + 'a>;
32
33// IDENTIFIER
34// ================================================================================================
35
36/// An identifier for a storage entry field.
37///
38/// An identifier consists of a name that identifies the field, and an optional description.
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct FieldIdentifier {
41    /// A human-readable identifier for the template.
42    pub name: StorageValueName,
43    /// An optional description explaining the purpose of this template.
44    pub description: Option<String>,
45}
46
47impl FieldIdentifier {
48    /// Creates a new `FieldIdentifier` with the given name and no description.
49    pub fn with_name(name: StorageValueName) -> Self {
50        Self { name, description: None }
51    }
52
53    /// Creates a new `FieldIdentifier` with the given name and description.
54    pub fn with_description(name: StorageValueName, description: impl Into<String>) -> Self {
55        Self {
56            name,
57            description: Some(description.into()),
58        }
59    }
60
61    /// Returns the identifier name.
62    pub fn name(&self) -> &StorageValueName {
63        &self.name
64    }
65
66    /// Returns the identifier description.
67    pub fn description(&self) -> Option<&String> {
68        self.description.as_ref()
69    }
70}
71
72impl From<StorageValueName> for FieldIdentifier {
73    fn from(value: StorageValueName) -> Self {
74        FieldIdentifier::with_name(value)
75    }
76}
77
78impl Serializable for FieldIdentifier {
79    fn write_into<W: ByteWriter>(&self, target: &mut W) {
80        target.write(&self.name);
81        target.write(&self.description);
82    }
83}
84
85impl Deserializable for FieldIdentifier {
86    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
87        let name = StorageValueName::read_from(source)?;
88        let description = Option::<String>::read_from(source)?;
89        Ok(FieldIdentifier { name, description })
90    }
91}
92
93// STORAGE ENTRY
94// ================================================================================================
95
96/// Represents a single entry in the component's storage layout.
97///
98/// Each entry can describe:
99/// - A value slot with a single word.
100/// - A map slot with a key-value map that occupies one storage slot.
101/// - A multi-slot entry spanning multiple contiguous slots with multiple words (but not maps) that
102///   represent a single logical value.
103#[derive(Debug, Clone, PartialEq, Eq)]
104#[allow(clippy::large_enum_variant)]
105pub enum StorageEntry {
106    /// A value slot, which can contain one word.
107    Value {
108        /// The numeric index of this map slot in the component's storage.
109        slot: u8,
110        /// A description of a word, representing either a predefined value or a templated one.
111        word_entry: WordRepresentation,
112    },
113
114    /// A map slot, containing multiple key-value pairs. Keys and values are hex-encoded strings.
115    Map {
116        /// The numeric index of this map slot in the component's storage.
117        slot: u8,
118        /// A list of key-value pairs to initialize in this map slot.
119        map: MapRepresentation,
120    },
121
122    /// A multi-slot entry, representing a single logical value across multiple slots.
123    MultiSlot {
124        /// The indices of the slots that form this multi-slot entry.
125        slots: Range<u8>,
126        /// A description of the values.
127        word_entries: MultiWordRepresentation,
128    },
129}
130
131impl StorageEntry {
132    pub fn new_value(slot: u8, word_entry: impl Into<WordRepresentation>) -> Self {
133        StorageEntry::Value { slot, word_entry: word_entry.into() }
134    }
135
136    pub fn new_map(slot: u8, map: MapRepresentation) -> Self {
137        StorageEntry::Map { slot, map }
138    }
139
140    pub fn new_multislot(
141        identifier: FieldIdentifier,
142        slots: Range<u8>,
143        values: Vec<[FeltRepresentation; 4]>,
144    ) -> Self {
145        StorageEntry::MultiSlot {
146            slots,
147            word_entries: MultiWordRepresentation::Value { identifier, values },
148        }
149    }
150
151    pub fn name(&self) -> Option<&StorageValueName> {
152        match self {
153            StorageEntry::Value { word_entry, .. } => word_entry.name(),
154            StorageEntry::Map { map, .. } => Some(map.name()),
155            StorageEntry::MultiSlot { word_entries, .. } => match word_entries {
156                MultiWordRepresentation::Value { identifier, .. } => Some(&identifier.name),
157            },
158        }
159    }
160
161    /// Returns the slot indices that the storage entry covers.
162    pub fn slot_indices(&self) -> Range<u8> {
163        match self {
164            StorageEntry::MultiSlot { slots, .. } => slots.clone(),
165            StorageEntry::Value { slot, .. } | StorageEntry::Map { slot, .. } => *slot..*slot + 1,
166        }
167    }
168
169    /// Returns an iterator over all of the storage entries's value names, alongside their
170    /// expected type.
171    pub fn template_requirements(&self) -> TemplateRequirementsIter {
172        match self {
173            StorageEntry::Value { word_entry, .. } => {
174                word_entry.template_requirements(StorageValueName::empty())
175            },
176            StorageEntry::Map { map, .. } => map.template_requirements(),
177            StorageEntry::MultiSlot { word_entries, .. } => match word_entries {
178                MultiWordRepresentation::Value { identifier, values } => {
179                    Box::new(values.iter().flat_map(move |word| {
180                        word.iter()
181                            .flat_map(move |f| f.template_requirements(identifier.name.clone()))
182                    }))
183                },
184            },
185        }
186    }
187
188    /// Attempts to convert the storage entry into a list of [`StorageSlot`].
189    ///
190    /// - [`StorageEntry::Value`] would convert to a [`StorageSlot::Value`]
191    /// - [`StorageEntry::MultiSlot`] would convert to as many [`StorageSlot::Value`] as required by
192    ///   the defined type
193    /// - [`StorageEntry::Map`] would convert to a [`StorageSlot::Map`]
194    ///
195    /// Each of the entry's values could be templated. These values are replaced for values found
196    /// in `init_storage_data`, identified by its key.
197    pub fn try_build_storage_slots(
198        &self,
199        init_storage_data: &InitStorageData,
200    ) -> Result<Vec<StorageSlot>, AccountComponentTemplateError> {
201        match self {
202            StorageEntry::Value { word_entry, .. } => {
203                let slot =
204                    word_entry.try_build_word(init_storage_data, StorageValueName::empty())?;
205                Ok(vec![StorageSlot::Value(slot)])
206            },
207            StorageEntry::Map { map, .. } => {
208                let storage_map = map.try_build_map(init_storage_data)?;
209                Ok(vec![StorageSlot::Map(storage_map)])
210            },
211            StorageEntry::MultiSlot { word_entries, .. } => {
212                match word_entries {
213                    MultiWordRepresentation::Value { identifier, values } => {
214                        Ok(values
215                            .iter()
216                            .map(|word_repr| {
217                                let mut result = [Felt::ZERO; 4];
218
219                                for (index, felt_repr) in word_repr.iter().enumerate() {
220                                    result[index] = felt_repr.try_build_felt(
221                                        init_storage_data,
222                                        identifier.name.clone(),
223                                    )?;
224                                }
225                                // SAFETY: result is guaranteed to have all its 4 indices rewritten
226                                Ok(StorageSlot::Value(result))
227                            })
228                            .collect::<Result<Vec<StorageSlot>, _>>()?)
229                    },
230                }
231            },
232        }
233    }
234
235    /// Validates the storage entry for internal consistency.
236    pub(super) fn validate(&self) -> Result<(), AccountComponentTemplateError> {
237        match self {
238            StorageEntry::Map { map, .. } => map.validate(),
239            StorageEntry::MultiSlot { slots, word_entries, .. } => {
240                if slots.len() == 1 {
241                    return Err(AccountComponentTemplateError::MultiSlotSpansOneSlot);
242                }
243
244                if slots.len() != word_entries.num_words() {
245                    return Err(AccountComponentTemplateError::MultiSlotArityMismatch);
246                }
247
248                word_entries.validate()
249            },
250            StorageEntry::Value { word_entry, .. } => Ok(word_entry.validate()?),
251        }
252    }
253}
254
255// SERIALIZATION
256// ================================================================================================
257
258impl Serializable for StorageEntry {
259    fn write_into<W: ByteWriter>(&self, target: &mut W) {
260        match self {
261            StorageEntry::Value { slot, word_entry } => {
262                target.write_u8(0u8);
263                target.write_u8(*slot);
264                target.write(word_entry);
265            },
266            StorageEntry::Map { slot, map } => {
267                target.write_u8(1u8);
268                target.write_u8(*slot);
269                target.write(map);
270            },
271            StorageEntry::MultiSlot { word_entries, slots } => {
272                target.write_u8(2u8);
273                target.write(word_entries);
274                target.write(slots.start);
275                target.write(slots.end);
276            },
277        }
278    }
279}
280
281impl Deserializable for StorageEntry {
282    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
283        let variant_tag = source.read_u8()?;
284        match variant_tag {
285            0 => {
286                let slot = source.read_u8()?;
287                let word_entry: WordRepresentation = source.read()?;
288                Ok(StorageEntry::Value { slot, word_entry })
289            },
290            1 => {
291                let slot = source.read_u8()?;
292                let map: MapRepresentation = source.read()?;
293                Ok(StorageEntry::Map { slot, map })
294            },
295            2 => {
296                let word_entries: MultiWordRepresentation = source.read()?;
297                let slots_start: u8 = source.read()?;
298                let slots_end: u8 = source.read()?;
299                Ok(StorageEntry::MultiSlot {
300                    slots: slots_start..slots_end,
301                    word_entries,
302                })
303            },
304            _ => Err(DeserializationError::InvalidValue(format!(
305                "unknown variant tag `{}` for StorageEntry",
306                variant_tag
307            ))),
308        }
309    }
310}
311
312// MAP ENTRY
313// ================================================================================================
314
315/// Key-value entry for storage maps.
316#[derive(Debug, Clone, PartialEq, Eq)]
317#[cfg_attr(feature = "std", derive(serde::Deserialize, serde::Serialize))]
318pub struct MapEntry {
319    key: WordRepresentation,
320    value: WordRepresentation,
321}
322
323impl MapEntry {
324    pub fn new(key: impl Into<WordRepresentation>, value: impl Into<WordRepresentation>) -> Self {
325        Self { key: key.into(), value: value.into() }
326    }
327
328    pub fn key(&self) -> &WordRepresentation {
329        &self.key
330    }
331
332    pub fn value(&self) -> &WordRepresentation {
333        &self.value
334    }
335
336    pub fn into_parts(self) -> (WordRepresentation, WordRepresentation) {
337        let MapEntry { key, value } = self;
338        (key, value)
339    }
340
341    pub fn template_requirements(
342        &self,
343        placeholder_prefix: StorageValueName,
344    ) -> TemplateRequirementsIter<'_> {
345        let key_iter = self.key.template_requirements(placeholder_prefix.clone());
346        let value_iter = self.value.template_requirements(placeholder_prefix);
347
348        Box::new(key_iter.chain(value_iter))
349    }
350}
351
352impl Serializable for MapEntry {
353    fn write_into<W: ByteWriter>(&self, target: &mut W) {
354        self.key.write_into(target);
355        self.value.write_into(target);
356    }
357}
358
359impl Deserializable for MapEntry {
360    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
361        let key = WordRepresentation::read_from(source)?;
362        let value = WordRepresentation::read_from(source)?;
363        Ok(MapEntry { key, value })
364    }
365}
366
367// TESTS
368// ================================================================================================
369
370#[cfg(test)]
371mod tests {
372    use alloc::{collections::BTreeSet, string::ToString};
373    use core::{error::Error, panic};
374
375    use assembly::Assembler;
376    use semver::Version;
377    use vm_core::{
378        Felt, FieldElement, Word,
379        utils::{Deserializable, Serializable},
380    };
381
382    use crate::{
383        AccountError,
384        account::{
385            AccountComponent, AccountComponentTemplate, AccountType, FeltRepresentation,
386            StorageEntry, StorageSlot, TemplateTypeError, WordRepresentation,
387            component::{
388                FieldIdentifier,
389                template::{
390                    AccountComponentMetadata, InitStorageData, MapEntry, MapRepresentation,
391                    StorageValueName, storage::placeholder::TemplateType,
392                },
393            },
394        },
395        digest,
396        errors::AccountComponentTemplateError,
397        testing::account_code::CODE,
398    };
399
400    #[test]
401    fn test_storage_entry_serialization() {
402        let felt_array: [FeltRepresentation; 4] = [
403            FeltRepresentation::from(Felt::new(0xabc)),
404            FeltRepresentation::from(Felt::new(1218)),
405            FeltRepresentation::from(Felt::new(0xdba3)),
406            FeltRepresentation::new_template(
407                TemplateType::native_felt(),
408                StorageValueName::new("slot3").unwrap(),
409            )
410            .with_description("dummy description"),
411        ];
412
413        let test_word: Word = digest!("0x000001").into();
414        let test_word = test_word.map(FeltRepresentation::from);
415
416        let map_representation = MapRepresentation::new(
417            vec![
418                MapEntry {
419                    key: WordRepresentation::new_template(
420                        TemplateType::native_word(),
421                        StorageValueName::new("foo").unwrap().into(),
422                    ),
423                    value: WordRepresentation::new_value(test_word.clone(), None),
424                },
425                MapEntry {
426                    key: WordRepresentation::new_value(test_word.clone(), None),
427                    value: WordRepresentation::new_template(
428                        TemplateType::native_word(),
429                        StorageValueName::new("bar").unwrap().into(),
430                    ),
431                },
432                MapEntry {
433                    key: WordRepresentation::new_template(
434                        TemplateType::native_word(),
435                        StorageValueName::new("baz").unwrap().into(),
436                    ),
437                    value: WordRepresentation::new_value(test_word, None),
438                },
439            ],
440            StorageValueName::new("map").unwrap(),
441        )
442        .with_description("a storage map description");
443
444        let storage = vec![
445            StorageEntry::new_value(0, felt_array.clone()),
446            StorageEntry::new_map(1, map_representation),
447            StorageEntry::new_multislot(
448                FieldIdentifier::with_description(
449                    StorageValueName::new("multi").unwrap(),
450                    "Multi slot entry",
451                ),
452                2..4,
453                vec![
454                    [
455                        FeltRepresentation::new_template(
456                            TemplateType::native_felt(),
457                            StorageValueName::new("test").unwrap(),
458                        ),
459                        FeltRepresentation::new_template(
460                            TemplateType::native_felt(),
461                            StorageValueName::new("test2").unwrap(),
462                        ),
463                        FeltRepresentation::new_template(
464                            TemplateType::native_felt(),
465                            StorageValueName::new("test3").unwrap(),
466                        ),
467                        FeltRepresentation::new_template(
468                            TemplateType::native_felt(),
469                            StorageValueName::new("test4").unwrap(),
470                        ),
471                    ],
472                    felt_array,
473                ],
474            ),
475            StorageEntry::new_value(
476                4,
477                WordRepresentation::new_template(
478                    TemplateType::native_word(),
479                    StorageValueName::new("single").unwrap().into(),
480                ),
481            ),
482        ];
483
484        let config = AccountComponentMetadata {
485            name: "Test Component".into(),
486            description: "This is a test component".into(),
487            version: Version::parse("1.0.0").unwrap(),
488            supported_types: BTreeSet::from([AccountType::FungibleFaucet]),
489            storage,
490        };
491        let toml = config.as_toml().unwrap();
492        let deserialized = AccountComponentMetadata::from_toml(&toml).unwrap();
493
494        assert_eq!(deserialized, config);
495    }
496
497    #[test]
498    pub fn toml_serde_roundtrip() {
499        let toml_text = r#"
500        name = "Test Component"
501        description = "This is a test component"
502        version = "1.0.1"
503        supported-types = ["FungibleFaucet", "RegularAccountImmutableCode"]
504
505        [[storage]]
506        name = "map_entry"
507        slot = 0
508        values = [
509            { key = "0x1", value = ["0x1","0x2","0x3","0"]},
510            { key = "0x3", value = "0x123" }, 
511            { key = { name = "map_key_template", description = "this tests that the default type is correctly set"}, value = "0x3" },
512        ]
513
514        [[storage]]
515        name = "token_metadata"
516        description = "Contains metadata about the token associated to the faucet account"
517        slot = 1
518        value = [
519            { type = "felt", name = "max_supply", description = "Maximum supply of the token in base units" }, # placeholder
520            { type = "token_symbol", value = "TST" }, # hardcoded non-felt type
521            { type = "u8", name = "decimals", description = "Number of decimal places" }, # placeholder
522            { value = "0" }, 
523        ]
524
525        [[storage]]
526        name = "default_recallable_height"
527        slot = 2
528        type = "word"
529        "#;
530
531        let component_metadata = AccountComponentMetadata::from_toml(toml_text).unwrap();
532        let requirements = component_metadata.get_placeholder_requirements();
533
534        assert_eq!(requirements.len(), 4);
535
536        let supply = requirements
537            .get(&StorageValueName::new("token_metadata.max_supply").unwrap())
538            .unwrap();
539        assert_eq!(supply.r#type.as_str(), "felt");
540
541        let decimals = requirements
542            .get(&StorageValueName::new("token_metadata.decimals").unwrap())
543            .unwrap();
544        assert_eq!(decimals.r#type.as_str(), "u8");
545
546        let default_recallable_height = requirements
547            .get(&StorageValueName::new("default_recallable_height").unwrap())
548            .unwrap();
549        assert_eq!(default_recallable_height.r#type.as_str(), "word");
550
551        let map_key_template = requirements
552            .get(&StorageValueName::new("map_entry.map_key_template").unwrap())
553            .unwrap();
554        assert_eq!(map_key_template.r#type.as_str(), "word");
555
556        let library = Assembler::default().assemble_library([CODE]).unwrap();
557        let template = AccountComponentTemplate::new(component_metadata, library);
558
559        let template_bytes = template.to_bytes();
560        let template_deserialized =
561            AccountComponentTemplate::read_from_bytes(&template_bytes).unwrap();
562        assert_eq!(template, template_deserialized);
563
564        // Fail to parse because 2800 > u8
565        let storage_placeholders = InitStorageData::new([
566            (
567                StorageValueName::new("map_entry.map_key_template").unwrap(),
568                "0x123".to_string(),
569            ),
570            (
571                StorageValueName::new("token_metadata.max_supply").unwrap(),
572                20_000u64.to_string(),
573            ),
574            (StorageValueName::new("token_metadata.decimals").unwrap(), "2800".into()),
575            (StorageValueName::new("default_recallable_height").unwrap(), "0".into()),
576        ]);
577
578        let component = AccountComponent::from_template(&template, &storage_placeholders);
579        assert_matches::assert_matches!(
580            component,
581            Err(AccountError::AccountComponentTemplateInstantiationError(
582                AccountComponentTemplateError::StorageValueParsingError(
583                    TemplateTypeError::ParseError { .. }
584                )
585            ))
586        );
587
588        // Instantiate successfully
589        let storage_placeholders = InitStorageData::new([
590            (
591                StorageValueName::new("map_entry.map_key_template").unwrap(),
592                "0x123".to_string(),
593            ),
594            (
595                StorageValueName::new("token_metadata.max_supply").unwrap(),
596                20_000u64.to_string(),
597            ),
598            (StorageValueName::new("token_metadata.decimals").unwrap(), "128".into()),
599            (StorageValueName::new("default_recallable_height").unwrap(), "0x0".into()),
600        ]);
601
602        let component = AccountComponent::from_template(&template, &storage_placeholders).unwrap();
603        assert_eq!(
604            component.supported_types(),
605            &[AccountType::FungibleFaucet, AccountType::RegularAccountImmutableCode]
606                .into_iter()
607                .collect()
608        );
609
610        let storage_map = component.storage_slots.first().unwrap();
611        match storage_map {
612            StorageSlot::Map(storage_map) => assert_eq!(storage_map.entries().count(), 3),
613            _ => panic!("should be map"),
614        }
615
616        let value_entry = component.storage_slots().get(2).unwrap();
617        match value_entry {
618            StorageSlot::Value(v) => {
619                assert_eq!(v, &[Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::ZERO])
620            },
621            _ => panic!("should be value"),
622        }
623
624        let failed_instantiation =
625            AccountComponent::from_template(&template, &InitStorageData::default());
626
627        assert_matches::assert_matches!(
628            failed_instantiation,
629            Err(AccountError::AccountComponentTemplateInstantiationError(
630                AccountComponentTemplateError::PlaceholderValueNotProvided(_)
631            ))
632        );
633    }
634
635    #[test]
636    fn test_no_duplicate_slot_names() {
637        let toml_text = r#"
638        name = "Test Component"
639        description = "This is a test component"
640        version = "1.0.1"
641        supported-types = ["FungibleFaucet", "RegularAccountImmutableCode"]
642
643        [[storage]]
644        name = "test_duplicate"
645        slot = 0
646        type = "felt" # Felt is not a valid type for word slots
647        "#;
648
649        let err = AccountComponentMetadata::from_toml(toml_text).unwrap_err();
650        assert_matches::assert_matches!(err, AccountComponentTemplateError::InvalidType(_, _))
651    }
652
653    #[test]
654    fn toml_fail_multislot_arity_mismatch() {
655        let toml_text = r#"
656        name = "Test Component"
657        description = "Test multislot arity mismatch"
658        version = "1.0.1"
659        supported-types = ["FungibleFaucet"]
660
661        [[storage]]
662        name = "multislot_test"
663        slots = [0, 1]
664        values = [
665            [ "0x1", "0x2", "0x3", "0x4" ]
666        ]
667    "#;
668
669        let err = AccountComponentMetadata::from_toml(toml_text).unwrap_err();
670        assert_matches::assert_matches!(err, AccountComponentTemplateError::MultiSlotArityMismatch);
671    }
672
673    #[test]
674    fn toml_fail_multislot_duplicate_slot() {
675        let toml_text = r#"
676        name = "Test Component"
677        description = "Test multislot duplicate slot"
678        version = "1.0.1"
679        supported-types = ["FungibleFaucet"]
680
681        [[storage]]
682        name = "multislot_duplicate"
683        slots = [0, 1]
684        values = [
685            [ "0x1", "0x2", "0x3", "0x4" ],
686            [ "0x5", "0x6", "0x7", "0x8" ]
687        ]
688
689        [[storage]]
690        name = "multislot_duplicate"
691        slots = [1, 2]
692        values = [
693            [ "0x1", "0x2", "0x3", "0x4" ],
694            [ "0x5", "0x6", "0x7", "0x8" ]
695        ]
696    "#;
697
698        let err = AccountComponentMetadata::from_toml(toml_text).unwrap_err();
699        assert_matches::assert_matches!(err, AccountComponentTemplateError::DuplicateSlot(1));
700    }
701
702    #[test]
703    fn toml_fail_multislot_non_contiguous_slots() {
704        let toml_text = r#"
705        name = "Test Component"
706        description = "Test multislot non contiguous"
707        version = "1.0.1"
708        supported-types = ["FungibleFaucet"]
709
710        [[storage]]
711        name = "multislot_non_contiguous"
712        slots = [0, 2]
713        values = [
714            [ "0x1", "0x2", "0x3", "0x4" ],
715            [ "0x5", "0x6", "0x7", "0x8" ]
716        ]
717    "#;
718
719        let err = AccountComponentMetadata::from_toml(toml_text).unwrap_err();
720        // validate inner serde error
721        assert!(err.source().unwrap().to_string().contains("are not contiguous"));
722    }
723
724    #[test]
725    fn toml_fail_duplicate_storage_entry_names() {
726        let toml_text = r#"
727        name = "Test Component"
728        description = "Component with duplicate storage entry names"
729        version = "1.0.1"
730        supported-types = ["FungibleFaucet"]
731
732        [[storage]]
733        # placeholder
734        name = "duplicate"
735        slot = 0
736        type = "word"
737
738        [[storage]]
739        name = "duplicate"
740        slot = 1
741        value = [ "0x1", "0x1", "0x1", "0x1" ]
742    "#;
743
744        let result = AccountComponentMetadata::from_toml(toml_text);
745        assert_matches::assert_matches!(
746            result.unwrap_err(),
747            AccountComponentTemplateError::DuplicateEntryNames(_)
748        );
749    }
750
751    #[test]
752    fn toml_fail_multislot_spans_one_slot() {
753        let toml_text = r#"
754        name = "Test Component"
755        description = "Test multislot spans one slot"
756        version = "1.0.1"
757        supported-types = ["RegularAccountImmutableCode"]
758
759        [[storage]]
760        name = "multislot_one_slot"
761        slots = [0]
762        values = [
763            [ "0x1", "0x2", "0x3", "0x4" ],
764        ]
765    "#;
766
767        let result = AccountComponentMetadata::from_toml(toml_text);
768        assert_matches::assert_matches!(
769            result.unwrap_err(),
770            AccountComponentTemplateError::MultiSlotSpansOneSlot
771        );
772    }
773
774    #[test]
775    fn test_toml_multislot_success() {
776        let toml_text = r#"
777        name = "Test Component"
778        description = "A multi-slot success scenario"
779        version = "1.0.1"
780        supported-types = ["FungibleFaucet"]
781
782        [[storage]]
783        name = "multi_slot_example"
784        slots = [0, 1, 2]
785        values = [
786            ["0x1", "0x2", "0x3", "0x4"],
787            ["0x5", "0x6", "0x7", "0x8"],
788            ["0x9", "0xa", "0xb", "0xc"]
789        ]
790    "#;
791
792        let metadata = AccountComponentMetadata::from_toml(toml_text).unwrap();
793        match &metadata.storage_entries()[0] {
794            StorageEntry::MultiSlot { slots, word_entries } => match word_entries {
795                crate::account::component::template::MultiWordRepresentation::Value {
796                    identifier,
797                    values,
798                } => {
799                    assert_eq!(identifier.name.as_str(), "multi_slot_example");
800                    assert_eq!(slots, &(0..3));
801                    assert_eq!(values.len(), 3);
802                },
803            },
804            _ => panic!("expected multislot"),
805        }
806    }
807}