miden_objects/account/builder/
mod.rs1use alloc::boxed::Box;
2use alloc::vec::Vec;
3
4use miden_core::FieldElement;
5
6use crate::account::{
7 Account,
8 AccountCode,
9 AccountComponent,
10 AccountId,
11 AccountIdV0,
12 AccountIdVersion,
13 AccountStorage,
14 AccountStorageMode,
15 AccountType,
16};
17use crate::asset::AssetVault;
18use crate::{AccountError, Felt, Word};
19
20#[derive(Debug, Clone)]
56pub struct AccountBuilder {
57 #[cfg(any(feature = "testing", test))]
58 assets: Vec<crate::asset::Asset>,
59 #[cfg(any(feature = "testing", test))]
60 nonce: Option<Felt>,
61 components: Vec<AccountComponent>,
62 auth_component: Option<AccountComponent>,
63 account_type: AccountType,
64 storage_mode: AccountStorageMode,
65 init_seed: [u8; 32],
66 id_version: AccountIdVersion,
67}
68
69impl AccountBuilder {
70 pub fn new(init_seed: [u8; 32]) -> Self {
75 Self {
76 #[cfg(any(feature = "testing", test))]
77 assets: vec![],
78 #[cfg(any(feature = "testing", test))]
79 nonce: None,
80 components: vec![],
81 auth_component: None,
82 init_seed,
83 account_type: AccountType::RegularAccountUpdatableCode,
84 storage_mode: AccountStorageMode::Private,
85 id_version: AccountIdVersion::Version0,
86 }
87 }
88
89 pub fn version(mut self, version: AccountIdVersion) -> Self {
91 self.id_version = version;
92 self
93 }
94
95 pub fn account_type(mut self, account_type: AccountType) -> Self {
97 self.account_type = account_type;
98 self
99 }
100
101 pub fn storage_mode(mut self, storage_mode: AccountStorageMode) -> Self {
103 self.storage_mode = storage_mode;
104 self
105 }
106
107 pub fn with_component(mut self, account_component: impl Into<AccountComponent>) -> Self {
112 self.components.push(account_component.into());
113 self
114 }
115
116 pub fn with_auth_component(mut self, account_component: impl Into<AccountComponent>) -> Self {
125 self.auth_component = Some(account_component.into());
126 self
127 }
128
129 fn build_inner(&mut self) -> Result<(AssetVault, AccountCode, AccountStorage), AccountError> {
131 #[cfg(any(feature = "testing", test))]
132 let vault = AssetVault::new(&self.assets).map_err(|err| {
133 AccountError::BuildError(format!("asset vault failed to build: {err}"), None)
134 })?;
135
136 #[cfg(all(not(feature = "testing"), not(test)))]
137 let vault = AssetVault::default();
138
139 let auth_component = self
140 .auth_component
141 .take()
142 .ok_or(AccountError::BuildError("auth component must be set".into(), None))?;
143
144 let mut components = vec![auth_component];
145 components.append(&mut self.components);
146
147 let (code, storage) = Account::initialize_from_components(self.account_type, &components)
148 .map_err(|err| {
149 AccountError::BuildError(
150 "account components failed to build".into(),
151 Some(Box::new(err)),
152 )
153 })?;
154
155 Ok((vault, code, storage))
156 }
157
158 fn grind_account_id(
160 &self,
161 init_seed: [u8; 32],
162 version: AccountIdVersion,
163 code_commitment: Word,
164 storage_commitment: Word,
165 ) -> Result<Word, AccountError> {
166 let seed = AccountIdV0::compute_account_seed(
167 init_seed,
168 self.account_type,
169 self.storage_mode,
170 version,
171 code_commitment,
172 storage_commitment,
173 )
174 .map_err(|err| {
175 AccountError::BuildError("account seed generation failed".into(), Some(Box::new(err)))
176 })?;
177
178 Ok(seed)
179 }
180
181 pub fn build(mut self) -> Result<Account, AccountError> {
198 let (vault, code, storage) = self.build_inner()?;
199
200 #[cfg(any(feature = "testing", test))]
201 if !vault.is_empty() {
202 return Err(AccountError::BuildError(
203 "account asset vault must be empty on new accounts".into(),
204 None,
205 ));
206 }
207
208 let seed = self.grind_account_id(
209 self.init_seed,
210 self.id_version,
211 code.commitment(),
212 storage.commitment(),
213 )?;
214
215 let account_id = AccountId::new(
216 seed,
217 AccountIdVersion::Version0,
218 code.commitment(),
219 storage.commitment(),
220 )
221 .expect("get_account_seed should provide a suitable seed");
222
223 debug_assert_eq!(account_id.account_type(), self.account_type);
224 debug_assert_eq!(account_id.storage_mode(), self.storage_mode);
225
226 let account =
229 Account::new_unchecked(account_id, vault, storage, code, Felt::ZERO, Some(seed));
230
231 Ok(account)
232 }
233}
234
235#[cfg(any(feature = "testing", test))]
236impl AccountBuilder {
237 pub fn with_assets<I: IntoIterator<Item = crate::asset::Asset>>(mut self, assets: I) -> Self {
242 self.assets.extend(assets);
243 self
244 }
245
246 pub fn nonce(mut self, nonce: Felt) -> Self {
251 self.nonce = Some(nonce);
252 self
253 }
254
255 pub fn build_existing(mut self) -> Result<Account, AccountError> {
261 let (vault, code, storage) = self.build_inner()?;
262
263 let account_id = {
264 let bytes = <[u8; 15]>::try_from(&self.init_seed[0..15])
265 .expect("we should have sliced exactly 15 bytes off");
266 AccountId::dummy(
267 bytes,
268 AccountIdVersion::Version0,
269 self.account_type,
270 self.storage_mode,
271 )
272 };
273
274 let nonce = self.nonce.unwrap_or(Felt::ONE);
276
277 Ok(Account::new_existing(account_id, vault, storage, code, nonce))
278 }
279}
280
281#[cfg(test)]
285mod tests {
286 use std::sync::LazyLock;
287
288 use assert_matches::assert_matches;
289 use miden_assembly::{Assembler, Library};
290 use miden_core::FieldElement;
291 use miden_processor::MastNodeExt;
292
293 use super::*;
294 use crate::account::StorageSlot;
295 use crate::testing::noop_auth_component::NoopAuthComponent;
296
297 const CUSTOM_CODE1: &str = "
298 export.foo
299 push.2.2 add eq.4
300 end
301 ";
302 const CUSTOM_CODE2: &str = "
303 export.bar
304 push.4.4 add eq.8
305 end
306 ";
307
308 static CUSTOM_LIBRARY1: LazyLock<Library> = LazyLock::new(|| {
309 Assembler::default()
310 .assemble_library([CUSTOM_CODE1])
311 .expect("code should be valid")
312 });
313 static CUSTOM_LIBRARY2: LazyLock<Library> = LazyLock::new(|| {
314 Assembler::default()
315 .assemble_library([CUSTOM_CODE2])
316 .expect("code should be valid")
317 });
318
319 struct CustomComponent1 {
320 slot0: u64,
321 }
322 impl From<CustomComponent1> for AccountComponent {
323 fn from(custom: CustomComponent1) -> Self {
324 let mut value = Word::empty();
325 value[0] = Felt::new(custom.slot0);
326
327 AccountComponent::new(CUSTOM_LIBRARY1.clone(), vec![StorageSlot::Value(value)])
328 .expect("component should be valid")
329 .with_supports_all_types()
330 }
331 }
332
333 struct CustomComponent2 {
334 slot0: u64,
335 slot1: u64,
336 }
337 impl From<CustomComponent2> for AccountComponent {
338 fn from(custom: CustomComponent2) -> Self {
339 let mut value0 = Word::empty();
340 value0[3] = Felt::new(custom.slot0);
341 let mut value1 = Word::empty();
342 value1[3] = Felt::new(custom.slot1);
343
344 AccountComponent::new(
345 CUSTOM_LIBRARY2.clone(),
346 vec![StorageSlot::Value(value0), StorageSlot::Value(value1)],
347 )
348 .expect("component should be valid")
349 .with_supports_all_types()
350 }
351 }
352
353 #[test]
354 fn account_builder() {
355 let storage_slot0 = 25;
356 let storage_slot1 = 12;
357 let storage_slot2 = 42;
358
359 let account = Account::builder([5; 32])
360 .with_auth_component(NoopAuthComponent)
361 .with_component(CustomComponent1 { slot0: storage_slot0 })
362 .with_component(CustomComponent2 {
363 slot0: storage_slot1,
364 slot1: storage_slot2,
365 })
366 .build()
367 .unwrap();
368
369 assert_eq!(account.nonce(), Felt::ZERO);
371
372 let computed_id = AccountId::new(
373 account.seed().unwrap(),
374 AccountIdVersion::Version0,
375 account.code.commitment(),
376 account.storage.commitment(),
377 )
378 .unwrap();
379 assert_eq!(account.id(), computed_id);
380
381 assert_eq!(account.code.procedure_roots().count(), 3);
383
384 let foo_root = CUSTOM_LIBRARY1.mast_forest()
385 [CUSTOM_LIBRARY1.get_export_node_id(&CUSTOM_LIBRARY1.exports().next().unwrap().name)]
386 .digest();
387 let bar_root = CUSTOM_LIBRARY2.mast_forest()
388 [CUSTOM_LIBRARY2.get_export_node_id(&CUSTOM_LIBRARY2.exports().next().unwrap().name)]
389 .digest();
390
391 let foo_procedure_info = &account
392 .code()
393 .procedures()
394 .iter()
395 .find(|info| info.mast_root() == &foo_root)
396 .unwrap();
397 assert_eq!(foo_procedure_info.storage_offset(), 0);
398 assert_eq!(foo_procedure_info.storage_size(), 1);
399
400 let bar_procedure_info = &account
401 .code()
402 .procedures()
403 .iter()
404 .find(|info| info.mast_root() == &bar_root)
405 .unwrap();
406 assert_eq!(bar_procedure_info.storage_offset(), 1);
407 assert_eq!(bar_procedure_info.storage_size(), 2);
408
409 assert_eq!(
410 account.storage().get_item(0).unwrap(),
411 [Felt::new(storage_slot0), Felt::new(0), Felt::new(0), Felt::new(0)].into()
412 );
413 assert_eq!(
414 account.storage().get_item(1).unwrap(),
415 [Felt::new(0), Felt::new(0), Felt::new(0), Felt::new(storage_slot1)].into()
416 );
417 assert_eq!(
418 account.storage().get_item(2).unwrap(),
419 [Felt::new(0), Felt::new(0), Felt::new(0), Felt::new(storage_slot2)].into()
420 );
421 }
422
423 #[test]
424 fn account_builder_non_empty_vault_on_new_account() {
425 let storage_slot0 = 25;
426
427 let build_error = Account::builder([0xff; 32])
428 .with_auth_component(NoopAuthComponent)
429 .with_component(CustomComponent1 { slot0: storage_slot0 })
430 .with_assets(AssetVault::mock().assets())
431 .build()
432 .unwrap_err();
433
434 assert_matches!(build_error, AccountError::BuildError(msg, _) if msg == "account asset vault must be empty on new accounts")
435 }
436
437 }