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