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(§ion.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}