miden_objects/account/component/template/
mod.rs

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