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::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/// A builder for a [`MockChain`]'s genesis block.
61///
62/// ## Example
63///
64/// ```
65/// # use anyhow::Result;
66/// # use miden_protocol::{
67/// #    asset::{Asset, FungibleAsset},
68/// #    note::NoteType,
69/// # };
70/// # use miden_testing::{Auth, MockChain};
71/// #
72/// # fn main() -> Result<()> {
73/// let mut builder = MockChain::builder();
74/// let existing_wallet =
75///     builder.add_existing_wallet_with_assets(Auth::IncrNonce, [FungibleAsset::mock(500)])?;
76/// let new_wallet = builder.create_new_wallet(Auth::IncrNonce)?;
77///
78/// let existing_note = builder.add_p2id_note(
79///     existing_wallet.id(),
80///     new_wallet.id(),
81///     &[FungibleAsset::mock(100)],
82///     NoteType::Private,
83/// )?;
84/// let chain = builder.build()?;
85///
86/// // The existing wallet and note should be part of the chain state.
87/// assert!(chain.committed_account(existing_wallet.id()).is_ok());
88/// assert!(chain.committed_notes().get(&existing_note.id()).is_some());
89///
90/// // The new wallet should *not* be part of the chain state - it must be created in
91/// // a transaction first.
92/// assert!(chain.committed_account(new_wallet.id()).is_err());
93///
94/// # Ok(())
95/// # }
96/// ```
97///
98/// Note the distinction between `add_` and `create_` APIs. Any `add_` APIs will add something to
99/// the genesis chain state while `create_` APIs do not mutate the genesis state. The latter are
100/// simply convenient for creating accounts or notes that will be created by transactions.
101///
102/// See also the [`MockChain`] docs for examples on using the mock chain.
103#[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    // Fee parameters.
110    native_asset_id: AccountId,
111    verification_base_fee: u32,
112}
113
114impl MockChainBuilder {
115    // CONSTRUCTORS
116    // ----------------------------------------------------------------------------------------
117
118    /// Initializes a new mock chain builder with an empty state.
119    ///
120    /// By default, the `native_asset_id` is set to [`ACCOUNT_ID_NATIVE_ASSET_FAUCET`] and can be
121    /// overwritten using [`Self::native_asset_id`].
122    ///
123    /// The `verification_base_fee` is initialized to 0 which means no fees are required by default.
124    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    /// Initializes a new mock chain builder with the provided accounts.
139    ///
140    /// This method only adds the accounts and cannot not register any authenticators for them.
141    /// Calling [`MockChain::build_tx_context`] on accounts added in this way will not work if the
142    /// account needs an authenticator.
143    ///
144    /// Due to these limitations, prefer using other methods to add accounts to the chain, e.g.
145    /// [`MockChainBuilder::add_account_from_builder`].
146    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    // BUILDER METHODS
157    // ----------------------------------------------------------------------------------------
158
159    /// Sets the native asset ID of the chain.
160    ///
161    /// This must be a fungible faucet [`AccountId`] and is the asset in which fees will be accepted
162    /// by the transaction kernel.
163    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    /// Sets the `verification_base_fee` of the chain.
169    ///
170    /// See [`FeeParameters`] for more details.
171    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    /// Consumes the builder, creates the genesis block of the chain and returns the [`MockChain`].
177    pub fn build(self) -> anyhow::Result<MockChain> {
178        // Create the genesis block, consisting of the provided accounts and notes.
179        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        // Extract full notes before shrinking for later use in MockChain
201        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    // ACCOUNT METHODS
278    // ----------------------------------------------------------------------------------------
279
280    /// Creates a new public [`BasicWallet`] account and registers the authenticator (if any) for
281    /// it.
282    ///
283    /// This does not add the account to the chain state, but it can still be used to call
284    /// [`MockChain::build_tx_context`] to automatically add the authenticator.
285    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    /// Adds an existing public [`BasicWallet`] account to the initial chain state and registers the
294    /// authenticator (if any).
295    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    /// Adds an existing public [`BasicWallet`] account to the initial chain state and registers the
300    /// authenticator (if any).
301    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    /// Creates a new public [`BasicFungibleFaucet`] account and registers the authenticator (if
315    /// any) for it.
316    ///
317    /// This does not add the account to the chain state, but it can still be used to call
318    /// [`MockChain::build_tx_context`] to automatically add the authenticator.
319    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    /// Adds an existing [`BasicFungibleFaucet`] account to the initial chain state and
341    /// registers the authenticator.
342    ///
343    /// Basic fungible faucets always use `AccountStorageMode::Public` and require authentication.
344    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    /// Adds an existing [`NetworkFungibleFaucet`] account to the initial chain state.
370    ///
371    /// Network fungible faucets always use `AccountStorageMode::Network` and `Auth::NoAuth`.
372    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        // Network faucets always use IncrNonce auth (no authentication)
396        self.add_account_from_builder(Auth::IncrNonce, account_builder, AccountState::Exists)
397    }
398
399    /// Creates a new public account with an [`MockAccountComponent`] and registers the
400    /// authenticator (if any).
401    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    /// Adds an existing public account with an [`MockAccountComponent`] to the initial chain state
410    /// and registers the authenticator (if any).
411    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    /// Adds an existing public account with an [`MockAccountComponent`] to the initial chain state
416    /// and registers the authenticator (if any).
417    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    /// 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_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    /// 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_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    /// Builds the provided [`AccountBuilder`] with the provided auth method and registers the
452    /// authenticator (if any).
453    ///
454    /// - If [`AccountState::Exists`] is given the account is built as an existing account and added
455    ///   to the initial chain state. It can then be used in a transaction without having to
456    ///   validate its seed.
457    /// - If [`AccountState::New`] is given the account is built as a new account and is **not**
458    ///   added to the chain. Its authenticator is registered (if present). Its first transaction
459    ///   will be its creation transaction. [`MockChain::build_tx_context`] can be called with the
460    ///   account to automatically add the authenticator.
461    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    /// Adds the provided account to the list of genesis accounts.
489    ///
490    /// This method only adds the account and does not store its account authenticator for it.
491    /// Calling [`MockChain::build_tx_context`] on accounts added in this way will not work if
492    /// the account needs an authenticator.
493    ///
494    /// Due to these limitations, prefer using other methods to add accounts to the chain, e.g.
495    /// [`MockChainBuilder::add_account_from_builder`].
496    pub fn add_account(&mut self, account: Account) -> anyhow::Result<()> {
497        self.accounts.insert(account.id(), account);
498
499        // This returns a Result to be conservative in case we need to return an error in the future
500        // and do not want to break this API.
501        Ok(())
502    }
503
504    // NOTE ADD METHODS
505    // ----------------------------------------------------------------------------------------
506
507    /// Adds the provided note to the initial chain state.
508    pub fn add_output_note(&mut self, note: impl Into<RawOutputNote>) {
509        self.notes.push(note.into());
510    }
511
512    /// Creates a new P2ANY note from the provided parameters and adds it to the list of
513    /// genesis notes.
514    ///
515    /// This note is similar to a P2ID note but can be consumed by any account.
516    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    /// Creates a new P2ID note from the provided parameters and adds it to the list of genesis
529    /// notes.
530    ///
531    /// In the created [`MockChain`], the note will be immediately spendable by `target_account_id`
532    /// and carries no additional reclaim or timelock conditions.
533    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    /// Adds a P2IDE note (pay‑to‑ID‑extended) to the list of genesis notes.
554    ///
555    /// A P2IDE note can include an optional `timelock_height` and/or an optional
556    /// `reclaim_height` after which the `sender_account_id` may reclaim the
557    /// funds.
558    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    /// Adds a public SWAP note to the list of genesis notes.
584    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    /// Adds a public `SPAWN` note to the list of genesis notes.
608    ///
609    /// A `SPAWN` note contains a note script that creates all `output_notes` that get passed as a
610    /// parameter.
611    ///
612    /// # Errors
613    ///
614    /// Returns an error if:
615    /// - the sender account ID of the provided output notes is not consistent or does not match the
616    ///   transaction's sender.
617    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    /// Creates a new P2ID note with the provided amount of the native fee asset of the chain.
631    ///
632    /// The native asset ID of the asset can be set using [`Self::native_asset_id`]. By default it
633    /// is [`ACCOUNT_ID_NATIVE_ASSET_FAUCET`].
634    ///
635    /// In the created [`MockChain`], the note will be immediately spendable by `target_account_id`.
636    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    // HELPER FUNCTIONS
653    // ----------------------------------------------------------------------------------------
654
655    /// Returns a mutable reference to the builder's RNG.
656    ///
657    /// This can be used when creating accounts or notes and randomness is required.
658    pub fn rng_mut(&mut self) -> &mut RpoRandomCoin {
659        &mut self.rng
660    }
661
662    /// Constructs a fungible asset based on the native asset ID and the provided amount.
663    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}