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