miden_protocol/account/builder/
mod.rs1use 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)]
61pub struct AccountBuilder {
62 #[cfg(any(feature = "testing", test))]
63 assets: Vec<crate::asset::Asset>,
64 #[cfg(any(feature = "testing", test))]
65 nonce: Option<Felt>,
66 components: Vec<AccountComponent>,
67 account_type: AccountType,
68 asset_callbacks: AssetCallbackFlag,
69 init_seed: [u8; 32],
70 id_version: AccountIdVersion,
71}
72
73impl AccountBuilder {
74 pub fn new(init_seed: [u8; 32]) -> Self {
79 Self {
80 #[cfg(any(feature = "testing", test))]
81 assets: vec![],
82 #[cfg(any(feature = "testing", test))]
83 nonce: None,
84 components: vec![],
85 init_seed,
86 account_type: AccountType::Private,
87 asset_callbacks: AssetCallbackFlag::Disabled,
88 id_version: AccountIdVersion::Version1,
89 }
90 }
91
92 pub fn version(mut self, version: AccountIdVersion) -> Self {
94 self.id_version = version;
95 self
96 }
97
98 pub fn account_type(mut self, account_type: AccountType) -> Self {
100 self.account_type = account_type;
101 self
102 }
103
104 pub fn with_asset_callbacks(mut self, asset_callbacks: AssetCallbackFlag) -> Self {
111 self.asset_callbacks = asset_callbacks;
112 self
113 }
114
115 pub fn with_component(mut self, account_component: impl Into<AccountComponent>) -> Self {
126 self.components.push(account_component.into());
127 self
128 }
129
130 pub fn with_components(
138 mut self,
139 components: impl IntoIterator<Item = impl Into<AccountComponent>>,
140 ) -> Self {
141 for component in components {
142 self = self.with_component(component);
143 }
144 self
145 }
146
147 pub fn storage_schemas(&self) -> impl Iterator<Item = &StorageSchema> + '_ {
149 self.components.iter().map(|component| component.storage_schema())
150 }
151
152 fn build_inner(&mut self) -> Result<(AssetVault, AccountCode, AccountStorage), AccountError> {
154 #[cfg(any(feature = "testing", test))]
155 let vault = AssetVault::new(&self.assets).map_err(|err| {
156 AccountError::BuildError(format!("asset vault failed to build: {err}"), None)
157 })?;
158
159 #[cfg(all(not(feature = "testing"), not(test)))]
160 let vault = AssetVault::default();
161
162 let components = core::mem::take(&mut self.components);
164 let (code, storage) = Account::initialize_from_components(components).map_err(|err| {
165 AccountError::BuildError(
166 "account components failed to build".into(),
167 Some(Box::new(err)),
168 )
169 })?;
170
171 Ok((vault, code, storage))
172 }
173
174 fn grind_account_id(
176 &self,
177 init_seed: [u8; 32],
178 version: AccountIdVersion,
179 code_commitment: Word,
180 storage_commitment: Word,
181 ) -> Result<Word, AccountError> {
182 let seed = AccountIdV1::compute_account_seed(
183 init_seed,
184 self.account_type,
185 self.asset_callbacks,
186 version,
187 code_commitment,
188 storage_commitment,
189 )
190 .map_err(|err| {
191 AccountError::BuildError("account seed generation failed".into(), Some(Box::new(err)))
192 })?;
193
194 Ok(seed)
195 }
196
197 pub fn build(mut self) -> Result<Account, AccountError> {
214 let (vault, code, storage) = self.build_inner()?;
215
216 #[cfg(any(feature = "testing", test))]
217 if !vault.is_empty() {
218 return Err(AccountError::BuildError(
219 "account asset vault must be empty on new accounts".into(),
220 None,
221 ));
222 }
223
224 let seed = self.grind_account_id(
225 self.init_seed,
226 self.id_version,
227 code.commitment(),
228 storage.to_commitment(),
229 )?;
230
231 let account_id = AccountId::new(
232 seed,
233 AccountIdVersion::Version1,
234 code.commitment(),
235 storage.to_commitment(),
236 )
237 .expect("get_account_seed should provide a suitable seed");
238
239 debug_assert_eq!(account_id.account_type(), self.account_type);
240 debug_assert_eq!(account_id.asset_callback_flag(), self.asset_callbacks);
241
242 let account =
245 Account::new_unchecked(account_id, vault, storage, code, Felt::ZERO, Some(seed));
246
247 Ok(account)
248 }
249}
250
251#[cfg(any(feature = "testing", test))]
252impl AccountBuilder {
253 pub fn with_assets<I: IntoIterator<Item = crate::asset::Asset>>(mut self, assets: I) -> Self {
258 self.assets.extend(assets);
259 self
260 }
261
262 pub fn nonce(mut self, nonce: Felt) -> Self {
267 self.nonce = Some(nonce);
268 self
269 }
270
271 pub fn build_existing(mut self) -> Result<Account, AccountError> {
277 let (vault, code, storage) = self.build_inner()?;
278
279 let account_id = {
280 let bytes = <[u8; 15]>::try_from(&self.init_seed[0..15])
281 .expect("we should have sliced exactly 15 bytes off");
282 AccountId::dummy(
283 bytes,
284 AccountIdVersion::Version1,
285 self.account_type,
286 self.asset_callbacks,
287 )
288 };
289
290 let nonce = self.nonce.unwrap_or(Felt::ONE);
292
293 Ok(Account::new_existing(account_id, vault, storage, code, nonce))
294 }
295}
296
297#[cfg(test)]
301mod tests {
302 use std::sync::LazyLock;
303
304 use assert_matches::assert_matches;
305 use miden_core::mast::MastNodeExt;
306 use miden_mast_package::Package;
307
308 use super::*;
309 use crate::account::component::AccountComponentMetadata;
310 use crate::account::{AccountProcedureRoot, StorageSlot, StorageSlotName};
311 use crate::testing::assembler::assemble_test_package;
312 use crate::testing::noop_auth_component::NoopAuthComponent;
313
314 const CUSTOM_CODE1: &str = "
315 @account_procedure
316 pub proc foo
317 push.2.2 add eq.4
318 end
319 ";
320 const CUSTOM_CODE2: &str = "
321 @account_procedure
322 pub proc bar
323 push.4.4 add eq.8
324 end
325 ";
326
327 static CUSTOM_PACKAGE1: LazyLock<Package> = LazyLock::new(|| {
328 assemble_test_package("custom-package-1", "custom::component1", CUSTOM_CODE1)
329 });
330 static CUSTOM_PACKAGE2: LazyLock<Package> = LazyLock::new(|| {
331 assemble_test_package("custom-package-2", "custom::component2", CUSTOM_CODE2)
332 });
333
334 static CUSTOM_COMPONENT1_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
335 StorageSlotName::new("custom::component1::slot0")
336 .expect("storage slot name should be valid")
337 });
338 static CUSTOM_COMPONENT2_SLOT_NAME0: LazyLock<StorageSlotName> = LazyLock::new(|| {
339 StorageSlotName::new("custom::component2::slot0")
340 .expect("storage slot name should be valid")
341 });
342 static CUSTOM_COMPONENT2_SLOT_NAME1: LazyLock<StorageSlotName> = LazyLock::new(|| {
343 StorageSlotName::new("custom::component2::slot1")
344 .expect("storage slot name should be valid")
345 });
346
347 struct CustomComponent1 {
348 slot0: u32,
349 }
350 impl From<CustomComponent1> for AccountComponent {
351 fn from(custom: CustomComponent1) -> Self {
352 let mut value = Word::empty();
353 value[0] = Felt::from(custom.slot0);
354
355 let metadata = AccountComponentMetadata::new("test::custom_component1");
356 AccountComponent::new(
357 CUSTOM_PACKAGE1.clone(),
358 vec![StorageSlot::with_value(CUSTOM_COMPONENT1_SLOT_NAME.clone(), value)],
359 metadata,
360 )
361 .expect("component should be valid")
362 }
363 }
364
365 struct CustomComponent2 {
366 slot0: u32,
367 slot1: u32,
368 }
369 impl From<CustomComponent2> for AccountComponent {
370 fn from(custom: CustomComponent2) -> Self {
371 let mut value0 = Word::empty();
372 value0[3] = Felt::from(custom.slot0);
373 let mut value1 = Word::empty();
374 value1[3] = Felt::from(custom.slot1);
375
376 let metadata = AccountComponentMetadata::new("test::custom_component2");
377 AccountComponent::new(
378 CUSTOM_PACKAGE2.clone(),
379 vec![
380 StorageSlot::with_value(CUSTOM_COMPONENT2_SLOT_NAME0.clone(), value0),
381 StorageSlot::with_value(CUSTOM_COMPONENT2_SLOT_NAME1.clone(), value1),
382 ],
383 metadata,
384 )
385 .expect("component should be valid")
386 }
387 }
388
389 #[test]
390 fn account_builder() {
391 let storage_slot0 = 25;
392 let storage_slot1 = 12;
393 let storage_slot2 = 42;
394
395 let account = Account::builder([5; 32])
396 .with_component(NoopAuthComponent)
397 .with_component(CustomComponent1 { slot0: storage_slot0 })
398 .with_component(CustomComponent2 {
399 slot0: storage_slot1,
400 slot1: storage_slot2,
401 })
402 .build()
403 .unwrap();
404
405 assert_eq!(account.nonce(), Felt::ZERO);
407
408 let computed_id = AccountId::new(
409 account.seed().unwrap(),
410 AccountIdVersion::Version1,
411 account.code.commitment(),
412 account.storage.to_commitment(),
413 )
414 .unwrap();
415 assert_eq!(account.id(), computed_id);
416
417 assert_eq!(account.code.procedure_roots().count(), 3);
419
420 let foo_root = CUSTOM_PACKAGE1.mast_forest()[CUSTOM_PACKAGE1
421 .get_export_node_id(CUSTOM_PACKAGE1.manifest.exports().next().unwrap().path())]
422 .digest();
423 let bar_root = CUSTOM_PACKAGE2.mast_forest()[CUSTOM_PACKAGE2
424 .get_export_node_id(CUSTOM_PACKAGE2.manifest.exports().next().unwrap().path())]
425 .digest();
426
427 assert!(account.code().procedures().contains(&AccountProcedureRoot::from_raw(foo_root)));
428 assert!(account.code().procedures().contains(&AccountProcedureRoot::from_raw(bar_root)));
429
430 assert_eq!(
431 account.storage().get_item(&CUSTOM_COMPONENT1_SLOT_NAME).unwrap(),
432 Word::from([Felt::from(storage_slot0), Felt::ZERO, Felt::ZERO, Felt::ZERO])
433 );
434 assert_eq!(
435 account.storage().get_item(&CUSTOM_COMPONENT2_SLOT_NAME0).unwrap(),
436 Word::from([Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::from(storage_slot1)])
437 );
438 assert_eq!(
439 account.storage().get_item(&CUSTOM_COMPONENT2_SLOT_NAME1).unwrap(),
440 Word::from([Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::from(storage_slot2)])
441 );
442 }
443
444 #[test]
445 fn account_builder_with_components() {
446 let storage_slot0 = 25;
447 let storage_slot1 = 12;
448 let storage_slot2 = 42;
449
450 let components: Vec<AccountComponent> = vec![
451 CustomComponent1 { slot0: storage_slot0 }.into(),
452 CustomComponent2 {
453 slot0: storage_slot1,
454 slot1: storage_slot2,
455 }
456 .into(),
457 ];
458
459 let account = Account::builder([5; 32])
460 .with_component(NoopAuthComponent)
461 .with_components(components)
462 .build()
463 .unwrap();
464
465 let expected = Account::builder([5; 32])
468 .with_component(NoopAuthComponent)
469 .with_component(CustomComponent1 { slot0: storage_slot0 })
470 .with_component(CustomComponent2 {
471 slot0: storage_slot1,
472 slot1: storage_slot2,
473 })
474 .build()
475 .unwrap();
476
477 assert_eq!(account.id(), expected.id());
478 assert_eq!(account.code().commitment(), expected.code().commitment());
479 assert_eq!(account.storage().to_commitment(), expected.storage().to_commitment());
480
481 let account_no_extra = Account::builder([6; 32])
483 .with_component(NoopAuthComponent)
484 .with_component(CustomComponent1 { slot0: storage_slot0 })
485 .with_components(core::iter::empty::<CustomComponent2>())
486 .build()
487 .unwrap();
488
489 let expected_no_extra = Account::builder([6; 32])
490 .with_component(NoopAuthComponent)
491 .with_component(CustomComponent1 { slot0: storage_slot0 })
492 .build()
493 .unwrap();
494
495 assert_eq!(account_no_extra.id(), expected_no_extra.id());
496 }
497
498 #[test]
499 fn account_builder_auth_component_position_is_irrelevant() {
500 let component1 = CustomComponent1 { slot0: 25 };
501 let component2 = CustomComponent2 { slot0: 12, slot1: 42 };
502 let common_components =
503 vec![AccountComponent::from(component1), AccountComponent::from(component2)];
504
505 let mut components_auth_1st = common_components.clone();
506 components_auth_1st.insert(0, AccountComponent::from(NoopAuthComponent));
507
508 let mut components_auth_2nd = common_components.clone();
509 components_auth_2nd.insert(1, AccountComponent::from(NoopAuthComponent));
510
511 let seed = [5; 32];
512 let auth_1st = Account::builder(seed).with_components(components_auth_1st).build().unwrap();
513 let auth_2nd = Account::builder(seed).with_components(components_auth_2nd).build().unwrap();
514
515 assert_eq!(auth_1st.id(), auth_2nd.id());
516 assert_eq!(auth_1st.code().commitment(), auth_2nd.code().commitment());
517 assert_eq!(auth_1st.storage().to_commitment(), auth_2nd.storage().to_commitment());
518 }
519
520 #[test]
521 fn account_builder_without_auth_component_fails() {
522 let build_error = Account::builder([5; 32])
523 .with_component(CustomComponent1 { slot0: 25 })
524 .build()
525 .unwrap_err();
526
527 assert_matches!(build_error, AccountError::BuildError(_, Some(source)) => {
528 assert_matches!(*source, AccountError::AccountCodeNoAuthComponent);
529 });
530 }
531
532 #[test]
533 fn account_builder_with_multiple_auth_components_fails() {
534 let build_error = Account::builder([5; 32])
535 .with_component(NoopAuthComponent)
536 .with_component(NoopAuthComponent)
537 .with_component(CustomComponent1 { slot0: 25 })
538 .build()
539 .unwrap_err();
540
541 assert_matches!(build_error, AccountError::BuildError(_, Some(source)) => {
542 assert_matches!(*source, AccountError::AccountCodeMultipleAuthComponents);
543 });
544 }
545
546 #[test]
547 fn account_builder_non_empty_vault_on_new_account() {
548 let storage_slot0 = 25;
549
550 let build_error = Account::builder([0xff; 32])
551 .with_component(NoopAuthComponent)
552 .with_component(CustomComponent1 { slot0: storage_slot0 })
553 .with_assets(AssetVault::mock().assets())
554 .build()
555 .unwrap_err();
556
557 assert_matches!(build_error, AccountError::BuildError(msg, _) if msg == "account asset vault must be empty on new accounts")
558 }
559
560 }