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::{BTreeMap, 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 = InitStorageData::new(
130///     [(StorageValueName::new("test_value.foo")?, "300".to_string())],
131///     BTreeMap::new(),
132/// );
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            && first_slot != 0
260        {
261            return Err(AccountComponentTemplateError::StorageSlotsDoNotStartAtZero(first_slot));
262        }
263
264        for slots in all_slots.windows(2) {
265            if slots[1] == slots[0] {
266                return Err(AccountComponentTemplateError::DuplicateSlot(slots[0]));
267            }
268
269            if slots[1] != slots[0] + 1 {
270                return Err(AccountComponentTemplateError::NonContiguousSlots(slots[0], slots[1]));
271            }
272        }
273
274        // Check for duplicate storage entry names
275        let mut seen_names = BTreeSet::new();
276        for entry in self.storage_entries() {
277            entry.validate()?;
278            if let Some(name) = entry.name() {
279                let name_existed = !seen_names.insert(name.as_str());
280                if name_existed {
281                    return Err(AccountComponentTemplateError::DuplicateEntryNames(name.clone()));
282                }
283            }
284        }
285
286        // Check for duplicate storage placeholder names
287        let mut seen_placeholder_names = BTreeSet::new();
288        for entry in self.storage_entries() {
289            for (name, _) in entry.template_requirements() {
290                if !seen_placeholder_names.insert(name.clone()) {
291                    return Err(AccountComponentTemplateError::DuplicatePlaceholderName(name));
292                }
293            }
294        }
295
296        Ok(())
297    }
298}
299
300// SERIALIZATION
301// ================================================================================================
302
303impl Serializable for AccountComponentMetadata {
304    fn write_into<W: ByteWriter>(&self, target: &mut W) {
305        self.name.write_into(target);
306        self.description.write_into(target);
307        self.version.to_string().write_into(target);
308        self.supported_types.write_into(target);
309        self.storage.write_into(target);
310    }
311}
312
313impl Deserializable for AccountComponentMetadata {
314    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
315        Ok(Self {
316            name: String::read_from(source)?,
317            description: String::read_from(source)?,
318            version: semver::Version::from_str(&String::read_from(source)?).map_err(
319                |err: semver::Error| DeserializationError::InvalidValue(err.to_string()),
320            )?,
321            supported_types: BTreeSet::<AccountType>::read_from(source)?,
322            storage: Vec::<StorageEntry>::read_from(source)?,
323        })
324    }
325}
326
327// TESTS
328// ================================================================================================
329
330#[cfg(test)]
331mod tests {
332    use std::collections::{BTreeMap, BTreeSet};
333    use std::string::ToString;
334
335    use assert_matches::assert_matches;
336    use miden_assembly::Assembler;
337    use miden_core::utils::{Deserializable, Serializable};
338    use miden_core::{Felt, FieldElement};
339    use semver::Version;
340
341    use super::FeltRepresentation;
342    use crate::AccountError;
343    use crate::account::component::FieldIdentifier;
344    use crate::account::component::template::storage::StorageEntry;
345    use crate::account::component::template::{
346        AccountComponentMetadata,
347        AccountComponentTemplate,
348        InitStorageData,
349    };
350    use crate::account::{AccountComponent, StorageValueName};
351    use crate::errors::AccountComponentTemplateError;
352    use crate::testing::account_code::CODE;
353
354    fn default_felt_array() -> [FeltRepresentation; 4] {
355        [
356            FeltRepresentation::from(Felt::ZERO),
357            FeltRepresentation::from(Felt::ZERO),
358            FeltRepresentation::from(Felt::ZERO),
359            FeltRepresentation::from(Felt::ZERO),
360        ]
361    }
362
363    #[test]
364    fn contiguous_value_slots() {
365        let storage = vec![
366            StorageEntry::new_value(0, default_felt_array()),
367            StorageEntry::new_multislot(
368                FieldIdentifier::with_name(StorageValueName::new("slot1").unwrap()),
369                1..3,
370                vec![default_felt_array(), default_felt_array()],
371            ),
372        ];
373
374        let original_config = AccountComponentMetadata {
375            name: "test".into(),
376            description: "desc".into(),
377            version: Version::parse("0.1.0").unwrap(),
378            supported_types: BTreeSet::new(),
379            storage,
380        };
381
382        let serialized = original_config.to_toml().unwrap();
383        let deserialized = AccountComponentMetadata::from_toml(&serialized).unwrap();
384        assert_eq!(deserialized, original_config);
385    }
386
387    #[test]
388    fn new_non_contiguous_value_slots() {
389        let storage = vec![
390            StorageEntry::new_value(0, default_felt_array()),
391            StorageEntry::new_value(2, default_felt_array()),
392        ];
393
394        let result = AccountComponentMetadata::new(
395            "test".into(),
396            "desc".into(),
397            Version::parse("0.1.0").unwrap(),
398            BTreeSet::new(),
399            storage,
400        );
401        assert_matches!(result, Err(AccountComponentTemplateError::NonContiguousSlots(0, 2)));
402    }
403
404    #[test]
405    fn binary_serde_roundtrip() {
406        let storage = vec![
407            StorageEntry::new_multislot(
408                FieldIdentifier::with_name(StorageValueName::new("slot1").unwrap()),
409                1..3,
410                vec![default_felt_array(), default_felt_array()],
411            ),
412            StorageEntry::new_value(0, default_felt_array()),
413        ];
414
415        let component_metadata = AccountComponentMetadata {
416            name: "test".into(),
417            description: "desc".into(),
418            version: Version::parse("0.1.0").unwrap(),
419            supported_types: BTreeSet::new(),
420            storage,
421        };
422
423        let library = Assembler::default().assemble_library([CODE]).unwrap();
424        let template = AccountComponentTemplate::new(component_metadata, library);
425        let _ = AccountComponent::from_template(&template, &InitStorageData::default()).unwrap();
426
427        let serialized = template.to_bytes();
428        let deserialized = AccountComponentTemplate::read_from_bytes(&serialized).unwrap();
429
430        assert_eq!(deserialized, template);
431    }
432
433    #[test]
434    pub fn fail_on_duplicate_key() {
435        let toml_text = r#"
436            name = "Test Component"
437            description = "This is a test component"
438            version = "1.0.1"
439            supported-types = ["FungibleFaucet"]
440
441            [[storage]]
442            name = "map"
443            description = "A storage map entry"
444            slot = 0
445            values = [
446                { key = "0x1", value = ["0x3", "0x1", "0x2", "0x3"] },
447                { key = "0x1", value = ["0x1", "0x2", "0x3", "0x10"] }
448            ]
449        "#;
450
451        let result = AccountComponentMetadata::from_toml(toml_text);
452        assert_matches!(result, Err(AccountComponentTemplateError::StorageMapHasDuplicateKeys(_)));
453    }
454
455    #[test]
456    pub fn fail_on_duplicate_placeholder_name() {
457        let toml_text = r#"
458            name = "Test Component"
459            description = "tests for two duplicate placeholders"
460            version = "1.0.1"
461            supported-types = ["FungibleFaucet"]
462
463            [[storage]]
464            name = "map"
465            slot = 0
466            values = [
467                { key = "0x1", value = [{type = "felt", name = "test"}, "0x1", "0x2", "0x3"] },
468                { key = "0x2", value = ["0x1", "0x2", "0x3", {type = "token_symbol", name = "test"}] }
469            ]
470        "#;
471
472        let result = AccountComponentMetadata::from_toml(toml_text).unwrap_err();
473        assert_matches::assert_matches!(
474            result,
475            AccountComponentTemplateError::DuplicatePlaceholderName(_)
476        );
477    }
478
479    #[test]
480    pub fn fail_duplicate_key_instance() {
481        let _ = color_eyre::install();
482
483        let toml_text = r#"
484            name = "Test Component"
485            description = "This is a test component"
486            version = "1.0.1"
487            supported-types = ["FungibleFaucet"]
488
489            [[storage]]
490            name = "map"
491            description = "A storage map entry"
492            slot = 0
493            values = [
494                { key = ["0", "0", "0", "1"], value = ["0x9", "0x12", "0x31", "0x18"] },
495                { key = { name="duplicate_key" }, value = ["0x1", "0x2", "0x3", "0x4"] }
496            ]
497        "#;
498
499        let metadata = AccountComponentMetadata::from_toml(toml_text).unwrap();
500        let library = Assembler::default().assemble_library([CODE]).unwrap();
501        let template = AccountComponentTemplate::new(metadata, library);
502
503        // Fail to instantiate on a duplicate key
504
505        let init_storage_data = InitStorageData::new(
506            [(
507                StorageValueName::new("map.duplicate_key").unwrap(),
508                "0x0000000000000000000000000000000000000000000000000100000000000000".to_string(),
509            )],
510            BTreeMap::new(),
511        );
512        let account_component = AccountComponent::from_template(&template, &init_storage_data);
513        assert_matches!(
514            account_component,
515            Err(AccountError::AccountComponentTemplateInstantiationError(
516                AccountComponentTemplateError::StorageMapHasDuplicateKeys(_)
517            ))
518        );
519
520        // Successfully instantiate a map (keys are not duplicate)
521        let valid_init_storage_data = InitStorageData::new(
522            [(StorageValueName::new("map.duplicate_key").unwrap(), "0x30".to_string())],
523            BTreeMap::new(),
524        );
525        AccountComponent::from_template(&template, &valid_init_storage_data).unwrap();
526    }
527}