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