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::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 Assembler::default()
316 .assemble_library([CUSTOM_CODE1])
317 .expect("code should be valid")
318 });
319 static CUSTOM_LIBRARY2: LazyLock<Library> = LazyLock::new(|| {
320 Assembler::default()
321 .assemble_library([CUSTOM_CODE2])
322 .expect("code should be valid")
323 });
324
325 static CUSTOM_COMPONENT1_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
326 StorageSlotName::new("custom::component1::slot0")
327 .expect("storage slot name should be valid")
328 });
329 static CUSTOM_COMPONENT2_SLOT_NAME0: LazyLock<StorageSlotName> = LazyLock::new(|| {
330 StorageSlotName::new("custom::component2::slot0")
331 .expect("storage slot name should be valid")
332 });
333 static CUSTOM_COMPONENT2_SLOT_NAME1: LazyLock<StorageSlotName> = LazyLock::new(|| {
334 StorageSlotName::new("custom::component2::slot1")
335 .expect("storage slot name should be valid")
336 });
337
338 struct CustomComponent1 {
339 slot0: u64,
340 }
341 impl From<CustomComponent1> for AccountComponent {
342 fn from(custom: CustomComponent1) -> Self {
343 let mut value = Word::empty();
344 value[0] = Felt::new(custom.slot0);
345
346 let metadata =
347 AccountComponentMetadata::new("test::custom_component1", AccountType::all());
348 AccountComponent::new(
349 CUSTOM_LIBRARY1.clone(),
350 vec![StorageSlot::with_value(CUSTOM_COMPONENT1_SLOT_NAME.clone(), value)],
351 metadata,
352 )
353 .expect("component should be valid")
354 }
355 }
356
357 struct CustomComponent2 {
358 slot0: u64,
359 slot1: u64,
360 }
361 impl From<CustomComponent2> for AccountComponent {
362 fn from(custom: CustomComponent2) -> Self {
363 let mut value0 = Word::empty();
364 value0[3] = Felt::new(custom.slot0);
365 let mut value1 = Word::empty();
366 value1[3] = Felt::new(custom.slot1);
367
368 let metadata =
369 AccountComponentMetadata::new("test::custom_component2", AccountType::all());
370 AccountComponent::new(
371 CUSTOM_LIBRARY2.clone(),
372 vec![
373 StorageSlot::with_value(CUSTOM_COMPONENT2_SLOT_NAME0.clone(), value0),
374 StorageSlot::with_value(CUSTOM_COMPONENT2_SLOT_NAME1.clone(), value1),
375 ],
376 metadata,
377 )
378 .expect("component should be valid")
379 }
380 }
381
382 #[test]
383 fn account_builder() {
384 let storage_slot0 = 25;
385 let storage_slot1 = 12;
386 let storage_slot2 = 42;
387
388 let account = Account::builder([5; 32])
389 .with_auth_component(NoopAuthComponent)
390 .with_component(CustomComponent1 { slot0: storage_slot0 })
391 .with_component(CustomComponent2 {
392 slot0: storage_slot1,
393 slot1: storage_slot2,
394 })
395 .build()
396 .unwrap();
397
398 assert_eq!(account.nonce(), Felt::ZERO);
400
401 let computed_id = AccountId::new(
402 account.seed().unwrap(),
403 AccountIdVersion::Version0,
404 account.code.commitment(),
405 account.storage.to_commitment(),
406 )
407 .unwrap();
408 assert_eq!(account.id(), computed_id);
409
410 assert_eq!(account.code.procedure_roots().count(), 3);
412
413 let foo_root = CUSTOM_LIBRARY1.mast_forest()
414 [CUSTOM_LIBRARY1.get_export_node_id(CUSTOM_LIBRARY1.exports().next().unwrap().path())]
415 .digest();
416 let bar_root = CUSTOM_LIBRARY2.mast_forest()
417 [CUSTOM_LIBRARY2.get_export_node_id(CUSTOM_LIBRARY2.exports().next().unwrap().path())]
418 .digest();
419
420 assert!(account.code().procedures().contains(&AccountProcedureRoot::from_raw(foo_root)));
421 assert!(account.code().procedures().contains(&AccountProcedureRoot::from_raw(bar_root)));
422
423 assert_eq!(
424 account.storage().get_item(&CUSTOM_COMPONENT1_SLOT_NAME).unwrap(),
425 [Felt::new(storage_slot0), Felt::new(0), Felt::new(0), Felt::new(0)].into()
426 );
427 assert_eq!(
428 account.storage().get_item(&CUSTOM_COMPONENT2_SLOT_NAME0).unwrap(),
429 [Felt::new(0), Felt::new(0), Felt::new(0), Felt::new(storage_slot1)].into()
430 );
431 assert_eq!(
432 account.storage().get_item(&CUSTOM_COMPONENT2_SLOT_NAME1).unwrap(),
433 [Felt::new(0), Felt::new(0), Felt::new(0), Felt::new(storage_slot2)].into()
434 );
435 }
436
437 #[test]
438 fn account_builder_non_empty_vault_on_new_account() {
439 let storage_slot0 = 25;
440
441 let build_error = Account::builder([0xff; 32])
442 .with_auth_component(NoopAuthComponent)
443 .with_component(CustomComponent1 { slot0: storage_slot0 })
444 .with_assets(AssetVault::mock().assets())
445 .build()
446 .unwrap_err();
447
448 assert_matches!(build_error, AccountError::BuildError(msg, _) if msg == "account asset vault must be empty on new accounts")
449 }
450
451 }