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