Skip to main content

miden_protocol/account/component/metadata/
mod.rs

1use alloc::collections::BTreeMap;
2use alloc::string::{String, ToString};
3use core::str::FromStr;
4
5use miden_mast_package::{Package, SectionId};
6use semver::Version;
7
8use super::{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::StorageSlotName;
45/// use miden_protocol::account::component::{
46///     AccountComponentMetadata,
47///     FeltSchema,
48///     InitStorageData,
49///     SchemaType,
50///     StorageSchema,
51///     StorageSlotSchema,
52///     StorageValueName,
53///     ValueSlotSchema,
54///     WordSchema,
55///     WordValue,
56/// };
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")
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    /// Storage schema defining the component's storage layout, defaults, and init-supplied values.
100    #[cfg_attr(feature = "std", serde(rename = "storage"))]
101    storage_schema: StorageSchema,
102}
103
104impl AccountComponentMetadata {
105    /// Create a new [AccountComponentMetadata] with the given name.
106    ///
107    /// Other fields are initialized to sensible defaults:
108    /// - `description`: empty string
109    /// - `version`: 1.0.0
110    /// - `storage_schema`: default (empty)
111    ///
112    /// Use the `with_*` mutator methods to customize these fields.
113    pub fn new(name: impl Into<String>) -> Self {
114        Self {
115            name: name.into(),
116            description: String::new(),
117            version: Version::new(1, 0, 0),
118            storage_schema: StorageSchema::default(),
119        }
120    }
121
122    /// Sets the description of the component.
123    pub fn with_description(mut self, description: impl Into<String>) -> Self {
124        self.description = description.into();
125        self
126    }
127
128    /// Sets the version of the component.
129    pub fn with_version(mut self, version: Version) -> Self {
130        self.version = version;
131        self
132    }
133
134    /// Sets the storage schema of the component.
135    pub fn with_storage_schema(mut self, schema: StorageSchema) -> Self {
136        self.storage_schema = schema;
137        self
138    }
139
140    /// Returns the init-time values requirements for this schema.
141    ///
142    /// These values are used for initializing storage slot values or storage map entries. For a
143    /// full example, refer to the docs for [AccountComponentMetadata].
144    ///
145    /// Types for returned init values are inferred based on their location in the storage layout.
146    pub fn schema_requirements(&self) -> BTreeMap<StorageValueName, SchemaRequirement> {
147        self.storage_schema.schema_requirements().expect("storage schema is validated")
148    }
149
150    /// Returns the name of the account component.
151    pub fn name(&self) -> &str {
152        &self.name
153    }
154
155    /// Returns the description of the account component.
156    pub fn description(&self) -> &str {
157        &self.description
158    }
159
160    /// Returns the semantic version of the account component.
161    pub fn version(&self) -> &Version {
162        &self.version
163    }
164
165    /// Returns the storage schema of the component.
166    pub fn storage_schema(&self) -> &StorageSchema {
167        &self.storage_schema
168    }
169}
170
171impl TryFrom<&Package> for AccountComponentMetadata {
172    type Error = AccountError;
173
174    fn try_from(package: &Package) -> Result<Self, Self::Error> {
175        package
176            .sections
177            .iter()
178            .find_map(|section| {
179                (section.id == SectionId::ACCOUNT_COMPONENT_METADATA).then(|| {
180                    AccountComponentMetadata::read_from_bytes(&section.data).map_err(|err| {
181                        AccountError::other_with_source(
182                            "failed to deserialize account component metadata",
183                            err,
184                        )
185                    })
186                })
187            })
188            .transpose()?
189            .ok_or_else(|| {
190                AccountError::other(
191                    "package does not contain account component metadata section - packages without explicit metadata may be intended for other purposes (e.g., note scripts, transaction scripts)",
192                )
193            })
194    }
195}
196
197// SERIALIZATION
198// ================================================================================================
199
200impl Serializable for AccountComponentMetadata {
201    fn write_into<W: ByteWriter>(&self, target: &mut W) {
202        self.name.write_into(target);
203        self.description.write_into(target);
204        self.version.to_string().write_into(target);
205        self.storage_schema.write_into(target);
206    }
207}
208
209impl Deserializable for AccountComponentMetadata {
210    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
211        let name = String::read_from(source)?;
212        let description = String::read_from(source)?;
213        if !description.is_ascii() {
214            return Err(DeserializationError::InvalidValue(
215                "description must contain only ASCII characters".to_string(),
216            ));
217        }
218        let version = semver::Version::from_str(&String::read_from(source)?)
219            .map_err(|err: semver::Error| DeserializationError::InvalidValue(err.to_string()))?;
220        let storage_schema = StorageSchema::read_from(source)?;
221
222        Ok(Self {
223            name,
224            description,
225            version,
226            storage_schema,
227        })
228    }
229}