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 `{}` for StorageEntry",
306 variant_tag
307 ))),
308 }
309 }
310}
311
312#[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#[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 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 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 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}