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 '{variant_tag}' for StorageEntry"
306            ))),
307        }
308    }
309}
310
311// MAP ENTRY
312// ================================================================================================
313
314/// Key-value entry for storage maps.
315#[derive(Debug, Clone, PartialEq, Eq)]
316#[cfg_attr(feature = "std", derive(serde::Deserialize, serde::Serialize))]
317pub struct MapEntry {
318    key: WordRepresentation,
319    value: WordRepresentation,
320}
321
322impl MapEntry {
323    pub fn new(key: impl Into<WordRepresentation>, value: impl Into<WordRepresentation>) -> Self {
324        Self { key: key.into(), value: value.into() }
325    }
326
327    pub fn key(&self) -> &WordRepresentation {
328        &self.key
329    }
330
331    pub fn value(&self) -> &WordRepresentation {
332        &self.value
333    }
334
335    pub fn into_parts(self) -> (WordRepresentation, WordRepresentation) {
336        let MapEntry { key, value } = self;
337        (key, value)
338    }
339
340    pub fn template_requirements(
341        &self,
342        placeholder_prefix: StorageValueName,
343    ) -> TemplateRequirementsIter<'_> {
344        let key_iter = self.key.template_requirements(placeholder_prefix.clone());
345        let value_iter = self.value.template_requirements(placeholder_prefix);
346
347        Box::new(key_iter.chain(value_iter))
348    }
349}
350
351impl Serializable for MapEntry {
352    fn write_into<W: ByteWriter>(&self, target: &mut W) {
353        self.key.write_into(target);
354        self.value.write_into(target);
355    }
356}
357
358impl Deserializable for MapEntry {
359    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
360        let key = WordRepresentation::read_from(source)?;
361        let value = WordRepresentation::read_from(source)?;
362        Ok(MapEntry { key, value })
363    }
364}
365
366// TESTS
367// ================================================================================================
368
369#[cfg(test)]
370mod tests {
371    use alloc::{collections::BTreeSet, string::ToString};
372    use core::{error::Error, panic};
373
374    use assembly::Assembler;
375    use semver::Version;
376    use vm_core::{
377        Felt, FieldElement, Word,
378        utils::{Deserializable, Serializable},
379    };
380
381    use crate::{
382        AccountError,
383        account::{
384            AccountComponent, AccountComponentTemplate, AccountType, FeltRepresentation,
385            StorageEntry, StorageSlot, TemplateTypeError, WordRepresentation,
386            component::{
387                FieldIdentifier,
388                template::{
389                    AccountComponentMetadata, InitStorageData, MapEntry, MapRepresentation,
390                    StorageValueName, storage::placeholder::TemplateType,
391                },
392            },
393        },
394        digest,
395        errors::AccountComponentTemplateError,
396        testing::account_code::CODE,
397    };
398
399    #[test]
400    fn test_storage_entry_serialization() {
401        let felt_array: [FeltRepresentation; 4] = [
402            FeltRepresentation::from(Felt::new(0xabc)),
403            FeltRepresentation::from(Felt::new(1218)),
404            FeltRepresentation::from(Felt::new(0xdba3)),
405            FeltRepresentation::new_template(
406                TemplateType::native_felt(),
407                StorageValueName::new("slot3").unwrap(),
408            )
409            .with_description("dummy description"),
410        ];
411
412        let test_word: Word = digest!("0x000001").into();
413        let test_word = test_word.map(FeltRepresentation::from);
414
415        let map_representation = MapRepresentation::new(
416            vec![
417                MapEntry {
418                    key: WordRepresentation::new_template(
419                        TemplateType::native_word(),
420                        StorageValueName::new("foo").unwrap().into(),
421                    ),
422                    value: WordRepresentation::new_value(test_word.clone(), None),
423                },
424                MapEntry {
425                    key: WordRepresentation::new_value(test_word.clone(), None),
426                    value: WordRepresentation::new_template(
427                        TemplateType::native_word(),
428                        StorageValueName::new("bar").unwrap().into(),
429                    ),
430                },
431                MapEntry {
432                    key: WordRepresentation::new_template(
433                        TemplateType::native_word(),
434                        StorageValueName::new("baz").unwrap().into(),
435                    ),
436                    value: WordRepresentation::new_value(test_word, None),
437                },
438            ],
439            StorageValueName::new("map").unwrap(),
440        )
441        .with_description("a storage map description");
442
443        let storage = vec![
444            StorageEntry::new_value(0, felt_array.clone()),
445            StorageEntry::new_map(1, map_representation),
446            StorageEntry::new_multislot(
447                FieldIdentifier::with_description(
448                    StorageValueName::new("multi").unwrap(),
449                    "Multi slot entry",
450                ),
451                2..4,
452                vec![
453                    [
454                        FeltRepresentation::new_template(
455                            TemplateType::native_felt(),
456                            StorageValueName::new("test").unwrap(),
457                        ),
458                        FeltRepresentation::new_template(
459                            TemplateType::native_felt(),
460                            StorageValueName::new("test2").unwrap(),
461                        ),
462                        FeltRepresentation::new_template(
463                            TemplateType::native_felt(),
464                            StorageValueName::new("test3").unwrap(),
465                        ),
466                        FeltRepresentation::new_template(
467                            TemplateType::native_felt(),
468                            StorageValueName::new("test4").unwrap(),
469                        ),
470                    ],
471                    felt_array,
472                ],
473            ),
474            StorageEntry::new_value(
475                4,
476                WordRepresentation::new_template(
477                    TemplateType::native_word(),
478                    StorageValueName::new("single").unwrap().into(),
479                ),
480            ),
481        ];
482
483        let config = AccountComponentMetadata {
484            name: "Test Component".into(),
485            description: "This is a test component".into(),
486            version: Version::parse("1.0.0").unwrap(),
487            supported_types: BTreeSet::from([AccountType::FungibleFaucet]),
488            storage,
489        };
490        let toml = config.as_toml().unwrap();
491        let deserialized = AccountComponentMetadata::from_toml(&toml).unwrap();
492
493        assert_eq!(deserialized, config);
494    }
495
496    #[test]
497    pub fn toml_serde_roundtrip() {
498        let toml_text = r#"
499        name = "Test Component"
500        description = "This is a test component"
501        version = "1.0.1"
502        supported-types = ["FungibleFaucet", "RegularAccountImmutableCode"]
503
504        [[storage]]
505        name = "map_entry"
506        slot = 0
507        values = [
508            { key = "0x1", value = ["0x1","0x2","0x3","0"]},
509            { key = "0x3", value = "0x123" }, 
510            { key = { name = "map_key_template", description = "this tests that the default type is correctly set"}, value = "0x3" },
511        ]
512
513        [[storage]]
514        name = "token_metadata"
515        description = "Contains metadata about the token associated to the faucet account"
516        slot = 1
517        value = [
518            { type = "felt", name = "max_supply", description = "Maximum supply of the token in base units" }, # placeholder
519            { type = "token_symbol", value = "TST" }, # hardcoded non-felt type
520            { type = "u8", name = "decimals", description = "Number of decimal places" }, # placeholder
521            { value = "0" }, 
522        ]
523
524        [[storage]]
525        name = "default_recallable_height"
526        slot = 2
527        type = "word"
528        "#;
529
530        let component_metadata = AccountComponentMetadata::from_toml(toml_text).unwrap();
531        let requirements = component_metadata.get_placeholder_requirements();
532
533        assert_eq!(requirements.len(), 4);
534
535        let supply = requirements
536            .get(&StorageValueName::new("token_metadata.max_supply").unwrap())
537            .unwrap();
538        assert_eq!(supply.r#type.as_str(), "felt");
539
540        let decimals = requirements
541            .get(&StorageValueName::new("token_metadata.decimals").unwrap())
542            .unwrap();
543        assert_eq!(decimals.r#type.as_str(), "u8");
544
545        let default_recallable_height = requirements
546            .get(&StorageValueName::new("default_recallable_height").unwrap())
547            .unwrap();
548        assert_eq!(default_recallable_height.r#type.as_str(), "word");
549
550        let map_key_template = requirements
551            .get(&StorageValueName::new("map_entry.map_key_template").unwrap())
552            .unwrap();
553        assert_eq!(map_key_template.r#type.as_str(), "word");
554
555        let library = Assembler::default().assemble_library([CODE]).unwrap();
556        let template = AccountComponentTemplate::new(component_metadata, library);
557
558        let template_bytes = template.to_bytes();
559        let template_deserialized =
560            AccountComponentTemplate::read_from_bytes(&template_bytes).unwrap();
561        assert_eq!(template, template_deserialized);
562
563        // Fail to parse because 2800 > u8
564        let storage_placeholders = InitStorageData::new([
565            (
566                StorageValueName::new("map_entry.map_key_template").unwrap(),
567                "0x123".to_string(),
568            ),
569            (
570                StorageValueName::new("token_metadata.max_supply").unwrap(),
571                20_000u64.to_string(),
572            ),
573            (StorageValueName::new("token_metadata.decimals").unwrap(), "2800".into()),
574            (StorageValueName::new("default_recallable_height").unwrap(), "0".into()),
575        ]);
576
577        let component = AccountComponent::from_template(&template, &storage_placeholders);
578        assert_matches::assert_matches!(
579            component,
580            Err(AccountError::AccountComponentTemplateInstantiationError(
581                AccountComponentTemplateError::StorageValueParsingError(
582                    TemplateTypeError::ParseError { .. }
583                )
584            ))
585        );
586
587        // Instantiate successfully
588        let storage_placeholders = InitStorageData::new([
589            (
590                StorageValueName::new("map_entry.map_key_template").unwrap(),
591                "0x123".to_string(),
592            ),
593            (
594                StorageValueName::new("token_metadata.max_supply").unwrap(),
595                20_000u64.to_string(),
596            ),
597            (StorageValueName::new("token_metadata.decimals").unwrap(), "128".into()),
598            (StorageValueName::new("default_recallable_height").unwrap(), "0x0".into()),
599        ]);
600
601        let component = AccountComponent::from_template(&template, &storage_placeholders).unwrap();
602        assert_eq!(
603            component.supported_types(),
604            &[AccountType::FungibleFaucet, AccountType::RegularAccountImmutableCode]
605                .into_iter()
606                .collect()
607        );
608
609        let storage_map = component.storage_slots.first().unwrap();
610        match storage_map {
611            StorageSlot::Map(storage_map) => assert_eq!(storage_map.entries().count(), 3),
612            _ => panic!("should be map"),
613        }
614
615        let value_entry = component.storage_slots().get(2).unwrap();
616        match value_entry {
617            StorageSlot::Value(v) => {
618                assert_eq!(v, &[Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::ZERO])
619            },
620            _ => panic!("should be value"),
621        }
622
623        let failed_instantiation =
624            AccountComponent::from_template(&template, &InitStorageData::default());
625
626        assert_matches::assert_matches!(
627            failed_instantiation,
628            Err(AccountError::AccountComponentTemplateInstantiationError(
629                AccountComponentTemplateError::PlaceholderValueNotProvided(_)
630            ))
631        );
632    }
633
634    #[test]
635    fn test_no_duplicate_slot_names() {
636        let toml_text = r#"
637        name = "Test Component"
638        description = "This is a test component"
639        version = "1.0.1"
640        supported-types = ["FungibleFaucet", "RegularAccountImmutableCode"]
641
642        [[storage]]
643        name = "test_duplicate"
644        slot = 0
645        type = "felt" # Felt is not a valid type for word slots
646        "#;
647
648        let err = AccountComponentMetadata::from_toml(toml_text).unwrap_err();
649        assert_matches::assert_matches!(err, AccountComponentTemplateError::InvalidType(_, _))
650    }
651
652    #[test]
653    fn toml_fail_multislot_arity_mismatch() {
654        let toml_text = r#"
655        name = "Test Component"
656        description = "Test multislot arity mismatch"
657        version = "1.0.1"
658        supported-types = ["FungibleFaucet"]
659
660        [[storage]]
661        name = "multislot_test"
662        slots = [0, 1]
663        values = [
664            [ "0x1", "0x2", "0x3", "0x4" ]
665        ]
666    "#;
667
668        let err = AccountComponentMetadata::from_toml(toml_text).unwrap_err();
669        assert_matches::assert_matches!(err, AccountComponentTemplateError::MultiSlotArityMismatch);
670    }
671
672    #[test]
673    fn toml_fail_multislot_duplicate_slot() {
674        let toml_text = r#"
675        name = "Test Component"
676        description = "Test multislot duplicate slot"
677        version = "1.0.1"
678        supported-types = ["FungibleFaucet"]
679
680        [[storage]]
681        name = "multislot_duplicate"
682        slots = [0, 1]
683        values = [
684            [ "0x1", "0x2", "0x3", "0x4" ],
685            [ "0x5", "0x6", "0x7", "0x8" ]
686        ]
687
688        [[storage]]
689        name = "multislot_duplicate"
690        slots = [1, 2]
691        values = [
692            [ "0x1", "0x2", "0x3", "0x4" ],
693            [ "0x5", "0x6", "0x7", "0x8" ]
694        ]
695    "#;
696
697        let err = AccountComponentMetadata::from_toml(toml_text).unwrap_err();
698        assert_matches::assert_matches!(err, AccountComponentTemplateError::DuplicateSlot(1));
699    }
700
701    #[test]
702    fn toml_fail_multislot_non_contiguous_slots() {
703        let toml_text = r#"
704        name = "Test Component"
705        description = "Test multislot non contiguous"
706        version = "1.0.1"
707        supported-types = ["FungibleFaucet"]
708
709        [[storage]]
710        name = "multislot_non_contiguous"
711        slots = [0, 2]
712        values = [
713            [ "0x1", "0x2", "0x3", "0x4" ],
714            [ "0x5", "0x6", "0x7", "0x8" ]
715        ]
716    "#;
717
718        let err = AccountComponentMetadata::from_toml(toml_text).unwrap_err();
719        // validate inner serde error
720        assert!(err.source().unwrap().to_string().contains("are not contiguous"));
721    }
722
723    #[test]
724    fn toml_fail_duplicate_storage_entry_names() {
725        let toml_text = r#"
726        name = "Test Component"
727        description = "Component with duplicate storage entry names"
728        version = "1.0.1"
729        supported-types = ["FungibleFaucet"]
730
731        [[storage]]
732        # placeholder
733        name = "duplicate"
734        slot = 0
735        type = "word"
736
737        [[storage]]
738        name = "duplicate"
739        slot = 1
740        value = [ "0x1", "0x1", "0x1", "0x1" ]
741    "#;
742
743        let result = AccountComponentMetadata::from_toml(toml_text);
744        assert_matches::assert_matches!(
745            result.unwrap_err(),
746            AccountComponentTemplateError::DuplicateEntryNames(_)
747        );
748    }
749
750    #[test]
751    fn toml_fail_multislot_spans_one_slot() {
752        let toml_text = r#"
753        name = "Test Component"
754        description = "Test multislot spans one slot"
755        version = "1.0.1"
756        supported-types = ["RegularAccountImmutableCode"]
757
758        [[storage]]
759        name = "multislot_one_slot"
760        slots = [0]
761        values = [
762            [ "0x1", "0x2", "0x3", "0x4" ],
763        ]
764    "#;
765
766        let result = AccountComponentMetadata::from_toml(toml_text);
767        assert_matches::assert_matches!(
768            result.unwrap_err(),
769            AccountComponentTemplateError::MultiSlotSpansOneSlot
770        );
771    }
772
773    #[test]
774    fn test_toml_multislot_success() {
775        let toml_text = r#"
776        name = "Test Component"
777        description = "A multi-slot success scenario"
778        version = "1.0.1"
779        supported-types = ["FungibleFaucet"]
780
781        [[storage]]
782        name = "multi_slot_example"
783        slots = [0, 1, 2]
784        values = [
785            ["0x1", "0x2", "0x3", "0x4"],
786            ["0x5", "0x6", "0x7", "0x8"],
787            ["0x9", "0xa", "0xb", "0xc"]
788        ]
789    "#;
790
791        let metadata = AccountComponentMetadata::from_toml(toml_text).unwrap();
792        match &metadata.storage_entries()[0] {
793            StorageEntry::MultiSlot { slots, word_entries } => match word_entries {
794                crate::account::component::template::MultiWordRepresentation::Value {
795                    identifier,
796                    values,
797                } => {
798                    assert_eq!(identifier.name.as_str(), "multi_slot_example");
799                    assert_eq!(slots, &(0..3));
800                    assert_eq!(values.len(), 3);
801                },
802            },
803            _ => panic!("expected multislot"),
804        }
805    }
806}