1use alloc::boxed::Box;
2use alloc::vec::Vec;
3
4use crate::account::component::StorageSchema;
5use crate::account::{
6 Account,
7 AccountCode,
8 AccountComponent,
9 AccountId,
10 AccountIdV1,
11 AccountIdVersion,
12 AccountStorage,
13 AccountType,
14 AssetCallbackFlag,
15};
16use crate::asset::AssetVault;
17use crate::errors::AccountError;
18use crate::{Felt, Word};
19
20#[derive(Debug, Clone)]
87pub struct AccountBuilder {
88 #[cfg(any(feature = "testing", test))]
89 assets: Vec<crate::asset::Asset>,
90 #[cfg(any(feature = "testing", test))]
91 nonce: Option<Felt>,
92 components: Vec<AccountComponent>,
93 account_type: AccountType,
94 asset_callbacks: AssetCallbackFlag,
95 init_seed: [u8; 32],
96 id_version: AccountIdVersion,
97}
98
99impl AccountBuilder {
100 pub fn new(init_seed: [u8; 32]) -> Self {
105 Self {
106 #[cfg(any(feature = "testing", test))]
107 assets: vec![],
108 #[cfg(any(feature = "testing", test))]
109 nonce: None,
110 components: vec![],
111 init_seed,
112 account_type: AccountType::Private,
113 asset_callbacks: AssetCallbackFlag::Disabled,
114 id_version: AccountIdVersion::Version1,
115 }
116 }
117
118 pub fn version(mut self, version: AccountIdVersion) -> Self {
120 self.id_version = version;
121 self
122 }
123
124 pub fn account_type(mut self, account_type: AccountType) -> Self {
126 self.account_type = account_type;
127 self
128 }
129
130 pub fn enable_asset_callbacks(mut self) -> Self {
135 self.asset_callbacks = AssetCallbackFlag::Enabled;
136 self
137 }
138
139 pub fn with_component(mut self, account_component: impl Into<AccountComponent>) -> Self {
151 self.components.push(account_component.into());
152 self
153 }
154
155 pub fn with_components(
163 mut self,
164 components: impl IntoIterator<Item = impl Into<AccountComponent>>,
165 ) -> Self {
166 for component in components {
167 self = self.with_component(component);
168 }
169 self
170 }
171
172 pub fn storage_schemas(&self) -> impl Iterator<Item = &StorageSchema> + '_ {
174 self.components.iter().map(|component| component.storage_schema())
175 }
176
177 fn build_inner(&mut self) -> Result<(AssetVault, AccountCode, AccountStorage), AccountError> {
179 #[cfg(any(feature = "testing", test))]
180 let vault = AssetVault::new(&self.assets).map_err(|err| {
181 AccountError::BuildError(format!("asset vault failed to build: {err}"), None)
182 })?;
183
184 #[cfg(all(not(feature = "testing"), not(test)))]
185 let vault = AssetVault::default();
186
187 let components = core::mem::take(&mut self.components);
189 let (code, storage) = Account::initialize_from_components(components).map_err(|err| {
190 AccountError::BuildError(
191 "account components failed to build".into(),
192 Some(Box::new(err)),
193 )
194 })?;
195
196 Ok((vault, code, storage))
197 }
198
199 fn derive_asset_callbacks(&self, storage: &AccountStorage) -> AssetCallbackFlag {
204 AssetCallbackFlag::from(self.asset_callbacks.is_enabled() || storage.has_callback_slots())
205 }
206
207 fn grind_account_id(
209 &self,
210 init_seed: [u8; 32],
211 version: AccountIdVersion,
212 asset_callbacks: AssetCallbackFlag,
213 code_commitment: Word,
214 storage_commitment: Word,
215 ) -> Result<Word, AccountError> {
216 let seed = AccountIdV1::compute_account_seed(
217 init_seed,
218 self.account_type,
219 asset_callbacks,
220 version,
221 code_commitment,
222 storage_commitment,
223 )
224 .map_err(|err| {
225 AccountError::BuildError("account seed generation failed".into(), Some(Box::new(err)))
226 })?;
227
228 Ok(seed)
229 }
230
231 pub fn build(mut self) -> Result<Account, AccountError> {
248 let (vault, code, storage) = self.build_inner()?;
249
250 #[cfg(any(feature = "testing", test))]
251 if !vault.is_empty() {
252 return Err(AccountError::BuildError(
253 "account asset vault must be empty on new accounts".into(),
254 None,
255 ));
256 }
257
258 let asset_callbacks = self.derive_asset_callbacks(&storage);
259
260 let seed = self.grind_account_id(
261 self.init_seed,
262 self.id_version,
263 asset_callbacks,
264 code.commitment(),
265 storage.to_commitment(),
266 )?;
267
268 let account_id = AccountId::new(
269 seed,
270 AccountIdVersion::Version1,
271 code.commitment(),
272 storage.to_commitment(),
273 )
274 .expect("get_account_seed should provide a suitable seed");
275
276 debug_assert_eq!(account_id.account_type(), self.account_type);
277 debug_assert_eq!(account_id.asset_callback_flag(), asset_callbacks);
278
279 let account =
282 Account::new_unchecked(account_id, vault, storage, code, Felt::ZERO, Some(seed));
283
284 Ok(account)
285 }
286}
287
288#[cfg(any(feature = "testing", test))]
289impl AccountBuilder {
290 pub fn with_assets<I: IntoIterator<Item = crate::asset::Asset>>(mut self, assets: I) -> Self {
295 self.assets.extend(assets);
296 self
297 }
298
299 pub fn nonce(mut self, nonce: Felt) -> Self {
304 self.nonce = Some(nonce);
305 self
306 }
307
308 pub fn build_existing(mut self) -> Result<Account, AccountError> {
314 let (vault, code, storage) = self.build_inner()?;
315
316 let account_id = {
317 let bytes = <[u8; 15]>::try_from(&self.init_seed[0..15])
318 .expect("we should have sliced exactly 15 bytes off");
319 AccountId::dummy(
320 bytes,
321 AccountIdVersion::Version1,
322 self.account_type,
323 self.derive_asset_callbacks(&storage),
324 )
325 };
326
327 let nonce = self.nonce.unwrap_or(Felt::ONE);
329
330 Ok(Account::new_existing(account_id, vault, storage, code, nonce))
331 }
332}
333
334#[cfg(test)]
338mod tests {
339 use std::sync::LazyLock;
340
341 use assert_matches::assert_matches;
342 use miden_core::mast::MastNodeExt;
343 use miden_mast_package::Package;
344
345 use super::*;
346 use crate::account::component::AccountComponentMetadata;
347 use crate::account::{AccountProcedureRoot, StorageSlot, StorageSlotName};
348 use crate::asset::AssetCallbacks;
349 use crate::testing::assembler::assemble_test_package;
350 use crate::testing::noop_auth_component::NoopAuthComponent;
351
352 const CUSTOM_CODE1: &str = "
353 @account_procedure
354 pub proc foo
355 push.2.2 add eq.4
356 end
357 ";
358 const CUSTOM_CODE2: &str = "
359 @account_procedure
360 pub proc bar
361 push.4.4 add eq.8
362 end
363 ";
364
365 static CUSTOM_PACKAGE1: LazyLock<Package> = LazyLock::new(|| {
366 assemble_test_package("custom-package-1", "custom::component1", CUSTOM_CODE1)
367 });
368 static CUSTOM_PACKAGE2: LazyLock<Package> = LazyLock::new(|| {
369 assemble_test_package("custom-package-2", "custom::component2", CUSTOM_CODE2)
370 });
371
372 static CUSTOM_COMPONENT1_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
373 StorageSlotName::new("custom::component1::slot0")
374 .expect("storage slot name should be valid")
375 });
376 static CUSTOM_COMPONENT2_SLOT_NAME0: LazyLock<StorageSlotName> = LazyLock::new(|| {
377 StorageSlotName::new("custom::component2::slot0")
378 .expect("storage slot name should be valid")
379 });
380 static CUSTOM_COMPONENT2_SLOT_NAME1: LazyLock<StorageSlotName> = LazyLock::new(|| {
381 StorageSlotName::new("custom::component2::slot1")
382 .expect("storage slot name should be valid")
383 });
384
385 struct CustomComponent1 {
386 slot0: u32,
387 }
388 impl From<CustomComponent1> for AccountComponent {
389 fn from(custom: CustomComponent1) -> Self {
390 let mut value = Word::empty();
391 value[0] = Felt::from(custom.slot0);
392
393 let metadata = AccountComponentMetadata::new("test::custom_component1");
394 AccountComponent::new(
395 CUSTOM_PACKAGE1.clone(),
396 vec![StorageSlot::with_value(CUSTOM_COMPONENT1_SLOT_NAME.clone(), value)],
397 metadata,
398 )
399 .expect("component should be valid")
400 }
401 }
402
403 struct CustomComponent2 {
404 slot0: u32,
405 slot1: u32,
406 }
407 impl From<CustomComponent2> for AccountComponent {
408 fn from(custom: CustomComponent2) -> Self {
409 let mut value0 = Word::empty();
410 value0[3] = Felt::from(custom.slot0);
411 let mut value1 = Word::empty();
412 value1[3] = Felt::from(custom.slot1);
413
414 let metadata = AccountComponentMetadata::new("test::custom_component2");
415 AccountComponent::new(
416 CUSTOM_PACKAGE2.clone(),
417 vec![
418 StorageSlot::with_value(CUSTOM_COMPONENT2_SLOT_NAME0.clone(), value0),
419 StorageSlot::with_value(CUSTOM_COMPONENT2_SLOT_NAME1.clone(), value1),
420 ],
421 metadata,
422 )
423 .expect("component should be valid")
424 }
425 }
426
427 #[test]
428 fn account_builder() {
429 let storage_slot0 = 25;
430 let storage_slot1 = 12;
431 let storage_slot2 = 42;
432
433 let account = Account::builder([5; 32])
434 .with_component(NoopAuthComponent)
435 .with_component(CustomComponent1 { slot0: storage_slot0 })
436 .with_component(CustomComponent2 {
437 slot0: storage_slot1,
438 slot1: storage_slot2,
439 })
440 .build()
441 .unwrap();
442
443 assert_eq!(account.nonce(), Felt::ZERO);
445
446 let computed_id = AccountId::new(
447 account.seed().unwrap(),
448 AccountIdVersion::Version1,
449 account.code.commitment(),
450 account.storage.to_commitment(),
451 )
452 .unwrap();
453 assert_eq!(account.id(), computed_id);
454
455 assert_eq!(account.code.procedure_roots().count(), 3);
457
458 let foo_root = CUSTOM_PACKAGE1.mast_forest()[CUSTOM_PACKAGE1
459 .get_export_node_id(CUSTOM_PACKAGE1.manifest.exports().next().unwrap().path())]
460 .digest();
461 let bar_root = CUSTOM_PACKAGE2.mast_forest()[CUSTOM_PACKAGE2
462 .get_export_node_id(CUSTOM_PACKAGE2.manifest.exports().next().unwrap().path())]
463 .digest();
464
465 assert!(account.code().procedures().contains(&AccountProcedureRoot::from_raw(foo_root)));
466 assert!(account.code().procedures().contains(&AccountProcedureRoot::from_raw(bar_root)));
467
468 assert_eq!(
469 account.storage().get_item(&CUSTOM_COMPONENT1_SLOT_NAME).unwrap(),
470 Word::from([Felt::from(storage_slot0), Felt::ZERO, Felt::ZERO, Felt::ZERO])
471 );
472 assert_eq!(
473 account.storage().get_item(&CUSTOM_COMPONENT2_SLOT_NAME0).unwrap(),
474 Word::from([Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::from(storage_slot1)])
475 );
476 assert_eq!(
477 account.storage().get_item(&CUSTOM_COMPONENT2_SLOT_NAME1).unwrap(),
478 Word::from([Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::from(storage_slot2)])
479 );
480 }
481
482 #[test]
483 fn account_builder_with_components() {
484 let storage_slot0 = 25;
485 let storage_slot1 = 12;
486 let storage_slot2 = 42;
487
488 let components: Vec<AccountComponent> = vec![
489 CustomComponent1 { slot0: storage_slot0 }.into(),
490 CustomComponent2 {
491 slot0: storage_slot1,
492 slot1: storage_slot2,
493 }
494 .into(),
495 ];
496
497 let account = Account::builder([5; 32])
498 .with_component(NoopAuthComponent)
499 .with_components(components)
500 .build()
501 .unwrap();
502
503 let expected = Account::builder([5; 32])
506 .with_component(NoopAuthComponent)
507 .with_component(CustomComponent1 { slot0: storage_slot0 })
508 .with_component(CustomComponent2 {
509 slot0: storage_slot1,
510 slot1: storage_slot2,
511 })
512 .build()
513 .unwrap();
514
515 assert_eq!(account.id(), expected.id());
516 assert_eq!(account.code().commitment(), expected.code().commitment());
517 assert_eq!(account.storage().to_commitment(), expected.storage().to_commitment());
518
519 let account_no_extra = Account::builder([6; 32])
521 .with_component(NoopAuthComponent)
522 .with_component(CustomComponent1 { slot0: storage_slot0 })
523 .with_components(core::iter::empty::<CustomComponent2>())
524 .build()
525 .unwrap();
526
527 let expected_no_extra = Account::builder([6; 32])
528 .with_component(NoopAuthComponent)
529 .with_component(CustomComponent1 { slot0: storage_slot0 })
530 .build()
531 .unwrap();
532
533 assert_eq!(account_no_extra.id(), expected_no_extra.id());
534 }
535
536 #[test]
537 fn account_builder_auth_component_position_is_irrelevant() {
538 let component1 = CustomComponent1 { slot0: 25 };
539 let component2 = CustomComponent2 { slot0: 12, slot1: 42 };
540 let common_components =
541 vec![AccountComponent::from(component1), AccountComponent::from(component2)];
542
543 let mut components_auth_1st = common_components.clone();
544 components_auth_1st.insert(0, AccountComponent::from(NoopAuthComponent));
545
546 let mut components_auth_2nd = common_components.clone();
547 components_auth_2nd.insert(1, AccountComponent::from(NoopAuthComponent));
548
549 let seed = [5; 32];
550 let auth_1st = Account::builder(seed).with_components(components_auth_1st).build().unwrap();
551 let auth_2nd = Account::builder(seed).with_components(components_auth_2nd).build().unwrap();
552
553 assert_eq!(auth_1st.id(), auth_2nd.id());
554 assert_eq!(auth_1st.code().commitment(), auth_2nd.code().commitment());
555 assert_eq!(auth_1st.storage().to_commitment(), auth_2nd.storage().to_commitment());
556 }
557
558 #[test]
559 fn account_builder_without_auth_component_fails() {
560 let build_error = Account::builder([5; 32])
561 .with_component(CustomComponent1 { slot0: 25 })
562 .build()
563 .unwrap_err();
564
565 assert_matches!(build_error, AccountError::BuildError(_, Some(source)) => {
566 assert_matches!(*source, AccountError::AccountCodeNoAuthComponent);
567 });
568 }
569
570 #[test]
571 fn account_builder_with_multiple_auth_components_fails() {
572 let build_error = Account::builder([5; 32])
573 .with_component(NoopAuthComponent)
574 .with_component(NoopAuthComponent)
575 .with_component(CustomComponent1 { slot0: 25 })
576 .build()
577 .unwrap_err();
578
579 assert_matches!(build_error, AccountError::BuildError(_, Some(source)) => {
580 assert_matches!(*source, AccountError::AccountCodeMultipleAuthComponents);
581 });
582 }
583
584 #[test]
585 fn account_builder_non_empty_vault_on_new_account() {
586 let storage_slot0 = 25;
587
588 let build_error = Account::builder([0xff; 32])
589 .with_component(NoopAuthComponent)
590 .with_component(CustomComponent1 { slot0: storage_slot0 })
591 .with_assets(AssetVault::mock().assets())
592 .build()
593 .unwrap_err();
594
595 assert_matches!(build_error, AccountError::BuildError(msg, _) if msg == "account asset vault must be empty on new accounts")
596 }
597
598 #[test]
603 fn account_builder_derives_asset_callback_flag_from_callback_slots() {
604 let callback_component = |slots| {
605 AccountComponent::new(
606 CUSTOM_PACKAGE1.clone(),
607 slots,
608 AccountComponentMetadata::new("test::callback_component"),
609 )
610 .expect("component should be valid")
611 };
612
613 for slots in [
614 AssetCallbacks::new()
615 .on_before_asset_added_to_note(Word::from([1u32, 2, 3, 4]))
616 .into_storage_slots(),
617 AssetCallbacks::new()
618 .on_before_asset_added_to_account(Word::from([1u32, 2, 3, 4]))
619 .into_storage_slots(),
620 ] {
621 let account = Account::builder([7; 32])
622 .with_component(NoopAuthComponent)
623 .with_component(callback_component(slots))
624 .build()
625 .unwrap();
626
627 assert_eq!(account.id().asset_callback_flag(), AssetCallbackFlag::Enabled);
628 }
629 }
630
631 #[test]
634 fn account_builder_derives_disabled_asset_callback_flag_without_callback_slots() {
635 let builder = Account::builder([7; 32])
636 .with_component(NoopAuthComponent)
637 .with_component(CustomComponent1 { slot0: 25 });
638
639 let account = builder.clone().build().unwrap();
640 assert_eq!(account.id().asset_callback_flag(), AssetCallbackFlag::Disabled);
641
642 let account = builder.enable_asset_callbacks().build().unwrap();
643 assert_eq!(account.id().asset_callback_flag(), AssetCallbackFlag::Enabled);
644 }
645
646 }