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
28pub type TemplateRequirementsIter<'a> =
31 Box<dyn Iterator<Item = (StorageValueName, PlaceholderTypeRequirement)> + 'a>;
32
33#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct FieldIdentifier {
41 pub name: StorageValueName,
43 pub description: Option<String>,
45}
46
47impl FieldIdentifier {
48 pub fn with_name(name: StorageValueName) -> Self {
50 Self { name, description: None }
51 }
52
53 pub fn with_description(name: StorageValueName, description: impl Into<String>) -> Self {
55 Self {
56 name,
57 description: Some(description.into()),
58 }
59 }
60
61 pub fn name(&self) -> &StorageValueName {
63 &self.name
64 }
65
66 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#[derive(Debug, Clone, PartialEq, Eq)]
104#[allow(clippy::large_enum_variant)]
105pub enum StorageEntry {
106 Value {
108 slot: u8,
110 word_entry: WordRepresentation,
112 },
113
114 Map {
116 slot: u8,
118 map: MapRepresentation,
120 },
121
122 MultiSlot {
124 slots: Range<u8>,
126 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 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 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 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 Ok(StorageSlot::Value(result))
227 })
228 .collect::<Result<Vec<StorageSlot>, _>>()?)
229 },
230 }
231 },
232 }
233 }
234
235 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
255impl 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#[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#[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 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 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 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}