Skip to main content

miden_protocol/account/component/metadata/
mod.rs

1use alloc::collections::{BTreeMap, BTreeSet};
2use alloc::string::{String, ToString};
3use core::str::FromStr;
4
5use miden_core::utils::{ByteReader, ByteWriter, Deserializable, Serializable};
6use miden_mast_package::{Package, SectionId};
7use miden_processor::DeserializationError;
8use semver::Version;
9
10use super::{AccountType, SchemaRequirement, StorageSchema, StorageValueName};
11use crate::errors::AccountError;
12
13// ACCOUNT COMPONENT METADATA
14// ================================================================================================
15
16/// Represents the full component metadata configuration.
17///
18/// An account component metadata describes the component alongside its storage layout.
19/// The storage layout can declare typed values which must be provided at instantiation time via
20/// [InitStorageData](`super::storage::InitStorageData`). These can appear either at the slot level
21/// (a singular word slot) or inside composed words as typed fields.
22///
23/// When the `std` feature is enabled, this struct allows for serialization and deserialization to
24/// and from a TOML file.
25///
26/// # Guarantees
27///
28/// - The metadata's storage schema does not contain duplicate slot names.
29/// - The schema cannot contain protocol-reserved slot names.
30/// - Each init-time value name uniquely identifies a single value. The expected init-time metadata
31///   can be retrieved with [AccountComponentMetadata::schema_requirements()], which returns a map
32///   from keys to [SchemaRequirement] (which indicates the expected value type and optional
33///   defaults).
34///
35/// # Example
36///
37/// ```
38/// use std::collections::{BTreeMap, BTreeSet};
39///
40/// use miden_protocol::account::StorageSlotName;
41/// use miden_protocol::account::component::{
42///     AccountComponentMetadata,
43///     FeltSchema,
44///     InitStorageData,
45///     SchemaTypeId,
46///     StorageSchema,
47///     StorageSlotSchema,
48///     StorageValueName,
49///     ValueSlotSchema,
50///     WordSchema,
51///     WordValue,
52/// };
53/// use semver::Version;
54///
55/// let slot_name = StorageSlotName::new("demo::test_value")?;
56///
57/// let word = WordSchema::new_value([
58///     FeltSchema::new_void(),
59///     FeltSchema::new_void(),
60///     FeltSchema::new_void(),
61///     FeltSchema::new_typed(SchemaTypeId::native_felt(), "foo"),
62/// ]);
63///
64/// let storage_schema = StorageSchema::new([(
65///     slot_name.clone(),
66///     StorageSlotSchema::Value(ValueSlotSchema::new(Some("demo slot".into()), word)),
67/// )])?;
68///
69/// let metadata = AccountComponentMetadata::new(
70///     "test name".into(),
71///     "description of the component".into(),
72///     Version::parse("0.1.0")?,
73///     BTreeSet::new(),
74///     storage_schema,
75/// );
76///
77/// // Init value keys are derived from slot name: `demo::test_value.foo`.
78/// let value_name = StorageValueName::from_slot_name_with_suffix(&slot_name, "foo")?;
79/// let mut init_storage_data = InitStorageData::default();
80/// init_storage_data.set_value(value_name, WordValue::Atomic("300".into()))?;
81///
82/// let storage_slots = metadata.storage_schema().build_storage_slots(&init_storage_data)?;
83/// assert_eq!(storage_slots.len(), 1);
84/// # Ok::<(), Box<dyn std::error::Error>>(())
85/// ```
86#[derive(Debug, Clone, PartialEq, Eq)]
87#[cfg_attr(feature = "std", derive(serde::Deserialize, serde::Serialize))]
88#[cfg_attr(feature = "std", serde(rename_all = "kebab-case"))]
89pub struct AccountComponentMetadata {
90    /// The human-readable name of the component.
91    name: String,
92
93    /// A brief description of what this component is and how it works.
94    description: String,
95
96    /// The version of the component using semantic versioning.
97    /// This can be used to track and manage component upgrades.
98    version: Version,
99
100    /// A set of supported target account types for this component.
101    supported_types: BTreeSet<AccountType>,
102
103    /// Storage schema defining the component's storage layout, defaults, and init-supplied values.
104    #[cfg_attr(feature = "std", serde(rename = "storage"))]
105    storage_schema: StorageSchema,
106}
107
108impl AccountComponentMetadata {
109    /// Create a new [AccountComponentMetadata].
110    pub fn new(
111        name: String,
112        description: String,
113        version: Version,
114        targets: BTreeSet<AccountType>,
115        storage_schema: StorageSchema,
116    ) -> Self {
117        Self {
118            name,
119            description,
120            version,
121            supported_types: targets,
122            storage_schema,
123        }
124    }
125
126    /// Returns the init-time values requirements for this schema.
127    ///
128    /// These values are used for initializing storage slot values or storage map entries. For a
129    /// full example, refer to the docs for [AccountComponentMetadata].
130    ///
131    /// Types for returned init values are inferred based on their location in the storage layout.
132    pub fn schema_requirements(&self) -> BTreeMap<StorageValueName, SchemaRequirement> {
133        self.storage_schema.schema_requirements().expect("storage schema is validated")
134    }
135
136    /// Returns the name of the account component.
137    pub fn name(&self) -> &str {
138        &self.name
139    }
140
141    /// Returns the description of the account component.
142    pub fn description(&self) -> &str {
143        &self.description
144    }
145
146    /// Returns the semantic version of the account component.
147    pub fn version(&self) -> &Version {
148        &self.version
149    }
150
151    /// Returns the account types supported by the component.
152    pub fn supported_types(&self) -> &BTreeSet<AccountType> {
153        &self.supported_types
154    }
155
156    /// Returns the storage schema of the component.
157    pub fn storage_schema(&self) -> &StorageSchema {
158        &self.storage_schema
159    }
160}
161
162impl TryFrom<&Package> for AccountComponentMetadata {
163    type Error = AccountError;
164
165    fn try_from(package: &Package) -> Result<Self, Self::Error> {
166        package
167            .sections
168            .iter()
169            .find_map(|section| {
170                (section.id == SectionId::ACCOUNT_COMPONENT_METADATA).then(|| {
171                    AccountComponentMetadata::read_from_bytes(&section.data).map_err(|err| {
172                        AccountError::other_with_source(
173                            "failed to deserialize account component metadata",
174                            err,
175                        )
176                    })
177                })
178            })
179            .transpose()?
180            .ok_or_else(|| {
181                AccountError::other(
182                    "package does not contain account component metadata section - packages without explicit metadata may be intended for other purposes (e.g., note scripts, transaction scripts)",
183                )
184            })
185    }
186}
187
188// SERIALIZATION
189// ================================================================================================
190
191impl Serializable for AccountComponentMetadata {
192    fn write_into<W: ByteWriter>(&self, target: &mut W) {
193        self.name.write_into(target);
194        self.description.write_into(target);
195        self.version.to_string().write_into(target);
196        self.supported_types.write_into(target);
197        self.storage_schema.write_into(target);
198    }
199}
200
201impl Deserializable for AccountComponentMetadata {
202    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
203        let name = String::read_from(source)?;
204        let description = String::read_from(source)?;
205        if !description.is_ascii() {
206            return Err(DeserializationError::InvalidValue(
207                "description must contain only ASCII characters".to_string(),
208            ));
209        }
210        let version = semver::Version::from_str(&String::read_from(source)?)
211            .map_err(|err: semver::Error| DeserializationError::InvalidValue(err.to_string()))?;
212        let supported_types = BTreeSet::<AccountType>::read_from(source)?;
213        let storage_schema = StorageSchema::read_from(source)?;
214
215        Ok(Self {
216            name,
217            description,
218            version,
219            supported_types,
220            storage_schema,
221        })
222    }
223}