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};
15use crate::asset::AssetVault;
16use crate::errors::AccountError;
17use crate::{Felt, Word};
18
19#[derive(Debug, Clone)]
49pub struct AccountBuilder {
50 #[cfg(any(feature = "testing", test))]
51 assets: Vec<crate::asset::Asset>,
52 #[cfg(any(feature = "testing", test))]
53 nonce: Option<Felt>,
54 components: Vec<AccountComponent>,
55 auth_component: Option<AccountComponent>,
56 account_type: AccountType,
57 init_seed: [u8; 32],
58 id_version: AccountIdVersion,
59}
60
61impl AccountBuilder {
62 pub fn new(init_seed: [u8; 32]) -> Self {
67 Self {
68 #[cfg(any(feature = "testing", test))]
69 assets: vec![],
70 #[cfg(any(feature = "testing", test))]
71 nonce: None,
72 components: vec![],
73 auth_component: None,
74 init_seed,
75 account_type: AccountType::Private,
76 id_version: AccountIdVersion::Version1,
77 }
78 }
79
80 pub fn version(mut self, version: AccountIdVersion) -> Self {
82 self.id_version = version;
83 self
84 }
85
86 pub fn account_type(mut self, account_type: AccountType) -> Self {
88 self.account_type = account_type;
89 self
90 }
91
92 pub fn with_component(mut self, account_component: impl Into<AccountComponent>) -> Self {
100 self.components.push(account_component.into());
101 self
102 }
103
104 pub fn with_components(
112 mut self,
113 components: impl IntoIterator<Item = impl Into<AccountComponent>>,
114 ) -> Self {
115 for component in components {
116 self = self.with_component(component);
117 }
118 self
119 }
120
121 pub fn with_auth_component(mut self, account_component: impl Into<AccountComponent>) -> Self {
130 self.auth_component = Some(account_component.into());
131 self
132 }
133
134 pub fn storage_schemas(&self) -> impl Iterator<Item = &StorageSchema> + '_ {
136 self.auth_component
137 .iter()
138 .chain(self.components.iter())
139 .map(|component| component.storage_schema())
140 }
141
142 fn build_inner(&mut self) -> Result<(AssetVault, AccountCode, AccountStorage), AccountError> {
144 #[cfg(any(feature = "testing", test))]
145 let vault = AssetVault::new(&self.assets).map_err(|err| {
146 AccountError::BuildError(format!("asset vault failed to build: {err}"), None)
147 })?;
148
149 #[cfg(all(not(feature = "testing"), not(test)))]
150 let vault = AssetVault::default();
151
152 let auth_component = self
153 .auth_component
154 .take()
155 .ok_or(AccountError::BuildError("auth component must be set".into(), None))?;
156
157 let mut components = vec![auth_component];
158 components.append(&mut self.components);
159
160 let (code, storage) = Account::initialize_from_components(components).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 = AccountIdV1::compute_account_seed(
179 init_seed,
180 self.account_type,
181 version,
182 code_commitment,
183 storage_commitment,
184 )
185 .map_err(|err| {
186 AccountError::BuildError("account seed generation failed".into(), Some(Box::new(err)))
187 })?;
188
189 Ok(seed)
190 }
191
192 pub fn build(mut self) -> Result<Account, AccountError> {
209 let (vault, code, storage) = self.build_inner()?;
210
211 #[cfg(any(feature = "testing", test))]
212 if !vault.is_empty() {
213 return Err(AccountError::BuildError(
214 "account asset vault must be empty on new accounts".into(),
215 None,
216 ));
217 }
218
219 let seed = self.grind_account_id(
220 self.init_seed,
221 self.id_version,
222 code.commitment(),
223 storage.to_commitment(),
224 )?;
225
226 let account_id = AccountId::new(
227 seed,
228 AccountIdVersion::Version1,
229 code.commitment(),
230 storage.to_commitment(),
231 )
232 .expect("get_account_seed should provide a suitable seed");
233
234 debug_assert_eq!(account_id.account_type(), self.account_type);
235
236 let account =
239 Account::new_unchecked(account_id, vault, storage, code, Felt::ZERO, Some(seed));
240
241 Ok(account)
242 }
243}
244
245#[cfg(any(feature = "testing", test))]
246impl AccountBuilder {
247 pub fn with_assets<I: IntoIterator<Item = crate::asset::Asset>>(mut self, assets: I) -> Self {
252 self.assets.extend(assets);
253 self
254 }
255
256 pub fn nonce(mut self, nonce: Felt) -> Self {
261 self.nonce = Some(nonce);
262 self
263 }
264
265 pub fn build_existing(mut self) -> Result<Account, AccountError> {
271 let (vault, code, storage) = self.build_inner()?;
272
273 let account_id = {
274 let bytes = <[u8; 15]>::try_from(&self.init_seed[0..15])
275 .expect("we should have sliced exactly 15 bytes off");
276 AccountId::dummy(bytes, AccountIdVersion::Version1, self.account_type)
277 };
278
279 let nonce = self.nonce.unwrap_or(Felt::ONE);
281
282 Ok(Account::new_existing(account_id, vault, storage, code, nonce))
283 }
284}
285
286#[cfg(test)]
290mod tests {
291 use std::sync::{Arc, LazyLock};
292
293 use assert_matches::assert_matches;
294 use miden_assembly::{Assembler, Library};
295 use miden_core::mast::MastNodeExt;
296
297 use super::*;
298 use crate::account::component::AccountComponentMetadata;
299 use crate::account::{AccountProcedureRoot, StorageSlot, StorageSlotName};
300 use crate::testing::noop_auth_component::NoopAuthComponent;
301
302 const CUSTOM_CODE1: &str = "
303 pub proc foo
304 push.2.2 add eq.4
305 end
306 ";
307 const CUSTOM_CODE2: &str = "
308 pub proc bar
309 push.4.4 add eq.8
310 end
311 ";
312
313 static CUSTOM_LIBRARY1: LazyLock<Library> = LazyLock::new(|| {
314 Arc::unwrap_or_clone(
315 Assembler::default()
316 .assemble_library([CUSTOM_CODE1])
317 .expect("code should be valid"),
318 )
319 });
320 static CUSTOM_LIBRARY2: LazyLock<Library> = LazyLock::new(|| {
321 Arc::unwrap_or_clone(
322 Assembler::default()
323 .assemble_library([CUSTOM_CODE2])
324 .expect("code should be valid"),
325 )
326 });
327
328 static CUSTOM_COMPONENT1_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
329 StorageSlotName::new("custom::component1::slot0")
330 .expect("storage slot name should be valid")
331 });
332 static CUSTOM_COMPONENT2_SLOT_NAME0: LazyLock<StorageSlotName> = LazyLock::new(|| {
333 StorageSlotName::new("custom::component2::slot0")
334 .expect("storage slot name should be valid")
335 });
336 static CUSTOM_COMPONENT2_SLOT_NAME1: LazyLock<StorageSlotName> = LazyLock::new(|| {
337 StorageSlotName::new("custom::component2::slot1")
338 .expect("storage slot name should be valid")
339 });
340
341 struct CustomComponent1 {
342 slot0: u32,
343 }
344 impl From<CustomComponent1> for AccountComponent {
345 fn from(custom: CustomComponent1) -> Self {
346 let mut value = Word::empty();
347 value[0] = Felt::from(custom.slot0);
348
349 let metadata = AccountComponentMetadata::new("test::custom_component1");
350 AccountComponent::new(
351 CUSTOM_LIBRARY1.clone(),
352 vec![StorageSlot::with_value(CUSTOM_COMPONENT1_SLOT_NAME.clone(), value)],
353 metadata,
354 )
355 .expect("component should be valid")
356 }
357 }
358
359 struct CustomComponent2 {
360 slot0: u32,
361 slot1: u32,
362 }
363 impl From<CustomComponent2> for AccountComponent {
364 fn from(custom: CustomComponent2) -> Self {
365 let mut value0 = Word::empty();
366 value0[3] = Felt::from(custom.slot0);
367 let mut value1 = Word::empty();
368 value1[3] = Felt::from(custom.slot1);
369
370 let metadata = AccountComponentMetadata::new("test::custom_component2");
371 AccountComponent::new(
372 CUSTOM_LIBRARY2.clone(),
373 vec![
374 StorageSlot::with_value(CUSTOM_COMPONENT2_SLOT_NAME0.clone(), value0),
375 StorageSlot::with_value(CUSTOM_COMPONENT2_SLOT_NAME1.clone(), value1),
376 ],
377 metadata,
378 )
379 .expect("component should be valid")
380 }
381 }
382
383 #[test]
384 fn account_builder() {
385 let storage_slot0 = 25;
386 let storage_slot1 = 12;
387 let storage_slot2 = 42;
388
389 let account = Account::builder([5; 32])
390 .with_auth_component(NoopAuthComponent)
391 .with_component(CustomComponent1 { slot0: storage_slot0 })
392 .with_component(CustomComponent2 {
393 slot0: storage_slot1,
394 slot1: storage_slot2,
395 })
396 .build()
397 .unwrap();
398
399 assert_eq!(account.nonce(), Felt::ZERO);
401
402 let computed_id = AccountId::new(
403 account.seed().unwrap(),
404 AccountIdVersion::Version1,
405 account.code.commitment(),
406 account.storage.to_commitment(),
407 )
408 .unwrap();
409 assert_eq!(account.id(), computed_id);
410
411 assert_eq!(account.code.procedure_roots().count(), 3);
413
414 let foo_root = CUSTOM_LIBRARY1.mast_forest()
415 [CUSTOM_LIBRARY1.get_export_node_id(CUSTOM_LIBRARY1.exports().next().unwrap().path())]
416 .digest();
417 let bar_root = CUSTOM_LIBRARY2.mast_forest()
418 [CUSTOM_LIBRARY2.get_export_node_id(CUSTOM_LIBRARY2.exports().next().unwrap().path())]
419 .digest();
420
421 assert!(account.code().procedures().contains(&AccountProcedureRoot::from_raw(foo_root)));
422 assert!(account.code().procedures().contains(&AccountProcedureRoot::from_raw(bar_root)));
423
424 assert_eq!(
425 account.storage().get_item(&CUSTOM_COMPONENT1_SLOT_NAME).unwrap(),
426 Word::from([Felt::from(storage_slot0), Felt::ZERO, Felt::ZERO, Felt::ZERO])
427 );
428 assert_eq!(
429 account.storage().get_item(&CUSTOM_COMPONENT2_SLOT_NAME0).unwrap(),
430 Word::from([Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::from(storage_slot1)])
431 );
432 assert_eq!(
433 account.storage().get_item(&CUSTOM_COMPONENT2_SLOT_NAME1).unwrap(),
434 Word::from([Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::from(storage_slot2)])
435 );
436 }
437
438 #[test]
439 fn account_builder_with_components() {
440 let storage_slot0 = 25;
441 let storage_slot1 = 12;
442 let storage_slot2 = 42;
443
444 let components: Vec<AccountComponent> = vec![
445 CustomComponent1 { slot0: storage_slot0 }.into(),
446 CustomComponent2 {
447 slot0: storage_slot1,
448 slot1: storage_slot2,
449 }
450 .into(),
451 ];
452
453 let account = Account::builder([5; 32])
454 .with_auth_component(NoopAuthComponent)
455 .with_components(components)
456 .build()
457 .unwrap();
458
459 let expected = Account::builder([5; 32])
462 .with_auth_component(NoopAuthComponent)
463 .with_component(CustomComponent1 { slot0: storage_slot0 })
464 .with_component(CustomComponent2 {
465 slot0: storage_slot1,
466 slot1: storage_slot2,
467 })
468 .build()
469 .unwrap();
470
471 assert_eq!(account.id(), expected.id());
472 assert_eq!(account.code().commitment(), expected.code().commitment());
473 assert_eq!(account.storage().to_commitment(), expected.storage().to_commitment());
474
475 let account_no_extra = Account::builder([6; 32])
477 .with_auth_component(NoopAuthComponent)
478 .with_component(CustomComponent1 { slot0: storage_slot0 })
479 .with_components(core::iter::empty::<CustomComponent2>())
480 .build()
481 .unwrap();
482
483 let expected_no_extra = Account::builder([6; 32])
484 .with_auth_component(NoopAuthComponent)
485 .with_component(CustomComponent1 { slot0: storage_slot0 })
486 .build()
487 .unwrap();
488
489 assert_eq!(account_no_extra.id(), expected_no_extra.id());
490 }
491
492 #[test]
493 fn account_builder_non_empty_vault_on_new_account() {
494 let storage_slot0 = 25;
495
496 let build_error = Account::builder([0xff; 32])
497 .with_auth_component(NoopAuthComponent)
498 .with_component(CustomComponent1 { slot0: storage_slot0 })
499 .with_assets(AssetVault::mock().assets())
500 .build()
501 .unwrap_err();
502
503 assert_matches!(build_error, AccountError::BuildError(msg, _) if msg == "account asset vault must be empty on new accounts")
504 }
505
506 }