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)]
55pub struct AccountBuilder {
56 #[cfg(any(feature = "testing", test))]
57 assets: Vec<crate::asset::Asset>,
58 #[cfg(any(feature = "testing", test))]
59 nonce: Option<Felt>,
60 components: Vec<AccountComponent>,
61 auth_component: Option<AccountComponent>,
62 account_type: AccountType,
63 storage_mode: AccountStorageMode,
64 init_seed: [u8; 32],
65 id_version: AccountIdVersion,
66}
67
68impl AccountBuilder {
69 pub fn new(init_seed: [u8; 32]) -> Self {
74 Self {
75 #[cfg(any(feature = "testing", test))]
76 assets: vec![],
77 #[cfg(any(feature = "testing", test))]
78 nonce: None,
79 components: vec![],
80 auth_component: None,
81 init_seed,
82 account_type: AccountType::RegularAccountUpdatableCode,
83 storage_mode: AccountStorageMode::Private,
84 id_version: AccountIdVersion::Version0,
85 }
86 }
87
88 pub fn version(mut self, version: AccountIdVersion) -> Self {
90 self.id_version = version;
91 self
92 }
93
94 pub fn account_type(mut self, account_type: AccountType) -> Self {
96 self.account_type = account_type;
97 self
98 }
99
100 pub fn storage_mode(mut self, storage_mode: AccountStorageMode) -> Self {
102 self.storage_mode = storage_mode;
103 self
104 }
105
106 pub fn with_component(mut self, account_component: impl Into<AccountComponent>) -> Self {
111 self.components.push(account_component.into());
112 self
113 }
114
115 pub fn with_auth_component(mut self, account_component: impl Into<AccountComponent>) -> Self {
124 self.auth_component = Some(account_component.into());
125 self
126 }
127
128 pub fn storage_schemas(&self) -> impl Iterator<Item = &StorageSchema> + '_ {
130 self.auth_component
131 .iter()
132 .chain(self.components.iter())
133 .map(|component| component.storage_schema())
134 }
135
136 fn build_inner(&mut self) -> Result<(AssetVault, AccountCode, AccountStorage), AccountError> {
138 #[cfg(any(feature = "testing", test))]
139 let vault = AssetVault::new(&self.assets).map_err(|err| {
140 AccountError::BuildError(format!("asset vault failed to build: {err}"), None)
141 })?;
142
143 #[cfg(all(not(feature = "testing"), not(test)))]
144 let vault = AssetVault::default();
145
146 let auth_component = self
147 .auth_component
148 .take()
149 .ok_or(AccountError::BuildError("auth component must be set".into(), None))?;
150
151 let mut components = vec![auth_component];
152 components.append(&mut self.components);
153
154 let (code, storage) = Account::initialize_from_components(self.account_type, components)
155 .map_err(|err| {
156 AccountError::BuildError(
157 "account components failed to build".into(),
158 Some(Box::new(err)),
159 )
160 })?;
161
162 Ok((vault, code, storage))
163 }
164
165 fn grind_account_id(
167 &self,
168 init_seed: [u8; 32],
169 version: AccountIdVersion,
170 code_commitment: Word,
171 storage_commitment: Word,
172 ) -> Result<Word, AccountError> {
173 let seed = AccountIdV0::compute_account_seed(
174 init_seed,
175 self.account_type,
176 self.storage_mode,
177 version,
178 code_commitment,
179 storage_commitment,
180 )
181 .map_err(|err| {
182 AccountError::BuildError("account seed generation failed".into(), Some(Box::new(err)))
183 })?;
184
185 Ok(seed)
186 }
187
188 pub fn build(mut self) -> Result<Account, AccountError> {
205 let (vault, code, storage) = self.build_inner()?;
206
207 #[cfg(any(feature = "testing", test))]
208 if !vault.is_empty() {
209 return Err(AccountError::BuildError(
210 "account asset vault must be empty on new accounts".into(),
211 None,
212 ));
213 }
214
215 let seed = self.grind_account_id(
216 self.init_seed,
217 self.id_version,
218 code.commitment(),
219 storage.to_commitment(),
220 )?;
221
222 let account_id = AccountId::new(
223 seed,
224 AccountIdVersion::Version0,
225 code.commitment(),
226 storage.to_commitment(),
227 )
228 .expect("get_account_seed should provide a suitable seed");
229
230 debug_assert_eq!(account_id.account_type(), self.account_type);
231 debug_assert_eq!(account_id.storage_mode(), self.storage_mode);
232
233 let account =
236 Account::new_unchecked(account_id, vault, storage, code, Felt::ZERO, Some(seed));
237
238 Ok(account)
239 }
240}
241
242#[cfg(any(feature = "testing", test))]
243impl AccountBuilder {
244 pub fn with_assets<I: IntoIterator<Item = crate::asset::Asset>>(mut self, assets: I) -> Self {
249 self.assets.extend(assets);
250 self
251 }
252
253 pub fn nonce(mut self, nonce: Felt) -> Self {
258 self.nonce = Some(nonce);
259 self
260 }
261
262 pub fn build_existing(mut self) -> Result<Account, AccountError> {
268 let (vault, code, storage) = self.build_inner()?;
269
270 let account_id = {
271 let bytes = <[u8; 15]>::try_from(&self.init_seed[0..15])
272 .expect("we should have sliced exactly 15 bytes off");
273 AccountId::dummy(
274 bytes,
275 AccountIdVersion::Version0,
276 self.account_type,
277 self.storage_mode,
278 )
279 };
280
281 let nonce = self.nonce.unwrap_or(Felt::ONE);
283
284 Ok(Account::new_existing(account_id, vault, storage, code, nonce))
285 }
286}
287
288#[cfg(test)]
292mod tests {
293 use std::sync::LazyLock;
294
295 use assert_matches::assert_matches;
296 use miden_assembly::{Assembler, Library};
297 use miden_core::FieldElement;
298 use miden_processor::MastNodeExt;
299
300 use super::*;
301 use crate::account::component::AccountComponentMetadata;
302 use crate::account::{AccountProcedureRoot, StorageSlot, StorageSlotName};
303 use crate::testing::noop_auth_component::NoopAuthComponent;
304
305 const CUSTOM_CODE1: &str = "
306 pub proc foo
307 push.2.2 add eq.4
308 end
309 ";
310 const CUSTOM_CODE2: &str = "
311 pub proc bar
312 push.4.4 add eq.8
313 end
314 ";
315
316 static CUSTOM_LIBRARY1: LazyLock<Library> = LazyLock::new(|| {
317 Assembler::default()
318 .assemble_library([CUSTOM_CODE1])
319 .expect("code should be valid")
320 });
321 static CUSTOM_LIBRARY2: LazyLock<Library> = LazyLock::new(|| {
322 Assembler::default()
323 .assemble_library([CUSTOM_CODE2])
324 .expect("code should be valid")
325 });
326
327 static CUSTOM_COMPONENT1_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
328 StorageSlotName::new("custom::component1::slot0")
329 .expect("storage slot name should be valid")
330 });
331 static CUSTOM_COMPONENT2_SLOT_NAME0: LazyLock<StorageSlotName> = LazyLock::new(|| {
332 StorageSlotName::new("custom::component2::slot0")
333 .expect("storage slot name should be valid")
334 });
335 static CUSTOM_COMPONENT2_SLOT_NAME1: LazyLock<StorageSlotName> = LazyLock::new(|| {
336 StorageSlotName::new("custom::component2::slot1")
337 .expect("storage slot name should be valid")
338 });
339
340 struct CustomComponent1 {
341 slot0: u64,
342 }
343 impl From<CustomComponent1> for AccountComponent {
344 fn from(custom: CustomComponent1) -> Self {
345 let mut value = Word::empty();
346 value[0] = Felt::new(custom.slot0);
347
348 let metadata =
349 AccountComponentMetadata::new("test::custom_component1").with_supports_all_types();
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: u64,
361 slot1: u64,
362 }
363 impl From<CustomComponent2> for AccountComponent {
364 fn from(custom: CustomComponent2) -> Self {
365 let mut value0 = Word::empty();
366 value0[3] = Felt::new(custom.slot0);
367 let mut value1 = Word::empty();
368 value1[3] = Felt::new(custom.slot1);
369
370 let metadata =
371 AccountComponentMetadata::new("test::custom_component2").with_supports_all_types();
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 metadata,
379 )
380 .expect("component should be valid")
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 }