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::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, RawOutputNote, TransactionKernel};
48use miden_protocol::{Felt, MAX_OUTPUT_NOTES_PER_BATCH, Word};
49use miden_standards::account::access::Ownable2Step;
50use miden_standards::account::faucets::{BasicFungibleFaucet, NetworkFungibleFaucet};
51use miden_standards::account::wallets::BasicWallet;
52use miden_standards::note::{P2idNote, P2ideNote, P2ideNoteStorage, SwapNote};
53use miden_standards::testing::account_component::MockAccountComponent;
54use rand::Rng;
55
56use crate::mock_chain::chain::AccountAuthenticator;
57use crate::utils::{create_p2any_note, create_spawn_note};
58use crate::{AccountState, Auth, MockChain};
59
60#[derive(Debug, Clone)]
104pub struct MockChainBuilder {
105 accounts: BTreeMap<AccountId, Account>,
106 account_authenticators: BTreeMap<AccountId, AccountAuthenticator>,
107 notes: Vec<RawOutputNote>,
108 rng: RpoRandomCoin,
109 native_asset_id: AccountId,
111 verification_base_fee: u32,
112}
113
114impl MockChainBuilder {
115 pub fn new() -> Self {
125 let native_asset_id =
126 ACCOUNT_ID_NATIVE_ASSET_FAUCET.try_into().expect("account ID should be valid");
127
128 Self {
129 accounts: BTreeMap::new(),
130 account_authenticators: BTreeMap::new(),
131 notes: Vec::new(),
132 rng: RpoRandomCoin::new(Default::default()),
133 native_asset_id,
134 verification_base_fee: 0,
135 }
136 }
137
138 pub fn with_accounts(accounts: impl IntoIterator<Item = Account>) -> anyhow::Result<Self> {
147 let mut builder = Self::new();
148
149 for account in accounts {
150 builder.add_account(account)?;
151 }
152
153 Ok(builder)
154 }
155
156 pub fn native_asset_id(mut self, native_asset_id: AccountId) -> Self {
164 self.native_asset_id = native_asset_id;
165 self
166 }
167
168 pub fn verification_base_fee(mut self, verification_base_fee: u32) -> Self {
172 self.verification_base_fee = verification_base_fee;
173 self
174 }
175
176 pub fn build(self) -> anyhow::Result<MockChain> {
178 let block_account_updates: Vec<BlockAccountUpdate> = self
180 .accounts
181 .into_values()
182 .map(|account| {
183 let account_id = account.id();
184 let account_commitment = account.to_commitment();
185 let account_delta = AccountDelta::try_from(account)
186 .expect("chain builder should only store existing accounts without seeds");
187 let update_details = AccountUpdateDetails::Delta(account_delta);
188
189 BlockAccountUpdate::new(account_id, account_commitment, update_details)
190 })
191 .collect();
192
193 let account_tree = AccountTree::with_entries(
194 block_account_updates
195 .iter()
196 .map(|account| (account.account_id(), account.final_state_commitment())),
197 )
198 .context("failed to create genesis account tree")?;
199
200 let full_notes: Vec<Note> = self
202 .notes
203 .iter()
204 .filter_map(|note| match note {
205 RawOutputNote::Full(n) => Some(n.clone()),
206 _ => None,
207 })
208 .collect();
209
210 let proven_notes: Vec<_> = self
211 .notes
212 .into_iter()
213 .map(|note| note.to_output_note().expect("genesis note should be valid"))
214 .collect();
215 let note_chunks = proven_notes.into_iter().chunks(MAX_OUTPUT_NOTES_PER_BATCH);
216 let output_note_batches: Vec<OutputNoteBatch> = note_chunks
217 .into_iter()
218 .map(|batch_notes| batch_notes.into_iter().enumerate().collect::<Vec<_>>())
219 .collect();
220
221 let created_nullifiers = Vec::new();
222 let transactions = OrderedTransactionHeaders::new_unchecked(Vec::new());
223
224 let note_tree = BlockNoteTree::from_note_batches(&output_note_batches)
225 .context("failed to create block note tree")?;
226
227 let version = 0;
228 let prev_block_commitment = Word::empty();
229 let block_num = BlockNumber::from(0u32);
230 let chain_commitment = Blockchain::new().commitment();
231 let account_root = account_tree.root();
232 let nullifier_root = NullifierTree::<Smt>::default().root();
233 let note_root = note_tree.root();
234 let tx_commitment = transactions.commitment();
235 let tx_kernel_commitment = TransactionKernel.to_commitment();
236 let timestamp = MockChain::TIMESTAMP_START_SECS;
237 let fee_parameters = FeeParameters::new(self.native_asset_id, self.verification_base_fee)
238 .context("failed to construct fee parameters")?;
239 let validator_secret_key = random_secret_key();
240 let validator_public_key = validator_secret_key.public_key();
241
242 let header = BlockHeader::new(
243 version,
244 prev_block_commitment,
245 block_num,
246 chain_commitment,
247 account_root,
248 nullifier_root,
249 note_root,
250 tx_commitment,
251 tx_kernel_commitment,
252 validator_public_key,
253 fee_parameters,
254 timestamp,
255 );
256
257 let body = BlockBody::new_unchecked(
258 block_account_updates,
259 output_note_batches,
260 created_nullifiers,
261 transactions,
262 );
263
264 let signature = validator_secret_key.sign(header.commitment());
265 let block_proof = BlockProof::new_dummy();
266 let genesis_block = ProvenBlock::new_unchecked(header, body, signature, block_proof);
267
268 MockChain::from_genesis_block(
269 genesis_block,
270 account_tree,
271 self.account_authenticators,
272 validator_secret_key,
273 full_notes,
274 )
275 }
276
277 pub fn create_new_wallet(&mut self, auth_method: Auth) -> anyhow::Result<Account> {
286 let account_builder = AccountBuilder::new(self.rng.random())
287 .storage_mode(AccountStorageMode::Public)
288 .with_component(BasicWallet);
289
290 self.add_account_from_builder(auth_method, account_builder, AccountState::New)
291 }
292
293 pub fn add_existing_wallet(&mut self, auth_method: Auth) -> anyhow::Result<Account> {
296 self.add_existing_wallet_with_assets(auth_method, [])
297 }
298
299 pub fn add_existing_wallet_with_assets(
302 &mut self,
303 auth_method: Auth,
304 assets: impl IntoIterator<Item = Asset>,
305 ) -> anyhow::Result<Account> {
306 let account_builder = Account::builder(self.rng.random())
307 .storage_mode(AccountStorageMode::Public)
308 .with_component(BasicWallet)
309 .with_assets(assets);
310
311 self.add_account_from_builder(auth_method, account_builder, AccountState::Exists)
312 }
313
314 pub fn create_new_faucet(
320 &mut self,
321 auth_method: Auth,
322 token_symbol: &str,
323 max_supply: u64,
324 ) -> anyhow::Result<Account> {
325 let token_symbol = TokenSymbol::new(token_symbol)
326 .with_context(|| format!("invalid token symbol: {token_symbol}"))?;
327 let max_supply_felt = Felt::try_from(max_supply)?;
328 let basic_faucet =
329 BasicFungibleFaucet::new(token_symbol, DEFAULT_FAUCET_DECIMALS, max_supply_felt)
330 .context("failed to create BasicFungibleFaucet")?;
331
332 let account_builder = AccountBuilder::new(self.rng.random())
333 .storage_mode(AccountStorageMode::Public)
334 .account_type(AccountType::FungibleFaucet)
335 .with_component(basic_faucet);
336
337 self.add_account_from_builder(auth_method, account_builder, AccountState::New)
338 }
339
340 pub fn add_existing_basic_faucet(
345 &mut self,
346 auth_method: Auth,
347 token_symbol: &str,
348 max_supply: u64,
349 token_supply: Option<u64>,
350 ) -> anyhow::Result<Account> {
351 let max_supply = Felt::try_from(max_supply)?;
352 let token_supply = Felt::try_from(token_supply.unwrap_or(0))?;
353 let token_symbol =
354 TokenSymbol::new(token_symbol).context("failed to create token symbol")?;
355
356 let basic_faucet =
357 BasicFungibleFaucet::new(token_symbol, DEFAULT_FAUCET_DECIMALS, max_supply)
358 .and_then(|fungible_faucet| fungible_faucet.with_token_supply(token_supply))
359 .context("failed to create basic fungible faucet")?;
360
361 let account_builder = AccountBuilder::new(self.rng.random())
362 .storage_mode(AccountStorageMode::Public)
363 .with_component(basic_faucet)
364 .account_type(AccountType::FungibleFaucet);
365
366 self.add_account_from_builder(auth_method, account_builder, AccountState::Exists)
367 }
368
369 pub fn add_existing_network_faucet(
373 &mut self,
374 token_symbol: &str,
375 max_supply: u64,
376 owner_account_id: AccountId,
377 token_supply: Option<u64>,
378 ) -> anyhow::Result<Account> {
379 let max_supply = Felt::try_from(max_supply)?;
380 let token_supply = Felt::try_from(token_supply.unwrap_or(0))?;
381 let token_symbol =
382 TokenSymbol::new(token_symbol).context("failed to create token symbol")?;
383
384 let network_faucet =
385 NetworkFungibleFaucet::new(token_symbol, DEFAULT_FAUCET_DECIMALS, max_supply)
386 .and_then(|fungible_faucet| fungible_faucet.with_token_supply(token_supply))
387 .context("failed to create network fungible faucet")?;
388
389 let account_builder = AccountBuilder::new(self.rng.random())
390 .storage_mode(AccountStorageMode::Network)
391 .with_component(network_faucet)
392 .with_component(Ownable2Step::new(owner_account_id))
393 .account_type(AccountType::FungibleFaucet);
394
395 self.add_account_from_builder(Auth::IncrNonce, account_builder, AccountState::Exists)
397 }
398
399 pub fn create_new_mock_account(&mut self, auth_method: Auth) -> anyhow::Result<Account> {
402 let account_builder = Account::builder(self.rng.random())
403 .storage_mode(AccountStorageMode::Public)
404 .with_component(MockAccountComponent::with_empty_slots());
405
406 self.add_account_from_builder(auth_method, account_builder, AccountState::New)
407 }
408
409 pub fn add_existing_mock_account(&mut self, auth_method: Auth) -> anyhow::Result<Account> {
412 self.add_existing_mock_account_with_storage_and_assets(auth_method, [], [])
413 }
414
415 pub fn add_existing_mock_account_with_storage(
418 &mut self,
419 auth_method: Auth,
420 slots: impl IntoIterator<Item = StorageSlot>,
421 ) -> anyhow::Result<Account> {
422 self.add_existing_mock_account_with_storage_and_assets(auth_method, slots, [])
423 }
424
425 pub fn add_existing_mock_account_with_assets(
428 &mut self,
429 auth_method: Auth,
430 assets: impl IntoIterator<Item = Asset>,
431 ) -> anyhow::Result<Account> {
432 self.add_existing_mock_account_with_storage_and_assets(auth_method, [], assets)
433 }
434
435 pub fn add_existing_mock_account_with_storage_and_assets(
438 &mut self,
439 auth_method: Auth,
440 slots: impl IntoIterator<Item = StorageSlot>,
441 assets: impl IntoIterator<Item = Asset>,
442 ) -> anyhow::Result<Account> {
443 let account_builder = Account::builder(self.rng.random())
444 .storage_mode(AccountStorageMode::Public)
445 .with_component(MockAccountComponent::with_slots(slots.into_iter().collect()))
446 .with_assets(assets);
447
448 self.add_account_from_builder(auth_method, account_builder, AccountState::Exists)
449 }
450
451 pub fn add_account_from_builder(
462 &mut self,
463 auth_method: Auth,
464 mut account_builder: AccountBuilder,
465 account_state: AccountState,
466 ) -> anyhow::Result<Account> {
467 let (auth_component, authenticator) = auth_method.build_component();
468 account_builder = account_builder.with_auth_component(auth_component);
469
470 let account = if let AccountState::New = account_state {
471 account_builder.build().context("failed to build account from builder")?
472 } else {
473 account_builder
474 .build_existing()
475 .context("failed to build account from builder")?
476 };
477
478 self.account_authenticators
479 .insert(account.id(), AccountAuthenticator::new(authenticator));
480
481 if let AccountState::Exists = account_state {
482 self.accounts.insert(account.id(), account.clone());
483 }
484
485 Ok(account)
486 }
487
488 pub fn add_account(&mut self, account: Account) -> anyhow::Result<()> {
497 self.accounts.insert(account.id(), account);
498
499 Ok(())
502 }
503
504 pub fn add_output_note(&mut self, note: impl Into<RawOutputNote>) {
509 self.notes.push(note.into());
510 }
511
512 pub fn add_p2any_note(
517 &mut self,
518 sender_account_id: AccountId,
519 note_type: NoteType,
520 assets: impl IntoIterator<Item = Asset>,
521 ) -> anyhow::Result<Note> {
522 let note = create_p2any_note(sender_account_id, note_type, assets, &mut self.rng);
523 self.add_output_note(RawOutputNote::Full(note.clone()));
524
525 Ok(note)
526 }
527
528 pub fn add_p2id_note(
534 &mut self,
535 sender_account_id: AccountId,
536 target_account_id: AccountId,
537 asset: &[Asset],
538 note_type: NoteType,
539 ) -> Result<Note, NoteError> {
540 let note = P2idNote::create(
541 sender_account_id,
542 target_account_id,
543 asset.to_vec(),
544 note_type,
545 NoteAttachment::default(),
546 &mut self.rng,
547 )?;
548 self.add_output_note(RawOutputNote::Full(note.clone()));
549
550 Ok(note)
551 }
552
553 pub fn add_p2ide_note(
559 &mut self,
560 sender_account_id: AccountId,
561 target_account_id: AccountId,
562 asset: &[Asset],
563 note_type: NoteType,
564 reclaim_height: Option<BlockNumber>,
565 timelock_height: Option<BlockNumber>,
566 ) -> Result<Note, NoteError> {
567 let storage = P2ideNoteStorage::new(target_account_id, reclaim_height, timelock_height);
568
569 let note = P2ideNote::create(
570 sender_account_id,
571 storage,
572 asset.to_vec(),
573 note_type,
574 Default::default(),
575 &mut self.rng,
576 )?;
577
578 self.add_output_note(RawOutputNote::Full(note.clone()));
579
580 Ok(note)
581 }
582
583 pub fn add_swap_note(
585 &mut self,
586 sender: AccountId,
587 offered_asset: Asset,
588 requested_asset: Asset,
589 payback_note_type: NoteType,
590 ) -> anyhow::Result<(Note, NoteDetails)> {
591 let (swap_note, payback_note) = SwapNote::create(
592 sender,
593 offered_asset,
594 requested_asset,
595 NoteType::Public,
596 NoteAttachment::default(),
597 payback_note_type,
598 NoteAttachment::default(),
599 &mut self.rng,
600 )?;
601
602 self.add_output_note(RawOutputNote::Full(swap_note.clone()));
603
604 Ok((swap_note, payback_note))
605 }
606
607 pub fn add_spawn_note<'note, I>(
618 &mut self,
619 output_notes: impl IntoIterator<Item = &'note Note, IntoIter = I>,
620 ) -> anyhow::Result<Note>
621 where
622 I: ExactSizeIterator<Item = &'note Note>,
623 {
624 let note = create_spawn_note(output_notes)?;
625 self.add_output_note(RawOutputNote::Full(note.clone()));
626
627 Ok(note)
628 }
629
630 pub fn add_p2id_note_with_fee(
637 &mut self,
638 target_account_id: AccountId,
639 amount: u64,
640 ) -> anyhow::Result<Note> {
641 let fee_asset = self.native_fee_asset(amount)?;
642 let note = self.add_p2id_note(
643 self.native_asset_id,
644 target_account_id,
645 &[Asset::from(fee_asset)],
646 NoteType::Public,
647 )?;
648
649 Ok(note)
650 }
651
652 pub fn rng_mut(&mut self) -> &mut RpoRandomCoin {
659 &mut self.rng
660 }
661
662 fn native_fee_asset(&self, amount: u64) -> anyhow::Result<FungibleAsset> {
664 FungibleAsset::new(self.native_asset_id, amount).context("failed to create fee asset")
665 }
666}
667
668impl Default for MockChainBuilder {
669 fn default() -> Self {
670 Self::new()
671 }
672}