Skip to main content

miden_testing/mock_chain/
chain_builder.rs

1use alloc::collections::BTreeMap;
2use alloc::vec::Vec;
3
4use anyhow::Context;
5
6// CONSTANTS
7// ================================================================================================
8
9/// Default number of decimals for faucets created in tests.
10const DEFAULT_FAUCET_DECIMALS: u8 = 10;
11
12// IMPORTS
13// ================================================================================================
14
15use 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/// A builder for a [`MockChain`]'s genesis block.
67///
68/// ## Example
69///
70/// ```
71/// # use anyhow::Result;
72/// # use miden_protocol::{
73/// #    asset::{Asset, FungibleAsset},
74/// #    note::NoteType,
75/// # };
76/// # use miden_testing::{Auth, MockChain};
77/// #
78/// # fn main() -> Result<()> {
79/// let mut builder = MockChain::builder();
80/// let existing_wallet =
81///     builder.add_existing_wallet_with_assets(Auth::IncrNonce, [FungibleAsset::mock(500)])?;
82/// let new_wallet = builder.create_new_wallet(Auth::IncrNonce)?;
83///
84/// let existing_note = builder.add_p2id_note(
85///     existing_wallet.id(),
86///     new_wallet.id(),
87///     &[FungibleAsset::mock(100)],
88///     NoteType::Private,
89/// )?;
90/// let chain = builder.build()?;
91///
92/// // The existing wallet and note should be part of the chain state.
93/// assert!(chain.committed_account(existing_wallet.id()).is_ok());
94/// assert!(chain.committed_notes().get(&existing_note.id()).is_some());
95///
96/// // The new wallet should *not* be part of the chain state - it must be created in
97/// // a transaction first.
98/// assert!(chain.committed_account(new_wallet.id()).is_err());
99///
100/// # Ok(())
101/// # }
102/// ```
103///
104/// Note the distinction between `add_` and `create_` APIs. Any `add_` APIs will add something to
105/// the genesis chain state while `create_` APIs do not mutate the genesis state. The latter are
106/// simply convenient for creating accounts or notes that will be created by transactions.
107///
108/// See also the [`MockChain`] docs for examples on using the mock chain.
109#[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    // Fee parameters.
116    native_asset_id: AccountId,
117    verification_base_fee: u32,
118}
119
120impl MockChainBuilder {
121    // CONSTRUCTORS
122    // ----------------------------------------------------------------------------------------
123
124    /// Initializes a new mock chain builder with an empty state.
125    ///
126    /// By default, the `native_asset_id` is set to [`ACCOUNT_ID_NATIVE_ASSET_FAUCET`] and can be
127    /// overwritten using [`Self::native_asset_id`].
128    ///
129    /// The `verification_base_fee` is initialized to 0 which means no fees are required by default.
130    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    /// Initializes a new mock chain builder with the provided accounts.
145    ///
146    /// This method only adds the accounts and cannot not register any authenticators for them.
147    /// Calling [`MockChain::build_tx_context`] on accounts added in this way will not work if the
148    /// account needs an authenticator.
149    ///
150    /// Due to these limitations, prefer using other methods to add accounts to the chain, e.g.
151    /// [`MockChainBuilder::add_account_from_builder`].
152    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    // BUILDER METHODS
163    // ----------------------------------------------------------------------------------------
164
165    /// Sets the native asset ID of the chain.
166    ///
167    /// This must be a fungible faucet [`AccountId`] and is the asset in which fees will be accepted
168    /// by the transaction kernel.
169    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    /// Sets the `verification_base_fee` of the chain.
175    ///
176    /// See [`FeeParameters`] for more details.
177    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    /// Consumes the builder, creates the genesis block of the chain and returns the [`MockChain`].
183    pub fn build(self) -> anyhow::Result<MockChain> {
184        // Create the genesis block, consisting of the provided accounts and notes.
185        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        // Extract full notes before shrinking for later use in MockChain
207        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    // ACCOUNT METHODS
284    // ----------------------------------------------------------------------------------------
285
286    /// Creates a new public [`BasicWallet`] account and registers the authenticator (if any) for
287    /// it.
288    ///
289    /// This does not add the account to the chain state, but it can still be used to call
290    /// [`MockChain::build_tx_context`] to automatically add the authenticator.
291    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    /// Adds an existing public [`BasicWallet`] account to the initial chain state and registers the
300    /// authenticator (if any).
301    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    /// Adds an existing public [`BasicWallet`] account to the initial chain state and registers the
306    /// authenticator (if any).
307    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    /// Creates a new public [`BasicFungibleFaucet`] account and registers the authenticator (if
321    /// any) for it.
322    ///
323    /// This does not add the account to the chain state, but it can still be used to call
324    /// [`MockChain::build_tx_context`] to automatically add the authenticator.
325    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    /// Adds an existing [`BasicFungibleFaucet`] account to the initial chain state and
348    /// registers the authenticator.
349    ///
350    /// Basic fungible faucets always use `AccountStorageMode::Public` and require authentication.
351    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    /// Adds an existing [`NetworkFungibleFaucet`] account to the initial chain state.
378    ///
379    /// Network fungible faucets always use `AccountStorageMode::Network` and `Auth::NoAuth`.
380    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        // Network faucets always use IncrNonce auth (no authentication)
406        self.add_account_from_builder(Auth::IncrNonce, account_builder, AccountState::Exists)
407    }
408
409    /// Creates a new public account with an [`MockAccountComponent`] and registers the
410    /// authenticator (if any).
411    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    /// Adds an existing public account with an [`MockAccountComponent`] to the initial chain state
420    /// and registers the authenticator (if any).
421    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    /// Adds an existing public account with an [`MockAccountComponent`] to the initial chain state
426    /// and registers the authenticator (if any).
427    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    /// Adds an existing public account with an [`MockAccountComponent`] to the initial chain state
436    /// and registers the authenticator (if any).
437    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    /// Adds an existing public account with an [`MockAccountComponent`] to the initial chain state
446    /// and registers the authenticator (if any).
447    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    /// Builds the provided [`AccountBuilder`] with the provided auth method and registers the
462    /// authenticator (if any).
463    ///
464    /// - If [`AccountState::Exists`] is given the account is built as an existing account and added
465    ///   to the initial chain state. It can then be used in a transaction without having to
466    ///   validate its seed.
467    /// - If [`AccountState::New`] is given the account is built as a new account and is **not**
468    ///   added to the chain. Its authenticator is registered (if present). Its first transaction
469    ///   will be its creation transaction. [`MockChain::build_tx_context`] can be called with the
470    ///   account to automatically add the authenticator.
471    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    /// Adds the provided account to the list of genesis accounts.
513    ///
514    /// This method only adds the account and does not store its account authenticator for it.
515    /// Calling [`MockChain::build_tx_context`] on accounts added in this way will not work if
516    /// the account needs an authenticator.
517    ///
518    /// Due to these limitations, prefer using other methods to add accounts to the chain, e.g.
519    /// [`MockChainBuilder::add_account_from_builder`].
520    pub fn add_account(&mut self, account: Account) -> anyhow::Result<()> {
521        self.accounts.insert(account.id(), account);
522
523        // This returns a Result to be conservative in case we need to return an error in the future
524        // and do not want to break this API.
525        Ok(())
526    }
527
528    // NOTE ADD METHODS
529    // ----------------------------------------------------------------------------------------
530
531    /// Adds the provided note to the initial chain state.
532    pub fn add_output_note(&mut self, note: impl Into<RawOutputNote>) {
533        self.notes.push(note.into());
534    }
535
536    /// Creates a new P2ANY note from the provided parameters and adds it to the list of
537    /// genesis notes.
538    ///
539    /// This note is similar to a P2ID note but can be consumed by any account.
540    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    /// Creates a new P2ID note from the provided parameters and adds it to the list of genesis
553    /// notes.
554    ///
555    /// In the created [`MockChain`], the note will be immediately spendable by `target_account_id`
556    /// and carries no additional reclaim or timelock conditions.
557    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    /// Adds a P2IDE note (pay‑to‑ID‑extended) to the list of genesis notes.
578    ///
579    /// A P2IDE note can include an optional `timelock_height` and/or an optional
580    /// `reclaim_height` after which the `sender_account_id` may reclaim the
581    /// funds.
582    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    /// Adds a public SWAP note to the list of genesis notes.
608    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    /// Adds a public `SPAWN` note to the list of genesis notes.
632    ///
633    /// A `SPAWN` note contains a note script that creates all `output_notes` that get passed as a
634    /// parameter.
635    ///
636    /// # Errors
637    ///
638    /// Returns an error if:
639    /// - the sender account ID of the provided output notes is not consistent or does not match the
640    ///   transaction's sender.
641    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    /// Creates a new P2ID note with the provided amount of the native fee asset of the chain.
655    ///
656    /// The native asset ID of the asset can be set using [`Self::native_asset_id`]. By default it
657    /// is [`ACCOUNT_ID_NATIVE_ASSET_FAUCET`].
658    ///
659    /// In the created [`MockChain`], the note will be immediately spendable by `target_account_id`.
660    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    // HELPER FUNCTIONS
677    // ----------------------------------------------------------------------------------------
678
679    /// Returns a mutable reference to the builder's RNG.
680    ///
681    /// This can be used when creating accounts or notes and randomness is required.
682    pub fn rng_mut(&mut self) -> &mut RandomCoin {
683        &mut self.rng
684    }
685
686    /// Constructs a fungible asset based on the native asset ID and the provided amount.
687    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}