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