Skip to main content

miden_testing/mock_chain/
chain_builder.rs

1use alloc::collections::{BTreeMap, BTreeSet};
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/// Default number of validators committed to by the genesis block of a mock chain.
13///
14/// This is purely a test default -- the protocol does not fix the size of a validator set.
15const DEFAULT_VALIDATOR_COUNT: usize = 3;
16
17// IMPORTS
18// ================================================================================================
19
20use itertools::Itertools;
21use miden_processor::crypto::random::RandomCoin;
22use miden_protocol::account::{
23    Account,
24    AccountBuilder,
25    AccountComponent,
26    AccountId,
27    AccountPatch,
28    AccountType,
29    AccountUpdateDetails,
30    StorageSlot,
31};
32use miden_protocol::asset::{Asset, AssetAmount, AssetId, FungibleAsset, TokenSymbol};
33use miden_protocol::block::account_tree::AccountTree;
34use miden_protocol::block::nullifier_tree::NullifierTree;
35use miden_protocol::block::{
36    BlockAccountUpdate,
37    BlockBody,
38    BlockHeader,
39    BlockNoteTree,
40    BlockNumber,
41    BlockSignatures,
42    Blockchain,
43    FeeParameters,
44    OutputNoteBatch,
45    ProvenBlock,
46    ValidatorConfig,
47};
48use miden_protocol::crypto::dsa::ecdsa_k256_keccak::SigningKey;
49use miden_protocol::crypto::merkle::smt::Smt;
50use miden_protocol::errors::NoteError;
51use miden_protocol::note::{Note, NoteDetails, NoteScriptRoot, NoteType};
52use miden_protocol::protocol_config::ProtocolConfig;
53use miden_protocol::testing::account_id::ACCOUNT_ID_FEE_FAUCET;
54use miden_protocol::testing::random_secret_key::random_secret_key;
55use miden_protocol::transaction::{OrderedTransactionHeaders, RawOutputNote};
56use miden_protocol::{MAX_OUTPUT_NOTES_PER_BATCH, Word};
57use miden_standards::account::access::{AccessControl, Authority, Pausable, PausableManager};
58use miden_standards::account::auth::{AuthNetworkAccount, SponsorshipPolicy};
59use miden_standards::account::faucets::{FungibleFaucet, NonFungibleFaucet, TokenName};
60use miden_standards::account::fees::{BasicConstantFeePolicy, FeePolicyManager};
61use miden_standards::account::note_creator::NoteCreator;
62use miden_standards::account::policies::{
63    BurnPolicy,
64    MintPolicy,
65    TokenPolicyManager,
66    TransferPolicy,
67};
68use miden_standards::account::wallets::BasicWallet;
69use miden_standards::note::{BurnNote, MintNote, P2idNote, P2ideNote, SwapNote, TxFeeNote};
70use miden_standards::testing::account_component::MockAccountComponent;
71use rand::RngExt;
72
73use crate::mock_chain::chain::AccountAuthenticator;
74use crate::utils::{create_p2any_note, create_spawn_note};
75use crate::{AccountState, Auth, MockChain};
76
77/// A builder for a [`MockChain`]'s genesis block.
78///
79/// ## Example
80///
81/// ```
82/// # use anyhow::Result;
83/// # use miden_protocol::{
84/// #    asset::{Asset, FungibleAsset},
85/// #    note::NoteType,
86/// # };
87/// # use miden_testing::{Auth, MockChain};
88/// #
89/// # fn main() -> Result<()> {
90/// let mut builder = MockChain::builder();
91/// let existing_wallet =
92///     builder.add_existing_wallet_with_assets(Auth::IncrNonce, [FungibleAsset::mock(500)])?;
93/// let new_wallet = builder.create_new_wallet(Auth::IncrNonce)?;
94///
95/// let existing_note = builder.add_p2id_note(
96///     existing_wallet.id(),
97///     new_wallet.id(),
98///     &[FungibleAsset::mock(100)],
99///     NoteType::Private,
100/// )?;
101/// let chain = builder.build()?;
102///
103/// // The existing wallet and note should be part of the chain state.
104/// assert!(chain.committed_account(existing_wallet.id()).is_ok());
105/// assert!(chain.committed_notes().get(&existing_note.id()).is_some());
106///
107/// // The new wallet should *not* be part of the chain state - it must be created in
108/// // a transaction first.
109/// assert!(chain.committed_account(new_wallet.id()).is_err());
110///
111/// # Ok(())
112/// # }
113/// ```
114///
115/// Note the distinction between `add_` and `create_` APIs. Any `add_` APIs will add something to
116/// the genesis chain state while `create_` APIs do not mutate the genesis state. The latter are
117/// simply convenient for creating accounts or notes that will be created by transactions.
118///
119/// See also the [`MockChain`] docs for examples on using the mock chain.
120#[derive(Debug, Clone)]
121pub struct MockChainBuilder {
122    accounts: BTreeMap<AccountId, Account>,
123    account_authenticators: BTreeMap<AccountId, AccountAuthenticator>,
124    notes: Vec<RawOutputNote>,
125    rng: RandomCoin,
126    validator_signing_keys: Option<Vec<SigningKey>>,
127    // Fee parameters.
128    fee_faucet_id: AccountId,
129    verification_base_fee: u32,
130}
131
132impl MockChainBuilder {
133    // CONSTRUCTORS
134    // ----------------------------------------------------------------------------------------
135
136    /// Initializes a new mock chain builder with an empty state.
137    ///
138    /// By default, the `fee_faucet_id` is set to [`ACCOUNT_ID_FEE_FAUCET`] and can be
139    /// overwritten using [`Self::fee_faucet_id`].
140    ///
141    /// The `verification_base_fee` is initialized to 0 which means no fees are required by default.
142    ///
143    /// By default, three random validator signing keys are generated when building the chain.
144    /// Use [`Self::validator_signing_keys`] to supply the keys instead.
145    pub fn new() -> Self {
146        let fee_faucet_id = ACCOUNT_ID_FEE_FAUCET.try_into().expect("account ID should be valid");
147
148        Self {
149            accounts: BTreeMap::new(),
150            account_authenticators: BTreeMap::new(),
151            notes: Vec::new(),
152            rng: RandomCoin::new(Default::default()),
153            validator_signing_keys: None,
154            fee_faucet_id,
155            verification_base_fee: 0,
156        }
157    }
158
159    /// Initializes a new mock chain builder with the provided accounts.
160    ///
161    /// This method only adds the accounts and cannot not register any authenticators for them.
162    /// Calling [`MockChain::build_transaction`] on accounts added in this way will not work if the
163    /// account needs an authenticator.
164    ///
165    /// Due to these limitations, prefer using other methods to add accounts to the chain, e.g.
166    /// [`MockChainBuilder::add_account_from_builder`].
167    pub fn with_accounts(accounts: impl IntoIterator<Item = Account>) -> anyhow::Result<Self> {
168        let mut builder = Self::new();
169
170        for account in accounts {
171            builder.add_account(account)?;
172        }
173
174        Ok(builder)
175    }
176
177    // BUILDER METHODS
178    // ----------------------------------------------------------------------------------------
179
180    /// Sets the fee faucet ID of the chain.
181    ///
182    /// This must be a fungible faucet [`AccountId`] and is the asset in which fees will be accepted
183    /// by the transaction kernel.
184    pub fn fee_faucet_id(mut self, fee_faucet_id: AccountId) -> Self {
185        self.fee_faucet_id = fee_faucet_id;
186        self
187    }
188
189    /// Sets the `verification_base_fee` of the chain.
190    ///
191    /// See [`FeeParameters`] for more details.
192    pub fn verification_base_fee(mut self, verification_base_fee: u32) -> Self {
193        self.verification_base_fee = verification_base_fee;
194        self
195    }
196
197    /// Sets the validator signing keys for genesis and subsequent blocks, until validator rotation.
198    ///
199    /// By default, three random keys are generated. Supplied keys may be in any order; the genesis
200    /// [`ValidatorConfig`] and signatures follow the config's canonical public key order. All keys
201    /// must sign each block.
202    ///
203    /// [`Self::build`] returns an error if the set is empty, contains duplicate keys, or exceeds
204    /// [`ValidatorConfig::MAX_VALIDATORS`].
205    pub fn validator_signing_keys(mut self, keys: Vec<SigningKey>) -> Self {
206        self.validator_signing_keys = Some(keys);
207        self
208    }
209
210    /// Consumes the builder, creates the genesis block of the chain and returns the [`MockChain`].
211    pub fn build(self) -> anyhow::Result<MockChain> {
212        // Create the genesis block, consisting of the provided accounts and notes.
213        let block_account_updates: Vec<BlockAccountUpdate> = self
214            .accounts
215            .into_values()
216            .map(|account| {
217                let account_id = account.id();
218                let account_commitment = account.to_commitment();
219                let update_details = if account_id.is_private() {
220                    AccountUpdateDetails::Private
221                } else {
222                    let account_patch = AccountPatch::try_from(account)
223                        .expect("chain builder should only store existing accounts without seeds");
224                    AccountUpdateDetails::Public(account_patch)
225                };
226
227                BlockAccountUpdate::new(account_id, account_commitment, update_details)
228            })
229            .collect::<Result<_, _>>()?;
230
231        let account_tree = AccountTree::with_entries(
232            block_account_updates
233                .iter()
234                .map(|account| (account.account_id(), account.final_state_commitment())),
235        )
236        .context("failed to create genesis account tree")?;
237
238        // Extract full notes before shrinking for later use in MockChain
239        let full_notes: Vec<Note> = self
240            .notes
241            .iter()
242            .filter_map(|note| match note {
243                RawOutputNote::Full(n) => Some(n.clone()),
244                _ => None,
245            })
246            .collect();
247
248        let proven_notes: Vec<_> = self
249            .notes
250            .into_iter()
251            .map(|note| note.into_output_note().expect("genesis note should be valid"))
252            .collect();
253        let note_chunks = proven_notes.into_iter().chunks(MAX_OUTPUT_NOTES_PER_BATCH);
254        let output_note_batches: Vec<OutputNoteBatch> = note_chunks
255            .into_iter()
256            .map(|batch_notes| batch_notes.into_iter().enumerate().collect::<Vec<_>>())
257            .collect();
258
259        let created_nullifiers = Vec::new();
260        let transactions = OrderedTransactionHeaders::new_unchecked(Vec::new());
261
262        let note_tree = BlockNoteTree::from_note_batches(&output_note_batches)
263            .context("failed to create block note tree")?;
264
265        let prev_block_commitment = Word::empty();
266        let block_num = BlockNumber::from(0u32);
267        let chain_commitment = Blockchain::new().commitment();
268        let account_root = account_tree.root();
269        let nullifier_root = NullifierTree::<Smt>::default().root();
270        let note_root = note_tree.root();
271        let tx_commitment = transactions.commitment();
272        let timestamp = MockChain::TIMESTAMP_START_SECS;
273        let fee_parameters = FeeParameters::new(self.verification_base_fee);
274        let protocol_config = ProtocolConfig::current(AssetId::new_fungible(self.fee_faucet_id))
275            .context("failed to build the genesis protocol config")?;
276        let validator_secret_keys = self
277            .validator_signing_keys
278            .unwrap_or_else(|| (0..DEFAULT_VALIDATOR_COUNT).map(|_| random_secret_key()).collect());
279        let quorum = u16::try_from(validator_secret_keys.len())
280            .context("genesis validator count exceeds u16::MAX")?;
281        let validator_config = ValidatorConfig::new(
282            validator_secret_keys.iter().map(|signer| signer.public_key()).collect(),
283            quorum,
284        )
285        .context("failed to build the genesis validator config")?;
286
287        let header = BlockHeader::new(
288            prev_block_commitment,
289            block_num,
290            chain_commitment,
291            account_root,
292            nullifier_root,
293            note_root,
294            tx_commitment,
295            validator_config.clone(),
296            fee_parameters,
297            protocol_config.to_commitment(),
298            None,
299            timestamp,
300        );
301
302        let body = BlockBody::new_unchecked(
303            block_account_updates,
304            output_note_batches,
305            created_nullifiers,
306            transactions,
307        );
308
309        // The genesis block is the trust root: it is self-signed by the validator set it commits
310        // as the signer of block 1.
311        let signatures = BlockSignatures::new(
312            validator_config
313                .keys()
314                .iter()
315                .map(|key| {
316                    let signer = validator_secret_keys
317                        .iter()
318                        .find(|sk| &sk.public_key() == key)
319                        .expect("a signer should exist for every validator key");
320                    signer.sign(header.commitment())
321                })
322                .collect(),
323        )
324        .expect("signature count same as validator key count");
325        let block_proof = miden_protocol::testing::dummy_execution_proof();
326        let genesis_block = ProvenBlock::new_unchecked(header, body, signatures, block_proof);
327
328        MockChain::from_genesis_block(
329            genesis_block,
330            account_tree,
331            self.account_authenticators,
332            validator_secret_keys,
333            protocol_config,
334            full_notes,
335        )
336    }
337
338    // ACCOUNT METHODS
339    // ----------------------------------------------------------------------------------------
340
341    /// Creates a new public [`BasicWallet`] account and registers the authenticator (if any) for
342    /// it.
343    ///
344    /// This does not add the account to the chain state, but it can still be used to call
345    /// [`MockChain::build_transaction`] to automatically add the authenticator.
346    pub fn create_new_wallet(&mut self, auth_method: Auth) -> anyhow::Result<Account> {
347        let account_builder = AccountBuilder::new(self.rng.random())
348            .account_type(AccountType::Public)
349            .with_component(BasicWallet);
350
351        self.add_account_from_builder(auth_method, account_builder, AccountState::New)
352    }
353
354    /// Adds an existing public [`BasicWallet`] account to the initial chain state and registers the
355    /// authenticator (if any).
356    pub fn add_existing_wallet(&mut self, auth_method: Auth) -> anyhow::Result<Account> {
357        self.add_existing_wallet_with_assets(auth_method, [])
358    }
359
360    /// Adds an existing public [`BasicWallet`] account to the initial chain state and registers the
361    /// authenticator (if any).
362    pub fn add_existing_wallet_with_assets(
363        &mut self,
364        auth_method: Auth,
365        assets: impl IntoIterator<Item = Asset>,
366    ) -> anyhow::Result<Account> {
367        let account_builder = Account::builder(self.rng.random())
368            .account_type(AccountType::Public)
369            .with_component(BasicWallet)
370            .with_assets(assets);
371
372        self.add_account_from_builder(auth_method, account_builder, AccountState::Exists)
373    }
374
375    /// Adds an existing public [`NoteCreator`] account to the initial chain state and registers the
376    /// authenticator (if any).
377    ///
378    /// Unlike [`add_existing_wallet`](Self::add_existing_wallet), the account exposes only the
379    /// `create_note` procedure, which is enough for tests that only create output notes.
380    pub fn add_existing_note_creator(&mut self, auth_method: Auth) -> anyhow::Result<Account> {
381        let account_builder = Account::builder(self.rng.random())
382            .account_type(AccountType::Public)
383            .with_component(NoteCreator);
384
385        self.add_account_from_builder(auth_method, account_builder, AccountState::Exists)
386    }
387
388    /// Internal helper: adds an existing network-style fungible faucet (Ownable2Step / Rbac).
389    /// Bundles [`PausableManager`] to match the `create_network_fungible_faucet` factory.
390    fn add_existing_network_fungible_faucet(
391        &mut self,
392        allowed_script_roots: BTreeSet<NoteScriptRoot>,
393        faucet: FungibleFaucet,
394        account_type: AccountType,
395        access_control: AccessControl,
396        token_policy_manager: TokenPolicyManager,
397        assets: Vec<Asset>,
398    ) -> anyhow::Result<Account> {
399        // network faucets authenticate with AuthNetworkAccount, which collects sponsored fees and
400        // answers sponsorship fee estimates; both require an active fee policy. A constant policy
401        // aborts fee estimation for note scripts without a schedule entry, so schedule an explicit
402        // 0 fee for every allowlisted note; this keeps fees a no-op on fee-free chains.
403        let mut basic_constant_fee_policy = BasicConstantFeePolicy::new();
404        for note_script in &allowed_script_roots {
405            basic_constant_fee_policy =
406                basic_constant_fee_policy.with_fee(*note_script, AssetAmount::ZERO);
407        }
408        // `AuthNetworkAccount::new` allowlists its default notes on top, which the network auth
409        // flow prices if they are ever consumed, so schedule them too.
410        for note_script in AuthNetworkAccount::default_allowed_note_scripts() {
411            basic_constant_fee_policy =
412                basic_constant_fee_policy.with_fee(note_script, AssetAmount::ZERO);
413        }
414
415        let fee_policy_manager = FeePolicyManager::builder()
416            .active_fee_policy(basic_constant_fee_policy.into())
417            .fee_faucet_id(self.fee_faucet_id)
418            .build();
419
420        let account_builder = AccountBuilder::new(self.rng.random())
421            .account_type(account_type)
422            .with_component(faucet)
423            .with_components(access_control)
424            .with_components(token_policy_manager)
425            .with_component(Pausable::unpaused())
426            .with_component(PausableManager)
427            .with_assets(assets);
428
429        let auth = Auth::NetworkAccount {
430            allowed_script_roots,
431            allowed_tx_script_roots: BTreeSet::new(),
432            fee_policy_manager,
433            sponsorship_policy: SponsorshipPolicy::default(),
434        };
435
436        self.add_account_from_builder(auth, account_builder, AccountState::Exists)
437    }
438
439    /// Convenience: builds a basic auth-controlled fungible faucet from a token-symbol shorthand
440    /// using default decimals and `AllowAll` policies, then adds it as an existing account with
441    /// [`Authority::AuthControlled`].
442    ///
443    /// The faucet installs only `AllowAll` mint and burn policies and no transfer policy, so its
444    /// account ID has asset callbacks disabled and its assets transfer freely without triggering a
445    /// faucet callback. For a faucet with transfer policies (and thus callbacks), construct a
446    /// [`FungibleFaucet`] with a [`TokenPolicyManager`] manually and use [`AccountBuilder`]
447    /// directly.
448    pub fn add_existing_basic_faucet(
449        &mut self,
450        auth_method: Auth,
451        token_symbol: &str,
452        max_supply: u64,
453        token_supply: Option<u64>,
454    ) -> anyhow::Result<Account> {
455        let token_supply = token_supply.unwrap_or(0);
456        let name = TokenName::new(token_symbol)?;
457        let symbol = TokenSymbol::new(token_symbol)
458            .with_context(|| format!("invalid token symbol: {token_symbol}"))?;
459        let max_supply = AssetAmount::new(max_supply).context("invalid max_supply")?;
460        let token_supply = AssetAmount::new(token_supply).context("invalid token_supply")?;
461        let faucet = FungibleFaucet::builder()
462            .name(name)
463            .symbol(symbol)
464            .decimals(DEFAULT_FAUCET_DECIMALS)
465            .max_supply(max_supply)
466            .token_supply(token_supply)
467            .build()
468            .context("failed to build FungibleFaucet")?;
469
470        let token_policy_manager = TokenPolicyManager::builder()
471            .active_mint_policy(MintPolicy::allow_all())
472            .active_burn_policy(BurnPolicy::allow_all())
473            .build();
474
475        let account_builder = AccountBuilder::new(self.rng.random())
476            .account_type(AccountType::Public)
477            .with_component(faucet)
478            .with_component(Authority::AuthControlled)
479            .with_components(token_policy_manager)
480            .with_component(Pausable::unpaused())
481            .with_component(PausableManager);
482
483        self.add_account_from_builder(auth_method, account_builder, AccountState::Exists)
484    }
485
486    /// Convenience: builds a non-fungible faucet from a token-symbol shorthand using `AllowAll`
487    /// policies, then adds it as an existing account with [`Authority::AuthControlled`].
488    ///
489    /// Being auth-controlled, the faucet is not a network faucet, so `mint_and_send` can be called
490    /// from a transaction script. Its transfer policies enable asset callbacks.
491    pub fn add_existing_non_fungible_faucet(
492        &mut self,
493        auth_method: Auth,
494        token_symbol: &str,
495    ) -> anyhow::Result<Account> {
496        let name = TokenName::new(token_symbol)?;
497        let symbol = TokenSymbol::new(token_symbol)
498            .with_context(|| format!("invalid token symbol: {token_symbol}"))?;
499        let faucet = NonFungibleFaucet::builder().name(name).symbol(symbol).build();
500
501        let token_policy_manager = TokenPolicyManager::builder()
502            .active_mint_policy(MintPolicy::allow_all())
503            .active_burn_policy(BurnPolicy::allow_all())
504            .active_send_policy(TransferPolicy::allow_all())
505            .active_receive_policy(TransferPolicy::allow_all())
506            .build();
507
508        let account_builder = AccountBuilder::new(self.rng.random())
509            .account_type(AccountType::Public)
510            .with_component(faucet)
511            .with_component(Authority::AuthControlled)
512            .with_components(token_policy_manager)
513            .with_component(Pausable::unpaused());
514
515        self.add_account_from_builder(auth_method, account_builder, AccountState::Exists)
516    }
517
518    /// Convenience: builds an owner-controlled (network-style) fungible faucet from a
519    /// token-symbol shorthand using default decimals, the given `mint_policy`, and `BurnAllowAll`.
520    ///
521    /// The faucet is added with [`AccountType::Public`] and [`Auth::IncrNonce`].
522    ///
523    /// `mint_policy` selects the initial active mint policy on the faucet. The installed
524    /// [`TokenPolicyManager`] is always owner-controlled.
525    ///
526    /// The [`MintNote`] and [`BurnNote`] script roots are always added to `allowed_script_roots`,
527    /// so callers only need to provide any additional roots their test scripts require.
528    pub fn add_existing_network_faucet(
529        &mut self,
530        token_symbol: &str,
531        max_supply: u64,
532        owner_account_id: AccountId,
533        token_supply: Option<u64>,
534        mint_policy: MintPolicy,
535        allowed_script_roots: impl IntoIterator<Item = NoteScriptRoot>,
536    ) -> anyhow::Result<Account> {
537        self.add_existing_network_faucet_with_assets(
538            token_symbol,
539            max_supply,
540            owner_account_id,
541            token_supply,
542            mint_policy,
543            allowed_script_roots,
544            [],
545        )
546    }
547
548    /// Same as [`Self::add_existing_network_faucet`], but the faucet's vault additionally holds
549    /// `assets` (e.g. the native fee asset, so the faucet can pay transaction fees).
550    #[allow(clippy::too_many_arguments)]
551    pub fn add_existing_network_faucet_with_assets(
552        &mut self,
553        token_symbol: &str,
554        max_supply: u64,
555        owner_account_id: AccountId,
556        token_supply: Option<u64>,
557        mint_policy: MintPolicy,
558        allowed_script_roots: impl IntoIterator<Item = NoteScriptRoot>,
559        assets: impl IntoIterator<Item = Asset>,
560    ) -> anyhow::Result<Account> {
561        let token_supply = token_supply.unwrap_or(0);
562        let name = TokenName::new(token_symbol)?;
563        let symbol = TokenSymbol::new(token_symbol)
564            .with_context(|| format!("invalid token symbol: {token_symbol}"))?;
565        let max_supply = AssetAmount::new(max_supply).context("invalid max_supply")?;
566        let token_supply = AssetAmount::new(token_supply).context("invalid token_supply")?;
567        let faucet = FungibleFaucet::builder()
568            .name(name)
569            .symbol(symbol)
570            .decimals(DEFAULT_FAUCET_DECIMALS)
571            .max_supply(max_supply)
572            .token_supply(token_supply)
573            .build()
574            .context("failed to build FungibleFaucet")?;
575
576        let token_policy_manager = TokenPolicyManager::builder()
577            .active_mint_policy(mint_policy)
578            .active_burn_policy(BurnPolicy::allow_all())
579            .active_send_policy(TransferPolicy::allow_all())
580            .active_receive_policy(TransferPolicy::allow_all())
581            .build();
582
583        let allowed_script_roots: BTreeSet<NoteScriptRoot> = allowed_script_roots
584            .into_iter()
585            .chain([MintNote::script_root(), BurnNote::script_root()])
586            .collect();
587
588        self.add_existing_network_fungible_faucet(
589            allowed_script_roots,
590            faucet,
591            AccountType::Public,
592            AccessControl::Ownable2Step { owner: owner_account_id },
593            token_policy_manager,
594            assets.into_iter().collect(),
595        )
596    }
597
598    /// Convenience: adds an existing owner-controlled (network-style) fungible faucet whose token
599    /// metadata is fully provided by the caller. Uses `OwnerOnly` mint policy and `AllowAll`
600    /// burn policy by default.
601    ///
602    /// The [`MintNote`] and [`BurnNote`] script roots are always added to `allowed_script_roots`,
603    /// so callers only need to provide any additional roots their test scripts require.
604    pub fn add_existing_network_faucet_with_metadata(
605        &mut self,
606        owner_account_id: AccountId,
607        faucet: FungibleFaucet,
608        allowed_script_roots: impl IntoIterator<Item = NoteScriptRoot>,
609    ) -> anyhow::Result<Account> {
610        let token_policy_manager = TokenPolicyManager::builder()
611            .active_mint_policy(MintPolicy::owner_only())
612            .active_burn_policy(BurnPolicy::allow_all())
613            .active_send_policy(TransferPolicy::allow_all())
614            .active_receive_policy(TransferPolicy::allow_all())
615            .build();
616
617        let allowed_script_roots: BTreeSet<NoteScriptRoot> = allowed_script_roots
618            .into_iter()
619            .chain([MintNote::script_root(), BurnNote::script_root()])
620            .collect();
621
622        self.add_existing_network_fungible_faucet(
623            allowed_script_roots,
624            faucet,
625            AccountType::Public,
626            AccessControl::Ownable2Step { owner: owner_account_id },
627            token_policy_manager,
628            Vec::new(),
629        )
630    }
631
632    /// Convenience: builds a new (uncreated) basic auth-controlled fungible faucet from a
633    /// token-symbol shorthand using default decimals and `AllowAll` mint/burn policies (no transfer
634    /// policy, so asset callbacks are disabled).
635    pub fn create_new_faucet(
636        &mut self,
637        auth_method: Auth,
638        token_symbol: &str,
639        max_supply: u64,
640    ) -> anyhow::Result<Account> {
641        let name = TokenName::new(token_symbol)?;
642        let symbol = TokenSymbol::new(token_symbol)
643            .with_context(|| format!("invalid token symbol: {token_symbol}"))?;
644        let max_supply = AssetAmount::new(max_supply).context("invalid max_supply")?;
645        let faucet = FungibleFaucet::builder()
646            .name(name)
647            .symbol(symbol)
648            .decimals(DEFAULT_FAUCET_DECIMALS)
649            .max_supply(max_supply)
650            .build()
651            .context("failed to build FungibleFaucet")?;
652
653        let token_policy_manager = TokenPolicyManager::builder()
654            .active_mint_policy(MintPolicy::allow_all())
655            .active_burn_policy(BurnPolicy::allow_all())
656            .build();
657
658        let account_builder = AccountBuilder::new(self.rng.random())
659            .account_type(AccountType::Public)
660            .with_component(faucet)
661            .with_component(Authority::AuthControlled)
662            .with_components(token_policy_manager)
663            .with_component(Pausable::unpaused())
664            .with_component(PausableManager);
665
666        self.add_account_from_builder(auth_method, account_builder, AccountState::New)
667    }
668
669    /// Creates a new public account with an [`MockAccountComponent`] and registers the
670    /// authenticator (if any).
671    pub fn create_new_mock_account(&mut self, auth_method: Auth) -> anyhow::Result<Account> {
672        let account_builder = Account::builder(self.rng.random())
673            .account_type(AccountType::Public)
674            .with_component(MockAccountComponent::with_empty_slots());
675
676        self.add_account_from_builder(auth_method, account_builder, AccountState::New)
677    }
678
679    /// Adds an existing public account with an [`MockAccountComponent`] to the initial chain state
680    /// and registers the authenticator (if any).
681    pub fn add_existing_mock_account(&mut self, auth_method: Auth) -> anyhow::Result<Account> {
682        self.add_existing_mock_account_with_storage_and_assets(auth_method, [], [])
683    }
684
685    /// Adds an existing public account with an [`MockAccountComponent`] to the initial chain state
686    /// and registers the authenticator (if any).
687    pub fn add_existing_mock_account_with_storage(
688        &mut self,
689        auth_method: Auth,
690        slots: impl IntoIterator<Item = StorageSlot>,
691    ) -> anyhow::Result<Account> {
692        self.add_existing_mock_account_with_storage_and_assets(auth_method, slots, [])
693    }
694
695    /// Adds an existing public account with an [`MockAccountComponent`] to the initial chain state
696    /// and registers the authenticator (if any).
697    pub fn add_existing_mock_account_with_assets(
698        &mut self,
699        auth_method: Auth,
700        assets: impl IntoIterator<Item = Asset>,
701    ) -> anyhow::Result<Account> {
702        self.add_existing_mock_account_with_storage_and_assets(auth_method, [], assets)
703    }
704
705    /// Adds an existing public account with an [`MockAccountComponent`] to the initial chain state
706    /// and registers the authenticator (if any).
707    pub fn add_existing_mock_account_with_storage_and_assets(
708        &mut self,
709        auth_method: Auth,
710        slots: impl IntoIterator<Item = StorageSlot>,
711        assets: impl IntoIterator<Item = Asset>,
712    ) -> anyhow::Result<Account> {
713        let account_builder = Account::builder(self.rng.random())
714            .account_type(AccountType::Public)
715            .with_component(MockAccountComponent::with_slots(slots.into_iter().collect()))
716            .with_assets(assets);
717
718        self.add_account_from_builder(auth_method, account_builder, AccountState::Exists)
719    }
720
721    /// Builds the provided [`AccountBuilder`] with the provided auth method and registers the
722    /// authenticator (if any).
723    ///
724    /// - If [`AccountState::Exists`] is given the account is built as an existing account and added
725    ///   to the initial chain state. It can then be used in a transaction without having to
726    ///   validate its seed.
727    /// - If [`AccountState::New`] is given the account is built as a new account and is **not**
728    ///   added to the chain. Its authenticator is registered (if present). Its first transaction
729    ///   will be its creation transaction. [`MockChain::build_transaction`] can be called with the
730    ///   account to automatically add the authenticator.
731    pub fn add_account_from_builder(
732        &mut self,
733        auth_method: Auth,
734        mut account_builder: AccountBuilder,
735        account_state: AccountState,
736    ) -> anyhow::Result<Account> {
737        let (auth_components, authenticator) = auth_method.build_components();
738        account_builder = account_builder.with_components(auth_components);
739
740        let account = if let AccountState::New = account_state {
741            account_builder.build().context("failed to build account from builder")?
742        } else {
743            account_builder
744                .build_existing()
745                .context("failed to build account from builder")?
746        };
747
748        self.account_authenticators
749            .insert(account.id(), AccountAuthenticator::new(authenticator));
750
751        if let AccountState::Exists = account_state {
752            self.accounts.insert(account.id(), account.clone());
753        }
754
755        Ok(account)
756    }
757    pub fn add_existing_account_from_components(
758        &mut self,
759        auth: Auth,
760        components: impl IntoIterator<Item = AccountComponent>,
761    ) -> anyhow::Result<Account> {
762        let mut account_builder =
763            Account::builder(rand::rng().random()).account_type(AccountType::Public);
764
765        for component in components {
766            account_builder = account_builder.with_component(component);
767        }
768
769        self.add_account_from_builder(auth, account_builder, AccountState::Exists)
770    }
771
772    /// Adds the provided account to the list of genesis accounts.
773    ///
774    /// This method only adds the account and does not store its account authenticator for it.
775    /// Calling [`MockChain::build_transaction`] on accounts added in this way will not work if
776    /// the account needs an authenticator.
777    ///
778    /// Due to these limitations, prefer using other methods to add accounts to the chain, e.g.
779    /// [`MockChainBuilder::add_account_from_builder`].
780    pub fn add_account(&mut self, account: Account) -> anyhow::Result<()> {
781        self.accounts.insert(account.id(), account);
782
783        // This returns a Result to be conservative in case we need to return an error in the future
784        // and do not want to break this API.
785        Ok(())
786    }
787
788    // NOTE ADD METHODS
789    // ----------------------------------------------------------------------------------------
790
791    /// Adds the provided note to the initial chain state.
792    pub fn add_output_note(&mut self, note: impl Into<RawOutputNote>) {
793        self.notes.push(note.into());
794    }
795
796    /// Creates a new P2ANY note from the provided parameters and adds it to the list of
797    /// genesis notes.
798    ///
799    /// This note is similar to a P2ID note but can be consumed by any account.
800    pub fn add_p2any_note(
801        &mut self,
802        sender_account_id: AccountId,
803        note_type: NoteType,
804        assets: impl IntoIterator<Item = Asset>,
805    ) -> anyhow::Result<Note> {
806        let note = create_p2any_note(sender_account_id, note_type, assets, &mut self.rng);
807        self.add_output_note(RawOutputNote::Full(note.clone()));
808
809        Ok(note)
810    }
811
812    /// Creates a new P2ID note from the provided parameters and adds it to the list of genesis
813    /// notes.
814    ///
815    /// In the created [`MockChain`], the note will be immediately spendable by `target_account_id`
816    /// and carries no additional reclaim or timelock conditions.
817    pub fn add_p2id_note(
818        &mut self,
819        sender_account_id: AccountId,
820        target_account_id: AccountId,
821        asset: &[Asset],
822        note_type: NoteType,
823    ) -> Result<Note, NoteError> {
824        let note: Note = P2idNote::builder()
825            .sender(sender_account_id)
826            .target(target_account_id)
827            .assets(asset.iter().copied())
828            .note_type(note_type)
829            .generate_serial_number(&mut self.rng)
830            .build()?
831            .into();
832        self.add_output_note(RawOutputNote::Full(note.clone()));
833
834        Ok(note)
835    }
836
837    /// Creates a new TX_FEE note from the provided parameters and adds it to the list of genesis
838    /// notes.
839    ///
840    /// In the created [`MockChain`], the note can be consumed right away by an account whose own
841    /// code collects its assets.
842    pub fn add_tx_fee_note(
843        &mut self,
844        sender_account_id: AccountId,
845        assets: &[Asset],
846    ) -> Result<Note, NoteError> {
847        let note: Note = TxFeeNote::builder()
848            .sender(sender_account_id)
849            .assets(assets.iter().copied())
850            .generate_serial_number(&mut self.rng)
851            .build()?
852            .into();
853        self.add_output_note(RawOutputNote::Full(note.clone()));
854
855        Ok(note)
856    }
857
858    /// Adds a P2IDE note (pay‑to‑ID‑extended) to the list of genesis notes.
859    ///
860    /// A P2IDE note can include an optional `timelock_height` and/or an optional
861    /// `reclaim_height` after which the note's reclaimer may reclaim the funds.
862    ///
863    /// The `reclaimer` is the account allowed to reclaim the note; when `None` it
864    /// defaults to `sender_account_id`.
865    pub fn add_p2ide_note(
866        &mut self,
867        sender_account_id: AccountId,
868        target_account_id: AccountId,
869        reclaimer: Option<AccountId>,
870        asset: &[Asset],
871        note_type: NoteType,
872        reclaim_height: Option<BlockNumber>,
873        timelock_height: Option<BlockNumber>,
874    ) -> Result<Note, NoteError> {
875        let note: Note = P2ideNote::builder()
876            .sender(sender_account_id)
877            .target(target_account_id)
878            .maybe_reclaimer(reclaimer)
879            .assets(asset.iter().copied())
880            .note_type(note_type)
881            .maybe_reclaim_height(reclaim_height)
882            .maybe_timelock_height(timelock_height)
883            .generate_serial_number(&mut self.rng)
884            .build()?
885            .into();
886
887        self.add_output_note(RawOutputNote::Full(note.clone()));
888
889        Ok(note)
890    }
891
892    /// Adds a public SWAP note to the list of genesis notes.
893    pub fn add_swap_note(
894        &mut self,
895        sender: AccountId,
896        offered_asset: Asset,
897        requested_asset: Asset,
898        payback_note_type: NoteType,
899    ) -> anyhow::Result<(Note, NoteDetails)> {
900        let swap_note = SwapNote::builder()
901            .sender(sender)
902            .offered_asset(offered_asset)
903            .requested_asset(requested_asset)
904            .note_type(NoteType::Public)
905            .payback_note_type(payback_note_type)
906            .generate_serial_number(&mut self.rng)
907            .build()?;
908
909        let payback_note = swap_note.payback_note_details();
910        let note = Note::from(swap_note);
911
912        self.add_output_note(RawOutputNote::Full(note.clone()));
913
914        Ok((note, payback_note))
915    }
916
917    /// Adds a public `SPAWN` note to the list of genesis notes.
918    ///
919    /// A `SPAWN` note contains a note script that creates all `output_notes` that get passed as a
920    /// parameter.
921    ///
922    /// # Errors
923    ///
924    /// Returns an error if:
925    /// - the sender account ID of the provided output notes is not consistent or does not match the
926    ///   transaction's sender.
927    pub fn add_spawn_note<'note, I>(
928        &mut self,
929        output_notes: impl IntoIterator<Item = &'note Note, IntoIter = I>,
930    ) -> anyhow::Result<Note>
931    where
932        I: ExactSizeIterator<Item = &'note Note>,
933    {
934        let note = create_spawn_note(output_notes)?;
935        self.add_output_note(RawOutputNote::Full(note.clone()));
936
937        Ok(note)
938    }
939
940    /// Creates a new P2ID note with the provided amount of the fee asset of the chain.
941    ///
942    /// The fee faucet ID of the asset can be set using [`Self::fee_faucet_id`]. By default it
943    /// is [`ACCOUNT_ID_FEE_FAUCET`].
944    ///
945    /// In the created [`MockChain`], the note will be immediately spendable by `target_account_id`.
946    pub fn add_p2id_note_with_fee(
947        &mut self,
948        target_account_id: AccountId,
949        amount: u64,
950    ) -> anyhow::Result<Note> {
951        let fee_asset = self.fee_asset(amount)?;
952        let note = self.add_p2id_note(
953            self.fee_faucet_id,
954            target_account_id,
955            &[Asset::from(fee_asset)],
956            NoteType::Public,
957        )?;
958
959        Ok(note)
960    }
961
962    // HELPER FUNCTIONS
963    // ----------------------------------------------------------------------------------------
964
965    /// Returns a mutable reference to the builder's RNG.
966    ///
967    /// This can be used when creating accounts or notes and randomness is required.
968    pub fn rng_mut(&mut self) -> &mut RandomCoin {
969        &mut self.rng
970    }
971
972    /// Constructs a fungible asset based on the fee faucet ID and the provided amount.
973    fn fee_asset(&self, amount: u64) -> anyhow::Result<FungibleAsset> {
974        FungibleAsset::new(self.fee_faucet_id, amount).context("failed to create fee asset")
975    }
976}
977
978impl Default for MockChainBuilder {
979    fn default() -> Self {
980        Self::new()
981    }
982}