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 AccountIdV0,
11 AccountIdVersion,
12 AccountStorage,
13 AccountStorageMode,
14 AccountType,
15};
16use crate::asset::AssetVault;
17use crate::errors::AccountError;
18use crate::{Felt, Word};
19
20#[derive(Debug, Clone)]
53pub struct AccountBuilder {
54 #[cfg(any(feature = "testing", test))]
55 assets: Vec<crate::asset::Asset>,
56 #[cfg(any(feature = "testing", test))]
57 nonce: Option<Felt>,
58 components: Vec<AccountComponent>,
59 auth_component: Option<AccountComponent>,
60 account_type: AccountType,
61 storage_mode: AccountStorageMode,
62 init_seed: [u8; 32],
63 id_version: AccountIdVersion,
64}
65
66impl AccountBuilder {
67 pub fn new(init_seed: [u8; 32]) -> Self {
72 Self {
73 #[cfg(any(feature = "testing", test))]
74 assets: vec![],
75 #[cfg(any(feature = "testing", test))]
76 nonce: None,
77 components: vec![],
78 auth_component: None,
79 init_seed,
80 account_type: AccountType::RegularAccountUpdatableCode,
81 storage_mode: AccountStorageMode::Private,
82 id_version: AccountIdVersion::Version0,
83 }
84 }
85
86 pub fn version(mut self, version: AccountIdVersion) -> Self {
88 self.id_version = version;
89 self
90 }
91
92 pub fn account_type(mut self, account_type: AccountType) -> Self {
94 self.account_type = account_type;
95 self
96 }
97
98 pub fn storage_mode(mut self, storage_mode: AccountStorageMode) -> Self {
100 self.storage_mode = storage_mode;
101 self
102 }
103
104 pub fn with_component(mut self, account_component: impl Into<AccountComponent>) -> Self {
109 self.components.push(account_component.into());
110 self
111 }
112
113 pub fn with_auth_component(mut self, account_component: impl Into<AccountComponent>) -> Self {
122 self.auth_component = Some(account_component.into());
123 self
124 }
125
126 pub fn storage_schemas(&self) -> impl Iterator<Item = &StorageSchema> + '_ {
128 self.auth_component
129 .iter()
130 .chain(self.components.iter())
131 .map(|component| component.storage_schema())
132 }
133
134 fn build_inner(&mut self) -> Result<(AssetVault, AccountCode, AccountStorage), AccountError> {
136 #[cfg(any(feature = "testing", test))]
137 let vault = AssetVault::new(&self.assets).map_err(|err| {
138 AccountError::BuildError(format!("asset vault failed to build: {err}"), None)
139 })?;
140
141 #[cfg(all(not(feature = "testing"), not(test)))]
142 let vault = AssetVault::default();
143
144 let auth_component = self
145 .auth_component
146 .take()
147 .ok_or(AccountError::BuildError("auth component must be set".into(), None))?;
148
149 let mut components = vec![auth_component];
150 components.append(&mut self.components);
151
152 let (code, storage) = Account::initialize_from_components(self.account_type, components)
153 .map_err(|err| {
154 AccountError::BuildError(
155 "account components failed to build".into(),
156 Some(Box::new(err)),
157 )
158 })?;
159
160 Ok((vault, code, storage))
161 }
162
163 fn grind_account_id(
165 &self,
166 init_seed: [u8; 32],
167 version: AccountIdVersion,
168 code_commitment: Word,
169 storage_commitment: Word,
170 ) -> Result<Word, AccountError> {
171 let seed = AccountIdV0::compute_account_seed(
172 init_seed,
173 self.account_type,
174 self.storage_mode,
175 version,
176 code_commitment,
177 storage_commitment,
178 )
179 .map_err(|err| {
180 AccountError::BuildError("account seed generation failed".into(), Some(Box::new(err)))
181 })?;
182
183 Ok(seed)
184 }
185
186 pub fn build(mut self) -> Result<Account, AccountError> {
204 let (vault, code, storage) = self.build_inner()?;
205
206 #[cfg(any(feature = "testing", test))]
207 if !vault.is_empty() {
208 return Err(AccountError::BuildError(
209 "account asset vault must be empty on new accounts".into(),
210 None,
211 ));
212 }
213
214 let seed = self.grind_account_id(
215 self.init_seed,
216 self.id_version,
217 code.commitment(),
218 storage.to_commitment(),
219 )?;
220
221 let account_id = AccountId::new(
222 seed,
223 AccountIdVersion::Version0,
224 code.commitment(),
225 storage.to_commitment(),
226 )
227 .expect("get_account_seed should provide a suitable seed");
228
229 debug_assert_eq!(account_id.account_type(), self.account_type);
230 debug_assert_eq!(account_id.storage_mode(), self.storage_mode);
231
232 let account =
235 Account::new_unchecked(account_id, vault, storage, code, Felt::ZERO, Some(seed));
236
237 Ok(account)
238 }
239}
240
241#[cfg(any(feature = "testing", test))]
242impl AccountBuilder {
243 pub fn with_assets<I: IntoIterator<Item = crate::asset::Asset>>(mut self, assets: I) -> Self {
248 self.assets.extend(assets);
249 self
250 }
251
252 pub fn nonce(mut self, nonce: Felt) -> Self {
257 self.nonce = Some(nonce);
258 self
259 }
260
261 pub fn build_existing(mut self) -> Result<Account, AccountError> {
267 let (vault, code, storage) = self.build_inner()?;
268
269 let account_id = {
270 let bytes = <[u8; 15]>::try_from(&self.init_seed[0..15])
271 .expect("we should have sliced exactly 15 bytes off");
272 AccountId::dummy(
273 bytes,
274 AccountIdVersion::Version0,
275 self.account_type,
276 self.storage_mode,
277 )
278 };
279
280 let nonce = self.nonce.unwrap_or(Felt::ONE);
282
283 Ok(Account::new_existing(account_id, vault, storage, code, nonce))
284 }
285}
286
287#[cfg(test)]
291mod tests {
292 use std::sync::{Arc, LazyLock};
293
294 use assert_matches::assert_matches;
295 use miden_assembly::{Assembler, Library};
296 use miden_core::mast::MastNodeExt;
297
298 use super::*;
299 use crate::account::component::AccountComponentMetadata;
300 use crate::account::{AccountProcedureRoot, StorageSlot, StorageSlotName};
301 use crate::testing::noop_auth_component::NoopAuthComponent;
302
303 const CUSTOM_CODE1: &str = "
304 pub proc foo
305 push.2.2 add eq.4
306 end
307 ";
308 const CUSTOM_CODE2: &str = "
309 pub proc bar
310 push.4.4 add eq.8
311 end
312 ";
313
314 static CUSTOM_LIBRARY1: LazyLock<Library> = LazyLock::new(|| {
315 Arc::unwrap_or_clone(
316 Assembler::default()
317 .assemble_library([CUSTOM_CODE1])
318 .expect("code should be valid"),
319 )
320 });
321 static CUSTOM_LIBRARY2: LazyLock<Library> = LazyLock::new(|| {
322 Arc::unwrap_or_clone(
323 Assembler::default()
324 .assemble_library([CUSTOM_CODE2])
325 .expect("code should be valid"),
326 )
327 });
328
329 static CUSTOM_COMPONENT1_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
330 StorageSlotName::new("custom::component1::slot0")
331 .expect("storage slot name should be valid")
332 });
333 static CUSTOM_COMPONENT2_SLOT_NAME0: LazyLock<StorageSlotName> = LazyLock::new(|| {
334 StorageSlotName::new("custom::component2::slot0")
335 .expect("storage slot name should be valid")
336 });
337 static CUSTOM_COMPONENT2_SLOT_NAME1: LazyLock<StorageSlotName> = LazyLock::new(|| {
338 StorageSlotName::new("custom::component2::slot1")
339 .expect("storage slot name should be valid")
340 });
341
342 struct CustomComponent1 {
343 slot0: u64,
344 }
345 impl From<CustomComponent1> for AccountComponent {
346 fn from(custom: CustomComponent1) -> Self {
347 let mut value = Word::empty();
348 value[0] = Felt::new(custom.slot0);
349
350 let metadata =
351 AccountComponentMetadata::new("test::custom_component1", AccountType::all());
352 AccountComponent::new(
353 CUSTOM_LIBRARY1.clone(),
354 vec![StorageSlot::with_value(CUSTOM_COMPONENT1_SLOT_NAME.clone(), value)],
355 metadata,
356 )
357 .expect("component should be valid")
358 }
359 }
360
361 struct CustomComponent2 {
362 slot0: u64,
363 slot1: u64,
364 }
365 impl From<CustomComponent2> for AccountComponent {
366 fn from(custom: CustomComponent2) -> Self {
367 let mut value0 = Word::empty();
368 value0[3] = Felt::new(custom.slot0);
369 let mut value1 = Word::empty();
370 value1[3] = Felt::new(custom.slot1);
371
372 let metadata =
373 AccountComponentMetadata::new("test::custom_component2", AccountType::all());
374 AccountComponent::new(
375 CUSTOM_LIBRARY2.clone(),
376 vec![
377 StorageSlot::with_value(CUSTOM_COMPONENT2_SLOT_NAME0.clone(), value0),
378 StorageSlot::with_value(CUSTOM_COMPONENT2_SLOT_NAME1.clone(), value1),
379 ],
380 metadata,
381 )
382 .expect("component should be valid")
383 }
384 }
385
386 #[test]
387 fn account_builder() {
388 let storage_slot0 = 25;
389 let storage_slot1 = 12;
390 let storage_slot2 = 42;
391
392 let account = Account::builder([5; 32])
393 .with_auth_component(NoopAuthComponent)
394 .with_component(CustomComponent1 { slot0: storage_slot0 })
395 .with_component(CustomComponent2 {
396 slot0: storage_slot1,
397 slot1: storage_slot2,
398 })
399 .build()
400 .unwrap();
401
402 assert_eq!(account.nonce(), Felt::ZERO);
404
405 let computed_id = AccountId::new(
406 account.seed().unwrap(),
407 AccountIdVersion::Version0,
408 account.code.commitment(),
409 account.storage.to_commitment(),
410 )
411 .unwrap();
412 assert_eq!(account.id(), computed_id);
413
414 assert_eq!(account.code.procedure_roots().count(), 3);
416
417 let foo_root = CUSTOM_LIBRARY1.mast_forest()
418 [CUSTOM_LIBRARY1.get_export_node_id(CUSTOM_LIBRARY1.exports().next().unwrap().path())]
419 .digest();
420 let bar_root = CUSTOM_LIBRARY2.mast_forest()
421 [CUSTOM_LIBRARY2.get_export_node_id(CUSTOM_LIBRARY2.exports().next().unwrap().path())]
422 .digest();
423
424 assert!(account.code().procedures().contains(&AccountProcedureRoot::from_raw(foo_root)));
425 assert!(account.code().procedures().contains(&AccountProcedureRoot::from_raw(bar_root)));
426
427 assert_eq!(
428 account.storage().get_item(&CUSTOM_COMPONENT1_SLOT_NAME).unwrap(),
429 [Felt::new(storage_slot0), Felt::new(0), Felt::new(0), Felt::new(0)].into()
430 );
431 assert_eq!(
432 account.storage().get_item(&CUSTOM_COMPONENT2_SLOT_NAME0).unwrap(),
433 [Felt::new(0), Felt::new(0), Felt::new(0), Felt::new(storage_slot1)].into()
434 );
435 assert_eq!(
436 account.storage().get_item(&CUSTOM_COMPONENT2_SLOT_NAME1).unwrap(),
437 [Felt::new(0), Felt::new(0), Felt::new(0), Felt::new(storage_slot2)].into()
438 );
439 }
440
441 #[test]
442 fn account_builder_non_empty_vault_on_new_account() {
443 let storage_slot0 = 25;
444
445 let build_error = Account::builder([0xff; 32])
446 .with_auth_component(NoopAuthComponent)
447 .with_component(CustomComponent1 { slot0: storage_slot0 })
448 .with_assets(AssetVault::mock().assets())
449 .build()
450 .unwrap_err();
451
452 assert_matches!(build_error, AccountError::BuildError(msg, _) if msg == "account asset vault must be empty on new accounts")
453 }
454
455 }