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    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, OutputNote, TransactionKernel};
48use miden_protocol::{Felt, MAX_OUTPUT_NOTES_PER_BATCH, Word};
49use miden_standards::account::faucets::{BasicFungibleFaucet, NetworkFungibleFaucet};
50use miden_standards::account::wallets::BasicWallet;
51use miden_standards::note::{P2idNote, P2ideNote, P2ideNoteStorage, SwapNote};
52use miden_standards::testing::account_component::MockAccountComponent;
53use rand::Rng;
54
55use crate::mock_chain::chain::AccountAuthenticator;
56use crate::utils::{create_p2any_note, create_spawn_note};
57use crate::{AccountState, Auth, MockChain};
58
59/// A builder for a [`MockChain`]'s genesis block.
60///
61/// ## Example
62///
63/// ```
64/// # use anyhow::Result;
65/// # use miden_protocol::{
66/// #    asset::{Asset, FungibleAsset},
67/// #    note::NoteType,
68/// # };
69/// # use miden_testing::{Auth, MockChain};
70/// #
71/// # fn main() -> Result<()> {
72/// let mut builder = MockChain::builder();
73/// let existing_wallet =
74///     builder.add_existing_wallet_with_assets(Auth::IncrNonce, [FungibleAsset::mock(500)])?;
75/// let new_wallet = builder.create_new_wallet(Auth::IncrNonce)?;
76///
77/// let existing_note = builder.add_p2id_note(
78///     existing_wallet.id(),
79///     new_wallet.id(),
80///     &[FungibleAsset::mock(100)],
81///     NoteType::Private,
82/// )?;
83/// let chain = builder.build()?;
84///
85/// // The existing wallet and note should be part of the chain state.
86/// assert!(chain.committed_account(existing_wallet.id()).is_ok());
87/// assert!(chain.committed_notes().get(&existing_note.id()).is_some());
88///
89/// // The new wallet should *not* be part of the chain state - it must be created in
90/// // a transaction first.
91/// assert!(chain.committed_account(new_wallet.id()).is_err());
92///
93/// # Ok(())
94/// # }
95/// ```
96///
97/// Note the distinction between `add_` and `create_` APIs. Any `add_` APIs will add something to
98/// the genesis chain state while `create_` APIs do not mutate the genesis state. The latter are
99/// simply convenient for creating accounts or notes that will be created by transactions.
100///
101/// See also the [`MockChain`] docs for examples on using the mock chain.
102#[derive(Debug, Clone)]
103pub struct MockChainBuilder {
104    accounts: BTreeMap<AccountId, Account>,
105    account_authenticators: BTreeMap<AccountId, AccountAuthenticator>,
106    notes: Vec<OutputNote>,
107    rng: RpoRandomCoin,
108    // Fee parameters.
109    native_asset_id: AccountId,
110    verification_base_fee: u32,
111}
112
113impl MockChainBuilder {
114    // CONSTRUCTORS
115    // ----------------------------------------------------------------------------------------
116
117    /// Initializes a new mock chain builder with an empty state.
118    ///
119    /// By default, the `native_asset_id` is set to [`ACCOUNT_ID_NATIVE_ASSET_FAUCET`] and can be
120    /// overwritten using [`Self::native_asset_id`].
121    ///
122    /// The `verification_base_fee` is initialized to 0 which means no fees are required by default.
123    pub fn new() -> Self {
124        let native_asset_id =
125            ACCOUNT_ID_NATIVE_ASSET_FAUCET.try_into().expect("account ID should be valid");
126
127        Self {
128            accounts: BTreeMap::new(),
129            account_authenticators: BTreeMap::new(),
130            notes: Vec::new(),
131            rng: RpoRandomCoin::new(Default::default()),
132            native_asset_id,
133            verification_base_fee: 0,
134        }
135    }
136
137    /// Initializes a new mock chain builder with the provided accounts.
138    ///
139    /// This method only adds the accounts and cannot not register any authenticators for them.
140    /// Calling [`MockChain::build_tx_context`] on accounts added in this way will not work if the
141    /// account needs an authenticator.
142    ///
143    /// Due to these limitations, prefer using other methods to add accounts to the chain, e.g.
144    /// [`MockChainBuilder::add_account_from_builder`].
145    pub fn with_accounts(accounts: impl IntoIterator<Item = Account>) -> anyhow::Result<Self> {
146        let mut builder = Self::new();
147
148        for account in accounts {
149            builder.add_account(account)?;
150        }
151
152        Ok(builder)
153    }
154
155    // BUILDER METHODS
156    // ----------------------------------------------------------------------------------------
157
158    /// Sets the native asset ID of the chain.
159    ///
160    /// This must be a fungible faucet [`AccountId`] and is the asset in which fees will be accepted
161    /// by the transaction kernel.
162    pub fn native_asset_id(mut self, native_asset_id: AccountId) -> Self {
163        self.native_asset_id = native_asset_id;
164        self
165    }
166
167    /// Sets the `verification_base_fee` of the chain.
168    ///
169    /// See [`FeeParameters`] for more details.
170    pub fn verification_base_fee(mut self, verification_base_fee: u32) -> Self {
171        self.verification_base_fee = verification_base_fee;
172        self
173    }
174
175    /// Consumes the builder, creates the genesis block of the chain and returns the [`MockChain`].
176    pub fn build(self) -> anyhow::Result<MockChain> {
177        // Create the genesis block, consisting of the provided accounts and notes.
178        let block_account_updates: Vec<BlockAccountUpdate> = self
179            .accounts
180            .into_values()
181            .map(|account| {
182                let account_id = account.id();
183                let account_commitment = account.to_commitment();
184                let account_delta = AccountDelta::try_from(account)
185                    .expect("chain builder should only store existing accounts without seeds");
186                let update_details = AccountUpdateDetails::Delta(account_delta);
187
188                BlockAccountUpdate::new(account_id, account_commitment, update_details)
189            })
190            .collect();
191
192        let account_tree = AccountTree::with_entries(
193            block_account_updates
194                .iter()
195                .map(|account| (account.account_id(), account.final_state_commitment())),
196        )
197        .context("failed to create genesis account tree")?;
198
199        let note_chunks = self.notes.into_iter().chunks(MAX_OUTPUT_NOTES_PER_BATCH);
200        let output_note_batches: Vec<OutputNoteBatch> = note_chunks
201            .into_iter()
202            .map(|batch_notes| batch_notes.into_iter().enumerate().collect::<Vec<_>>())
203            .collect();
204
205        let created_nullifiers = Vec::new();
206        let transactions = OrderedTransactionHeaders::new_unchecked(Vec::new());
207
208        let note_tree = BlockNoteTree::from_note_batches(&output_note_batches)
209            .context("failed to create block note tree")?;
210
211        let version = 0;
212        let prev_block_commitment = Word::empty();
213        let block_num = BlockNumber::from(0u32);
214        let chain_commitment = Blockchain::new().commitment();
215        let account_root = account_tree.root();
216        let nullifier_root = NullifierTree::<Smt>::default().root();
217        let note_root = note_tree.root();
218        let tx_commitment = transactions.commitment();
219        let tx_kernel_commitment = TransactionKernel.to_commitment();
220        let timestamp = MockChain::TIMESTAMP_START_SECS;
221        let fee_parameters = FeeParameters::new(self.native_asset_id, self.verification_base_fee)
222            .context("failed to construct fee parameters")?;
223        let validator_secret_key = random_secret_key();
224        let validator_public_key = validator_secret_key.public_key();
225
226        let header = BlockHeader::new(
227            version,
228            prev_block_commitment,
229            block_num,
230            chain_commitment,
231            account_root,
232            nullifier_root,
233            note_root,
234            tx_commitment,
235            tx_kernel_commitment,
236            validator_public_key,
237            fee_parameters,
238            timestamp,
239        );
240
241        let body = BlockBody::new_unchecked(
242            block_account_updates,
243            output_note_batches,
244            created_nullifiers,
245            transactions,
246        );
247
248        let signature = validator_secret_key.sign(header.commitment());
249        let block_proof = BlockProof::new_dummy();
250        let genesis_block = ProvenBlock::new_unchecked(header, body, signature, block_proof);
251
252        MockChain::from_genesis_block(
253            genesis_block,
254            account_tree,
255            self.account_authenticators,
256            validator_secret_key,
257        )
258    }
259
260    // ACCOUNT METHODS
261    // ----------------------------------------------------------------------------------------
262
263    /// Creates a new public [`BasicWallet`] account and registers the authenticator (if any) for
264    /// it.
265    ///
266    /// This does not add the account to the chain state, but it can still be used to call
267    /// [`MockChain::build_tx_context`] to automatically add the authenticator.
268    pub fn create_new_wallet(&mut self, auth_method: Auth) -> anyhow::Result<Account> {
269        let account_builder = AccountBuilder::new(self.rng.random())
270            .storage_mode(AccountStorageMode::Public)
271            .with_component(BasicWallet);
272
273        self.add_account_from_builder(auth_method, account_builder, AccountState::New)
274    }
275
276    /// Adds an existing public [`BasicWallet`] account to the initial chain state and registers the
277    /// authenticator (if any).
278    pub fn add_existing_wallet(&mut self, auth_method: Auth) -> anyhow::Result<Account> {
279        self.add_existing_wallet_with_assets(auth_method, [])
280    }
281
282    /// Adds an existing public [`BasicWallet`] account to the initial chain state and registers the
283    /// authenticator (if any).
284    pub fn add_existing_wallet_with_assets(
285        &mut self,
286        auth_method: Auth,
287        assets: impl IntoIterator<Item = Asset>,
288    ) -> anyhow::Result<Account> {
289        let account_builder = Account::builder(self.rng.random())
290            .storage_mode(AccountStorageMode::Public)
291            .with_component(BasicWallet)
292            .with_assets(assets);
293
294        self.add_account_from_builder(auth_method, account_builder, AccountState::Exists)
295    }
296
297    /// Creates a new public [`BasicFungibleFaucet`] account and registers the authenticator (if
298    /// any) for it.
299    ///
300    /// This does not add the account to the chain state, but it can still be used to call
301    /// [`MockChain::build_tx_context`] to automatically add the authenticator.
302    pub fn create_new_faucet(
303        &mut self,
304        auth_method: Auth,
305        token_symbol: &str,
306        max_supply: u64,
307    ) -> anyhow::Result<Account> {
308        let token_symbol = TokenSymbol::new(token_symbol)
309            .with_context(|| format!("invalid token symbol: {token_symbol}"))?;
310        let max_supply_felt = max_supply.try_into().map_err(|_| {
311            anyhow::anyhow!("max supply value cannot be converted to Felt: {max_supply}")
312        })?;
313        let basic_faucet =
314            BasicFungibleFaucet::new(token_symbol, DEFAULT_FAUCET_DECIMALS, max_supply_felt)
315                .context("failed to create BasicFungibleFaucet")?;
316
317        let account_builder = AccountBuilder::new(self.rng.random())
318            .storage_mode(AccountStorageMode::Public)
319            .account_type(AccountType::FungibleFaucet)
320            .with_component(basic_faucet);
321
322        self.add_account_from_builder(auth_method, account_builder, AccountState::New)
323    }
324
325    /// Adds an existing [`BasicFungibleFaucet`] account to the initial chain state and
326    /// registers the authenticator.
327    ///
328    /// Basic fungible faucets always use `AccountStorageMode::Public` and require authentication.
329    pub fn add_existing_basic_faucet(
330        &mut self,
331        auth_method: Auth,
332        token_symbol: &str,
333        max_supply: u64,
334        token_supply: Option<u64>,
335    ) -> anyhow::Result<Account> {
336        let max_supply = Felt::try_from(max_supply)
337            .map_err(|err| anyhow::anyhow!("failed to convert max_supply to felt: {err}"))?;
338        let token_supply = Felt::try_from(token_supply.unwrap_or(0))
339            .map_err(|err| anyhow::anyhow!("failed to convert token_supply to felt: {err}"))?;
340        let token_symbol =
341            TokenSymbol::new(token_symbol).context("failed to create token symbol")?;
342
343        let basic_faucet =
344            BasicFungibleFaucet::new(token_symbol, DEFAULT_FAUCET_DECIMALS, max_supply)
345                .and_then(|fungible_faucet| fungible_faucet.with_token_supply(token_supply))
346                .context("failed to create basic fungible faucet")?;
347
348        let account_builder = AccountBuilder::new(self.rng.random())
349            .storage_mode(AccountStorageMode::Public)
350            .with_component(basic_faucet)
351            .account_type(AccountType::FungibleFaucet);
352
353        self.add_account_from_builder(auth_method, account_builder, AccountState::Exists)
354    }
355
356    /// Adds an existing [`NetworkFungibleFaucet`] account to the initial chain state.
357    ///
358    /// Network fungible faucets always use `AccountStorageMode::Network` and `Auth::NoAuth`.
359    pub fn add_existing_network_faucet(
360        &mut self,
361        token_symbol: &str,
362        max_supply: u64,
363        owner_account_id: AccountId,
364        token_supply: Option<u64>,
365    ) -> anyhow::Result<Account> {
366        let max_supply = Felt::try_from(max_supply)
367            .map_err(|err| anyhow::anyhow!("failed to convert max_supply to felt: {err}"))?;
368        let token_supply = Felt::try_from(token_supply.unwrap_or(0))
369            .map_err(|err| anyhow::anyhow!("failed to convert token_supply to felt: {err}"))?;
370        let token_symbol =
371            TokenSymbol::new(token_symbol).context("failed to create token symbol")?;
372
373        let network_faucet = NetworkFungibleFaucet::new(
374            token_symbol,
375            DEFAULT_FAUCET_DECIMALS,
376            max_supply,
377            owner_account_id,
378        )
379        .and_then(|fungible_faucet| fungible_faucet.with_token_supply(token_supply))
380        .context("failed to create network fungible faucet")?;
381
382        let account_builder = AccountBuilder::new(self.rng.random())
383            .storage_mode(AccountStorageMode::Network)
384            .with_component(network_faucet)
385            .account_type(AccountType::FungibleFaucet);
386
387        // Network faucets always use IncrNonce auth (no authentication)
388        self.add_account_from_builder(Auth::IncrNonce, account_builder, AccountState::Exists)
389    }
390
391    /// Creates a new public account with an [`MockAccountComponent`] and registers the
392    /// authenticator (if any).
393    pub fn create_new_mock_account(&mut self, auth_method: Auth) -> anyhow::Result<Account> {
394        let account_builder = Account::builder(self.rng.random())
395            .storage_mode(AccountStorageMode::Public)
396            .with_component(MockAccountComponent::with_empty_slots());
397
398        self.add_account_from_builder(auth_method, account_builder, AccountState::New)
399    }
400
401    /// Adds an existing public account with an [`MockAccountComponent`] to the initial chain state
402    /// and registers the authenticator (if any).
403    pub fn add_existing_mock_account(&mut self, auth_method: Auth) -> anyhow::Result<Account> {
404        self.add_existing_mock_account_with_storage_and_assets(auth_method, [], [])
405    }
406
407    /// Adds an existing public account with an [`MockAccountComponent`] to the initial chain state
408    /// and registers the authenticator (if any).
409    pub fn add_existing_mock_account_with_storage(
410        &mut self,
411        auth_method: Auth,
412        slots: impl IntoIterator<Item = StorageSlot>,
413    ) -> anyhow::Result<Account> {
414        self.add_existing_mock_account_with_storage_and_assets(auth_method, slots, [])
415    }
416
417    /// Adds an existing public account with an [`MockAccountComponent`] to the initial chain state
418    /// and registers the authenticator (if any).
419    pub fn add_existing_mock_account_with_assets(
420        &mut self,
421        auth_method: Auth,
422        assets: impl IntoIterator<Item = Asset>,
423    ) -> anyhow::Result<Account> {
424        self.add_existing_mock_account_with_storage_and_assets(auth_method, [], assets)
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_and_assets(
430        &mut self,
431        auth_method: Auth,
432        slots: impl IntoIterator<Item = StorageSlot>,
433        assets: impl IntoIterator<Item = Asset>,
434    ) -> anyhow::Result<Account> {
435        let account_builder = Account::builder(self.rng.random())
436            .storage_mode(AccountStorageMode::Public)
437            .with_component(MockAccountComponent::with_slots(slots.into_iter().collect()))
438            .with_assets(assets);
439
440        self.add_account_from_builder(auth_method, account_builder, AccountState::Exists)
441    }
442
443    /// Builds the provided [`AccountBuilder`] with the provided auth method and registers the
444    /// authenticator (if any).
445    ///
446    /// - If [`AccountState::Exists`] is given the account is built as an existing account and added
447    ///   to the initial chain state. It can then be used in a transaction without having to
448    ///   validate its seed.
449    /// - If [`AccountState::New`] is given the account is built as a new account and is **not**
450    ///   added to the chain. Its authenticator is registered (if present). Its first transaction
451    ///   will be its creation transaction. [`MockChain::build_tx_context`] can be called with the
452    ///   account to automatically add the authenticator.
453    pub fn add_account_from_builder(
454        &mut self,
455        auth_method: Auth,
456        mut account_builder: AccountBuilder,
457        account_state: AccountState,
458    ) -> anyhow::Result<Account> {
459        let (auth_component, authenticator) = auth_method.build_component();
460        account_builder = account_builder.with_auth_component(auth_component);
461
462        let account = if let AccountState::New = account_state {
463            account_builder.build().context("failed to build account from builder")?
464        } else {
465            account_builder
466                .build_existing()
467                .context("failed to build account from builder")?
468        };
469
470        self.account_authenticators
471            .insert(account.id(), AccountAuthenticator::new(authenticator));
472
473        if let AccountState::Exists = account_state {
474            self.accounts.insert(account.id(), account.clone());
475        }
476
477        Ok(account)
478    }
479
480    /// Adds the provided account to the list of genesis accounts.
481    ///
482    /// This method only adds the account and does not store its account authenticator for it.
483    /// Calling [`MockChain::build_tx_context`] on accounts added in this way will not work if
484    /// the account needs an authenticator.
485    ///
486    /// Due to these limitations, prefer using other methods to add accounts to the chain, e.g.
487    /// [`MockChainBuilder::add_account_from_builder`].
488    pub fn add_account(&mut self, account: Account) -> anyhow::Result<()> {
489        self.accounts.insert(account.id(), account);
490
491        // This returns a Result to be conservative in case we need to return an error in the future
492        // and do not want to break this API.
493        Ok(())
494    }
495
496    // NOTE ADD METHODS
497    // ----------------------------------------------------------------------------------------
498
499    /// Adds the provided note to the initial chain state.
500    pub fn add_output_note(&mut self, note: impl Into<OutputNote>) {
501        self.notes.push(note.into());
502    }
503
504    /// Creates a new P2ANY note from the provided parameters and adds it to the list of
505    /// genesis notes.
506    ///
507    /// This note is similar to a P2ID note but can be consumed by any account.
508    pub fn add_p2any_note(
509        &mut self,
510        sender_account_id: AccountId,
511        note_type: NoteType,
512        assets: impl IntoIterator<Item = Asset>,
513    ) -> anyhow::Result<Note> {
514        let note = create_p2any_note(sender_account_id, note_type, assets, &mut self.rng);
515        self.add_output_note(OutputNote::Full(note.clone()));
516
517        Ok(note)
518    }
519
520    /// Creates a new P2ID note from the provided parameters and adds it to the list of genesis
521    /// notes.
522    ///
523    /// In the created [`MockChain`], the note will be immediately spendable by `target_account_id`
524    /// and carries no additional reclaim or timelock conditions.
525    pub fn add_p2id_note(
526        &mut self,
527        sender_account_id: AccountId,
528        target_account_id: AccountId,
529        asset: &[Asset],
530        note_type: NoteType,
531    ) -> Result<Note, NoteError> {
532        let note = P2idNote::create(
533            sender_account_id,
534            target_account_id,
535            asset.to_vec(),
536            note_type,
537            NoteAttachment::default(),
538            &mut self.rng,
539        )?;
540        self.add_output_note(OutputNote::Full(note.clone()));
541
542        Ok(note)
543    }
544
545    /// Adds a P2IDE [`OutputNote`] (pay‑to‑ID‑extended) to the list of genesis notes.
546    ///
547    /// A P2IDE note can include an optional `timelock_height` and/or an optional
548    /// `reclaim_height` after which the `sender_account_id` may reclaim the
549    /// funds.
550    pub fn add_p2ide_note(
551        &mut self,
552        sender_account_id: AccountId,
553        target_account_id: AccountId,
554        asset: &[Asset],
555        note_type: NoteType,
556        reclaim_height: Option<BlockNumber>,
557        timelock_height: Option<BlockNumber>,
558    ) -> Result<Note, NoteError> {
559        let storage = P2ideNoteStorage::new(target_account_id, reclaim_height, timelock_height);
560
561        let note = P2ideNote::create(
562            sender_account_id,
563            storage,
564            asset.to_vec(),
565            note_type,
566            Default::default(),
567            &mut self.rng,
568        )?;
569
570        self.add_output_note(OutputNote::Full(note.clone()));
571
572        Ok(note)
573    }
574
575    /// Adds a public SWAP [`OutputNote`] to the list of genesis notes.
576    pub fn add_swap_note(
577        &mut self,
578        sender: AccountId,
579        offered_asset: Asset,
580        requested_asset: Asset,
581        payback_note_type: NoteType,
582    ) -> anyhow::Result<(Note, NoteDetails)> {
583        let (swap_note, payback_note) = SwapNote::create(
584            sender,
585            offered_asset,
586            requested_asset,
587            NoteType::Public,
588            NoteAttachment::default(),
589            payback_note_type,
590            NoteAttachment::default(),
591            &mut self.rng,
592        )?;
593
594        self.add_output_note(OutputNote::Full(swap_note.clone()));
595
596        Ok((swap_note, payback_note))
597    }
598
599    /// Adds a public `SPAWN` note to the list of genesis notes.
600    ///
601    /// A `SPAWN` note contains a note script that creates all `output_notes` that get passed as a
602    /// parameter.
603    ///
604    /// # Errors
605    ///
606    /// Returns an error if:
607    /// - the sender account ID of the provided output notes is not consistent or does not match the
608    ///   transaction's sender.
609    pub fn add_spawn_note<'note, I>(
610        &mut self,
611        output_notes: impl IntoIterator<Item = &'note Note, IntoIter = I>,
612    ) -> anyhow::Result<Note>
613    where
614        I: ExactSizeIterator<Item = &'note Note>,
615    {
616        let note = create_spawn_note(output_notes)?;
617        self.add_output_note(OutputNote::Full(note.clone()));
618
619        Ok(note)
620    }
621
622    /// Creates a new P2ID note with the provided amount of the native fee asset of the chain.
623    ///
624    /// The native asset ID of the asset can be set using [`Self::native_asset_id`]. By default it
625    /// is [`ACCOUNT_ID_NATIVE_ASSET_FAUCET`].
626    ///
627    /// In the created [`MockChain`], the note will be immediately spendable by `target_account_id`.
628    pub fn add_p2id_note_with_fee(
629        &mut self,
630        target_account_id: AccountId,
631        amount: u64,
632    ) -> anyhow::Result<Note> {
633        let fee_asset = self.native_fee_asset(amount)?;
634        let note = self.add_p2id_note(
635            self.native_asset_id,
636            target_account_id,
637            &[Asset::from(fee_asset)],
638            NoteType::Public,
639        )?;
640
641        Ok(note)
642    }
643
644    // HELPER FUNCTIONS
645    // ----------------------------------------------------------------------------------------
646
647    /// Returns a mutable reference to the builder's RNG.
648    ///
649    /// This can be used when creating accounts or notes and randomness is required.
650    pub fn rng_mut(&mut self) -> &mut RpoRandomCoin {
651        &mut self.rng
652    }
653
654    /// Constructs a fungible asset based on the native asset ID and the provided amount.
655    fn native_fee_asset(&self, amount: u64) -> anyhow::Result<FungibleAsset> {
656        FungibleAsset::new(self.native_asset_id, amount).context("failed to create fee asset")
657    }
658}
659
660impl Default for MockChainBuilder {
661    fn default() -> Self {
662        Self::new()
663    }
664}