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 AccountStorage,
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::dsa::ecdsa_k256_keccak::SecretKey;
44use miden_protocol::crypto::merkle::smt::Smt;
45use miden_protocol::errors::NoteError;
46use miden_protocol::note::{Note, NoteAttachment, NoteDetails, NoteType};
47use miden_protocol::testing::account_id::ACCOUNT_ID_NATIVE_ASSET_FAUCET;
48use miden_protocol::testing::random_signer::RandomBlockSigner;
49use miden_protocol::transaction::{OrderedTransactionHeaders, OutputNote, TransactionKernel};
50use miden_protocol::{Felt, MAX_OUTPUT_NOTES_PER_BATCH, Word, ZERO};
51use miden_standards::account::faucets::{BasicFungibleFaucet, NetworkFungibleFaucet};
52use miden_standards::account::wallets::BasicWallet;
53use miden_standards::note::{create_p2id_note, create_p2ide_note, create_swap_note};
54use miden_standards::testing::account_component::MockAccountComponent;
55use rand::Rng;
56
57use crate::mock_chain::chain::AccountAuthenticator;
58use crate::utils::{create_p2any_note, create_spawn_note};
59use crate::{AccountState, Auth, MockChain};
60
61#[derive(Debug, Clone)]
105pub struct MockChainBuilder {
106 accounts: BTreeMap<AccountId, Account>,
107 account_authenticators: BTreeMap<AccountId, AccountAuthenticator>,
108 notes: Vec<OutputNote>,
109 rng: RpoRandomCoin,
110 native_asset_id: AccountId,
112 verification_base_fee: u32,
113}
114
115impl MockChainBuilder {
116 pub fn new() -> Self {
126 let native_asset_id =
127 ACCOUNT_ID_NATIVE_ASSET_FAUCET.try_into().expect("account ID should be valid");
128
129 Self {
130 accounts: BTreeMap::new(),
131 account_authenticators: BTreeMap::new(),
132 notes: Vec::new(),
133 rng: RpoRandomCoin::new(Default::default()),
134 native_asset_id,
135 verification_base_fee: 0,
136 }
137 }
138
139 pub fn with_accounts(accounts: impl IntoIterator<Item = Account>) -> anyhow::Result<Self> {
148 let mut builder = Self::new();
149
150 for account in accounts {
151 builder.add_account(account)?;
152 }
153
154 Ok(builder)
155 }
156
157 pub fn native_asset_id(mut self, native_asset_id: AccountId) -> Self {
165 self.native_asset_id = native_asset_id;
166 self
167 }
168
169 pub fn verification_base_fee(mut self, verification_base_fee: u32) -> Self {
173 self.verification_base_fee = verification_base_fee;
174 self
175 }
176
177 pub fn build(self) -> anyhow::Result<MockChain> {
179 let block_account_updates: Vec<BlockAccountUpdate> = self
181 .accounts
182 .into_values()
183 .map(|account| {
184 let account_id = account.id();
185 let account_commitment = account.commitment();
186 let account_delta = AccountDelta::try_from(account)
187 .expect("chain builder should only store existing accounts without seeds");
188 let update_details = AccountUpdateDetails::Delta(account_delta);
189
190 BlockAccountUpdate::new(account_id, account_commitment, update_details)
191 })
192 .collect();
193
194 let account_tree = AccountTree::with_entries(
195 block_account_updates
196 .iter()
197 .map(|account| (account.account_id(), account.final_state_commitment())),
198 )
199 .context("failed to create genesis account tree")?;
200
201 let note_chunks = self.notes.into_iter().chunks(MAX_OUTPUT_NOTES_PER_BATCH);
202 let output_note_batches: Vec<OutputNoteBatch> = note_chunks
203 .into_iter()
204 .map(|batch_notes| batch_notes.into_iter().enumerate().collect::<Vec<_>>())
205 .collect();
206
207 let created_nullifiers = Vec::new();
208 let transactions = OrderedTransactionHeaders::new_unchecked(Vec::new());
209
210 let note_tree = BlockNoteTree::from_note_batches(&output_note_batches)
211 .context("failed to create block note tree")?;
212
213 let version = 0;
214 let prev_block_commitment = Word::empty();
215 let block_num = BlockNumber::from(0u32);
216 let chain_commitment = Blockchain::new().commitment();
217 let account_root = account_tree.root();
218 let nullifier_root = NullifierTree::<Smt>::default().root();
219 let note_root = note_tree.root();
220 let tx_commitment = transactions.commitment();
221 let tx_kernel_commitment = TransactionKernel.to_commitment();
222 let timestamp = MockChain::TIMESTAMP_START_SECS;
223 let fee_parameters = FeeParameters::new(self.native_asset_id, self.verification_base_fee)
224 .context("failed to construct fee parameters")?;
225 let validator_secret_key = SecretKey::random();
226 let validator_public_key = validator_secret_key.public_key();
227
228 let header = BlockHeader::new(
229 version,
230 prev_block_commitment,
231 block_num,
232 chain_commitment,
233 account_root,
234 nullifier_root,
235 note_root,
236 tx_commitment,
237 tx_kernel_commitment,
238 validator_public_key,
239 fee_parameters,
240 timestamp,
241 );
242
243 let body = BlockBody::new_unchecked(
244 block_account_updates,
245 output_note_batches,
246 created_nullifiers,
247 transactions,
248 );
249
250 let signature = validator_secret_key.sign(header.commitment());
251 let block_proof = BlockProof::new_dummy();
252 let genesis_block = ProvenBlock::new_unchecked(header, body, signature, block_proof);
253
254 MockChain::from_genesis_block(
255 genesis_block,
256 account_tree,
257 self.account_authenticators,
258 validator_secret_key,
259 )
260 }
261
262 pub fn create_new_wallet(&mut self, auth_method: Auth) -> anyhow::Result<Account> {
271 let account_builder = AccountBuilder::new(self.rng.random())
272 .storage_mode(AccountStorageMode::Public)
273 .with_component(BasicWallet);
274
275 self.add_account_from_builder(auth_method, account_builder, AccountState::New)
276 }
277
278 pub fn add_existing_wallet(&mut self, auth_method: Auth) -> anyhow::Result<Account> {
281 self.add_existing_wallet_with_assets(auth_method, [])
282 }
283
284 pub fn add_existing_wallet_with_assets(
287 &mut self,
288 auth_method: Auth,
289 assets: impl IntoIterator<Item = Asset>,
290 ) -> anyhow::Result<Account> {
291 let account_builder = Account::builder(self.rng.random())
292 .storage_mode(AccountStorageMode::Public)
293 .with_component(BasicWallet)
294 .with_assets(assets);
295
296 self.add_account_from_builder(auth_method, account_builder, AccountState::Exists)
297 }
298
299 pub fn create_new_faucet(
305 &mut self,
306 auth_method: Auth,
307 token_symbol: &str,
308 max_supply: u64,
309 ) -> anyhow::Result<Account> {
310 let token_symbol = TokenSymbol::new(token_symbol)
311 .with_context(|| format!("invalid token symbol: {token_symbol}"))?;
312 let max_supply_felt = max_supply.try_into().map_err(|_| {
313 anyhow::anyhow!("max supply value cannot be converted to Felt: {max_supply}")
314 })?;
315 let basic_faucet =
316 BasicFungibleFaucet::new(token_symbol, DEFAULT_FAUCET_DECIMALS, max_supply_felt)
317 .context("failed to create BasicFungibleFaucet")?;
318
319 let account_builder = AccountBuilder::new(self.rng.random())
320 .storage_mode(AccountStorageMode::Public)
321 .account_type(AccountType::FungibleFaucet)
322 .with_component(basic_faucet);
323
324 self.add_account_from_builder(auth_method, account_builder, AccountState::New)
325 }
326
327 pub fn add_existing_basic_faucet(
332 &mut self,
333 auth_method: Auth,
334 token_symbol: &str,
335 max_supply: u64,
336 total_issuance: Option<u64>,
337 ) -> anyhow::Result<Account> {
338 let token_symbol = TokenSymbol::new(token_symbol).context("invalid argument")?;
339 let basic_faucet =
340 BasicFungibleFaucet::new(token_symbol, DEFAULT_FAUCET_DECIMALS, Felt::new(max_supply))
341 .context("invalid argument")?;
342
343 let account_builder = AccountBuilder::new(self.rng.random())
344 .storage_mode(AccountStorageMode::Public)
345 .with_component(basic_faucet)
346 .account_type(AccountType::FungibleFaucet);
347
348 let mut account =
349 self.add_account_from_builder(auth_method, account_builder, AccountState::Exists)?;
350
351 if let Some(issuance) = total_issuance {
354 account
355 .storage_mut()
356 .set_item(
357 AccountStorage::faucet_sysdata_slot(),
358 Word::from([ZERO, ZERO, ZERO, Felt::new(issuance)]),
359 )
360 .context("failed to set faucet storage")?;
361 self.accounts.insert(account.id(), account.clone());
362 }
363
364 Ok(account)
365 }
366
367 pub fn add_existing_network_faucet(
371 &mut self,
372 token_symbol: &str,
373 max_supply: u64,
374 owner_account_id: AccountId,
375 total_issuance: Option<u64>,
376 ) -> anyhow::Result<Account> {
377 let token_symbol = TokenSymbol::new(token_symbol).context("invalid argument")?;
378 let network_faucet = NetworkFungibleFaucet::new(
379 token_symbol,
380 DEFAULT_FAUCET_DECIMALS,
381 Felt::new(max_supply),
382 owner_account_id,
383 )
384 .context("invalid argument")?;
385
386 let account_builder = AccountBuilder::new(self.rng.random())
387 .storage_mode(AccountStorageMode::Network)
388 .with_component(network_faucet)
389 .account_type(AccountType::FungibleFaucet);
390
391 let mut account =
393 self.add_account_from_builder(Auth::IncrNonce, account_builder, AccountState::Exists)?;
394
395 if let Some(issuance) = total_issuance {
398 account
399 .storage_mut()
400 .set_item(
401 AccountStorage::faucet_sysdata_slot(),
402 Word::from([ZERO, ZERO, ZERO, Felt::new(issuance)]),
403 )
404 .context("failed to set faucet storage")?;
405 self.accounts.insert(account.id(), account.clone());
406 }
407
408 Ok(account)
409 }
410
411 pub fn create_new_mock_account(&mut self, auth_method: Auth) -> anyhow::Result<Account> {
414 let account_builder = Account::builder(self.rng.random())
415 .storage_mode(AccountStorageMode::Public)
416 .with_component(MockAccountComponent::with_empty_slots());
417
418 self.add_account_from_builder(auth_method, account_builder, AccountState::New)
419 }
420
421 pub fn add_existing_mock_account(&mut self, auth_method: Auth) -> anyhow::Result<Account> {
424 self.add_existing_mock_account_with_storage_and_assets(auth_method, [], [])
425 }
426
427 pub fn add_existing_mock_account_with_storage(
430 &mut self,
431 auth_method: Auth,
432 slots: impl IntoIterator<Item = StorageSlot>,
433 ) -> anyhow::Result<Account> {
434 self.add_existing_mock_account_with_storage_and_assets(auth_method, slots, [])
435 }
436
437 pub fn add_existing_mock_account_with_assets(
440 &mut self,
441 auth_method: Auth,
442 assets: impl IntoIterator<Item = Asset>,
443 ) -> anyhow::Result<Account> {
444 self.add_existing_mock_account_with_storage_and_assets(auth_method, [], assets)
445 }
446
447 pub fn add_existing_mock_account_with_storage_and_assets(
450 &mut self,
451 auth_method: Auth,
452 slots: impl IntoIterator<Item = StorageSlot>,
453 assets: impl IntoIterator<Item = Asset>,
454 ) -> anyhow::Result<Account> {
455 let account_builder = Account::builder(self.rng.random())
456 .storage_mode(AccountStorageMode::Public)
457 .with_component(MockAccountComponent::with_slots(slots.into_iter().collect()))
458 .with_assets(assets);
459
460 self.add_account_from_builder(auth_method, account_builder, AccountState::Exists)
461 }
462
463 pub fn add_account_from_builder(
474 &mut self,
475 auth_method: Auth,
476 mut account_builder: AccountBuilder,
477 account_state: AccountState,
478 ) -> anyhow::Result<Account> {
479 let (auth_component, authenticator) = auth_method.build_component();
480 account_builder = account_builder.with_auth_component(auth_component);
481
482 let account = if let AccountState::New = account_state {
483 account_builder.build().context("failed to build account from builder")?
484 } else {
485 account_builder
486 .build_existing()
487 .context("failed to build account from builder")?
488 };
489
490 self.account_authenticators
491 .insert(account.id(), AccountAuthenticator::new(authenticator));
492
493 if let AccountState::Exists = account_state {
494 self.accounts.insert(account.id(), account.clone());
495 }
496
497 Ok(account)
498 }
499
500 pub fn add_account(&mut self, account: Account) -> anyhow::Result<()> {
509 self.accounts.insert(account.id(), account);
510
511 Ok(())
514 }
515
516 pub fn add_output_note(&mut self, note: impl Into<OutputNote>) {
521 self.notes.push(note.into());
522 }
523
524 pub fn add_p2any_note(
529 &mut self,
530 sender_account_id: AccountId,
531 note_type: NoteType,
532 assets: impl IntoIterator<Item = Asset>,
533 ) -> anyhow::Result<Note> {
534 let note = create_p2any_note(sender_account_id, note_type, assets, &mut self.rng);
535 self.add_output_note(OutputNote::Full(note.clone()));
536
537 Ok(note)
538 }
539
540 pub fn add_p2id_note(
546 &mut self,
547 sender_account_id: AccountId,
548 target_account_id: AccountId,
549 asset: &[Asset],
550 note_type: NoteType,
551 ) -> Result<Note, NoteError> {
552 let note = create_p2id_note(
553 sender_account_id,
554 target_account_id,
555 asset.to_vec(),
556 note_type,
557 NoteAttachment::default(),
558 &mut self.rng,
559 )?;
560 self.add_output_note(OutputNote::Full(note.clone()));
561
562 Ok(note)
563 }
564
565 pub fn add_p2ide_note(
571 &mut self,
572 sender_account_id: AccountId,
573 target_account_id: AccountId,
574 asset: &[Asset],
575 note_type: NoteType,
576 reclaim_height: Option<BlockNumber>,
577 timelock_height: Option<BlockNumber>,
578 ) -> Result<Note, NoteError> {
579 let note = create_p2ide_note(
580 sender_account_id,
581 target_account_id,
582 asset.to_vec(),
583 reclaim_height,
584 timelock_height,
585 note_type,
586 Default::default(),
587 &mut self.rng,
588 )?;
589
590 self.add_output_note(OutputNote::Full(note.clone()));
591
592 Ok(note)
593 }
594
595 pub fn add_swap_note(
597 &mut self,
598 sender: AccountId,
599 offered_asset: Asset,
600 requested_asset: Asset,
601 payback_note_type: NoteType,
602 ) -> anyhow::Result<(Note, NoteDetails)> {
603 let (swap_note, payback_note) = create_swap_note(
604 sender,
605 offered_asset,
606 requested_asset,
607 NoteType::Public,
608 NoteAttachment::default(),
609 payback_note_type,
610 NoteAttachment::default(),
611 &mut self.rng,
612 )?;
613
614 self.add_output_note(OutputNote::Full(swap_note.clone()));
615
616 Ok((swap_note, payback_note))
617 }
618
619 pub fn add_spawn_note<'note, I>(
630 &mut self,
631 output_notes: impl IntoIterator<Item = &'note Note, IntoIter = I>,
632 ) -> anyhow::Result<Note>
633 where
634 I: ExactSizeIterator<Item = &'note Note>,
635 {
636 let note = create_spawn_note(output_notes)?;
637 self.add_output_note(OutputNote::Full(note.clone()));
638
639 Ok(note)
640 }
641
642 pub fn add_p2id_note_with_fee(
649 &mut self,
650 target_account_id: AccountId,
651 amount: u64,
652 ) -> anyhow::Result<Note> {
653 let fee_asset = self.native_fee_asset(amount)?;
654 let note = self.add_p2id_note(
655 self.native_asset_id,
656 target_account_id,
657 &[Asset::from(fee_asset)],
658 NoteType::Public,
659 )?;
660
661 Ok(note)
662 }
663
664 pub fn rng_mut(&mut self) -> &mut RpoRandomCoin {
671 &mut self.rng
672 }
673
674 fn native_fee_asset(&self, amount: u64) -> anyhow::Result<FungibleAsset> {
676 FungibleAsset::new(self.native_asset_id, amount).context("failed to create fee asset")
677 }
678}
679
680impl Default for MockChainBuilder {
681 fn default() -> Self {
682 Self::new()
683 }
684}