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)]
50pub struct AccountBuilder {
51 #[cfg(any(feature = "testing", test))]
52 assets: Vec<crate::asset::Asset>,
53 #[cfg(any(feature = "testing", test))]
54 nonce: Option<Felt>,
55 components: Vec<AccountComponent>,
56 auth_component: Option<AccountComponent>,
57 account_type: AccountType,
58 asset_callbacks: AssetCallbackFlag,
59 init_seed: [u8; 32],
60 id_version: AccountIdVersion,
61}
62
63impl AccountBuilder {
64 pub fn new(init_seed: [u8; 32]) -> Self {
69 Self {
70 #[cfg(any(feature = "testing", test))]
71 assets: vec![],
72 #[cfg(any(feature = "testing", test))]
73 nonce: None,
74 components: vec![],
75 auth_component: None,
76 init_seed,
77 account_type: AccountType::Private,
78 asset_callbacks: AssetCallbackFlag::Disabled,
79 id_version: AccountIdVersion::Version1,
80 }
81 }
82
83 pub fn version(mut self, version: AccountIdVersion) -> Self {
85 self.id_version = version;
86 self
87 }
88
89 pub fn account_type(mut self, account_type: AccountType) -> Self {
91 self.account_type = account_type;
92 self
93 }
94
95 pub fn with_asset_callbacks(mut self, asset_callbacks: AssetCallbackFlag) -> Self {
102 self.asset_callbacks = asset_callbacks;
103 self
104 }
105
106 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_components(
126 mut self,
127 components: impl IntoIterator<Item = impl Into<AccountComponent>>,
128 ) -> Self {
129 for component in components {
130 self = self.with_component(component);
131 }
132 self
133 }
134
135 pub fn with_auth_component(mut self, account_component: impl Into<AccountComponent>) -> Self {
144 self.auth_component = Some(account_component.into());
145 self
146 }
147
148 pub fn storage_schemas(&self) -> impl Iterator<Item = &StorageSchema> + '_ {
150 self.auth_component
151 .iter()
152 .chain(self.components.iter())
153 .map(|component| component.storage_schema())
154 }
155
156 fn build_inner(&mut self) -> Result<(AssetVault, AccountCode, AccountStorage), AccountError> {
158 #[cfg(any(feature = "testing", test))]
159 let vault = AssetVault::new(&self.assets).map_err(|err| {
160 AccountError::BuildError(format!("asset vault failed to build: {err}"), None)
161 })?;
162
163 #[cfg(all(not(feature = "testing"), not(test)))]
164 let vault = AssetVault::default();
165
166 let auth_component = self
167 .auth_component
168 .take()
169 .ok_or(AccountError::BuildError("auth component must be set".into(), None))?;
170
171 let mut components = vec![auth_component];
172 components.append(&mut self.components);
173
174 let (code, storage) = Account::initialize_from_components(components).map_err(|err| {
175 AccountError::BuildError(
176 "account components failed to build".into(),
177 Some(Box::new(err)),
178 )
179 })?;
180
181 Ok((vault, code, storage))
182 }
183
184 fn grind_account_id(
186 &self,
187 init_seed: [u8; 32],
188 version: AccountIdVersion,
189 code_commitment: Word,
190 storage_commitment: Word,
191 ) -> Result<Word, AccountError> {
192 let seed = AccountIdV1::compute_account_seed(
193 init_seed,
194 self.account_type,
195 self.asset_callbacks,
196 version,
197 code_commitment,
198 storage_commitment,
199 )
200 .map_err(|err| {
201 AccountError::BuildError("account seed generation failed".into(), Some(Box::new(err)))
202 })?;
203
204 Ok(seed)
205 }
206
207 pub fn build(mut self) -> Result<Account, AccountError> {
224 let (vault, code, storage) = self.build_inner()?;
225
226 #[cfg(any(feature = "testing", test))]
227 if !vault.is_empty() {
228 return Err(AccountError::BuildError(
229 "account asset vault must be empty on new accounts".into(),
230 None,
231 ));
232 }
233
234 let seed = self.grind_account_id(
235 self.init_seed,
236 self.id_version,
237 code.commitment(),
238 storage.to_commitment(),
239 )?;
240
241 let account_id = AccountId::new(
242 seed,
243 AccountIdVersion::Version1,
244 code.commitment(),
245 storage.to_commitment(),
246 )
247 .expect("get_account_seed should provide a suitable seed");
248
249 debug_assert_eq!(account_id.account_type(), self.account_type);
250 debug_assert_eq!(account_id.asset_callback_flag(), self.asset_callbacks);
251
252 let account =
255 Account::new_unchecked(account_id, vault, storage, code, Felt::ZERO, Some(seed));
256
257 Ok(account)
258 }
259}
260
261#[cfg(any(feature = "testing", test))]
262impl AccountBuilder {
263 pub fn with_assets<I: IntoIterator<Item = crate::asset::Asset>>(mut self, assets: I) -> Self {
268 self.assets.extend(assets);
269 self
270 }
271
272 pub fn nonce(mut self, nonce: Felt) -> Self {
277 self.nonce = Some(nonce);
278 self
279 }
280
281 pub fn build_existing(mut self) -> Result<Account, AccountError> {
287 let (vault, code, storage) = self.build_inner()?;
288
289 let account_id = {
290 let bytes = <[u8; 15]>::try_from(&self.init_seed[0..15])
291 .expect("we should have sliced exactly 15 bytes off");
292 AccountId::dummy(
293 bytes,
294 AccountIdVersion::Version1,
295 self.account_type,
296 self.asset_callbacks,
297 )
298 };
299
300 let nonce = self.nonce.unwrap_or(Felt::ONE);
302
303 Ok(Account::new_existing(account_id, vault, storage, code, nonce))
304 }
305}
306
307#[cfg(test)]
311mod tests {
312 use std::sync::LazyLock;
313
314 use assert_matches::assert_matches;
315 use miden_core::mast::MastNodeExt;
316 use miden_mast_package::Package;
317
318 use super::*;
319 use crate::account::component::AccountComponentMetadata;
320 use crate::account::{AccountProcedureRoot, StorageSlot, StorageSlotName};
321 use crate::testing::assembler::assemble_test_library;
322 use crate::testing::noop_auth_component::NoopAuthComponent;
323
324 const CUSTOM_CODE1: &str = "
325 @account_procedure
326 pub proc foo
327 push.2.2 add eq.4
328 end
329 ";
330 const CUSTOM_CODE2: &str = "
331 @account_procedure
332 pub proc bar
333 push.4.4 add eq.8
334 end
335 ";
336
337 static CUSTOM_LIBRARY1: LazyLock<Package> = LazyLock::new(|| {
338 assemble_test_library("custom-library-1", "custom::component1", CUSTOM_CODE1)
339 });
340 static CUSTOM_LIBRARY2: LazyLock<Package> = LazyLock::new(|| {
341 assemble_test_library("custom-library-2", "custom::component2", CUSTOM_CODE2)
342 });
343
344 static CUSTOM_COMPONENT1_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
345 StorageSlotName::new("custom::component1::slot0")
346 .expect("storage slot name should be valid")
347 });
348 static CUSTOM_COMPONENT2_SLOT_NAME0: LazyLock<StorageSlotName> = LazyLock::new(|| {
349 StorageSlotName::new("custom::component2::slot0")
350 .expect("storage slot name should be valid")
351 });
352 static CUSTOM_COMPONENT2_SLOT_NAME1: LazyLock<StorageSlotName> = LazyLock::new(|| {
353 StorageSlotName::new("custom::component2::slot1")
354 .expect("storage slot name should be valid")
355 });
356
357 struct CustomComponent1 {
358 slot0: u32,
359 }
360 impl From<CustomComponent1> for AccountComponent {
361 fn from(custom: CustomComponent1) -> Self {
362 let mut value = Word::empty();
363 value[0] = Felt::from(custom.slot0);
364
365 let metadata = AccountComponentMetadata::new("test::custom_component1");
366 AccountComponent::new(
367 CUSTOM_LIBRARY1.clone(),
368 vec![StorageSlot::with_value(CUSTOM_COMPONENT1_SLOT_NAME.clone(), value)],
369 metadata,
370 )
371 .expect("component should be valid")
372 }
373 }
374
375 struct CustomComponent2 {
376 slot0: u32,
377 slot1: u32,
378 }
379 impl From<CustomComponent2> for AccountComponent {
380 fn from(custom: CustomComponent2) -> Self {
381 let mut value0 = Word::empty();
382 value0[3] = Felt::from(custom.slot0);
383 let mut value1 = Word::empty();
384 value1[3] = Felt::from(custom.slot1);
385
386 let metadata = AccountComponentMetadata::new("test::custom_component2");
387 AccountComponent::new(
388 CUSTOM_LIBRARY2.clone(),
389 vec![
390 StorageSlot::with_value(CUSTOM_COMPONENT2_SLOT_NAME0.clone(), value0),
391 StorageSlot::with_value(CUSTOM_COMPONENT2_SLOT_NAME1.clone(), value1),
392 ],
393 metadata,
394 )
395 .expect("component should be valid")
396 }
397 }
398
399 #[test]
400 fn account_builder() {
401 let storage_slot0 = 25;
402 let storage_slot1 = 12;
403 let storage_slot2 = 42;
404
405 let account = Account::builder([5; 32])
406 .with_auth_component(NoopAuthComponent)
407 .with_component(CustomComponent1 { slot0: storage_slot0 })
408 .with_component(CustomComponent2 {
409 slot0: storage_slot1,
410 slot1: storage_slot2,
411 })
412 .build()
413 .unwrap();
414
415 assert_eq!(account.nonce(), Felt::ZERO);
417
418 let computed_id = AccountId::new(
419 account.seed().unwrap(),
420 AccountIdVersion::Version1,
421 account.code.commitment(),
422 account.storage.to_commitment(),
423 )
424 .unwrap();
425 assert_eq!(account.id(), computed_id);
426
427 assert_eq!(account.code.procedure_roots().count(), 3);
429
430 let foo_root = CUSTOM_LIBRARY1.mast_forest()[CUSTOM_LIBRARY1
431 .get_export_node_id(CUSTOM_LIBRARY1.manifest.exports().next().unwrap().path())]
432 .digest();
433 let bar_root = CUSTOM_LIBRARY2.mast_forest()[CUSTOM_LIBRARY2
434 .get_export_node_id(CUSTOM_LIBRARY2.manifest.exports().next().unwrap().path())]
435 .digest();
436
437 assert!(account.code().procedures().contains(&AccountProcedureRoot::from_raw(foo_root)));
438 assert!(account.code().procedures().contains(&AccountProcedureRoot::from_raw(bar_root)));
439
440 assert_eq!(
441 account.storage().get_item(&CUSTOM_COMPONENT1_SLOT_NAME).unwrap(),
442 Word::from([Felt::from(storage_slot0), Felt::ZERO, Felt::ZERO, Felt::ZERO])
443 );
444 assert_eq!(
445 account.storage().get_item(&CUSTOM_COMPONENT2_SLOT_NAME0).unwrap(),
446 Word::from([Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::from(storage_slot1)])
447 );
448 assert_eq!(
449 account.storage().get_item(&CUSTOM_COMPONENT2_SLOT_NAME1).unwrap(),
450 Word::from([Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::from(storage_slot2)])
451 );
452 }
453
454 #[test]
455 fn account_builder_with_components() {
456 let storage_slot0 = 25;
457 let storage_slot1 = 12;
458 let storage_slot2 = 42;
459
460 let components: Vec<AccountComponent> = vec![
461 CustomComponent1 { slot0: storage_slot0 }.into(),
462 CustomComponent2 {
463 slot0: storage_slot1,
464 slot1: storage_slot2,
465 }
466 .into(),
467 ];
468
469 let account = Account::builder([5; 32])
470 .with_auth_component(NoopAuthComponent)
471 .with_components(components)
472 .build()
473 .unwrap();
474
475 let expected = Account::builder([5; 32])
478 .with_auth_component(NoopAuthComponent)
479 .with_component(CustomComponent1 { slot0: storage_slot0 })
480 .with_component(CustomComponent2 {
481 slot0: storage_slot1,
482 slot1: storage_slot2,
483 })
484 .build()
485 .unwrap();
486
487 assert_eq!(account.id(), expected.id());
488 assert_eq!(account.code().commitment(), expected.code().commitment());
489 assert_eq!(account.storage().to_commitment(), expected.storage().to_commitment());
490
491 let account_no_extra = Account::builder([6; 32])
493 .with_auth_component(NoopAuthComponent)
494 .with_component(CustomComponent1 { slot0: storage_slot0 })
495 .with_components(core::iter::empty::<CustomComponent2>())
496 .build()
497 .unwrap();
498
499 let expected_no_extra = Account::builder([6; 32])
500 .with_auth_component(NoopAuthComponent)
501 .with_component(CustomComponent1 { slot0: storage_slot0 })
502 .build()
503 .unwrap();
504
505 assert_eq!(account_no_extra.id(), expected_no_extra.id());
506 }
507
508 #[test]
509 fn account_builder_non_empty_vault_on_new_account() {
510 let storage_slot0 = 25;
511
512 let build_error = Account::builder([0xff; 32])
513 .with_auth_component(NoopAuthComponent)
514 .with_component(CustomComponent1 { slot0: storage_slot0 })
515 .with_assets(AssetVault::mock().assets())
516 .build()
517 .unwrap_err();
518
519 assert_matches!(build_error, AccountError::BuildError(msg, _) if msg == "account asset vault must be empty on new accounts")
520 }
521
522 }