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