miden_protocol/account/builder/
mod.rs1use alloc::boxed::Box;
2use alloc::vec::Vec;
3
4use miden_core::FieldElement;
5
6use crate::account::component::StorageSchema;
7use crate::account::{
8 Account,
9 AccountCode,
10 AccountComponent,
11 AccountId,
12 AccountIdV0,
13 AccountIdVersion,
14 AccountStorage,
15 AccountStorageMode,
16 AccountType,
17};
18use crate::asset::AssetVault;
19use crate::errors::AccountError;
20use crate::{Felt, Word};
21
22#[derive(Debug, Clone)]
58pub struct AccountBuilder {
59 #[cfg(any(feature = "testing", test))]
60 assets: Vec<crate::asset::Asset>,
61 #[cfg(any(feature = "testing", test))]
62 nonce: Option<Felt>,
63 components: Vec<AccountComponent>,
64 auth_component: Option<AccountComponent>,
65 account_type: AccountType,
66 storage_mode: AccountStorageMode,
67 init_seed: [u8; 32],
68 id_version: AccountIdVersion,
69}
70
71impl AccountBuilder {
72 pub fn new(init_seed: [u8; 32]) -> Self {
77 Self {
78 #[cfg(any(feature = "testing", test))]
79 assets: vec![],
80 #[cfg(any(feature = "testing", test))]
81 nonce: None,
82 components: vec![],
83 auth_component: None,
84 init_seed,
85 account_type: AccountType::RegularAccountUpdatableCode,
86 storage_mode: AccountStorageMode::Private,
87 id_version: AccountIdVersion::Version0,
88 }
89 }
90
91 pub fn version(mut self, version: AccountIdVersion) -> Self {
93 self.id_version = version;
94 self
95 }
96
97 pub fn account_type(mut self, account_type: AccountType) -> Self {
99 self.account_type = account_type;
100 self
101 }
102
103 pub fn storage_mode(mut self, storage_mode: AccountStorageMode) -> Self {
105 self.storage_mode = storage_mode;
106 self
107 }
108
109 pub fn with_component(mut self, account_component: impl Into<AccountComponent>) -> Self {
114 self.components.push(account_component.into());
115 self
116 }
117
118 pub fn with_auth_component(mut self, account_component: impl Into<AccountComponent>) -> Self {
127 self.auth_component = Some(account_component.into());
128 self
129 }
130
131 pub fn storage_schemas(&self) -> impl Iterator<Item = &StorageSchema> + '_ {
135 self.auth_component
136 .iter()
137 .chain(self.components.iter())
138 .filter_map(|component| component.storage_schema())
139 }
140
141 fn build_inner(&mut self) -> Result<(AssetVault, AccountCode, AccountStorage), AccountError> {
143 #[cfg(any(feature = "testing", test))]
144 let vault = AssetVault::new(&self.assets).map_err(|err| {
145 AccountError::BuildError(format!("asset vault failed to build: {err}"), None)
146 })?;
147
148 #[cfg(all(not(feature = "testing"), not(test)))]
149 let vault = AssetVault::default();
150
151 let auth_component = self
152 .auth_component
153 .take()
154 .ok_or(AccountError::BuildError("auth component must be set".into(), None))?;
155
156 let mut components = vec![auth_component];
157 components.append(&mut self.components);
158
159 let (code, storage) = Account::initialize_from_components(self.account_type, components)
160 .map_err(|err| {
161 AccountError::BuildError(
162 "account components failed to build".into(),
163 Some(Box::new(err)),
164 )
165 })?;
166
167 Ok((vault, code, storage))
168 }
169
170 fn grind_account_id(
172 &self,
173 init_seed: [u8; 32],
174 version: AccountIdVersion,
175 code_commitment: Word,
176 storage_commitment: Word,
177 ) -> Result<Word, AccountError> {
178 let seed = AccountIdV0::compute_account_seed(
179 init_seed,
180 self.account_type,
181 self.storage_mode,
182 version,
183 code_commitment,
184 storage_commitment,
185 )
186 .map_err(|err| {
187 AccountError::BuildError("account seed generation failed".into(), Some(Box::new(err)))
188 })?;
189
190 Ok(seed)
191 }
192
193 pub fn build(mut self) -> Result<Account, AccountError> {
210 let (vault, code, storage) = self.build_inner()?;
211
212 #[cfg(any(feature = "testing", test))]
213 if !vault.is_empty() {
214 return Err(AccountError::BuildError(
215 "account asset vault must be empty on new accounts".into(),
216 None,
217 ));
218 }
219
220 let seed = self.grind_account_id(
221 self.init_seed,
222 self.id_version,
223 code.commitment(),
224 storage.to_commitment(),
225 )?;
226
227 let account_id = AccountId::new(
228 seed,
229 AccountIdVersion::Version0,
230 code.commitment(),
231 storage.to_commitment(),
232 )
233 .expect("get_account_seed should provide a suitable seed");
234
235 debug_assert_eq!(account_id.account_type(), self.account_type);
236 debug_assert_eq!(account_id.storage_mode(), self.storage_mode);
237
238 let account =
241 Account::new_unchecked(account_id, vault, storage, code, Felt::ZERO, Some(seed));
242
243 Ok(account)
244 }
245}
246
247#[cfg(any(feature = "testing", test))]
248impl AccountBuilder {
249 pub fn with_assets<I: IntoIterator<Item = crate::asset::Asset>>(mut self, assets: I) -> Self {
254 self.assets.extend(assets);
255 self
256 }
257
258 pub fn nonce(mut self, nonce: Felt) -> Self {
263 self.nonce = Some(nonce);
264 self
265 }
266
267 pub fn build_existing(mut self) -> Result<Account, AccountError> {
273 let (vault, code, storage) = self.build_inner()?;
274
275 let account_id = {
276 let bytes = <[u8; 15]>::try_from(&self.init_seed[0..15])
277 .expect("we should have sliced exactly 15 bytes off");
278 AccountId::dummy(
279 bytes,
280 AccountIdVersion::Version0,
281 self.account_type,
282 self.storage_mode,
283 )
284 };
285
286 let nonce = self.nonce.unwrap_or(Felt::ONE);
288
289 Ok(Account::new_existing(account_id, vault, storage, code, nonce))
290 }
291}
292
293#[cfg(test)]
297mod tests {
298 use std::sync::LazyLock;
299
300 use assert_matches::assert_matches;
301 use miden_assembly::{Assembler, Library};
302 use miden_core::FieldElement;
303 use miden_processor::MastNodeExt;
304
305 use super::*;
306 use crate::account::{AccountProcedureRoot, StorageSlot, StorageSlotName};
307 use crate::testing::noop_auth_component::NoopAuthComponent;
308
309 const CUSTOM_CODE1: &str = "
310 pub proc foo
311 push.2.2 add eq.4
312 end
313 ";
314 const CUSTOM_CODE2: &str = "
315 pub proc bar
316 push.4.4 add eq.8
317 end
318 ";
319
320 static CUSTOM_LIBRARY1: LazyLock<Library> = LazyLock::new(|| {
321 Assembler::default()
322 .assemble_library([CUSTOM_CODE1])
323 .expect("code should be valid")
324 });
325 static CUSTOM_LIBRARY2: LazyLock<Library> = LazyLock::new(|| {
326 Assembler::default()
327 .assemble_library([CUSTOM_CODE2])
328 .expect("code should be valid")
329 });
330
331 static CUSTOM_COMPONENT1_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
332 StorageSlotName::new("custom::component1::slot0")
333 .expect("storage slot name should be valid")
334 });
335 static CUSTOM_COMPONENT2_SLOT_NAME0: LazyLock<StorageSlotName> = LazyLock::new(|| {
336 StorageSlotName::new("custom::component2::slot0")
337 .expect("storage slot name should be valid")
338 });
339 static CUSTOM_COMPONENT2_SLOT_NAME1: LazyLock<StorageSlotName> = LazyLock::new(|| {
340 StorageSlotName::new("custom::component2::slot1")
341 .expect("storage slot name should be valid")
342 });
343
344 struct CustomComponent1 {
345 slot0: u64,
346 }
347 impl From<CustomComponent1> for AccountComponent {
348 fn from(custom: CustomComponent1) -> Self {
349 let mut value = Word::empty();
350 value[0] = Felt::new(custom.slot0);
351
352 AccountComponent::new(
353 CUSTOM_LIBRARY1.clone(),
354 vec![StorageSlot::with_value(CUSTOM_COMPONENT1_SLOT_NAME.clone(), value)],
355 )
356 .expect("component should be valid")
357 .with_supports_all_types()
358 }
359 }
360
361 struct CustomComponent2 {
362 slot0: u64,
363 slot1: u64,
364 }
365 impl From<CustomComponent2> for AccountComponent {
366 fn from(custom: CustomComponent2) -> Self {
367 let mut value0 = Word::empty();
368 value0[3] = Felt::new(custom.slot0);
369 let mut value1 = Word::empty();
370 value1[3] = Felt::new(custom.slot1);
371
372 AccountComponent::new(
373 CUSTOM_LIBRARY2.clone(),
374 vec![
375 StorageSlot::with_value(CUSTOM_COMPONENT2_SLOT_NAME0.clone(), value0),
376 StorageSlot::with_value(CUSTOM_COMPONENT2_SLOT_NAME1.clone(), value1),
377 ],
378 )
379 .expect("component should be valid")
380 .with_supports_all_types()
381 }
382 }
383
384 #[test]
385 fn account_builder() {
386 let storage_slot0 = 25;
387 let storage_slot1 = 12;
388 let storage_slot2 = 42;
389
390 let account = Account::builder([5; 32])
391 .with_auth_component(NoopAuthComponent)
392 .with_component(CustomComponent1 { slot0: storage_slot0 })
393 .with_component(CustomComponent2 {
394 slot0: storage_slot1,
395 slot1: storage_slot2,
396 })
397 .build()
398 .unwrap();
399
400 assert_eq!(account.nonce(), Felt::ZERO);
402
403 let computed_id = AccountId::new(
404 account.seed().unwrap(),
405 AccountIdVersion::Version0,
406 account.code.commitment(),
407 account.storage.to_commitment(),
408 )
409 .unwrap();
410 assert_eq!(account.id(), computed_id);
411
412 assert_eq!(account.code.procedure_roots().count(), 3);
414
415 let foo_root = CUSTOM_LIBRARY1.mast_forest()
416 [CUSTOM_LIBRARY1.get_export_node_id(CUSTOM_LIBRARY1.exports().next().unwrap().path())]
417 .digest();
418 let bar_root = CUSTOM_LIBRARY2.mast_forest()
419 [CUSTOM_LIBRARY2.get_export_node_id(CUSTOM_LIBRARY2.exports().next().unwrap().path())]
420 .digest();
421
422 assert!(account.code().procedures().contains(&AccountProcedureRoot::from_raw(foo_root)));
423 assert!(account.code().procedures().contains(&AccountProcedureRoot::from_raw(bar_root)));
424
425 assert_eq!(
426 account.storage().get_item(&CUSTOM_COMPONENT1_SLOT_NAME).unwrap(),
427 [Felt::new(storage_slot0), Felt::new(0), Felt::new(0), Felt::new(0)].into()
428 );
429 assert_eq!(
430 account.storage().get_item(&CUSTOM_COMPONENT2_SLOT_NAME0).unwrap(),
431 [Felt::new(0), Felt::new(0), Felt::new(0), Felt::new(storage_slot1)].into()
432 );
433 assert_eq!(
434 account.storage().get_item(&CUSTOM_COMPONENT2_SLOT_NAME1).unwrap(),
435 [Felt::new(0), Felt::new(0), Felt::new(0), Felt::new(storage_slot2)].into()
436 );
437 }
438
439 #[test]
440 fn account_builder_non_empty_vault_on_new_account() {
441 let storage_slot0 = 25;
442
443 let build_error = Account::builder([0xff; 32])
444 .with_auth_component(NoopAuthComponent)
445 .with_component(CustomComponent1 { slot0: storage_slot0 })
446 .with_assets(AssetVault::mock().assets())
447 .build()
448 .unwrap_err();
449
450 assert_matches!(build_error, AccountError::BuildError(msg, _) if msg == "account asset vault must be empty on new accounts")
451 }
452
453 }