miden_protocol/account/component/
mod.rs

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