miden_objects/account/component/template/
mod.rs

1use alloc::{
2    collections::{BTreeMap, BTreeSet},
3    string::{String, ToString},
4    vec::Vec,
5};
6use core::str::FromStr;
7
8use assembly::Library;
9use semver::Version;
10use vm_core::utils::{ByteReader, ByteWriter, Deserializable, Serializable};
11use vm_processor::DeserializationError;
12
13use super::AccountType;
14use crate::errors::AccountComponentTemplateError;
15
16mod storage;
17pub use storage::*;
18
19// ACCOUNT COMPONENT TEMPLATE
20// ================================================================================================
21
22/// Represents a template containing a component's metadata and its associated library.
23///
24/// The [AccountComponentTemplate] encapsulates all necessary information to initialize and manage
25/// an account component within the system. It includes the configuration details and the compiled
26/// library code required for the component's operation.
27///
28/// A template can be instantiated into [AccountComponent](super::AccountComponent) objects.
29/// The component metadata can be defined with placeholders that can be replaced at instantiation
30/// time.
31#[derive(Clone, Debug, PartialEq, Eq)]
32pub struct AccountComponentTemplate {
33    /// The component's metadata. This describes the component and how the storage is laid out,
34    /// alongside how storage values are initialized.
35    metadata: AccountComponentMetadata,
36    /// The account component's assembled code. This defines all functionality related to the
37    /// component.
38    library: Library,
39}
40
41impl AccountComponentTemplate {
42    /// Creates a new [AccountComponentTemplate].
43    ///
44    /// This template holds everything needed to describe and implement a component, including the
45    /// compiled procedures (via the [Library]) and the metadata that defines the component’s
46    /// storage layout ([AccountComponentMetadata]). The metadata can include storage placeholders
47    /// that get filled in at the time of the [AccountComponent](super::AccountComponent)
48    /// instantiation.
49    pub fn new(metadata: AccountComponentMetadata, library: Library) -> Self {
50        Self { metadata, library }
51    }
52
53    /// Returns a reference to the template's [AccountComponentMetadata].
54    pub fn metadata(&self) -> &AccountComponentMetadata {
55        &self.metadata
56    }
57
58    /// Returns a reference to the underlying [Library] of this component.
59    pub fn library(&self) -> &Library {
60        &self.library
61    }
62}
63
64impl Serializable for AccountComponentTemplate {
65    fn write_into<W: vm_core::utils::ByteWriter>(&self, target: &mut W) {
66        target.write(&self.metadata);
67        target.write(&self.library);
68    }
69}
70
71impl Deserializable for AccountComponentTemplate {
72    fn read_from<R: vm_core::utils::ByteReader>(
73        source: &mut R,
74    ) -> Result<Self, vm_processor::DeserializationError> {
75        // Read and deserialize the configuration from a TOML string.
76        let metadata: AccountComponentMetadata = source.read()?;
77        let library = Library::read_from(source)?;
78
79        Ok(AccountComponentTemplate::new(metadata, library))
80    }
81}
82
83// ACCOUNT COMPONENT METADATA
84// ================================================================================================
85
86/// Represents the full component template configuration.
87///
88/// An account component metadata describes the component alongside its storage layout.
89/// On the storage layout, placeholders can be utilized to identify values that should be provided
90/// at the moment of instantiation.
91///
92/// When the `std` feature is enabled, this struct allows for serialization and deserialization to
93/// and from a TOML file.
94///
95/// # Guarantees
96///
97/// - The metadata's storage layout does not contain duplicate slots, and it always starts at slot
98///   index 0.
99/// - Storage slots are laid out in a contiguous manner.
100/// - Each placeholder represents a single value. The expected placeholders can be retrieved with
101///   [AccountComponentMetadata::get_placeholder_requirements()], which returns a map from keys to
102///   [PlaceholderTypeRequirement] (which, in turn, indicates the expected value type for the
103///   placeholder).
104///
105/// # Example
106///
107/// ```
108/// # use semver::Version;
109/// # use std::collections::BTreeSet;
110/// # use miden_objects::{testing::account_code::CODE, account::{
111/// #     AccountComponent, AccountComponentMetadata, StorageEntry,
112/// #     StorageValueName,
113/// #     AccountComponentTemplate, FeltRepresentation, WordRepresentation, TemplateType},
114/// #     assembly::Assembler, Felt};
115/// # use miden_objects::account::InitStorageData;
116/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
117/// let first_felt = FeltRepresentation::from(Felt::new(0u64));
118/// let second_felt = FeltRepresentation::from(Felt::new(1u64));
119/// let third_felt = FeltRepresentation::from(Felt::new(2u64));
120/// // Templated element:
121/// let last_element =
122///     FeltRepresentation::new_template(TemplateType::new("felt")?, StorageValueName::new("foo")?);
123///
124/// let word_representation = WordRepresentation::new_value(
125///     [first_felt, second_felt, third_felt, last_element],
126///     Some(StorageValueName::new("test_value")?.into()),
127/// )
128/// .with_description("this is the first entry in the storage layout");
129/// let storage_entry = StorageEntry::new_value(0, word_representation);
130///
131/// let init_storage_data =
132///     InitStorageData::new([(StorageValueName::new("test_value.foo")?, "300".to_string())]);
133///
134/// let component_template = AccountComponentMetadata::new(
135///     "test name".into(),
136///     "description of the component".into(),
137///     Version::parse("0.1.0")?,
138///     BTreeSet::new(),
139///     vec![storage_entry],
140/// )?;
141///
142/// let library = Assembler::default().assemble_library([CODE]).unwrap();
143/// let template = AccountComponentTemplate::new(component_template, library);
144///
145/// let component = AccountComponent::from_template(&template, &init_storage_data)?;
146/// # Ok(())
147/// # }
148/// ```
149#[derive(Debug, Clone, PartialEq, Eq)]
150#[cfg_attr(feature = "std", derive(serde::Deserialize, serde::Serialize))]
151#[cfg_attr(feature = "std", serde(rename_all = "kebab-case"))]
152pub struct AccountComponentMetadata {
153    /// The human-readable name of the component.
154    name: String,
155
156    /// A brief description of what this component is and how it works.
157    description: String,
158
159    /// The version of the component using semantic versioning.
160    /// This can be used to track and manage component upgrades.
161    version: Version,
162
163    /// A set of supported target account types for this component.
164    supported_types: BTreeSet<AccountType>,
165
166    /// A list of storage entries defining the component's storage layout and initialization
167    /// values.
168    storage: Vec<StorageEntry>,
169}
170
171impl AccountComponentMetadata {
172    /// Create a new [AccountComponentMetadata].
173    ///
174    /// # Errors
175    ///
176    /// - If the specified storage slots contain duplicates.
177    /// - If the slot numbers do not start at zero.
178    /// - If the slots are not contiguous.
179    pub fn new(
180        name: String,
181        description: String,
182        version: Version,
183        targets: BTreeSet<AccountType>,
184        storage: Vec<StorageEntry>,
185    ) -> Result<Self, AccountComponentTemplateError> {
186        let component = Self {
187            name,
188            description,
189            version,
190            supported_types: targets,
191            storage,
192        };
193        component.validate()?;
194        Ok(component)
195    }
196
197    /// Retrieves a map of unique storage placeholder names mapped to their expected type that
198    /// require a value at the moment of component instantiation.
199    ///
200    /// These values will be used for initializing storage slot values, or storage map entries.
201    /// For a full example on how a placeholder may be utilized, please refer to the docs for
202    /// [AccountComponentMetadata].
203    ///
204    /// Types for the returned storage placeholders are inferred based on their location in the
205    /// storage layout structure.
206    pub fn get_placeholder_requirements(
207        &self,
208    ) -> BTreeMap<StorageValueName, PlaceholderTypeRequirement> {
209        let mut templates = BTreeMap::new();
210        for entry in self.storage_entries() {
211            for (name, requirement) in entry.template_requirements() {
212                templates.insert(name, requirement);
213            }
214        }
215
216        templates
217    }
218
219    /// Returns the name of the account component.
220    pub fn name(&self) -> &str {
221        &self.name
222    }
223
224    /// Returns the description of the account component.
225    pub fn description(&self) -> &str {
226        &self.description
227    }
228
229    /// Returns the semantic version of the account component.
230    pub fn version(&self) -> &Version {
231        &self.version
232    }
233
234    /// Returns the account types supported by the component.
235    pub fn supported_types(&self) -> &BTreeSet<AccountType> {
236        &self.supported_types
237    }
238
239    /// Returns the list of storage entries of the component.
240    pub fn storage_entries(&self) -> &Vec<StorageEntry> {
241        &self.storage
242    }
243
244    /// Validate the [AccountComponentMetadata].
245    ///
246    /// # Errors
247    ///
248    /// - If the specified storage entries contain duplicate names.
249    /// - If the template contains duplicate placeholder names.
250    /// - If the slot numbers do not start at zero.
251    /// - If the slots are not contiguous.
252    fn validate(&self) -> Result<(), AccountComponentTemplateError> {
253        let mut all_slots: Vec<u8> =
254            self.storage.iter().flat_map(|entry| entry.slot_indices()).collect();
255
256        // Check that slots start at 0 and are contiguous
257        all_slots.sort_unstable();
258        if let Some(&first_slot) = all_slots.first() {
259            if first_slot != 0 {
260                return Err(AccountComponentTemplateError::StorageSlotsDoNotStartAtZero(
261                    first_slot,
262                ));
263            }
264        }
265
266        for slots in all_slots.windows(2) {
267            if slots[1] == slots[0] {
268                return Err(AccountComponentTemplateError::DuplicateSlot(slots[0]));
269            }
270
271            if slots[1] != slots[0] + 1 {
272                return Err(AccountComponentTemplateError::NonContiguousSlots(slots[0], slots[1]));
273            }
274        }
275
276        // Check for duplicate storage entry names
277        let mut seen_names = BTreeSet::new();
278        for entry in self.storage_entries() {
279            entry.validate()?;
280            if let Some(name) = entry.name() {
281                let name_existed = !seen_names.insert(name.as_str());
282                if name_existed {
283                    return Err(AccountComponentTemplateError::DuplicateEntryNames(name.clone()));
284                }
285            }
286        }
287
288        // Check for duplicate storage placeholder names
289        let mut seen_placeholder_names = BTreeSet::new();
290        for entry in self.storage_entries() {
291            for (name, _) in entry.template_requirements() {
292                if !seen_placeholder_names.insert(name.clone()) {
293                    return Err(AccountComponentTemplateError::DuplicatePlaceholderName(name));
294                }
295            }
296        }
297
298        Ok(())
299    }
300}
301
302// SERIALIZATION
303// ================================================================================================
304
305impl Serializable for AccountComponentMetadata {
306    fn write_into<W: ByteWriter>(&self, target: &mut W) {
307        self.name.write_into(target);
308        self.description.write_into(target);
309        self.version.to_string().write_into(target);
310        self.supported_types.write_into(target);
311        self.storage.write_into(target);
312    }
313}
314
315impl Deserializable for AccountComponentMetadata {
316    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
317        Ok(Self {
318            name: String::read_from(source)?,
319            description: String::read_from(source)?,
320            version: semver::Version::from_str(&String::read_from(source)?).map_err(
321                |err: semver::Error| DeserializationError::InvalidValue(err.to_string()),
322            )?,
323            supported_types: BTreeSet::<AccountType>::read_from(source)?,
324            storage: Vec::<StorageEntry>::read_from(source)?,
325        })
326    }
327}
328
329// TESTS
330// ================================================================================================
331
332#[cfg(test)]
333mod tests {
334    use std::{collections::BTreeSet, string::ToString};
335
336    use assembly::Assembler;
337    use assert_matches::assert_matches;
338    use semver::Version;
339    use vm_core::{
340        Felt, FieldElement,
341        utils::{Deserializable, Serializable},
342    };
343
344    use super::FeltRepresentation;
345    use crate::{
346        AccountError,
347        account::{
348            AccountComponent, StorageValueName,
349            component::{
350                FieldIdentifier,
351                template::{
352                    AccountComponentMetadata, AccountComponentTemplate, InitStorageData,
353                    storage::StorageEntry,
354                },
355            },
356        },
357        errors::AccountComponentTemplateError,
358        testing::account_code::CODE,
359    };
360
361    fn default_felt_array() -> [FeltRepresentation; 4] {
362        [
363            FeltRepresentation::from(Felt::ZERO),
364            FeltRepresentation::from(Felt::ZERO),
365            FeltRepresentation::from(Felt::ZERO),
366            FeltRepresentation::from(Felt::ZERO),
367        ]
368    }
369
370    #[test]
371    fn contiguous_value_slots() {
372        let storage = vec![
373            StorageEntry::new_value(0, default_felt_array()),
374            StorageEntry::new_multislot(
375                FieldIdentifier::with_name(StorageValueName::new("slot1").unwrap()),
376                1..3,
377                vec![default_felt_array(), default_felt_array()],
378            ),
379        ];
380
381        let original_config = AccountComponentMetadata {
382            name: "test".into(),
383            description: "desc".into(),
384            version: Version::parse("0.1.0").unwrap(),
385            supported_types: BTreeSet::new(),
386            storage,
387        };
388
389        let serialized = original_config.as_toml().unwrap();
390        let deserialized = AccountComponentMetadata::from_toml(&serialized).unwrap();
391        assert_eq!(deserialized, original_config);
392    }
393
394    #[test]
395    fn new_non_contiguous_value_slots() {
396        let storage = vec![
397            StorageEntry::new_value(0, default_felt_array()),
398            StorageEntry::new_value(2, default_felt_array()),
399        ];
400
401        let result = AccountComponentMetadata::new(
402            "test".into(),
403            "desc".into(),
404            Version::parse("0.1.0").unwrap(),
405            BTreeSet::new(),
406            storage,
407        );
408        assert_matches!(result, Err(AccountComponentTemplateError::NonContiguousSlots(0, 2)));
409    }
410
411    #[test]
412    fn binary_serde_roundtrip() {
413        let storage = vec![
414            StorageEntry::new_multislot(
415                FieldIdentifier::with_name(StorageValueName::new("slot1").unwrap()),
416                1..3,
417                vec![default_felt_array(), default_felt_array()],
418            ),
419            StorageEntry::new_value(0, default_felt_array()),
420        ];
421
422        let component_metadata = AccountComponentMetadata {
423            name: "test".into(),
424            description: "desc".into(),
425            version: Version::parse("0.1.0").unwrap(),
426            supported_types: BTreeSet::new(),
427            storage,
428        };
429
430        let library = Assembler::default().assemble_library([CODE]).unwrap();
431        let template = AccountComponentTemplate::new(component_metadata, library);
432        let _ = AccountComponent::from_template(&template, &InitStorageData::default()).unwrap();
433
434        let serialized = template.to_bytes();
435        let deserialized = AccountComponentTemplate::read_from_bytes(&serialized).unwrap();
436
437        assert_eq!(deserialized, template);
438    }
439
440    #[test]
441    pub fn fail_on_duplicate_key() {
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"]
447
448            [[storage]]
449            name = "map"
450            description = "A storage map entry"
451            slot = 0
452            values = [
453                { key = "0x1", value = ["0x3", "0x1", "0x2", "0x3"] },
454                { key = "0x1", value = ["0x1", "0x2", "0x3", "0x10"] }
455            ]
456        "#;
457
458        let result = AccountComponentMetadata::from_toml(toml_text);
459        assert_matches!(result, Err(AccountComponentTemplateError::StorageMapHasDuplicateKeys(_)));
460    }
461
462    #[test]
463    pub fn fail_on_duplicate_placeholder_name() {
464        let toml_text = r#"
465            name = "Test Component"
466            description = "tests for two duplicate placeholders"
467            version = "1.0.1"
468            supported-types = ["FungibleFaucet"]
469
470            [[storage]]
471            name = "map"
472            slot = 0
473            values = [
474                { key = "0x1", value = [{type = "felt", name = "test"}, "0x1", "0x2", "0x3"] },
475                { key = "0x2", value = ["0x1", "0x2", "0x3", {type = "token_symbol", name = "test"}] }
476            ]
477        "#;
478
479        let result = AccountComponentMetadata::from_toml(toml_text).unwrap_err();
480        assert_matches::assert_matches!(
481            result,
482            AccountComponentTemplateError::DuplicatePlaceholderName(_)
483        );
484    }
485
486    #[test]
487    pub fn fail_duplicate_key_instance() {
488        let toml_text = r#"
489            name = "Test Component"
490            description = "This is a test component"
491            version = "1.0.1"
492            supported-types = ["FungibleFaucet"]
493
494            [[storage]]
495            name = "map"
496            description = "A storage map entry"
497            slot = 0
498            values = [
499                { key = ["0", "0", "0", "1"], value = ["0x9", "0x12", "0x31", "0x18"] },
500                { key = { name="duplicate_key" }, value = ["0x1", "0x2", "0x3", "0x4"] }
501            ]
502        "#;
503
504        let metadata = AccountComponentMetadata::from_toml(toml_text).unwrap();
505        let library = Assembler::default().assemble_library([CODE]).unwrap();
506        let template = AccountComponentTemplate::new(metadata, library);
507
508        // Fail to instantiate on a duplicate key
509
510        let init_storage_data = InitStorageData::new([(
511            StorageValueName::new("map.duplicate_key").unwrap(),
512            "0x0000000000000000000000000000000000000000000000000100000000000000".to_string(),
513        )]);
514        let account_component = AccountComponent::from_template(&template, &init_storage_data);
515        assert_matches!(
516            account_component,
517            Err(AccountError::AccountComponentTemplateInstantiationError(
518                AccountComponentTemplateError::StorageMapHasDuplicateKeys(_)
519            ))
520        );
521
522        // Successfully instantiate a map (keys are not duplicate)
523        let valid_init_storage_data = InitStorageData::new([(
524            StorageValueName::new("map.duplicate_key").unwrap(),
525            "0x30".to_string(),
526        )]);
527        AccountComponent::from_template(&template, &valid_init_storage_data).unwrap();
528    }
529}