miden_objects/account/builder/
mod.rs1use alloc::{boxed::Box, vec::Vec};
2
3use vm_core::FieldElement;
4use vm_processor::Digest;
5
6use crate::{
7 AccountError, Felt, Word,
8 account::{
9 Account, AccountCode, AccountComponent, AccountId, AccountIdV0, AccountIdVersion,
10 AccountStorage, AccountStorageMode, AccountType,
11 },
12 asset::AssetVault,
13};
14
15#[derive(Debug, Clone)]
40pub struct AccountBuilder {
41 #[cfg(any(feature = "testing", test))]
42 assets: Vec<crate::asset::Asset>,
43 components: Vec<AccountComponent>,
44 auth_component: Option<AccountComponent>,
45 account_type: AccountType,
46 storage_mode: AccountStorageMode,
47 init_seed: [u8; 32],
48 id_version: AccountIdVersion,
49}
50
51impl AccountBuilder {
52 pub fn new(init_seed: [u8; 32]) -> Self {
57 Self {
58 #[cfg(any(feature = "testing", test))]
59 assets: vec![],
60 components: vec![],
61 auth_component: None,
62 init_seed,
63 account_type: AccountType::RegularAccountUpdatableCode,
64 storage_mode: AccountStorageMode::Private,
65 id_version: AccountIdVersion::Version0,
66 }
67 }
68
69 pub fn version(mut self, version: AccountIdVersion) -> Self {
71 self.id_version = version;
72 self
73 }
74
75 pub fn account_type(mut self, account_type: AccountType) -> Self {
77 self.account_type = account_type;
78 self
79 }
80
81 pub fn storage_mode(mut self, storage_mode: AccountStorageMode) -> Self {
83 self.storage_mode = storage_mode;
84 self
85 }
86
87 pub fn with_component(mut self, account_component: impl Into<AccountComponent>) -> Self {
92 self.components.push(account_component.into());
93 self
94 }
95
96 pub fn with_auth_component(mut self, account_component: impl Into<AccountComponent>) -> Self {
105 self.auth_component = Some(account_component.into());
106 self
107 }
108
109 fn build_inner(&mut self) -> Result<(AssetVault, AccountCode, AccountStorage), AccountError> {
111 #[cfg(any(feature = "testing", test))]
112 let vault = AssetVault::new(&self.assets).map_err(|err| {
113 AccountError::BuildError(format!("asset vault failed to build: {err}"), None)
114 })?;
115
116 #[cfg(all(not(feature = "testing"), not(test)))]
117 let vault = AssetVault::default();
118
119 let auth_component = self
120 .auth_component
121 .take()
122 .ok_or(AccountError::BuildError("auth component must be set".into(), None))?;
123
124 let mut components = vec![auth_component];
125 components.append(&mut self.components);
126
127 let (code, storage) = Account::initialize_from_components(self.account_type, &components)
128 .map_err(|err| {
129 AccountError::BuildError(
130 "account components failed to build".into(),
131 Some(Box::new(err)),
132 )
133 })?;
134
135 Ok((vault, code, storage))
136 }
137
138 fn grind_account_id(
140 &self,
141 init_seed: [u8; 32],
142 version: AccountIdVersion,
143 code_commitment: Digest,
144 storage_commitment: Digest,
145 ) -> Result<Word, AccountError> {
146 let seed = AccountIdV0::compute_account_seed(
147 init_seed,
148 self.account_type,
149 self.storage_mode,
150 version,
151 code_commitment,
152 storage_commitment,
153 )
154 .map_err(|err| {
155 AccountError::BuildError("account seed generation failed".into(), Some(Box::new(err)))
156 })?;
157
158 Ok(seed)
159 }
160
161 pub fn build(mut self) -> Result<(Account, Word), AccountError> {
178 let (vault, code, storage) = self.build_inner()?;
179
180 #[cfg(any(feature = "testing", test))]
181 if !vault.is_empty() {
182 return Err(AccountError::BuildError(
183 "account asset vault must be empty on new accounts".into(),
184 None,
185 ));
186 }
187
188 let seed = self.grind_account_id(
189 self.init_seed,
190 self.id_version,
191 code.commitment(),
192 storage.commitment(),
193 )?;
194
195 let account_id = AccountId::new(
196 seed,
197 AccountIdVersion::Version0,
198 code.commitment(),
199 storage.commitment(),
200 )
201 .expect("get_account_seed should provide a suitable seed");
202
203 debug_assert_eq!(account_id.account_type(), self.account_type);
204 debug_assert_eq!(account_id.storage_mode(), self.storage_mode);
205
206 let account = Account::from_parts(account_id, vault, storage, code, Felt::ZERO);
207
208 Ok((account, seed))
209 }
210}
211
212#[cfg(any(feature = "testing", test))]
213impl AccountBuilder {
214 pub fn with_assets<I: IntoIterator<Item = crate::asset::Asset>>(mut self, assets: I) -> Self {
219 self.assets.extend(assets);
220 self
221 }
222
223 pub fn build_existing(mut self) -> Result<Account, AccountError> {
229 let (vault, code, storage) = self.build_inner()?;
230
231 let account_id = {
232 let bytes = <[u8; 15]>::try_from(&self.init_seed[0..15])
233 .expect("we should have sliced exactly 15 bytes off");
234 AccountId::dummy(
235 bytes,
236 AccountIdVersion::Version0,
237 self.account_type,
238 self.storage_mode,
239 )
240 };
241
242 Ok(Account::from_parts(account_id, vault, storage, code, Felt::ONE))
243 }
244}
245
246#[cfg(test)]
250mod tests {
251 use std::sync::LazyLock;
252
253 use assembly::{Assembler, Library};
254 use assert_matches::assert_matches;
255 use vm_core::FieldElement;
256
257 use super::*;
258 use crate::{account::StorageSlot, testing::account_component::NoopAuthComponent};
259
260 const CUSTOM_CODE1: &str = "
261 export.foo
262 push.2.2 add eq.4
263 end
264 ";
265 const CUSTOM_CODE2: &str = "
266 export.bar
267 push.4.4 add eq.8
268 end
269 ";
270
271 static CUSTOM_LIBRARY1: LazyLock<Library> = LazyLock::new(|| {
272 Assembler::default()
273 .assemble_library([CUSTOM_CODE1])
274 .expect("code should be valid")
275 });
276 static CUSTOM_LIBRARY2: LazyLock<Library> = LazyLock::new(|| {
277 Assembler::default()
278 .assemble_library([CUSTOM_CODE2])
279 .expect("code should be valid")
280 });
281
282 struct CustomComponent1 {
283 slot0: u64,
284 }
285 impl From<CustomComponent1> for AccountComponent {
286 fn from(custom: CustomComponent1) -> Self {
287 let mut value = Word::default();
288 value[0] = Felt::new(custom.slot0);
289
290 AccountComponent::new(CUSTOM_LIBRARY1.clone(), vec![StorageSlot::Value(value)])
291 .expect("component should be valid")
292 .with_supports_all_types()
293 }
294 }
295
296 struct CustomComponent2 {
297 slot0: u64,
298 slot1: u64,
299 }
300 impl From<CustomComponent2> for AccountComponent {
301 fn from(custom: CustomComponent2) -> Self {
302 let mut value0 = Word::default();
303 value0[3] = Felt::new(custom.slot0);
304 let mut value1 = Word::default();
305 value1[3] = Felt::new(custom.slot1);
306
307 AccountComponent::new(
308 CUSTOM_LIBRARY2.clone(),
309 vec![StorageSlot::Value(value0), StorageSlot::Value(value1)],
310 )
311 .expect("component should be valid")
312 .with_supports_all_types()
313 }
314 }
315
316 #[test]
317 fn account_builder() {
318 let storage_slot0 = 25;
319 let storage_slot1 = 12;
320 let storage_slot2 = 42;
321
322 let (account, seed) = Account::builder([5; 32])
323 .with_auth_component(NoopAuthComponent::new(Assembler::default()).unwrap())
324 .with_component(CustomComponent1 { slot0: storage_slot0 })
325 .with_component(CustomComponent2 {
326 slot0: storage_slot1,
327 slot1: storage_slot2,
328 })
329 .build()
330 .unwrap();
331
332 assert_eq!(account.nonce(), Felt::ZERO);
334
335 let computed_id = AccountId::new(
336 seed,
337 AccountIdVersion::Version0,
338 account.code.commitment(),
339 account.storage.commitment(),
340 )
341 .unwrap();
342 assert_eq!(account.id(), computed_id);
343
344 assert_eq!(account.code.procedure_roots().count(), 3);
346
347 let foo_root = CUSTOM_LIBRARY1.mast_forest()
348 [CUSTOM_LIBRARY1.get_export_node_id(CUSTOM_LIBRARY1.exports().next().unwrap())]
349 .digest();
350 let bar_root = CUSTOM_LIBRARY2.mast_forest()
351 [CUSTOM_LIBRARY2.get_export_node_id(CUSTOM_LIBRARY2.exports().next().unwrap())]
352 .digest();
353
354 let foo_procedure_info = &account
355 .code()
356 .procedures()
357 .iter()
358 .find(|info| info.mast_root() == &foo_root)
359 .unwrap();
360 assert_eq!(foo_procedure_info.storage_offset(), 0);
361 assert_eq!(foo_procedure_info.storage_size(), 1);
362
363 let bar_procedure_info = &account
364 .code()
365 .procedures()
366 .iter()
367 .find(|info| info.mast_root() == &bar_root)
368 .unwrap();
369 assert_eq!(bar_procedure_info.storage_offset(), 1);
370 assert_eq!(bar_procedure_info.storage_size(), 2);
371
372 assert_eq!(
373 account.storage().get_item(0).unwrap(),
374 [Felt::new(storage_slot0), Felt::new(0), Felt::new(0), Felt::new(0)].into()
375 );
376 assert_eq!(
377 account.storage().get_item(1).unwrap(),
378 [Felt::new(0), Felt::new(0), Felt::new(0), Felt::new(storage_slot1)].into()
379 );
380 assert_eq!(
381 account.storage().get_item(2).unwrap(),
382 [Felt::new(0), Felt::new(0), Felt::new(0), Felt::new(storage_slot2)].into()
383 );
384 }
385
386 #[test]
387 fn account_builder_non_empty_vault_on_new_account() {
388 let storage_slot0 = 25;
389
390 let build_error = Account::builder([0xff; 32])
391 .with_auth_component(NoopAuthComponent::new(Assembler::default()).unwrap())
392 .with_component(CustomComponent1 { slot0: storage_slot0 })
393 .with_assets(AssetVault::mock().assets())
394 .build()
395 .unwrap_err();
396
397 assert_matches!(build_error, AccountError::BuildError(msg, _) if msg == "account asset vault must be empty on new accounts")
398 }
399
400 }