1use alloc::collections::BTreeMap;
2use alloc::vec::Vec;
3
4use anyhow::Context;
5
6const DEFAULT_FAUCET_DECIMALS: u8 = 10;
11
12use itertools::Itertools;
16use miden_processor::crypto::random::RandomCoin;
17use miden_protocol::account::delta::AccountUpdateDetails;
18use miden_protocol::account::{
19 Account,
20 AccountBuilder,
21 AccountComponent,
22 AccountDelta,
23 AccountId,
24 AccountStorageMode,
25 AccountType,
26 StorageSlot,
27};
28use miden_protocol::asset::{Asset, FungibleAsset, TokenSymbol};
29use miden_protocol::block::account_tree::AccountTree;
30use miden_protocol::block::nullifier_tree::NullifierTree;
31use miden_protocol::block::{
32 BlockAccountUpdate,
33 BlockBody,
34 BlockHeader,
35 BlockNoteTree,
36 BlockNumber,
37 BlockProof,
38 Blockchain,
39 FeeParameters,
40 OutputNoteBatch,
41 ProvenBlock,
42};
43use miden_protocol::crypto::merkle::smt::Smt;
44use miden_protocol::errors::NoteError;
45use miden_protocol::note::{Note, NoteAttachment, NoteDetails, NoteType};
46use miden_protocol::testing::account_id::ACCOUNT_ID_NATIVE_ASSET_FAUCET;
47use miden_protocol::testing::random_secret_key::random_secret_key;
48use miden_protocol::transaction::{OrderedTransactionHeaders, RawOutputNote, TransactionKernel};
49use miden_protocol::{Felt, MAX_OUTPUT_NOTES_PER_BATCH, Word};
50use miden_standards::account::access::Ownable2Step;
51use miden_standards::account::faucets::{BasicFungibleFaucet, NetworkFungibleFaucet};
52use miden_standards::account::mint_policies::{
53 AuthControlled,
54 OwnerControlled,
55 OwnerControlledInitConfig,
56};
57use miden_standards::account::wallets::BasicWallet;
58use miden_standards::note::{P2idNote, P2ideNote, P2ideNoteStorage, SwapNote};
59use miden_standards::testing::account_component::MockAccountComponent;
60use rand::Rng;
61
62use crate::mock_chain::chain::AccountAuthenticator;
63use crate::utils::{create_p2any_note, create_spawn_note};
64use crate::{AccountState, Auth, MockChain};
65
66#[derive(Debug, Clone)]
110pub struct MockChainBuilder {
111 accounts: BTreeMap<AccountId, Account>,
112 account_authenticators: BTreeMap<AccountId, AccountAuthenticator>,
113 notes: Vec<RawOutputNote>,
114 rng: RandomCoin,
115 native_asset_id: AccountId,
117 verification_base_fee: u32,
118}
119
120impl MockChainBuilder {
121 pub fn new() -> Self {
131 let native_asset_id =
132 ACCOUNT_ID_NATIVE_ASSET_FAUCET.try_into().expect("account ID should be valid");
133
134 Self {
135 accounts: BTreeMap::new(),
136 account_authenticators: BTreeMap::new(),
137 notes: Vec::new(),
138 rng: RandomCoin::new(Default::default()),
139 native_asset_id,
140 verification_base_fee: 0,
141 }
142 }
143
144 pub fn with_accounts(accounts: impl IntoIterator<Item = Account>) -> anyhow::Result<Self> {
153 let mut builder = Self::new();
154
155 for account in accounts {
156 builder.add_account(account)?;
157 }
158
159 Ok(builder)
160 }
161
162 pub fn native_asset_id(mut self, native_asset_id: AccountId) -> Self {
170 self.native_asset_id = native_asset_id;
171 self
172 }
173
174 pub fn verification_base_fee(mut self, verification_base_fee: u32) -> Self {
178 self.verification_base_fee = verification_base_fee;
179 self
180 }
181
182 pub fn build(self) -> anyhow::Result<MockChain> {
184 let block_account_updates: Vec<BlockAccountUpdate> = self
186 .accounts
187 .into_values()
188 .map(|account| {
189 let account_id = account.id();
190 let account_commitment = account.to_commitment();
191 let account_delta = AccountDelta::try_from(account)
192 .expect("chain builder should only store existing accounts without seeds");
193 let update_details = AccountUpdateDetails::Delta(account_delta);
194
195 BlockAccountUpdate::new(account_id, account_commitment, update_details)
196 })
197 .collect();
198
199 let account_tree = AccountTree::with_entries(
200 block_account_updates
201 .iter()
202 .map(|account| (account.account_id(), account.final_state_commitment())),
203 )
204 .context("failed to create genesis account tree")?;
205
206 let full_notes: Vec<Note> = self
208 .notes
209 .iter()
210 .filter_map(|note| match note {
211 RawOutputNote::Full(n) => Some(n.clone()),
212 _ => None,
213 })
214 .collect();
215
216 let proven_notes: Vec<_> = self
217 .notes
218 .into_iter()
219 .map(|note| note.into_output_note().expect("genesis note should be valid"))
220 .collect();
221 let note_chunks = proven_notes.into_iter().chunks(MAX_OUTPUT_NOTES_PER_BATCH);
222 let output_note_batches: Vec<OutputNoteBatch> = note_chunks
223 .into_iter()
224 .map(|batch_notes| batch_notes.into_iter().enumerate().collect::<Vec<_>>())
225 .collect();
226
227 let created_nullifiers = Vec::new();
228 let transactions = OrderedTransactionHeaders::new_unchecked(Vec::new());
229
230 let note_tree = BlockNoteTree::from_note_batches(&output_note_batches)
231 .context("failed to create block note tree")?;
232
233 let version = 0;
234 let prev_block_commitment = Word::empty();
235 let block_num = BlockNumber::from(0u32);
236 let chain_commitment = Blockchain::new().commitment();
237 let account_root = account_tree.root();
238 let nullifier_root = NullifierTree::<Smt>::default().root();
239 let note_root = note_tree.root();
240 let tx_commitment = transactions.commitment();
241 let tx_kernel_commitment = TransactionKernel.to_commitment();
242 let timestamp = MockChain::TIMESTAMP_START_SECS;
243 let fee_parameters = FeeParameters::new(self.native_asset_id, self.verification_base_fee)
244 .context("failed to construct fee parameters")?;
245 let validator_secret_key = random_secret_key();
246 let validator_public_key = validator_secret_key.public_key();
247
248 let header = BlockHeader::new(
249 version,
250 prev_block_commitment,
251 block_num,
252 chain_commitment,
253 account_root,
254 nullifier_root,
255 note_root,
256 tx_commitment,
257 tx_kernel_commitment,
258 validator_public_key,
259 fee_parameters,
260 timestamp,
261 );
262
263 let body = BlockBody::new_unchecked(
264 block_account_updates,
265 output_note_batches,
266 created_nullifiers,
267 transactions,
268 );
269
270 let signature = validator_secret_key.sign(header.commitment());
271 let block_proof = BlockProof::new_dummy();
272 let genesis_block = ProvenBlock::new_unchecked(header, body, signature, block_proof);
273
274 MockChain::from_genesis_block(
275 genesis_block,
276 account_tree,
277 self.account_authenticators,
278 validator_secret_key,
279 full_notes,
280 )
281 }
282
283 pub fn create_new_wallet(&mut self, auth_method: Auth) -> anyhow::Result<Account> {
292 let account_builder = AccountBuilder::new(self.rng.random())
293 .storage_mode(AccountStorageMode::Public)
294 .with_component(BasicWallet);
295
296 self.add_account_from_builder(auth_method, account_builder, AccountState::New)
297 }
298
299 pub fn add_existing_wallet(&mut self, auth_method: Auth) -> anyhow::Result<Account> {
302 self.add_existing_wallet_with_assets(auth_method, [])
303 }
304
305 pub fn add_existing_wallet_with_assets(
308 &mut self,
309 auth_method: Auth,
310 assets: impl IntoIterator<Item = Asset>,
311 ) -> anyhow::Result<Account> {
312 let account_builder = Account::builder(self.rng.random())
313 .storage_mode(AccountStorageMode::Public)
314 .with_component(BasicWallet)
315 .with_assets(assets);
316
317 self.add_account_from_builder(auth_method, account_builder, AccountState::Exists)
318 }
319
320 pub fn create_new_faucet(
326 &mut self,
327 auth_method: Auth,
328 token_symbol: &str,
329 max_supply: u64,
330 ) -> anyhow::Result<Account> {
331 let token_symbol = TokenSymbol::new(token_symbol)
332 .with_context(|| format!("invalid token symbol: {token_symbol}"))?;
333 let max_supply_felt = Felt::try_from(max_supply)?;
334 let basic_faucet =
335 BasicFungibleFaucet::new(token_symbol, DEFAULT_FAUCET_DECIMALS, max_supply_felt)
336 .context("failed to create BasicFungibleFaucet")?;
337
338 let account_builder = AccountBuilder::new(self.rng.random())
339 .storage_mode(AccountStorageMode::Public)
340 .account_type(AccountType::FungibleFaucet)
341 .with_component(basic_faucet)
342 .with_component(AuthControlled::allow_all());
343
344 self.add_account_from_builder(auth_method, account_builder, AccountState::New)
345 }
346
347 pub fn add_existing_basic_faucet(
352 &mut self,
353 auth_method: Auth,
354 token_symbol: &str,
355 max_supply: u64,
356 token_supply: Option<u64>,
357 ) -> anyhow::Result<Account> {
358 let max_supply = Felt::try_from(max_supply)?;
359 let token_supply = Felt::try_from(token_supply.unwrap_or(0))?;
360 let token_symbol =
361 TokenSymbol::new(token_symbol).context("failed to create token symbol")?;
362
363 let basic_faucet =
364 BasicFungibleFaucet::new(token_symbol, DEFAULT_FAUCET_DECIMALS, max_supply)
365 .and_then(|fungible_faucet| fungible_faucet.with_token_supply(token_supply))
366 .context("failed to create basic fungible faucet")?;
367
368 let account_builder = AccountBuilder::new(self.rng.random())
369 .storage_mode(AccountStorageMode::Public)
370 .with_component(basic_faucet)
371 .with_component(AuthControlled::allow_all())
372 .account_type(AccountType::FungibleFaucet);
373
374 self.add_account_from_builder(auth_method, account_builder, AccountState::Exists)
375 }
376
377 pub fn add_existing_network_faucet(
381 &mut self,
382 token_symbol: &str,
383 max_supply: u64,
384 owner_account_id: AccountId,
385 token_supply: Option<u64>,
386 mint_policy: OwnerControlledInitConfig,
387 ) -> anyhow::Result<Account> {
388 let max_supply = Felt::try_from(max_supply)?;
389 let token_supply = Felt::try_from(token_supply.unwrap_or(0))?;
390 let token_symbol =
391 TokenSymbol::new(token_symbol).context("failed to create token symbol")?;
392
393 let network_faucet =
394 NetworkFungibleFaucet::new(token_symbol, DEFAULT_FAUCET_DECIMALS, max_supply)
395 .and_then(|fungible_faucet| fungible_faucet.with_token_supply(token_supply))
396 .context("failed to create network fungible faucet")?;
397
398 let account_builder = AccountBuilder::new(self.rng.random())
399 .storage_mode(AccountStorageMode::Network)
400 .with_component(network_faucet)
401 .with_component(Ownable2Step::new(owner_account_id))
402 .with_component(OwnerControlled::new(mint_policy))
403 .account_type(AccountType::FungibleFaucet);
404
405 self.add_account_from_builder(Auth::IncrNonce, account_builder, AccountState::Exists)
407 }
408
409 pub fn create_new_mock_account(&mut self, auth_method: Auth) -> anyhow::Result<Account> {
412 let account_builder = Account::builder(self.rng.random())
413 .storage_mode(AccountStorageMode::Public)
414 .with_component(MockAccountComponent::with_empty_slots());
415
416 self.add_account_from_builder(auth_method, account_builder, AccountState::New)
417 }
418
419 pub fn add_existing_mock_account(&mut self, auth_method: Auth) -> anyhow::Result<Account> {
422 self.add_existing_mock_account_with_storage_and_assets(auth_method, [], [])
423 }
424
425 pub fn add_existing_mock_account_with_storage(
428 &mut self,
429 auth_method: Auth,
430 slots: impl IntoIterator<Item = StorageSlot>,
431 ) -> anyhow::Result<Account> {
432 self.add_existing_mock_account_with_storage_and_assets(auth_method, slots, [])
433 }
434
435 pub fn add_existing_mock_account_with_assets(
438 &mut self,
439 auth_method: Auth,
440 assets: impl IntoIterator<Item = Asset>,
441 ) -> anyhow::Result<Account> {
442 self.add_existing_mock_account_with_storage_and_assets(auth_method, [], assets)
443 }
444
445 pub fn add_existing_mock_account_with_storage_and_assets(
448 &mut self,
449 auth_method: Auth,
450 slots: impl IntoIterator<Item = StorageSlot>,
451 assets: impl IntoIterator<Item = Asset>,
452 ) -> anyhow::Result<Account> {
453 let account_builder = Account::builder(self.rng.random())
454 .storage_mode(AccountStorageMode::Public)
455 .with_component(MockAccountComponent::with_slots(slots.into_iter().collect()))
456 .with_assets(assets);
457
458 self.add_account_from_builder(auth_method, account_builder, AccountState::Exists)
459 }
460
461 pub fn add_account_from_builder(
472 &mut self,
473 auth_method: Auth,
474 mut account_builder: AccountBuilder,
475 account_state: AccountState,
476 ) -> anyhow::Result<Account> {
477 let (auth_component, authenticator) = auth_method.build_component();
478 account_builder = account_builder.with_auth_component(auth_component);
479
480 let account = if let AccountState::New = account_state {
481 account_builder.build().context("failed to build account from builder")?
482 } else {
483 account_builder
484 .build_existing()
485 .context("failed to build account from builder")?
486 };
487
488 self.account_authenticators
489 .insert(account.id(), AccountAuthenticator::new(authenticator));
490
491 if let AccountState::Exists = account_state {
492 self.accounts.insert(account.id(), account.clone());
493 }
494
495 Ok(account)
496 }
497 pub fn add_existing_account_from_components(
498 &mut self,
499 auth: Auth,
500 components: impl IntoIterator<Item = AccountComponent>,
501 ) -> anyhow::Result<Account> {
502 let mut account_builder =
503 Account::builder(rand::rng().random()).storage_mode(AccountStorageMode::Public);
504
505 for component in components {
506 account_builder = account_builder.with_component(component);
507 }
508
509 self.add_account_from_builder(auth, account_builder, AccountState::Exists)
510 }
511
512 pub fn add_account(&mut self, account: Account) -> anyhow::Result<()> {
521 self.accounts.insert(account.id(), account);
522
523 Ok(())
526 }
527
528 pub fn add_output_note(&mut self, note: impl Into<RawOutputNote>) {
533 self.notes.push(note.into());
534 }
535
536 pub fn add_p2any_note(
541 &mut self,
542 sender_account_id: AccountId,
543 note_type: NoteType,
544 assets: impl IntoIterator<Item = Asset>,
545 ) -> anyhow::Result<Note> {
546 let note = create_p2any_note(sender_account_id, note_type, assets, &mut self.rng);
547 self.add_output_note(RawOutputNote::Full(note.clone()));
548
549 Ok(note)
550 }
551
552 pub fn add_p2id_note(
558 &mut self,
559 sender_account_id: AccountId,
560 target_account_id: AccountId,
561 asset: &[Asset],
562 note_type: NoteType,
563 ) -> Result<Note, NoteError> {
564 let note = P2idNote::create(
565 sender_account_id,
566 target_account_id,
567 asset.to_vec(),
568 note_type,
569 NoteAttachment::default(),
570 &mut self.rng,
571 )?;
572 self.add_output_note(RawOutputNote::Full(note.clone()));
573
574 Ok(note)
575 }
576
577 pub fn add_p2ide_note(
583 &mut self,
584 sender_account_id: AccountId,
585 target_account_id: AccountId,
586 asset: &[Asset],
587 note_type: NoteType,
588 reclaim_height: Option<BlockNumber>,
589 timelock_height: Option<BlockNumber>,
590 ) -> Result<Note, NoteError> {
591 let storage = P2ideNoteStorage::new(target_account_id, reclaim_height, timelock_height);
592
593 let note = P2ideNote::create(
594 sender_account_id,
595 storage,
596 asset.to_vec(),
597 note_type,
598 Default::default(),
599 &mut self.rng,
600 )?;
601
602 self.add_output_note(RawOutputNote::Full(note.clone()));
603
604 Ok(note)
605 }
606
607 pub fn add_swap_note(
609 &mut self,
610 sender: AccountId,
611 offered_asset: Asset,
612 requested_asset: Asset,
613 payback_note_type: NoteType,
614 ) -> anyhow::Result<(Note, NoteDetails)> {
615 let (swap_note, payback_note) = SwapNote::create(
616 sender,
617 offered_asset,
618 requested_asset,
619 NoteType::Public,
620 NoteAttachment::default(),
621 payback_note_type,
622 NoteAttachment::default(),
623 &mut self.rng,
624 )?;
625
626 self.add_output_note(RawOutputNote::Full(swap_note.clone()));
627
628 Ok((swap_note, payback_note))
629 }
630
631 pub fn add_spawn_note<'note, I>(
642 &mut self,
643 output_notes: impl IntoIterator<Item = &'note Note, IntoIter = I>,
644 ) -> anyhow::Result<Note>
645 where
646 I: ExactSizeIterator<Item = &'note Note>,
647 {
648 let note = create_spawn_note(output_notes)?;
649 self.add_output_note(RawOutputNote::Full(note.clone()));
650
651 Ok(note)
652 }
653
654 pub fn add_p2id_note_with_fee(
661 &mut self,
662 target_account_id: AccountId,
663 amount: u64,
664 ) -> anyhow::Result<Note> {
665 let fee_asset = self.native_fee_asset(amount)?;
666 let note = self.add_p2id_note(
667 self.native_asset_id,
668 target_account_id,
669 &[Asset::from(fee_asset)],
670 NoteType::Public,
671 )?;
672
673 Ok(note)
674 }
675
676 pub fn rng_mut(&mut self) -> &mut RandomCoin {
683 &mut self.rng
684 }
685
686 fn native_fee_asset(&self, amount: u64) -> anyhow::Result<FungibleAsset> {
688 FungibleAsset::new(self.native_asset_id, amount).context("failed to create fee asset")
689 }
690}
691
692impl Default for MockChainBuilder {
693 fn default() -> Self {
694 Self::new()
695 }
696}