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/// - Each init-time value name uniquely identifies a single value. The expected init-time metadata
30/// can be retrieved with [AccountComponentMetadata::schema_requirements()], which returns a map
31/// from keys to [SchemaRequirement] (which indicates the expected value type and optional
32/// defaults).
33///
34/// # Example
35///
36/// ```
37/// use std::collections::BTreeMap;
38///
39/// use miden_protocol::account::StorageSlotName;
40/// use miden_protocol::account::component::{
41/// AccountComponentMetadata,
42/// FeltSchema,
43/// InitStorageData,
44/// SchemaType,
45/// StorageSchema,
46/// StorageSlotSchema,
47/// StorageValueName,
48/// ValueSlotSchema,
49/// WordSchema,
50/// WordValue,
51/// };
52///
53/// let slot_name = StorageSlotName::new("demo::test_value")?;
54///
55/// let word = WordSchema::new_value([
56/// FeltSchema::new_void(),
57/// FeltSchema::new_void(),
58/// FeltSchema::new_void(),
59/// FeltSchema::felt("foo"),
60/// ]);
61///
62/// let storage_schema = StorageSchema::new([(
63/// slot_name.clone(),
64/// StorageSlotSchema::Value(ValueSlotSchema::new(Some("demo slot".into()), word)),
65/// )])?;
66///
67/// let metadata = AccountComponentMetadata::new("test name")
68/// .with_description("description of the component")
69/// .with_storage_schema(storage_schema);
70///
71/// // Init value keys are derived from slot name: `demo::test_value.foo`.
72/// let value_name = StorageValueName::from_slot_name_with_suffix(&slot_name, "foo")?;
73/// let mut init_storage_data = InitStorageData::default();
74/// init_storage_data.set_value(value_name, WordValue::Atomic("300".into()))?;
75///
76/// let storage_slots = metadata.storage_schema().build_storage_slots(&init_storage_data)?;
77/// assert_eq!(storage_slots.len(), 1);
78/// # Ok::<(), Box<dyn std::error::Error>>(())
79/// ```
80#[derive(Debug, Clone, PartialEq, Eq)]
81#[cfg_attr(feature = "std", derive(serde::Deserialize, serde::Serialize))]
82#[cfg_attr(feature = "std", serde(rename_all = "kebab-case"))]
83pub struct AccountComponentMetadata {
84 /// The human-readable name of the component.
85 name: String,
86
87 /// A brief description of what this component is and how it works.
88 description: String,
89
90 /// The version of the component using semantic versioning.
91 /// This can be used to track and manage component upgrades.
92 version: Version,
93
94 /// A set of supported target account types for this component.
95 supported_types: BTreeSet<AccountType>,
96
97 /// Storage schema defining the component's storage layout, defaults, and init-supplied values.
98 #[cfg_attr(feature = "std", serde(rename = "storage"))]
99 storage_schema: StorageSchema,
100}
101
102impl AccountComponentMetadata {
103 /// Create a new [AccountComponentMetadata] with the given name.
104 ///
105 /// Other fields are initialized to sensible defaults:
106 /// - `description`: empty string
107 /// - `version`: 1.0.0
108 /// - `supported_types`: empty set
109 /// - `storage_schema`: default (empty)
110 ///
111 /// Use the `with_*` mutator methods to customize these fields.
112 pub fn new(name: impl Into<String>) -> Self {
113 Self {
114 name: name.into(),
115 description: String::new(),
116 version: Version::new(1, 0, 0),
117 supported_types: BTreeSet::new(),
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 /// Adds a supported account type to the component.
135 pub fn with_supported_type(mut self, account_type: AccountType) -> Self {
136 self.supported_types.insert(account_type);
137 self
138 }
139
140 /// Sets the supported account types of the component.
141 pub fn with_supported_types(mut self, supported_types: BTreeSet<AccountType>) -> Self {
142 self.supported_types = supported_types;
143 self
144 }
145
146 /// Sets the component to support all account types.
147 pub fn with_supports_all_types(mut self) -> Self {
148 self.supported_types.extend([
149 AccountType::FungibleFaucet,
150 AccountType::NonFungibleFaucet,
151 AccountType::RegularAccountImmutableCode,
152 AccountType::RegularAccountUpdatableCode,
153 ]);
154 self
155 }
156
157 /// Sets the component to support regular account types (immutable and updatable code).
158 pub fn with_supports_regular_types(mut self) -> Self {
159 self.supported_types.extend([
160 AccountType::RegularAccountImmutableCode,
161 AccountType::RegularAccountUpdatableCode,
162 ]);
163 self
164 }
165
166 /// Sets the storage schema of the component.
167 pub fn with_storage_schema(mut self, schema: StorageSchema) -> Self {
168 self.storage_schema = schema;
169 self
170 }
171
172 /// Returns the init-time values requirements for this schema.
173 ///
174 /// These values are used for initializing storage slot values or storage map entries. For a
175 /// full example, refer to the docs for [AccountComponentMetadata].
176 ///
177 /// Types for returned init values are inferred based on their location in the storage layout.
178 pub fn schema_requirements(&self) -> BTreeMap<StorageValueName, SchemaRequirement> {
179 self.storage_schema.schema_requirements().expect("storage schema is validated")
180 }
181
182 /// Returns the name of the account component.
183 pub fn name(&self) -> &str {
184 &self.name
185 }
186
187 /// Returns the description of the account component.
188 pub fn description(&self) -> &str {
189 &self.description
190 }
191
192 /// Returns the semantic version of the account component.
193 pub fn version(&self) -> &Version {
194 &self.version
195 }
196
197 /// Returns the account types supported by the component.
198 pub fn supported_types(&self) -> &BTreeSet<AccountType> {
199 &self.supported_types
200 }
201
202 /// Returns the storage schema of the component.
203 pub fn storage_schema(&self) -> &StorageSchema {
204 &self.storage_schema
205 }
206}
207
208impl TryFrom<&Package> for AccountComponentMetadata {
209 type Error = AccountError;
210
211 fn try_from(package: &Package) -> Result<Self, Self::Error> {
212 package
213 .sections
214 .iter()
215 .find_map(|section| {
216 (section.id == SectionId::ACCOUNT_COMPONENT_METADATA).then(|| {
217 AccountComponentMetadata::read_from_bytes(§ion.data).map_err(|err| {
218 AccountError::other_with_source(
219 "failed to deserialize account component metadata",
220 err,
221 )
222 })
223 })
224 })
225 .transpose()?
226 .ok_or_else(|| {
227 AccountError::other(
228 "package does not contain account component metadata section - packages without explicit metadata may be intended for other purposes (e.g., note scripts, transaction scripts)",
229 )
230 })
231 }
232}
233
234// SERIALIZATION
235// ================================================================================================
236
237impl Serializable for AccountComponentMetadata {
238 fn write_into<W: ByteWriter>(&self, target: &mut W) {
239 self.name.write_into(target);
240 self.description.write_into(target);
241 self.version.to_string().write_into(target);
242 self.supported_types.write_into(target);
243 self.storage_schema.write_into(target);
244 }
245}
246
247impl Deserializable for AccountComponentMetadata {
248 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
249 let name = String::read_from(source)?;
250 let description = String::read_from(source)?;
251 if !description.is_ascii() {
252 return Err(DeserializationError::InvalidValue(
253 "description must contain only ASCII characters".to_string(),
254 ));
255 }
256 let version = semver::Version::from_str(&String::read_from(source)?)
257 .map_err(|err: semver::Error| DeserializationError::InvalidValue(err.to_string()))?;
258 let supported_types = BTreeSet::<AccountType>::read_from(source)?;
259 let storage_schema = StorageSchema::read_from(source)?;
260
261 Ok(Self {
262 name,
263 description,
264 version,
265 supported_types,
266 storage_schema,
267 })
268 }
269}