Skip to main content

miden_client/account/
mod.rs

1//! The `account` module provides types and client APIs for managing accounts within the Miden
2//! network.
3//!
4//! Accounts are foundational entities of the Miden protocol. They store assets and define rules for
5//! manipulating them. Once an account is registered with the client, its state will be updated
6//! accordingly, and validated against the network state on every sync.
7//!
8//! # Example
9//!
10//! To add a new account to the client's store, you might use the [`Client::add_account`] method as
11//! follows:
12//!
13//! ```rust
14//! # use miden_client::{
15//! #   account::{Account, AccountBuilder, AccountBuilderSchemaCommitmentExt, AccountType, component::BasicWallet},
16//! #   crypto::FeltRng
17//! # };
18//! # async fn add_new_account_example<AUTH>(
19//! #     client: &mut miden_client::Client<AUTH>
20//! # ) -> Result<(), miden_client::ClientError> {
21//! #   let random_seed = Default::default();
22//! let account = AccountBuilder::new(random_seed)
23//!     .account_type(AccountType::Private)
24//!     .with_component(BasicWallet)
25//!     .build_with_schema_commitment()?;
26//!
27//! // Add the account to the client. The account already embeds its seed information.
28//! client.add_account(&account, false).await?;
29//! #   Ok(())
30//! # }
31//! ```
32//!
33//! For more details on accounts, refer to the [Account] documentation.
34
35use alloc::string::{String, ToString};
36use alloc::vec::Vec;
37
38use miden_protocol::Felt;
39use miden_protocol::account::auth::PublicKey;
40pub use miden_protocol::account::{
41    Account,
42    AccountBuilder,
43    AccountCode,
44    AccountComponent,
45    AccountComponentCode,
46    AccountDelta,
47    AccountFile,
48    AccountHeader,
49    AccountId,
50    AccountIdPrefix,
51    AccountIdPrefixV1,
52    AccountIdV1,
53    AccountIdVersion,
54    AccountPatch,
55    AccountProcedureRoot,
56    AccountStorage,
57    AccountStoragePatch,
58    AccountType,
59    AccountUpdateDetails,
60    AccountVaultPatch,
61    PartialAccount,
62    PartialStorage,
63    PartialStorageMap,
64    RoleSymbol,
65    StorageMap,
66    StorageMapKey,
67    StorageMapKeyHash,
68    StorageMapPatch,
69    StorageMapPatchEntries,
70    StorageMapWitness,
71    StorageSlot,
72    StorageSlotContent,
73    StorageSlotId,
74    StorageSlotName,
75    StorageSlotPatch,
76    StorageSlotType,
77    StorageValuePatch,
78};
79pub use miden_protocol::address::{Address, AddressInterface, AddressType, NetworkId};
80use miden_protocol::asset::AssetVault;
81pub use miden_protocol::errors::{AccountIdError, AddressError, NetworkIdError};
82use miden_protocol::note::NoteTag;
83use miden_tx::utils::serde::{
84    ByteReader,
85    ByteWriter,
86    Deserializable,
87    DeserializationError,
88    Serializable,
89};
90
91/// Display-only metadata for a faucet account, persisted in the client's settings store.
92///
93/// Populated lazily by the CLI resolver from the on-chain token config of a public faucet and
94/// persisted under a `faucet_metadata:<faucet-id>` key.
95#[derive(Debug, Clone, PartialEq, Eq)]
96pub struct FaucetMetadata {
97    pub symbol: String,
98    pub decimals: u8,
99}
100
101impl Serializable for FaucetMetadata {
102    fn write_into<W: ByteWriter>(&self, target: &mut W) {
103        self.symbol.write_into(target);
104        target.write_u8(self.decimals);
105    }
106}
107
108impl Deserializable for FaucetMetadata {
109    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
110        let symbol = String::read_from(source)?;
111        let decimals = source.read_u8()?;
112        Ok(Self { symbol, decimals })
113    }
114}
115
116/// Decodes a fungible faucet token config slot value into display metadata.
117///
118/// Returns `None` when the value does not describe a fungible faucet config the protocol would
119/// accept: the symbol must decode as a [`TokenSymbol`], and the decimals must be within
120/// [`FungibleFaucet::MAX_DECIMALS`], which is what [`FungibleFaucet`] enforces when the component
121/// is built.
122fn faucet_metadata_from_token_config(token_config: [Felt; 4]) -> Option<FaucetMetadata> {
123    let [_token_supply, _max_supply, decimals, symbol] = token_config;
124
125    let symbol = TokenSymbol::try_from(symbol).ok()?;
126    let decimals = u8::try_from(decimals.as_canonical_u64()).ok()?;
127    if decimals > FungibleFaucet::MAX_DECIMALS {
128        return None;
129    }
130
131    Some(FaucetMetadata { symbol: symbol.to_string(), decimals })
132}
133
134mod account_reader;
135pub use account_reader::AccountReader;
136/// Raw access to `miden-standards` account modules for items not curated by `miden-client`.
137pub use miden_standards::account as standards;
138use miden_standards::account::auth::{Approver, AuthSingleSig};
139use miden_standards::account::faucets::FungibleFaucet;
140pub use miden_standards::account::inspection::{
141    AccountBuilderSchemaCommitmentExt,
142    AccountSchemaCommitment,
143};
144// RE-EXPORTS
145// ================================================================================================
146pub use miden_standards::account::interface::{
147    AccountComponentInterface,
148    AccountComponentInterfaceExt,
149    AccountInterface,
150    AccountInterfaceExt,
151};
152use miden_standards::account::wallets::BasicWallet;
153
154use super::Client;
155use crate::asset::TokenSymbol;
156use crate::errors::ClientError;
157use crate::rpc::domain::account::GetAccountRequest;
158use crate::rpc::node::{EndpointError, GetAccountError};
159use crate::store::{AccountStatus, AccountStorageFilter, ClientAccountType};
160use crate::sync::NoteTagRecord;
161
162pub mod component {
163    pub const MIDEN_PACKAGE_EXTENSION: &str = "masp";
164
165    pub use miden_protocol::account::auth::*;
166    pub use miden_protocol::account::component::{
167        FeltSchema,
168        InitStorageData,
169        InitStorageDataError,
170        MapSlotSchema,
171        SchemaRequirement,
172        SchemaType,
173        SchemaTypeError,
174        StorageSchema,
175        StorageSlotSchema,
176        StorageValueName,
177        StorageValueNameError,
178        ValueSlotSchema,
179        WordSchema,
180        WordValue,
181    };
182    pub use miden_protocol::account::{
183        AccountComponent,
184        AccountComponentMetadata,
185        AccountComponentName,
186        AccountProcedureRoot,
187        RoleSymbol,
188    };
189    pub use miden_standards::account::access::{
190        AccessControl,
191        Authority,
192        AuthorityError,
193        Ownable2Step,
194        Ownable2StepError,
195        Pausable,
196        PausableManager,
197        PausableStorage,
198        RoleBasedAccessControl,
199    };
200    pub use miden_standards::account::auth::*;
201    pub use miden_standards::account::components::StandardAccountComponent;
202    pub use miden_standards::account::faucets::{
203        Description,
204        ExternalLink,
205        FungibleFaucet,
206        FungibleFaucetBuilder,
207        FungibleFaucetError,
208        LogoURI,
209        NonFungibleFaucet,
210        TokenMetadata,
211        TokenMetadataError,
212        TokenName,
213        create_network_fungible_faucet,
214        create_singlesig_user_fungible_faucet,
215    };
216    pub use miden_standards::account::fees::{
217        BasicConstantFeePolicy,
218        FeePolicy,
219        FeePolicyError,
220        FeePolicyManager,
221        FeePolicyManagerBuilder,
222    };
223    pub use miden_standards::account::policies::{
224        AllowlistManager,
225        AllowlistStorage,
226        BasicAllowlist,
227        BasicBlocklist,
228        BlocklistManager,
229        BlocklistStorage,
230        BurnAllowAll,
231        BurnOwnerOnly,
232        BurnPolicy,
233        BurnPolicyError,
234        MinBurnAmount,
235        MintAllowAll,
236        MintOwnerOnly,
237        MintPolicy,
238        MintPolicyError,
239        TokenPolicyManager,
240        TokenPolicyManagerBuilder,
241        TransferAllowAll,
242        TransferPolicy,
243        TransferPolicyError,
244    };
245    pub use miden_standards::account::wallets::BasicWallet;
246}
247
248// CLIENT METHODS
249// ================================================================================================
250
251/// This section of the [Client] contains methods for:
252///
253/// - **Account creation:** Use the [`AccountBuilder`] to construct new accounts, specifying account
254///   visibility (`AccountType::Public` / `AccountType::Private`) and attaching necessary components
255///   (e.g., basic wallet or fungible faucet). Prefer
256///   [`AccountBuilderSchemaCommitmentExt::build_with_schema_commitment`] so the account includes
257///   merged storage schema commitment metadata; use plain [`AccountBuilder::build`] only when you
258///   need to opt out. After creation, accounts can be added to the client.
259///
260/// - **Account tracking:** Accounts added via the client are persisted to the local store, where
261///   their state (including nonce, balance, and metadata) is updated upon every synchronization
262///   with the network.
263///
264/// - **Data retrieval:** The module also provides methods to fetch account-related data.
265impl<AUTH> Client<AUTH> {
266    // ACCOUNT CREATION
267    // --------------------------------------------------------------------------------------------
268
269    /// Adds the provided [Account] in the store so it can start being tracked by the client.
270    ///
271    /// If the account is already being tracked and `overwrite` is set to `true`, the account will
272    /// be overwritten. Newly created accounts must embed their seed (`account.seed()` must return
273    /// `Some(_)`).
274    ///
275    /// # Errors
276    ///
277    /// - If the account is new but it does not contain the seed.
278    /// - If the account is already tracked and `overwrite` is set to `false`.
279    /// - If `overwrite` is set to `true` and the `account_data` nonce is lower than the one already
280    ///   being tracked.
281    /// - If `overwrite` is set to `true` and the `account_data` commitment doesn't match the
282    ///   network's account commitment.
283    pub async fn add_account(
284        &mut self,
285        account: &Account,
286        overwrite: bool,
287    ) -> Result<(), ClientError> {
288        self.add_account_inner(account, ClientAccountType::Native, overwrite).await
289    }
290
291    /// Inserts `account` into the store (or overwrites it if `overwrite` is true) and registers the
292    /// per-account note tag if `client_account_type` is [`ClientAccountType::Native`].
293    ///
294    /// Switching the [`ClientAccountType`] of an already-tracked account is not supported and
295    /// returns [`ClientError::AccountWatchedMismatch`].
296    async fn add_account_inner(
297        &mut self,
298        account: &Account,
299        client_account_type: ClientAccountType,
300        overwrite: bool,
301    ) -> Result<(), ClientError> {
302        if account.is_new() {
303            if account.seed().is_none() {
304                return Err(ClientError::AddNewAccountWithoutSeed);
305            }
306        } else {
307            // Ignore the seed since it's not a new account
308            if account.seed().is_some() {
309                tracing::warn!(
310                    "Added an existing account and still provided a seed when it is not needed. It's possible that the account's file was incorrectly generated. The seed will be ignored."
311                );
312            }
313        }
314
315        let tracked_account = self.store.get_minimal_partial_account(account.id()).await?;
316
317        match tracked_account {
318            None => {
319                let default_address = Address::new(account.id());
320
321                self.store
322                    .insert_account(account, default_address.clone(), client_account_type)
323                    .await
324                    .map_err(ClientError::StoreError)?;
325
326                if matches!(client_account_type, ClientAccountType::Native) {
327                    // Set the default address note tag so sync pulls notes.
328                    let default_address_note_tag = default_address.to_note_tag();
329                    let note_tag_record =
330                        NoteTagRecord::with_account_source(default_address_note_tag, account.id());
331                    self.store.add_note_tag(note_tag_record).await?;
332                }
333
334                Ok(())
335            },
336            Some(tracked_account) => {
337                if !overwrite {
338                    // Only overwrite the account if the flag is set to `true`
339                    return Err(ClientError::AccountAlreadyTracked(account.id()));
340                }
341
342                if client_account_type != tracked_account.client_account_type() {
343                    // Switching between Watched and Native after the account is tracked is not
344                    // supported: the per-account note tag and any client-side state derived from
345                    // that mode are set up at insertion time and not migrated on the fly.
346                    return Err(ClientError::AccountWatchedMismatch(account.id()));
347                }
348
349                if tracked_account.nonce().as_canonical_u64() > account.nonce().as_canonical_u64() {
350                    // If the new account is older than the one being tracked, return an error
351                    return Err(ClientError::AccountNonceTooLow);
352                }
353
354                if tracked_account.is_locked() {
355                    // If the tracked account is locked, check that the account commitment matches
356                    // the one in the network
357                    let network_account_commitment = self
358                        .rpc_api
359                        .get_account(account.id(), GetAccountRequest::new())
360                        .await?
361                        .1
362                        .account_commitment();
363                    if network_account_commitment != account.to_commitment() {
364                        return Err(ClientError::AccountCommitmentMismatch(
365                            network_account_commitment,
366                        ));
367                    }
368                }
369
370                self.store.update_account(account).await?;
371
372                Ok(())
373            },
374        }
375    }
376
377    /// Imports an account from the network to the client's store. The account needs to be public
378    /// and be tracked by the network, it will be fetched by its ID. If the account was already
379    /// being tracked by the client, its state will be overwritten.
380    ///
381    /// To import an account as watched (state-tracking only, no note sync), use
382    /// [`Self::import_watched_account_by_id`] instead. Switching an already-tracked account between
383    /// Native and Watched is not supported.
384    ///
385    /// # Errors
386    /// - If the account is not found on the network.
387    /// - If the account is private.
388    /// - If the account is already tracked as watched.
389    /// - There was an error sending the request to the network.
390    pub async fn import_account_by_id(&mut self, account_id: AccountId) -> Result<(), ClientError> {
391        let account = self.fetch_public_account(account_id).await?;
392        self.add_account_inner(&account, ClientAccountType::Native, true).await
393    }
394
395    /// Starts watching an on-chain account ([`ClientAccountType::Watched`]).
396    ///
397    /// Like [`Self::import_account_by_id`], the account is fetched from the network by its ID.
398    /// Unlike `import_account_by_id`, the account is added without registering its derived note
399    /// tag: `sync_state` will keep the account's commitment, nonce and storage up to date but will
400    /// **not** pull notes targeted at it.
401    ///
402    /// If the account is already being tracked as watched its state is overwritten. Switching an
403    /// already-tracked native account to watched is not supported.
404    ///
405    /// # Errors
406    /// - If the account is not found on the network.
407    /// - If the account is private.
408    /// - If the account is already tracked as native.
409    /// - There was an error sending the request to the network.
410    pub async fn import_watched_account_by_id(
411        &mut self,
412        account_id: AccountId,
413    ) -> Result<(), ClientError> {
414        let account = self.fetch_public_account(account_id).await?;
415        self.add_account_inner(&account, ClientAccountType::Watched, true).await
416    }
417
418    /// Fetches a public [`Account`] from the network, returning a typed error when the account
419    /// doesn't exist on chain or is private.
420    async fn fetch_public_account(&self, account_id: AccountId) -> Result<Account, ClientError> {
421        let fetched_account =
422            self.rpc_api.get_account_details(account_id).await.map_err(|err| {
423                match err.endpoint_error() {
424                    Some(EndpointError::GetAccount(GetAccountError::AccountNotFound)) => {
425                        ClientError::AccountNotFoundOnChain(account_id)
426                    },
427                    _ => ClientError::RpcError(err),
428                }
429            })?;
430
431        fetched_account.ok_or(ClientError::AccountIsPrivate(account_id))
432    }
433
434    /// Fetches a public faucet's display metadata from the network.
435    ///
436    /// Uses [`get_account`](crate::rpc::NodeRpcClient::get_account) with a minimal request so that
437    /// the node does not return vault data. The faucet's token config lives in a single value slot,
438    /// which is always present in the returned storage header.
439    ///
440    /// Returns:
441    /// - `Ok(Some(_))` — the account is public and its token config storage slot decoded.
442    /// - `Ok(None)`    — the account is private, not on chain, or the storage slot does not parse
443    ///   as a token config. Caller should fall back to a raw display.
444    /// - `Err(_)`      — transport-level RPC error.
445    pub async fn fetch_remote_token_metadata(
446        &self,
447        faucet_id: AccountId,
448    ) -> Result<Option<FaucetMetadata>, ClientError> {
449        let proof = match self.rpc_api.get_account(faucet_id, GetAccountRequest::new()).await {
450            Ok((_, proof)) => proof,
451            Err(err) => match err.endpoint_error() {
452                Some(EndpointError::GetAccount(
453                    GetAccountError::AccountNotFound | GetAccountError::AccountNotPublic,
454                )) => return Ok(None),
455                _ => return Err(ClientError::RpcError(err)),
456            },
457        };
458
459        let Some(storage_header) = proof.storage_header() else {
460            return Ok(None);
461        };
462
463        let Some(slot_header) =
464            storage_header.find_slot_header_by_name(FungibleFaucet::token_config_slot())
465        else {
466            return Ok(None);
467        };
468
469        Ok(faucet_metadata_from_token_config(*slot_header.value()))
470    }
471
472    /// Adds an [`Address`] to the associated [`AccountId`], alongside its derived [`NoteTag`]. If
473    /// the account is tracked as watched, the note tag is not registered.
474    ///
475    /// # Errors
476    /// - If the account is not found on the network.
477    /// - If the address is already being tracked.
478    pub async fn add_address(
479        &mut self,
480        address: Address,
481        account_id: AccountId,
482    ) -> Result<(), ClientError> {
483        let network_id = self.rpc_api.get_network_id().await?;
484        let address_bench32 = address.encode(network_id);
485        if self.store.get_addresses_by_account_id(account_id).await?.contains(&address) {
486            return Err(ClientError::AddressAlreadyTracked(address_bench32));
487        }
488
489        let tracked_account = self.store.get_minimal_partial_account(account_id).await?;
490        match tracked_account {
491            None => Err(ClientError::AccountDataNotFound(account_id)),
492            Some(tracked_account) => {
493                self.store.insert_address(address.clone(), account_id).await?;
494                // Watched accounts intentionally have no derived note tag registered to avoid sync
495                // state pulling notes for them.
496                if !tracked_account.is_watched() {
497                    let derived_note_tag: NoteTag = address.to_note_tag();
498                    let note_tag_record =
499                        NoteTagRecord::with_account_source(derived_note_tag, account_id);
500                    self.store.add_note_tag(note_tag_record).await?;
501                }
502                Ok(())
503            },
504        }
505    }
506
507    /// Removes an [`Address`] from the associated [`AccountId`], alongside its derived [`NoteTag`].
508    ///
509    /// Returns `true` if the address was tracked. If it wasn't, this is a no-op: the derived tag is
510    /// left in place, since it may have been registered by something other than this address.
511    pub async fn remove_address(
512        &mut self,
513        address: Address,
514        account_id: AccountId,
515    ) -> Result<bool, ClientError> {
516        let derived_note_tag = address.to_note_tag();
517        let note_tag_record = NoteTagRecord::with_account_source(derived_note_tag, account_id);
518        if !self.store.remove_address(address).await? {
519            return Ok(false);
520        }
521        // Remove the note tag if no other address are associated with it.
522        let addresses = self.store.get_addresses_by_account_id(account_id).await?;
523        if addresses.iter().all(|address| address.to_note_tag() != derived_note_tag) {
524            self.store.remove_note_tag(note_tag_record).await?;
525        }
526        Ok(true)
527    }
528
529    // ACCOUNT DATA RETRIEVAL
530    // --------------------------------------------------------------------------------------------
531
532    /// Retrieves the asset vault for a specific account.
533    ///
534    /// To check the balance for a single asset, use [`Client::account_reader`] instead.
535    pub async fn get_account_vault(
536        &self,
537        account_id: AccountId,
538    ) -> Result<AssetVault, ClientError> {
539        self.store.get_account_vault(account_id).await.map_err(ClientError::StoreError)
540    }
541
542    /// Retrieves the whole account storage for a specific account.
543    ///
544    /// To only load a specific slot, use [`Client::account_reader`] instead.
545    pub async fn get_account_storage(
546        &self,
547        account_id: AccountId,
548    ) -> Result<AccountStorage, ClientError> {
549        self.store
550            .get_account_storage(account_id, AccountStorageFilter::All)
551            .await
552            .map_err(ClientError::StoreError)
553    }
554
555    /// Retrieves the account code for a specific account.
556    ///
557    /// Returns `None` if the account is not found.
558    pub async fn get_account_code(
559        &self,
560        account_id: AccountId,
561    ) -> Result<Option<AccountCode>, ClientError> {
562        self.store.get_account_code(account_id).await.map_err(ClientError::StoreError)
563    }
564
565    /// Returns a list of [`AccountHeader`] of all accounts stored in the database along with their
566    /// statuses.
567    ///
568    /// Said accounts' state is the state after the last performed sync.
569    pub async fn get_account_headers(
570        &self,
571    ) -> Result<Vec<(AccountHeader, AccountStatus)>, ClientError> {
572        self.store.get_account_headers().await.map_err(Into::into)
573    }
574
575    /// Returns the [`AccountHeader`] of the account with the specified ID along with its status, or
576    /// `None` if the account isn't tracked by the client.
577    ///
578    /// Said account's state is the state after the last performed sync.
579    pub async fn get_account_header(
580        &self,
581        account_id: AccountId,
582    ) -> Result<Option<(AccountHeader, AccountStatus)>, ClientError> {
583        self.store.get_account_header(account_id).await.map_err(Into::into)
584    }
585
586    /// Retrieves the full [`Account`] object from the store, returning `None` if not found.
587    ///
588    /// This method loads the complete account state including vault, storage, and code — including
589    /// building the vault's Merkle tree. For lazy access that fetches only the data you need
590    /// (existence checks, single fields, storage items), use [`Client::account_reader`] instead.
591    pub async fn get_account(&self, account_id: AccountId) -> Result<Option<Account>, ClientError> {
592        match self.store.get_account(account_id).await? {
593            Some(record) => Ok(Some(record.try_into()?)),
594            None => Ok(None),
595        }
596    }
597
598    /// Creates an [`AccountReader`] for lazy access to account data.
599    ///
600    /// The `AccountReader` provides lazy access to account state - each method call fetches fresh
601    /// data from storage, ensuring you always see the current state.
602    ///
603    /// For loading the full [`Account`] object, use [`Client::get_account`] instead.
604    ///
605    /// # Example
606    /// ```ignore
607    /// let reader = client.account_reader(account_id);
608    ///
609    /// // Each call fetches fresh data
610    /// let nonce = reader.nonce().await?;
611    /// let balance = reader.get_balance(faucet_id).await?;
612    ///
613    /// // Storage access is integrated
614    /// let value = reader.get_storage_item("my_slot").await?;
615    /// let (map_value, witness) = reader.get_storage_map_witness("balances", key).await?;
616    /// ```
617    pub fn account_reader(&self, account_id: AccountId) -> AccountReader {
618        AccountReader::new(self.store.clone(), account_id)
619    }
620
621    /// Prunes historical account states for the specified account up to the given nonce.
622    ///
623    /// Deletes all historical entries with `replaced_at_nonce <= up_to_nonce` and any orphaned
624    /// account code.
625    ///
626    /// Returns the total number of rows deleted, including historical entries and orphaned account
627    /// code.
628    pub async fn prune_account_history(
629        &self,
630        account_id: AccountId,
631        up_to_nonce: Felt,
632    ) -> Result<usize, ClientError> {
633        Ok(self.store.prune_account_history(account_id, up_to_nonce).await?)
634    }
635}
636
637// UTILITY FUNCTIONS
638// ================================================================================================
639
640/// Builds an regular account ID from the provided parameters. The ID may be used along
641/// `Client::import_account_by_id` to import a public account from the network (provided that the
642/// used seed is known).
643///
644/// This function currently supports accounts composed of the [`BasicWallet`] component and one of
645/// the supported authentication schemes ([`AuthSingleSig`]).
646///
647/// # Arguments
648/// - `init_seed`: Initial seed used to create the account. This is the seed passed to
649///   [`AccountBuilder::new`].
650/// - `public_key`: Public key of the account used for the authentication component.
651/// - `account_visibility`: Public/private visibility of the account.
652///
653/// # Errors
654/// - If the account cannot be built.
655pub fn build_wallet_id(
656    init_seed: [u8; 32],
657    public_key: &PublicKey,
658    account_visibility: AccountType,
659) -> Result<AccountId, ClientError> {
660    let auth_scheme = public_key.auth_scheme();
661    let auth_component: AccountComponent =
662        AuthSingleSig::new(Approver::new(public_key.to_commitment(), auth_scheme)).into();
663
664    let account = AccountBuilder::new(init_seed)
665        .account_type(account_visibility)
666        .with_component(auth_component)
667        .with_component(BasicWallet)
668        .build_with_schema_commitment()?;
669
670    Ok(account.id())
671}
672
673#[cfg(test)]
674mod schema_commitment_tests {
675    use miden_protocol::EMPTY_WORD;
676    use miden_protocol::account::auth::AuthSecretKey;
677    use miden_standards::account::inspection::AccountSchemaCommitment;
678
679    use super::{
680        AccountBuilder,
681        AccountBuilderSchemaCommitmentExt,
682        AccountType,
683        Approver,
684        AuthSingleSig,
685        BasicWallet,
686    };
687    use crate::auth::AuthSchemeId;
688
689    #[test]
690    fn wallet_build_includes_schema_commitment_metadata_slot() {
691        let key = AuthSecretKey::new_falcon512_poseidon2();
692        let account = AccountBuilder::new([2u8; 32])
693            .account_type(AccountType::Private)
694            .with_component(AuthSingleSig::new(Approver::new(
695                key.public_key().to_commitment(),
696                AuthSchemeId::Falcon512Poseidon2,
697            )))
698            .with_component(BasicWallet)
699            .build_with_schema_commitment()
700            .expect("build_with_schema_commitment");
701
702        let commitment = account
703            .storage()
704            .get_item(AccountSchemaCommitment::schema_commitment_slot())
705            .expect("schema commitment slot");
706        assert_ne!(commitment, EMPTY_WORD);
707    }
708}
709
710#[cfg(test)]
711mod faucet_metadata_tests {
712    use miden_protocol::Felt;
713
714    use super::{FungibleFaucet, TokenSymbol, faucet_metadata_from_token_config};
715
716    /// Builds a token config slot value carrying the given decimals and the symbol "TST".
717    fn token_config(decimals: u32) -> [Felt; 4] {
718        [
719            Felt::from(0u32),
720            Felt::from(0u32),
721            Felt::from(decimals),
722            TokenSymbol::new("TST").unwrap().as_element(),
723        ]
724    }
725
726    #[test]
727    fn decodes_a_config_within_the_protocol_bounds() {
728        let metadata = faucet_metadata_from_token_config(token_config(8)).unwrap();
729
730        assert_eq!(metadata.symbol, "TST");
731        assert_eq!(metadata.decimals, 8);
732    }
733
734    #[test]
735    fn accepts_the_maximum_supported_decimals() {
736        let max = u32::from(FungibleFaucet::MAX_DECIMALS);
737        let metadata = faucet_metadata_from_token_config(token_config(max)).unwrap();
738
739        assert_eq!(metadata.decimals, FungibleFaucet::MAX_DECIMALS);
740    }
741
742    #[test]
743    fn rejects_decimals_above_the_maximum() {
744        let above_max = u32::from(FungibleFaucet::MAX_DECIMALS) + 1;
745
746        assert!(faucet_metadata_from_token_config(token_config(above_max)).is_none());
747        assert!(faucet_metadata_from_token_config(token_config(200)).is_none());
748    }
749
750    #[test]
751    fn rejects_decimals_that_do_not_fit_a_u8() {
752        assert!(faucet_metadata_from_token_config(token_config(300)).is_none());
753    }
754
755    #[test]
756    fn rejects_a_symbol_that_is_not_a_token_symbol() {
757        let mut config = token_config(8);
758        config[3] = Felt::from(0u32);
759
760        assert!(faucet_metadata_from_token_config(config).is_none());
761    }
762}