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
32pub type TemplateRequirementsIter<'a> =
35 Box<dyn Iterator<Item = (StorageValueName, PlaceholderTypeRequirement)> + 'a>;
36
37#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct FieldIdentifier {
45 pub name: StorageValueName,
47 pub description: Option<String>,
49}
50
51impl FieldIdentifier {
52 pub fn with_name(name: StorageValueName) -> Self {
54 Self { name, description: None }
55 }
56
57 pub fn with_description(name: StorageValueName, description: impl Into<String>) -> Self {
59 Self {
60 name,
61 description: Some(description.into()),
62 }
63 }
64
65 pub fn name(&self) -> &StorageValueName {
67 &self.name
68 }
69
70 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#[derive(Debug, Clone, PartialEq, Eq)]
108#[allow(clippy::large_enum_variant)]
109pub enum StorageEntry {
110 Value {
112 slot: u8,
114 word_entry: WordRepresentation,
116 },
117
118 Map {
120 slot: u8,
122 map: MapRepresentation,
124 },
125
126 MultiSlot {
128 slots: Range<u8>,
130 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 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 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 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 Ok(StorageSlot::Value(Word::from(result)))
231 })
232 .collect::<Result<Vec<StorageSlot>, _>>()?)
233 },
234 }
235 },
236 }
237 }
238
239 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
259impl 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#[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#[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 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 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 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 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 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}