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::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/// A builder for a [`MockChain`]'s genesis block.
62///
63/// ## Example
64///
65/// ```
66/// # use anyhow::Result;
67/// # use miden_protocol::{
68/// #    asset::{Asset, FungibleAsset},
69/// #    note::NoteType,
70/// # };
71/// # use miden_testing::{Auth, MockChain};
72/// #
73/// # fn main() -> Result<()> {
74/// let mut builder = MockChain::builder();
75/// let existing_wallet =
76///     builder.add_existing_wallet_with_assets(Auth::IncrNonce, [FungibleAsset::mock(500)])?;
77/// let new_wallet = builder.create_new_wallet(Auth::IncrNonce)?;
78///
79/// let existing_note = builder.add_p2id_note(
80///     existing_wallet.id(),
81///     new_wallet.id(),
82///     &[FungibleAsset::mock(100)],
83///     NoteType::Private,
84/// )?;
85/// let chain = builder.build()?;
86///
87/// // The existing wallet and note should be part of the chain state.
88/// assert!(chain.committed_account(existing_wallet.id()).is_ok());
89/// assert!(chain.committed_notes().get(&existing_note.id()).is_some());
90///
91/// // The new wallet should *not* be part of the chain state - it must be created in
92/// // a transaction first.
93/// assert!(chain.committed_account(new_wallet.id()).is_err());
94///
95/// # Ok(())
96/// # }
97/// ```
98///
99/// Note the distinction between `add_` and `create_` APIs. Any `add_` APIs will add something to
100/// the genesis chain state while `create_` APIs do not mutate the genesis state. The latter are
101/// simply convenient for creating accounts or notes that will be created by transactions.
102///
103/// See also the [`MockChain`] docs for examples on using the mock chain.
104#[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    // Fee parameters.
111    native_asset_id: AccountId,
112    verification_base_fee: u32,
113}
114
115impl MockChainBuilder {
116    // CONSTRUCTORS
117    // ----------------------------------------------------------------------------------------
118
119    /// Initializes a new mock chain builder with an empty state.
120    ///
121    /// By default, the `native_asset_id` is set to [`ACCOUNT_ID_NATIVE_ASSET_FAUCET`] and can be
122    /// overwritten using [`Self::native_asset_id`].
123    ///
124    /// The `verification_base_fee` is initialized to 0 which means no fees are required by default.
125    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    /// Initializes a new mock chain builder with the provided accounts.
140    ///
141    /// This method only adds the accounts and cannot not register any authenticators for them.
142    /// Calling [`MockChain::build_tx_context`] on accounts added in this way will not work if the
143    /// account needs an authenticator.
144    ///
145    /// Due to these limitations, prefer using other methods to add accounts to the chain, e.g.
146    /// [`MockChainBuilder::add_account_from_builder`].
147    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    // BUILDER METHODS
158    // ----------------------------------------------------------------------------------------
159
160    /// Sets the native asset ID of the chain.
161    ///
162    /// This must be a fungible faucet [`AccountId`] and is the asset in which fees will be accepted
163    /// by the transaction kernel.
164    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    /// Sets the `verification_base_fee` of the chain.
170    ///
171    /// See [`FeeParameters`] for more details.
172    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    /// Consumes the builder, creates the genesis block of the chain and returns the [`MockChain`].
178    pub fn build(self) -> anyhow::Result<MockChain> {
179        // Create the genesis block, consisting of the provided accounts and notes.
180        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    // ACCOUNT METHODS
263    // ----------------------------------------------------------------------------------------
264
265    /// Creates a new public [`BasicWallet`] account and registers the authenticator (if any) for
266    /// it.
267    ///
268    /// This does not add the account to the chain state, but it can still be used to call
269    /// [`MockChain::build_tx_context`] to automatically add the authenticator.
270    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    /// Adds an existing public [`BasicWallet`] account to the initial chain state and registers the
279    /// authenticator (if any).
280    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    /// Adds an existing public [`BasicWallet`] account to the initial chain state and registers the
285    /// authenticator (if any).
286    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    /// Creates a new public [`BasicFungibleFaucet`] account and registers the authenticator (if
300    /// any) for it.
301    ///
302    /// This does not add the account to the chain state, but it can still be used to call
303    /// [`MockChain::build_tx_context`] to automatically add the authenticator.
304    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    /// Adds an existing [`BasicFungibleFaucet`] account to the initial chain state and
328    /// registers the authenticator.
329    ///
330    /// Basic fungible faucets always use `AccountStorageMode::Public` and require authentication.
331    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        // The faucet's sysdata slot is initialized to an empty word by default.
352        // If total_issuance is set, overwrite it and reinsert the account.
353        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    /// Adds an existing [`NetworkFungibleFaucet`] account to the initial chain state.
368    ///
369    /// Network fungible faucets always use `AccountStorageMode::Network` and `Auth::NoAuth`.
370    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        // Network faucets always use Noop auth (no authentication)
392        let mut account =
393            self.add_account_from_builder(Auth::IncrNonce, account_builder, AccountState::Exists)?;
394
395        // The faucet's sysdata slot is initialized to an empty word by default.
396        // If total_issuance is set, overwrite it and reinsert the account.
397        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    /// Creates a new public account with an [`MockAccountComponent`] and registers the
412    /// authenticator (if any).
413    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    /// Adds an existing public account with an [`MockAccountComponent`] to the initial chain state
422    /// and registers the authenticator (if any).
423    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    /// Adds an existing public account with an [`MockAccountComponent`] to the initial chain state
428    /// and registers the authenticator (if any).
429    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    /// Adds an existing public account with an [`MockAccountComponent`] to the initial chain state
438    /// and registers the authenticator (if any).
439    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    /// Adds an existing public account with an [`MockAccountComponent`] to the initial chain state
448    /// and registers the authenticator (if any).
449    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    /// Builds the provided [`AccountBuilder`] with the provided auth method and registers the
464    /// authenticator (if any).
465    ///
466    /// - If [`AccountState::Exists`] is given the account is built as an existing account and added
467    ///   to the initial chain state. It can then be used in a transaction without having to
468    ///   validate its seed.
469    /// - If [`AccountState::New`] is given the account is built as a new account and is **not**
470    ///   added to the chain. Its authenticator is registered (if present). Its first transaction
471    ///   will be its creation transaction. [`MockChain::build_tx_context`] can be called with the
472    ///   account to automatically add the authenticator.
473    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    /// Adds the provided account to the list of genesis accounts.
501    ///
502    /// This method only adds the account and does not store its account authenticator for it.
503    /// Calling [`MockChain::build_tx_context`] on accounts added in this way will not work if
504    /// the account needs an authenticator.
505    ///
506    /// Due to these limitations, prefer using other methods to add accounts to the chain, e.g.
507    /// [`MockChainBuilder::add_account_from_builder`].
508    pub fn add_account(&mut self, account: Account) -> anyhow::Result<()> {
509        self.accounts.insert(account.id(), account);
510
511        // This returns a Result to be conservative in case we need to return an error in the future
512        // and do not want to break this API.
513        Ok(())
514    }
515
516    // NOTE ADD METHODS
517    // ----------------------------------------------------------------------------------------
518
519    /// Adds the provided note to the initial chain state.
520    pub fn add_output_note(&mut self, note: impl Into<OutputNote>) {
521        self.notes.push(note.into());
522    }
523
524    /// Creates a new P2ANY note from the provided parameters and adds it to the list of
525    /// genesis notes.
526    ///
527    /// This note is similar to a P2ID note but can be consumed by any account.
528    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    /// Creates a new P2ID note from the provided parameters and adds it to the list of genesis
541    /// notes.
542    ///
543    /// In the created [`MockChain`], the note will be immediately spendable by `target_account_id`
544    /// and carries no additional reclaim or timelock conditions.
545    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    /// Adds a P2IDE [`OutputNote`] (pay‑to‑ID‑extended) to the list of genesis notes.
566    ///
567    /// A P2IDE note can include an optional `timelock_height` and/or an optional
568    /// `reclaim_height` after which the `sender_account_id` may reclaim the
569    /// funds.
570    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    /// Adds a public SWAP [`OutputNote`] to the list of genesis notes.
596    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    /// Adds a public `SPAWN` note to the list of genesis notes.
620    ///
621    /// A `SPAWN` note contains a note script that creates all `output_notes` that get passed as a
622    /// parameter.
623    ///
624    /// # Errors
625    ///
626    /// Returns an error if:
627    /// - the sender account ID of the provided output notes is not consistent or does not match the
628    ///   transaction's sender.
629    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    /// Creates a new P2ID note with the provided amount of the native fee asset of the chain.
643    ///
644    /// The native asset ID of the asset can be set using [`Self::native_asset_id`]. By default it
645    /// is [`ACCOUNT_ID_NATIVE_ASSET_FAUCET`].
646    ///
647    /// In the created [`MockChain`], the note will be immediately spendable by `target_account_id`.
648    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    // HELPER FUNCTIONS
665    // ----------------------------------------------------------------------------------------
666
667    /// Returns a mutable reference to the builder's RNG.
668    ///
669    /// This can be used when creating accounts or notes and randomness is required.
670    pub fn rng_mut(&mut self) -> &mut RpoRandomCoin {
671        &mut self.rng
672    }
673
674    /// Constructs a fungible asset based on the native asset ID and the provided amount.
675    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}