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