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")?),
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::{
335        collections::BTreeSet,
336        string::{String, ToString},
337    };
338
339    use assembly::Assembler;
340    use assert_matches::assert_matches;
341    use semver::Version;
342    use vm_core::{
343        Felt, FieldElement,
344        utils::{Deserializable, Serializable},
345    };
346
347    use super::FeltRepresentation;
348    use crate::{
349        AccountError,
350        account::{
351            AccountComponent, StorageValueName,
352            component::template::{
353                AccountComponentMetadata, AccountComponentTemplate, InitStorageData,
354                storage::StorageEntry,
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                StorageValueName::new("slot1").unwrap(),
376                Some("multi-slot value of arity 2".into()),
377                1..3,
378                vec![default_felt_array(), default_felt_array()],
379            ),
380        ];
381
382        let original_config = AccountComponentMetadata {
383            name: "test".into(),
384            description: "desc".into(),
385            version: Version::parse("0.1.0").unwrap(),
386            supported_types: BTreeSet::new(),
387            storage,
388        };
389
390        let serialized = original_config.as_toml().unwrap();
391        let deserialized = AccountComponentMetadata::from_toml(&serialized).unwrap();
392        assert_eq!(deserialized, original_config);
393    }
394
395    #[test]
396    fn new_non_contiguous_value_slots() {
397        let storage = vec![
398            StorageEntry::new_value(0, default_felt_array()),
399            StorageEntry::new_value(2, default_felt_array()),
400        ];
401
402        let result = AccountComponentMetadata::new(
403            "test".into(),
404            "desc".into(),
405            Version::parse("0.1.0").unwrap(),
406            BTreeSet::new(),
407            storage,
408        );
409        assert_matches!(result, Err(AccountComponentTemplateError::NonContiguousSlots(0, 2)));
410    }
411
412    #[test]
413    fn binary_serde_roundtrip() {
414        let storage = vec![
415            StorageEntry::new_multislot(
416                StorageValueName::new("slot1").unwrap(),
417                Option::<String>::None,
418                1..3,
419                vec![default_felt_array(), default_felt_array()],
420            ),
421            StorageEntry::new_value(0, default_felt_array()),
422        ];
423
424        let component_metadata = AccountComponentMetadata {
425            name: "test".into(),
426            description: "desc".into(),
427            version: Version::parse("0.1.0").unwrap(),
428            supported_types: BTreeSet::new(),
429            storage,
430        };
431
432        let library = Assembler::default().assemble_library([CODE]).unwrap();
433        let template = AccountComponentTemplate::new(component_metadata, library);
434        let _ = AccountComponent::from_template(&template, &InitStorageData::default()).unwrap();
435
436        let serialized = template.to_bytes();
437        let deserialized = AccountComponentTemplate::read_from_bytes(&serialized).unwrap();
438
439        assert_eq!(deserialized, template);
440    }
441
442    #[test]
443    pub fn fail_on_duplicate_key() {
444        let toml_text = r#"
445            name = "Test Component"
446            description = "This is a test component"
447            version = "1.0.1"
448            supported-types = ["FungibleFaucet"]
449
450            [[storage]]
451            name = "map"
452            description = "A storage map entry"
453            slot = 0
454            values = [
455                { key = "0x1", value = ["0x3", "0x1", "0x2", "0x3"] },
456                { key = "0x1", value = ["0x1", "0x2", "0x3", "0x10"] }
457            ]
458        "#;
459
460        let result = AccountComponentMetadata::from_toml(toml_text);
461        assert_matches!(result, Err(AccountComponentTemplateError::StorageMapHasDuplicateKeys(_)));
462    }
463
464    #[test]
465    pub fn fail_on_duplicate_placeholder_name() {
466        let toml_text = r#"
467            name = "Test Component"
468            description = "tests for two duplicate placeholders"
469            version = "1.0.1"
470            supported-types = ["FungibleFaucet"]
471
472            [[storage]]
473            name = "map"
474            slot = 0
475            values = [
476                { key = "0x1", value = [{type = "felt", name = "test"}, "0x1", "0x2", "0x3"] },
477                { key = "0x2", value = ["0x1", "0x2", "0x3", {type = "token_symbol", name = "test"}] }
478            ]
479        "#;
480
481        let result = AccountComponentMetadata::from_toml(toml_text).unwrap_err();
482        assert_matches::assert_matches!(
483            result,
484            AccountComponentTemplateError::DuplicatePlaceholderName(_)
485        );
486    }
487
488    #[test]
489    pub fn fail_duplicate_key_instance() {
490        let toml_text = r#"
491            name = "Test Component"
492            description = "This is a test component"
493            version = "1.0.1"
494            supported-types = ["FungibleFaucet"]
495
496            [[storage]]
497            name = "map"
498            description = "A storage map entry"
499            slot = 0
500            values = [
501                { key = ["0", "0", "0", "1"], value = ["0x9", "0x12", "0x31", "0x18"] },
502                { key = { name="duplicate_key" }, value = ["0x1", "0x2", "0x3", "0x4"] }
503            ]
504        "#;
505
506        let metadata = AccountComponentMetadata::from_toml(toml_text).unwrap();
507        let library = Assembler::default().assemble_library([CODE]).unwrap();
508        let template = AccountComponentTemplate::new(metadata, library);
509
510        // Fail to instantiate on a duplicate key
511
512        let init_storage_data = InitStorageData::new([(
513            StorageValueName::new("map.duplicate_key").unwrap(),
514            "0x0000000000000000000000000000000000000000000000000100000000000000".to_string(),
515        )]);
516        let account_component = AccountComponent::from_template(&template, &init_storage_data);
517        assert_matches!(
518            account_component,
519            Err(AccountError::AccountComponentTemplateInstantiationError(
520                AccountComponentTemplateError::StorageMapHasDuplicateKeys(_)
521            ))
522        );
523
524        // Successfully instantiate a map (keys are not duplicate)
525        let valid_init_storage_data = InitStorageData::new([(
526            StorageValueName::new("map.duplicate_key").unwrap(),
527            "0x30".to_string(),
528        )]);
529        AccountComponent::from_template(&template, &valid_init_storage_data).unwrap();
530    }
531}