Skip to main content

miden_protocol/account/component/
mod.rs

1use alloc::collections::BTreeSet;
2use alloc::vec::Vec;
3
4use miden_mast_package::Package;
5use miden_processor::mast::MastNodeExt;
6
7mod metadata;
8pub use metadata::*;
9
10pub mod storage;
11pub use storage::*;
12
13mod code;
14pub use code::AccountComponentCode;
15
16use crate::account::{AccountProcedureRoot, AccountType, StorageSlot};
17use crate::assembly::Path;
18use crate::errors::AccountError;
19use crate::{MastForest, Word};
20
21/// The attribute name used to mark the authentication procedure in an account component.
22const AUTH_SCRIPT_ATTRIBUTE: &str = "auth_script";
23
24// ACCOUNT COMPONENT
25// ================================================================================================
26
27/// An [`AccountComponent`] defines a [`Library`](miden_assembly::Library) of code and the initial
28/// value and types of the [`StorageSlot`]s it accesses.
29///
30/// One or more components can be used to built [`AccountCode`](crate::account::AccountCode) and
31/// [`AccountStorage`](crate::account::AccountStorage).
32///
33/// Each component is independent of other components and can only access its own storage slots.
34/// Each component defines its own storage layout starting at index 0 up to the length of the
35/// storage slots vector.
36///
37/// Components define the [`AccountType`]s they support, meaning whether the component can be used
38/// to instantiate an account of that type. For example, a component implementing a fungible faucet
39/// would only specify support for [`AccountType::FungibleFaucet`]. Using it to instantiate a
40/// regular account would fail. By default, the set of supported types is empty, so each component
41/// is forced to explicitly define what it supports.
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct AccountComponent {
44    pub(super) code: AccountComponentCode,
45    pub(super) storage_slots: Vec<StorageSlot>,
46    pub(super) metadata: AccountComponentMetadata,
47}
48
49impl AccountComponent {
50    // CONSTRUCTORS
51    // --------------------------------------------------------------------------------------------
52
53    /// Returns a new [`AccountComponent`] constructed from the provided `library`,
54    /// `storage_slots`, and `metadata`.
55    ///
56    /// All procedures exported from the provided code will become members of the account's public
57    /// interface when added to an [`AccountCode`](crate::account::AccountCode).
58    ///
59    /// # Errors
60    ///
61    /// The following list of errors is exhaustive and can be relied upon for `expect`ing the call
62    /// to this function. It is recommended that custom components ensure these conditions by design
63    /// or in their fallible constructors.
64    ///
65    /// Returns an error if:
66    /// - The number of given [`StorageSlot`]s exceeds 255.
67    pub fn new(
68        code: impl Into<AccountComponentCode>,
69        storage_slots: Vec<StorageSlot>,
70        metadata: AccountComponentMetadata,
71    ) -> Result<Self, AccountError> {
72        // Check that we have less than 256 storage slots.
73        u8::try_from(storage_slots.len())
74            .map_err(|_| AccountError::StorageTooManySlots(storage_slots.len() as u64))?;
75
76        Ok(Self {
77            code: code.into(),
78            storage_slots,
79            metadata,
80        })
81    }
82
83    /// Creates an [`AccountComponent`] from a [`Package`] using [`InitStorageData`].
84    ///
85    /// This method provides type safety by leveraging the component's metadata to validate
86    /// storage initialization data. The package must contain explicit account component metadata.
87    ///
88    /// # Arguments
89    ///
90    /// * `package` - The package containing the [`Library`](miden_assembly::Library) and account
91    ///   component metadata
92    /// * `init_storage_data` - The initialization data for storage slots
93    ///
94    /// # Errors
95    ///
96    /// Returns an error if:
97    /// - The package does not contain a library artifact
98    /// - The package does not contain account component metadata
99    /// - The metadata cannot be deserialized from the package
100    /// - The storage initialization fails due to invalid or missing data
101    /// - The component creation fails
102    pub fn from_package(
103        package: &Package,
104        init_storage_data: &InitStorageData,
105    ) -> Result<Self, AccountError> {
106        let metadata = AccountComponentMetadata::try_from(package)?;
107        let library = package.mast.as_ref().clone();
108
109        let component_code = AccountComponentCode::from(library);
110        Self::from_library(&component_code, &metadata, init_storage_data)
111    }
112
113    /// Creates an [`AccountComponent`] from an [`AccountComponentCode`] and
114    /// [`AccountComponentMetadata`].
115    ///
116    /// This method provides type safety by leveraging the component's metadata to validate
117    /// the passed storage initialization data ([`InitStorageData`]).
118    ///
119    /// # Arguments
120    ///
121    /// * `library` - The component's assembled code
122    /// * `metadata` - The component's metadata, which describes the storage layout
123    /// * `init_storage_data` - The initialization data for storage slots
124    ///
125    /// # Errors
126    ///
127    /// Returns an error if:
128    /// - The package does not contain a library artifact
129    /// - The package does not contain account component metadata
130    /// - The metadata cannot be deserialized from the package
131    /// - The storage initialization fails due to invalid or missing data
132    /// - The component creation fails
133    pub fn from_library(
134        library: &AccountComponentCode,
135        metadata: &AccountComponentMetadata,
136        init_storage_data: &InitStorageData,
137    ) -> Result<Self, AccountError> {
138        let storage_slots = metadata
139            .storage_schema()
140            .build_storage_slots(init_storage_data)
141            .map_err(|err| {
142                AccountError::other_with_source("failed to instantiate account component", err)
143            })?;
144
145        AccountComponent::new(library.clone(), storage_slots, metadata.clone())
146    }
147
148    // ACCESSORS
149    // --------------------------------------------------------------------------------------------
150
151    /// Returns the number of storage slots accessible from this component.
152    pub fn storage_size(&self) -> u8 {
153        u8::try_from(self.storage_slots.len())
154            .expect("storage slots len should fit in u8 per the constructor")
155    }
156
157    /// Returns a reference to the underlying [`AccountComponentCode`] of this component.
158    pub fn component_code(&self) -> &AccountComponentCode {
159        &self.code
160    }
161
162    /// Returns a reference to the underlying [`MastForest`] of this component.
163    pub fn mast_forest(&self) -> &MastForest {
164        self.code.mast_forest()
165    }
166
167    /// Returns a slice of the underlying [`StorageSlot`]s of this component.
168    pub fn storage_slots(&self) -> &[StorageSlot] {
169        self.storage_slots.as_slice()
170    }
171
172    /// Returns the component metadata.
173    pub fn metadata(&self) -> &AccountComponentMetadata {
174        &self.metadata
175    }
176
177    /// Returns the storage schema associated with this component.
178    pub fn storage_schema(&self) -> &StorageSchema {
179        self.metadata.storage_schema()
180    }
181
182    /// Returns a reference to the supported [`AccountType`]s.
183    pub fn supported_types(&self) -> &BTreeSet<AccountType> {
184        self.metadata.supported_types()
185    }
186
187    /// Returns `true` if this component supports the given `account_type`, `false` otherwise.
188    pub fn supports_type(&self, account_type: AccountType) -> bool {
189        self.metadata.supported_types().contains(&account_type)
190    }
191
192    /// Returns an iterator over ([`AccountProcedureRoot`], is_auth) for all procedures in this
193    /// component.
194    ///
195    /// A procedure is considered an authentication procedure if it has the `@auth_script`
196    /// attribute.
197    pub fn procedures(&self) -> impl Iterator<Item = (AccountProcedureRoot, bool)> + '_ {
198        let library = self.code.as_library();
199        library.exports().filter_map(|export| {
200            export.as_procedure().map(|proc_export| {
201                let digest = library
202                    .mast_forest()
203                    .get_node_by_id(proc_export.node)
204                    .expect("export node not in the forest")
205                    .digest();
206                let is_auth = proc_export.attributes.has(AUTH_SCRIPT_ATTRIBUTE);
207                (AccountProcedureRoot::from_raw(digest), is_auth)
208            })
209        })
210    }
211
212    /// Returns the digest of the procedure with the specified path, or `None` if it was not found
213    /// in this component's library or its library path is malformed.
214    pub fn get_procedure_root_by_path(&self, proc_name: impl AsRef<Path>) -> Option<Word> {
215        self.code.as_library().get_procedure_root_by_path(proc_name)
216    }
217}
218
219impl From<AccountComponent> for AccountComponentCode {
220    fn from(component: AccountComponent) -> Self {
221        component.code
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use alloc::string::ToString;
228    use alloc::sync::Arc;
229
230    use miden_assembly::Assembler;
231    use miden_mast_package::{Package, PackageManifest, Section, SectionId, TargetType};
232    use semver::Version;
233
234    use super::*;
235    use crate::testing::account_code::CODE;
236    use crate::utils::serde::Serializable;
237
238    #[test]
239    fn test_extract_metadata_from_package() {
240        // Create a simple library for testing
241        let library = Assembler::default().assemble_library([CODE]).unwrap();
242
243        // Test with metadata
244        let metadata = AccountComponentMetadata::new(
245            "test_component",
246            [AccountType::RegularAccountImmutableCode],
247        )
248        .with_description("A test component")
249        .with_version(Version::new(1, 0, 0));
250
251        let metadata_bytes = metadata.to_bytes();
252        let package_with_metadata = Package {
253            name: "test_package".into(),
254            mast: library.clone(),
255            manifest: PackageManifest::new(core::iter::empty()).unwrap(),
256            kind: TargetType::AccountComponent,
257            sections: vec![Section::new(
258                SectionId::ACCOUNT_COMPONENT_METADATA,
259                metadata_bytes.clone(),
260            )],
261            version: Version::new(0, 0, 0),
262            description: None,
263        };
264
265        let extracted_metadata =
266            AccountComponentMetadata::try_from(&package_with_metadata).unwrap();
267        assert_eq!(extracted_metadata.name(), "test_component");
268        assert!(
269            extracted_metadata
270                .supported_types()
271                .contains(&AccountType::RegularAccountImmutableCode)
272        );
273
274        // Test without metadata - should fail
275        let package_without_metadata = Package {
276            name: "test_package_no_metadata".into(),
277            mast: library,
278            manifest: PackageManifest::new(core::iter::empty()).unwrap(),
279            kind: TargetType::AccountComponent,
280            sections: vec![], // No metadata section
281            version: Version::new(0, 0, 0),
282            description: None,
283        };
284
285        let result = AccountComponentMetadata::try_from(&package_without_metadata);
286        assert!(result.is_err());
287        let error_msg = result.unwrap_err().to_string();
288        assert!(error_msg.contains("package does not contain account component metadata"));
289    }
290
291    #[test]
292    fn test_from_library_with_init_data() {
293        // Create a simple library for testing
294        let library = Assembler::default().assemble_library([CODE]).unwrap();
295        let component_code = AccountComponentCode::from(Arc::unwrap_or_clone(library.clone()));
296
297        // Create metadata for the component
298        let metadata = AccountComponentMetadata::new("test_component", AccountType::regular())
299            .with_description("A test component")
300            .with_version(Version::new(1, 0, 0));
301
302        // Test with empty init data - this tests the complete workflow:
303        // Library + Metadata -> AccountComponent
304        let init_data = InitStorageData::default();
305        let component =
306            AccountComponent::from_library(&component_code, &metadata, &init_data).unwrap();
307
308        // Verify the component was created correctly
309        assert_eq!(component.storage_size(), 0);
310        assert!(component.supports_type(AccountType::RegularAccountImmutableCode));
311        assert!(component.supports_type(AccountType::RegularAccountUpdatableCode));
312        assert!(!component.supports_type(AccountType::FungibleFaucet));
313
314        // Test without metadata - should fail
315        let package_without_metadata = Package {
316            name: "test_package_no_metadata".into(),
317            mast: library,
318            kind: TargetType::AccountComponent,
319            manifest: PackageManifest::new(core::iter::empty()).unwrap(),
320            sections: vec![], // No metadata section
321            version: Version::new(0, 0, 0),
322            description: None,
323        };
324
325        let result = AccountComponent::from_package(&package_without_metadata, &init_data);
326        assert!(result.is_err());
327        let error_msg = result.unwrap_err().to_string();
328        assert!(error_msg.contains("package does not contain account component metadata"));
329    }
330}