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::RpoRandomCoin;
17use miden_protocol::account::delta::AccountUpdateDetails;
18use miden_protocol::account::{
19 Account,
20 AccountBuilder,
21 AccountDelta,
22 AccountId,
23 AccountStorageMode,
24 AccountType,
25 StorageSlot,
26};
27use miden_protocol::asset::{Asset, FungibleAsset, TokenSymbol};
28use miden_protocol::block::account_tree::AccountTree;
29use miden_protocol::block::nullifier_tree::NullifierTree;
30use miden_protocol::block::{
31 BlockAccountUpdate,
32 BlockBody,
33 BlockHeader,
34 BlockNoteTree,
35 BlockNumber,
36 BlockProof,
37 Blockchain,
38 FeeParameters,
39 OutputNoteBatch,
40 ProvenBlock,
41};
42use miden_protocol::crypto::merkle::smt::Smt;
43use miden_protocol::errors::NoteError;
44use miden_protocol::note::{Note, NoteAttachment, NoteDetails, NoteType};
45use miden_protocol::testing::account_id::ACCOUNT_ID_NATIVE_ASSET_FAUCET;
46use miden_protocol::testing::random_secret_key::random_secret_key;
47use miden_protocol::transaction::{OrderedTransactionHeaders, OutputNote, TransactionKernel};
48use miden_protocol::{Felt, MAX_OUTPUT_NOTES_PER_BATCH, Word};
49use miden_standards::account::faucets::{BasicFungibleFaucet, NetworkFungibleFaucet};
50use miden_standards::account::wallets::BasicWallet;
51use miden_standards::note::{P2idNote, P2ideNote, P2ideNoteStorage, SwapNote};
52use miden_standards::testing::account_component::MockAccountComponent;
53use rand::Rng;
54
55use crate::mock_chain::chain::AccountAuthenticator;
56use crate::utils::{create_p2any_note, create_spawn_note};
57use crate::{AccountState, Auth, MockChain};
58
59#[derive(Debug, Clone)]
103pub struct MockChainBuilder {
104 accounts: BTreeMap<AccountId, Account>,
105 account_authenticators: BTreeMap<AccountId, AccountAuthenticator>,
106 notes: Vec<OutputNote>,
107 rng: RpoRandomCoin,
108 native_asset_id: AccountId,
110 verification_base_fee: u32,
111}
112
113impl MockChainBuilder {
114 pub fn new() -> Self {
124 let native_asset_id =
125 ACCOUNT_ID_NATIVE_ASSET_FAUCET.try_into().expect("account ID should be valid");
126
127 Self {
128 accounts: BTreeMap::new(),
129 account_authenticators: BTreeMap::new(),
130 notes: Vec::new(),
131 rng: RpoRandomCoin::new(Default::default()),
132 native_asset_id,
133 verification_base_fee: 0,
134 }
135 }
136
137 pub fn with_accounts(accounts: impl IntoIterator<Item = Account>) -> anyhow::Result<Self> {
146 let mut builder = Self::new();
147
148 for account in accounts {
149 builder.add_account(account)?;
150 }
151
152 Ok(builder)
153 }
154
155 pub fn native_asset_id(mut self, native_asset_id: AccountId) -> Self {
163 self.native_asset_id = native_asset_id;
164 self
165 }
166
167 pub fn verification_base_fee(mut self, verification_base_fee: u32) -> Self {
171 self.verification_base_fee = verification_base_fee;
172 self
173 }
174
175 pub fn build(self) -> anyhow::Result<MockChain> {
177 let block_account_updates: Vec<BlockAccountUpdate> = self
179 .accounts
180 .into_values()
181 .map(|account| {
182 let account_id = account.id();
183 let account_commitment = account.to_commitment();
184 let account_delta = AccountDelta::try_from(account)
185 .expect("chain builder should only store existing accounts without seeds");
186 let update_details = AccountUpdateDetails::Delta(account_delta);
187
188 BlockAccountUpdate::new(account_id, account_commitment, update_details)
189 })
190 .collect();
191
192 let account_tree = AccountTree::with_entries(
193 block_account_updates
194 .iter()
195 .map(|account| (account.account_id(), account.final_state_commitment())),
196 )
197 .context("failed to create genesis account tree")?;
198
199 let note_chunks = self.notes.into_iter().chunks(MAX_OUTPUT_NOTES_PER_BATCH);
200 let output_note_batches: Vec<OutputNoteBatch> = note_chunks
201 .into_iter()
202 .map(|batch_notes| batch_notes.into_iter().enumerate().collect::<Vec<_>>())
203 .collect();
204
205 let created_nullifiers = Vec::new();
206 let transactions = OrderedTransactionHeaders::new_unchecked(Vec::new());
207
208 let note_tree = BlockNoteTree::from_note_batches(&output_note_batches)
209 .context("failed to create block note tree")?;
210
211 let version = 0;
212 let prev_block_commitment = Word::empty();
213 let block_num = BlockNumber::from(0u32);
214 let chain_commitment = Blockchain::new().commitment();
215 let account_root = account_tree.root();
216 let nullifier_root = NullifierTree::<Smt>::default().root();
217 let note_root = note_tree.root();
218 let tx_commitment = transactions.commitment();
219 let tx_kernel_commitment = TransactionKernel.to_commitment();
220 let timestamp = MockChain::TIMESTAMP_START_SECS;
221 let fee_parameters = FeeParameters::new(self.native_asset_id, self.verification_base_fee)
222 .context("failed to construct fee parameters")?;
223 let validator_secret_key = random_secret_key();
224 let validator_public_key = validator_secret_key.public_key();
225
226 let header = BlockHeader::new(
227 version,
228 prev_block_commitment,
229 block_num,
230 chain_commitment,
231 account_root,
232 nullifier_root,
233 note_root,
234 tx_commitment,
235 tx_kernel_commitment,
236 validator_public_key,
237 fee_parameters,
238 timestamp,
239 );
240
241 let body = BlockBody::new_unchecked(
242 block_account_updates,
243 output_note_batches,
244 created_nullifiers,
245 transactions,
246 );
247
248 let signature = validator_secret_key.sign(header.commitment());
249 let block_proof = BlockProof::new_dummy();
250 let genesis_block = ProvenBlock::new_unchecked(header, body, signature, block_proof);
251
252 MockChain::from_genesis_block(
253 genesis_block,
254 account_tree,
255 self.account_authenticators,
256 validator_secret_key,
257 )
258 }
259
260 pub fn create_new_wallet(&mut self, auth_method: Auth) -> anyhow::Result<Account> {
269 let account_builder = AccountBuilder::new(self.rng.random())
270 .storage_mode(AccountStorageMode::Public)
271 .with_component(BasicWallet);
272
273 self.add_account_from_builder(auth_method, account_builder, AccountState::New)
274 }
275
276 pub fn add_existing_wallet(&mut self, auth_method: Auth) -> anyhow::Result<Account> {
279 self.add_existing_wallet_with_assets(auth_method, [])
280 }
281
282 pub fn add_existing_wallet_with_assets(
285 &mut self,
286 auth_method: Auth,
287 assets: impl IntoIterator<Item = Asset>,
288 ) -> anyhow::Result<Account> {
289 let account_builder = Account::builder(self.rng.random())
290 .storage_mode(AccountStorageMode::Public)
291 .with_component(BasicWallet)
292 .with_assets(assets);
293
294 self.add_account_from_builder(auth_method, account_builder, AccountState::Exists)
295 }
296
297 pub fn create_new_faucet(
303 &mut self,
304 auth_method: Auth,
305 token_symbol: &str,
306 max_supply: u64,
307 ) -> anyhow::Result<Account> {
308 let token_symbol = TokenSymbol::new(token_symbol)
309 .with_context(|| format!("invalid token symbol: {token_symbol}"))?;
310 let max_supply_felt = max_supply.try_into().map_err(|_| {
311 anyhow::anyhow!("max supply value cannot be converted to Felt: {max_supply}")
312 })?;
313 let basic_faucet =
314 BasicFungibleFaucet::new(token_symbol, DEFAULT_FAUCET_DECIMALS, max_supply_felt)
315 .context("failed to create BasicFungibleFaucet")?;
316
317 let account_builder = AccountBuilder::new(self.rng.random())
318 .storage_mode(AccountStorageMode::Public)
319 .account_type(AccountType::FungibleFaucet)
320 .with_component(basic_faucet);
321
322 self.add_account_from_builder(auth_method, account_builder, AccountState::New)
323 }
324
325 pub fn add_existing_basic_faucet(
330 &mut self,
331 auth_method: Auth,
332 token_symbol: &str,
333 max_supply: u64,
334 token_supply: Option<u64>,
335 ) -> anyhow::Result<Account> {
336 let max_supply = Felt::try_from(max_supply)
337 .map_err(|err| anyhow::anyhow!("failed to convert max_supply to felt: {err}"))?;
338 let token_supply = Felt::try_from(token_supply.unwrap_or(0))
339 .map_err(|err| anyhow::anyhow!("failed to convert token_supply to felt: {err}"))?;
340 let token_symbol =
341 TokenSymbol::new(token_symbol).context("failed to create token symbol")?;
342
343 let basic_faucet =
344 BasicFungibleFaucet::new(token_symbol, DEFAULT_FAUCET_DECIMALS, max_supply)
345 .and_then(|fungible_faucet| fungible_faucet.with_token_supply(token_supply))
346 .context("failed to create basic fungible faucet")?;
347
348 let account_builder = AccountBuilder::new(self.rng.random())
349 .storage_mode(AccountStorageMode::Public)
350 .with_component(basic_faucet)
351 .account_type(AccountType::FungibleFaucet);
352
353 self.add_account_from_builder(auth_method, account_builder, AccountState::Exists)
354 }
355
356 pub fn add_existing_network_faucet(
360 &mut self,
361 token_symbol: &str,
362 max_supply: u64,
363 owner_account_id: AccountId,
364 token_supply: Option<u64>,
365 ) -> anyhow::Result<Account> {
366 let max_supply = Felt::try_from(max_supply)
367 .map_err(|err| anyhow::anyhow!("failed to convert max_supply to felt: {err}"))?;
368 let token_supply = Felt::try_from(token_supply.unwrap_or(0))
369 .map_err(|err| anyhow::anyhow!("failed to convert token_supply to felt: {err}"))?;
370 let token_symbol =
371 TokenSymbol::new(token_symbol).context("failed to create token symbol")?;
372
373 let network_faucet = NetworkFungibleFaucet::new(
374 token_symbol,
375 DEFAULT_FAUCET_DECIMALS,
376 max_supply,
377 owner_account_id,
378 )
379 .and_then(|fungible_faucet| fungible_faucet.with_token_supply(token_supply))
380 .context("failed to create network fungible faucet")?;
381
382 let account_builder = AccountBuilder::new(self.rng.random())
383 .storage_mode(AccountStorageMode::Network)
384 .with_component(network_faucet)
385 .account_type(AccountType::FungibleFaucet);
386
387 self.add_account_from_builder(Auth::IncrNonce, account_builder, AccountState::Exists)
389 }
390
391 pub fn create_new_mock_account(&mut self, auth_method: Auth) -> anyhow::Result<Account> {
394 let account_builder = Account::builder(self.rng.random())
395 .storage_mode(AccountStorageMode::Public)
396 .with_component(MockAccountComponent::with_empty_slots());
397
398 self.add_account_from_builder(auth_method, account_builder, AccountState::New)
399 }
400
401 pub fn add_existing_mock_account(&mut self, auth_method: Auth) -> anyhow::Result<Account> {
404 self.add_existing_mock_account_with_storage_and_assets(auth_method, [], [])
405 }
406
407 pub fn add_existing_mock_account_with_storage(
410 &mut self,
411 auth_method: Auth,
412 slots: impl IntoIterator<Item = StorageSlot>,
413 ) -> anyhow::Result<Account> {
414 self.add_existing_mock_account_with_storage_and_assets(auth_method, slots, [])
415 }
416
417 pub fn add_existing_mock_account_with_assets(
420 &mut self,
421 auth_method: Auth,
422 assets: impl IntoIterator<Item = Asset>,
423 ) -> anyhow::Result<Account> {
424 self.add_existing_mock_account_with_storage_and_assets(auth_method, [], assets)
425 }
426
427 pub fn add_existing_mock_account_with_storage_and_assets(
430 &mut self,
431 auth_method: Auth,
432 slots: impl IntoIterator<Item = StorageSlot>,
433 assets: impl IntoIterator<Item = Asset>,
434 ) -> anyhow::Result<Account> {
435 let account_builder = Account::builder(self.rng.random())
436 .storage_mode(AccountStorageMode::Public)
437 .with_component(MockAccountComponent::with_slots(slots.into_iter().collect()))
438 .with_assets(assets);
439
440 self.add_account_from_builder(auth_method, account_builder, AccountState::Exists)
441 }
442
443 pub fn add_account_from_builder(
454 &mut self,
455 auth_method: Auth,
456 mut account_builder: AccountBuilder,
457 account_state: AccountState,
458 ) -> anyhow::Result<Account> {
459 let (auth_component, authenticator) = auth_method.build_component();
460 account_builder = account_builder.with_auth_component(auth_component);
461
462 let account = if let AccountState::New = account_state {
463 account_builder.build().context("failed to build account from builder")?
464 } else {
465 account_builder
466 .build_existing()
467 .context("failed to build account from builder")?
468 };
469
470 self.account_authenticators
471 .insert(account.id(), AccountAuthenticator::new(authenticator));
472
473 if let AccountState::Exists = account_state {
474 self.accounts.insert(account.id(), account.clone());
475 }
476
477 Ok(account)
478 }
479
480 pub fn add_account(&mut self, account: Account) -> anyhow::Result<()> {
489 self.accounts.insert(account.id(), account);
490
491 Ok(())
494 }
495
496 pub fn add_output_note(&mut self, note: impl Into<OutputNote>) {
501 self.notes.push(note.into());
502 }
503
504 pub fn add_p2any_note(
509 &mut self,
510 sender_account_id: AccountId,
511 note_type: NoteType,
512 assets: impl IntoIterator<Item = Asset>,
513 ) -> anyhow::Result<Note> {
514 let note = create_p2any_note(sender_account_id, note_type, assets, &mut self.rng);
515 self.add_output_note(OutputNote::Full(note.clone()));
516
517 Ok(note)
518 }
519
520 pub fn add_p2id_note(
526 &mut self,
527 sender_account_id: AccountId,
528 target_account_id: AccountId,
529 asset: &[Asset],
530 note_type: NoteType,
531 ) -> Result<Note, NoteError> {
532 let note = P2idNote::create(
533 sender_account_id,
534 target_account_id,
535 asset.to_vec(),
536 note_type,
537 NoteAttachment::default(),
538 &mut self.rng,
539 )?;
540 self.add_output_note(OutputNote::Full(note.clone()));
541
542 Ok(note)
543 }
544
545 pub fn add_p2ide_note(
551 &mut self,
552 sender_account_id: AccountId,
553 target_account_id: AccountId,
554 asset: &[Asset],
555 note_type: NoteType,
556 reclaim_height: Option<BlockNumber>,
557 timelock_height: Option<BlockNumber>,
558 ) -> Result<Note, NoteError> {
559 let storage = P2ideNoteStorage::new(target_account_id, reclaim_height, timelock_height);
560
561 let note = P2ideNote::create(
562 sender_account_id,
563 storage,
564 asset.to_vec(),
565 note_type,
566 Default::default(),
567 &mut self.rng,
568 )?;
569
570 self.add_output_note(OutputNote::Full(note.clone()));
571
572 Ok(note)
573 }
574
575 pub fn add_swap_note(
577 &mut self,
578 sender: AccountId,
579 offered_asset: Asset,
580 requested_asset: Asset,
581 payback_note_type: NoteType,
582 ) -> anyhow::Result<(Note, NoteDetails)> {
583 let (swap_note, payback_note) = SwapNote::create(
584 sender,
585 offered_asset,
586 requested_asset,
587 NoteType::Public,
588 NoteAttachment::default(),
589 payback_note_type,
590 NoteAttachment::default(),
591 &mut self.rng,
592 )?;
593
594 self.add_output_note(OutputNote::Full(swap_note.clone()));
595
596 Ok((swap_note, payback_note))
597 }
598
599 pub fn add_spawn_note<'note, I>(
610 &mut self,
611 output_notes: impl IntoIterator<Item = &'note Note, IntoIter = I>,
612 ) -> anyhow::Result<Note>
613 where
614 I: ExactSizeIterator<Item = &'note Note>,
615 {
616 let note = create_spawn_note(output_notes)?;
617 self.add_output_note(OutputNote::Full(note.clone()));
618
619 Ok(note)
620 }
621
622 pub fn add_p2id_note_with_fee(
629 &mut self,
630 target_account_id: AccountId,
631 amount: u64,
632 ) -> anyhow::Result<Note> {
633 let fee_asset = self.native_fee_asset(amount)?;
634 let note = self.add_p2id_note(
635 self.native_asset_id,
636 target_account_id,
637 &[Asset::from(fee_asset)],
638 NoteType::Public,
639 )?;
640
641 Ok(note)
642 }
643
644 pub fn rng_mut(&mut self) -> &mut RpoRandomCoin {
651 &mut self.rng
652 }
653
654 fn native_fee_asset(&self, amount: u64) -> anyhow::Result<FungibleAsset> {
656 FungibleAsset::new(self.native_asset_id, amount).context("failed to create fee asset")
657 }
658}
659
660impl Default for MockChainBuilder {
661 fn default() -> Self {
662 Self::new()
663 }
664}