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
5//! rules for manipulating them. Once an account is registered with the client, its state will
6//! be updated 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
94/// and 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
116mod account_reader;
117pub use account_reader::AccountReader;
118/// Raw access to `miden-standards` account modules for items not curated by `miden-client`.
119pub use miden_standards::account as standards;
120use miden_standards::account::auth::{Approver, AuthSingleSig};
121use miden_standards::account::faucets::FungibleFaucet;
122pub use miden_standards::account::inspection::{
123    AccountBuilderSchemaCommitmentExt,
124    AccountSchemaCommitment,
125};
126// RE-EXPORTS
127// ================================================================================================
128pub use miden_standards::account::interface::{
129    AccountComponentInterface,
130    AccountComponentInterfaceExt,
131    AccountInterface,
132    AccountInterfaceExt,
133};
134use miden_standards::account::wallets::BasicWallet;
135
136use super::Client;
137use crate::asset::TokenSymbol;
138use crate::errors::ClientError;
139use crate::rpc::domain::account::GetAccountRequest;
140use crate::rpc::node::{EndpointError, GetAccountError};
141use crate::store::{AccountStatus, AccountStorageFilter, ClientAccountType};
142use crate::sync::NoteTagRecord;
143
144pub mod component {
145    pub const MIDEN_PACKAGE_EXTENSION: &str = "masp";
146
147    pub use miden_protocol::account::auth::*;
148    pub use miden_protocol::account::component::{
149        FeltSchema,
150        InitStorageData,
151        InitStorageDataError,
152        MapSlotSchema,
153        SchemaRequirement,
154        SchemaType,
155        SchemaTypeError,
156        StorageSchema,
157        StorageSlotSchema,
158        StorageValueName,
159        StorageValueNameError,
160        ValueSlotSchema,
161        WordSchema,
162        WordValue,
163    };
164    pub use miden_protocol::account::{
165        AccountComponent,
166        AccountComponentMetadata,
167        AccountComponentName,
168        AccountProcedureRoot,
169        RoleSymbol,
170    };
171    pub use miden_standards::account::access::{
172        AccessControl,
173        Authority,
174        AuthorityError,
175        Ownable2Step,
176        Ownable2StepError,
177        Pausable,
178        PausableManager,
179        PausableStorage,
180        RoleBasedAccessControl,
181    };
182    pub use miden_standards::account::auth::*;
183    pub use miden_standards::account::components::StandardAccountComponent;
184    pub use miden_standards::account::faucets::{
185        Description,
186        ExternalLink,
187        FungibleFaucet,
188        FungibleFaucetBuilder,
189        FungibleFaucetError,
190        LogoURI,
191        TokenMetadata,
192        TokenMetadataError,
193        TokenName,
194        create_network_fungible_faucet,
195        create_singlesig_user_fungible_faucet,
196    };
197    pub use miden_standards::account::policies::{
198        AllowlistOwnerControlled,
199        AllowlistStorage,
200        BasicAllowlist,
201        BasicBlocklist,
202        BlocklistOwnerControlled,
203        BlocklistStorage,
204        BurnAllowAll,
205        BurnOwnerOnly,
206        BurnPolicy,
207        BurnPolicyError,
208        MinBurnAmount,
209        MintAllowAll,
210        MintOwnerOnly,
211        MintPolicy,
212        MintPolicyError,
213        TokenPolicyManager,
214        TokenPolicyManagerBuilder,
215        TransferAllowAll,
216        TransferPolicy,
217        TransferPolicyError,
218    };
219    pub use miden_standards::account::wallets::BasicWallet;
220}
221
222// CLIENT METHODS
223// ================================================================================================
224
225/// This section of the [Client] contains methods for:
226///
227/// - **Account creation:** Use the [`AccountBuilder`] to construct new accounts, specifying account
228///   visibility (`AccountType::Public` / `AccountType::Private`) and attaching necessary components
229///   (e.g., basic wallet or fungible faucet). Prefer
230///   [`AccountBuilderSchemaCommitmentExt::build_with_schema_commitment`] so the account includes
231///   merged storage schema commitment metadata; use plain [`AccountBuilder::build`] only when you
232///   need to opt out. After creation, accounts can be added to the client.
233///
234/// - **Account tracking:** Accounts added via the client are persisted to the local store, where
235///   their state (including nonce, balance, and metadata) is updated upon every synchronization
236///   with the network.
237///
238/// - **Data retrieval:** The module also provides methods to fetch account-related data.
239impl<AUTH> Client<AUTH> {
240    // ACCOUNT CREATION
241    // --------------------------------------------------------------------------------------------
242
243    /// Adds the provided [Account] in the store so it can start being tracked by the client.
244    ///
245    /// If the account is already being tracked and `overwrite` is set to `true`, the account will
246    /// be overwritten. Newly created accounts must embed their seed (`account.seed()` must return
247    /// `Some(_)`).
248    ///
249    /// # Errors
250    ///
251    /// - If the account is new but it does not contain the seed.
252    /// - If the account is already tracked and `overwrite` is set to `false`.
253    /// - If `overwrite` is set to `true` and the `account_data` nonce is lower than the one already
254    ///   being tracked.
255    /// - If `overwrite` is set to `true` and the `account_data` commitment doesn't match the
256    ///   network's account commitment.
257    pub async fn add_account(
258        &mut self,
259        account: &Account,
260        overwrite: bool,
261    ) -> Result<(), ClientError> {
262        self.add_account_inner(account, ClientAccountType::Native, overwrite).await
263    }
264
265    /// Inserts `account` into the store (or overwrites it if `overwrite` is true) and registers
266    /// the per-account note tag if `client_account_type` is [`ClientAccountType::Native`].
267    ///
268    /// Switching the [`ClientAccountType`] of an already-tracked account is not supported and
269    /// returns [`ClientError::AccountWatchedMismatch`].
270    async fn add_account_inner(
271        &mut self,
272        account: &Account,
273        client_account_type: ClientAccountType,
274        overwrite: bool,
275    ) -> Result<(), ClientError> {
276        if account.is_new() {
277            if account.seed().is_none() {
278                return Err(ClientError::AddNewAccountWithoutSeed);
279            }
280        } else {
281            // Ignore the seed since it's not a new account
282            if account.seed().is_some() {
283                tracing::warn!(
284                    "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."
285                );
286            }
287        }
288
289        let tracked_account = self.store.get_account(account.id()).await?;
290
291        match tracked_account {
292            None => {
293                let default_address = Address::new(account.id());
294
295                self.store
296                    .insert_account(account, default_address.clone(), client_account_type)
297                    .await
298                    .map_err(ClientError::StoreError)?;
299
300                if matches!(client_account_type, ClientAccountType::Native) {
301                    // Set the default address note tag so sync pulls notes.
302                    let default_address_note_tag = default_address.to_note_tag();
303                    let note_tag_record =
304                        NoteTagRecord::with_account_source(default_address_note_tag, account.id());
305                    self.store.add_note_tag(note_tag_record).await?;
306                }
307
308                Ok(())
309            },
310            Some(tracked_account) => {
311                if !overwrite {
312                    // Only overwrite the account if the flag is set to `true`
313                    return Err(ClientError::AccountAlreadyTracked(account.id()));
314                }
315
316                if client_account_type != tracked_account.client_account_type() {
317                    // Switching between Watched and Native after the account is tracked is not
318                    // supported: the per-account note tag and any client-side state derived from
319                    // that mode are set up at insertion time and not migrated on the fly.
320                    return Err(ClientError::AccountWatchedMismatch(account.id()));
321                }
322
323                if tracked_account.nonce().as_canonical_u64() > account.nonce().as_canonical_u64() {
324                    // If the new account is older than the one being tracked, return an error
325                    return Err(ClientError::AccountNonceTooLow);
326                }
327
328                if tracked_account.is_locked() {
329                    // If the tracked account is locked, check that the account commitment matches
330                    // the one in the network
331                    let network_account_commitment = self
332                        .rpc_api
333                        .get_account(account.id(), GetAccountRequest::new())
334                        .await?
335                        .1
336                        .account_commitment();
337                    if network_account_commitment != account.to_commitment() {
338                        return Err(ClientError::AccountCommitmentMismatch(
339                            network_account_commitment,
340                        ));
341                    }
342                }
343
344                self.store.update_account(account).await?;
345
346                Ok(())
347            },
348        }
349    }
350
351    /// Imports an account from the network to the client's store. The account needs to be public
352    /// and be tracked by the network, it will be fetched by its ID. If the account was already
353    /// being tracked by the client, its state will be overwritten.
354    ///
355    /// To import an account as watched (state-tracking only, no note sync), use
356    /// [`Self::import_watched_account_by_id`] instead. Switching an already-tracked account
357    /// between Native and Watched is not supported.
358    ///
359    /// # Errors
360    /// - If the account is not found on the network.
361    /// - If the account is private.
362    /// - If the account is already tracked as watched.
363    /// - There was an error sending the request to the network.
364    pub async fn import_account_by_id(&mut self, account_id: AccountId) -> Result<(), ClientError> {
365        let account = self.fetch_public_account(account_id).await?;
366        self.add_account_inner(&account, ClientAccountType::Native, true).await
367    }
368
369    /// Starts watching an on-chain account ([`ClientAccountType::Watched`]).
370    ///
371    /// Like [`Self::import_account_by_id`], the account is fetched from the network by its ID.
372    /// Unlike `import_account_by_id`, the account is added without registering its derived note
373    /// tag: `sync_state` will keep the account's commitment, nonce and storage up to date but
374    /// will **not** pull notes targeted at it.
375    ///
376    /// If the account is already being tracked as watched its state is overwritten. Switching an
377    /// already-tracked native account to watched is not supported.
378    ///
379    /// # Errors
380    /// - If the account is not found on the network.
381    /// - If the account is private.
382    /// - If the account is already tracked as native.
383    /// - There was an error sending the request to the network.
384    pub async fn import_watched_account_by_id(
385        &mut self,
386        account_id: AccountId,
387    ) -> Result<(), ClientError> {
388        let account = self.fetch_public_account(account_id).await?;
389        self.add_account_inner(&account, ClientAccountType::Watched, true).await
390    }
391
392    /// Fetches a public [`Account`] from the network, returning a typed error when the account
393    /// doesn't exist on chain or is private.
394    async fn fetch_public_account(&self, account_id: AccountId) -> Result<Account, ClientError> {
395        let fetched_account =
396            self.rpc_api.get_account_details(account_id).await.map_err(|err| {
397                match err.endpoint_error() {
398                    Some(EndpointError::GetAccount(GetAccountError::AccountNotFound)) => {
399                        ClientError::AccountNotFoundOnChain(account_id)
400                    },
401                    _ => ClientError::RpcError(err),
402                }
403            })?;
404
405        fetched_account.ok_or(ClientError::AccountIsPrivate(account_id))
406    }
407
408    /// Fetches a public faucet's display metadata from the network.
409    ///
410    /// Uses [`get_account`](crate::rpc::NodeRpcClient::get_account) with a minimal request so that
411    /// the node does not return vault data. The faucet's token config lives in a single value slot,
412    /// which is always present in the returned storage header.
413    ///
414    /// Returns:
415    /// - `Ok(Some(_))` — the account is public and its token config storage slot decoded.
416    /// - `Ok(None)`    — the account is private, not on chain, or the storage slot does not parse
417    ///   as a token config. Caller should fall back to a raw display.
418    /// - `Err(_)`      — transport-level RPC error.
419    pub async fn fetch_remote_token_metadata(
420        &self,
421        faucet_id: AccountId,
422    ) -> Result<Option<FaucetMetadata>, ClientError> {
423        let proof = match self.rpc_api.get_account(faucet_id, GetAccountRequest::new()).await {
424            Ok((_, proof)) => proof,
425            Err(err) => match err.endpoint_error() {
426                Some(EndpointError::GetAccount(
427                    GetAccountError::AccountNotFound | GetAccountError::AccountNotPublic,
428                )) => return Ok(None),
429                _ => return Err(ClientError::RpcError(err)),
430            },
431        };
432
433        let Some(storage_header) = proof.storage_header() else {
434            return Ok(None);
435        };
436
437        let Some(slot_header) =
438            storage_header.find_slot_header_by_name(FungibleFaucet::token_config_slot())
439        else {
440            return Ok(None);
441        };
442
443        let [_token_supply, _max_supply, decimals, symbol] = *slot_header.value();
444        let Ok(symbol) = TokenSymbol::try_from(symbol) else {
445            return Ok(None);
446        };
447        let Ok(decimals) = u8::try_from(decimals.as_canonical_u64()) else {
448            return Ok(None);
449        };
450        Ok(Some(FaucetMetadata { symbol: symbol.to_string(), decimals }))
451    }
452
453    /// Adds an [`Address`] to the associated [`AccountId`], alongside its derived [`NoteTag`]. If
454    /// the account is tracked as watched, the note tag is not registered.
455    ///
456    /// # Errors
457    /// - If the account is not found on the network.
458    /// - If the address is already being tracked.
459    pub async fn add_address(
460        &mut self,
461        address: Address,
462        account_id: AccountId,
463    ) -> Result<(), ClientError> {
464        let network_id = self.rpc_api.get_network_id().await?;
465        let address_bench32 = address.encode(network_id);
466        if self.store.get_addresses_by_account_id(account_id).await?.contains(&address) {
467            return Err(ClientError::AddressAlreadyTracked(address_bench32));
468        }
469
470        let tracked_account = self.store.get_account(account_id).await?;
471        match tracked_account {
472            None => Err(ClientError::AccountDataNotFound(account_id)),
473            Some(tracked_account) => {
474                self.store.insert_address(address.clone(), account_id).await?;
475                // Watched accounts intentionally have no derived note tag registered to avoid sync
476                // state pulling notes for them.
477                if !tracked_account.is_watched() {
478                    let derived_note_tag: NoteTag = address.to_note_tag();
479                    let note_tag_record =
480                        NoteTagRecord::with_account_source(derived_note_tag, account_id);
481                    self.store.add_note_tag(note_tag_record).await?;
482                }
483                Ok(())
484            },
485        }
486    }
487
488    /// Removes an [`Address`] from the associated [`AccountId`], alongside its derived [`NoteTag`].
489    /// If no address was tracked for the given account, this is a no-op.
490    pub async fn remove_address(
491        &mut self,
492        address: Address,
493        account_id: AccountId,
494    ) -> Result<(), ClientError> {
495        let derived_note_tag = address.to_note_tag();
496        let note_tag_record = NoteTagRecord::with_account_source(derived_note_tag, account_id);
497        self.store.remove_address(address).await?;
498        // Remove the note tag if no other address are associated with it.
499        let addresses = self.store.get_addresses_by_account_id(account_id).await?;
500        if addresses.iter().all(|address| address.to_note_tag() != derived_note_tag) {
501            self.store.remove_note_tag(note_tag_record).await?;
502        }
503        Ok(())
504    }
505
506    // ACCOUNT DATA RETRIEVAL
507    // --------------------------------------------------------------------------------------------
508
509    /// Retrieves the asset vault for a specific account.
510    ///
511    /// To check the balance for a single asset, use [`Client::account_reader`] instead.
512    pub async fn get_account_vault(
513        &self,
514        account_id: AccountId,
515    ) -> Result<AssetVault, ClientError> {
516        self.store.get_account_vault(account_id).await.map_err(ClientError::StoreError)
517    }
518
519    /// Retrieves the whole account storage for a specific account.
520    ///
521    /// To only load a specific slot, use [`Client::account_reader`] instead.
522    pub async fn get_account_storage(
523        &self,
524        account_id: AccountId,
525    ) -> Result<AccountStorage, ClientError> {
526        self.store
527            .get_account_storage(account_id, AccountStorageFilter::All)
528            .await
529            .map_err(ClientError::StoreError)
530    }
531
532    /// Retrieves the account code for a specific account.
533    ///
534    /// Returns `None` if the account is not found.
535    pub async fn get_account_code(
536        &self,
537        account_id: AccountId,
538    ) -> Result<Option<AccountCode>, ClientError> {
539        self.store.get_account_code(account_id).await.map_err(ClientError::StoreError)
540    }
541
542    /// Returns a list of [`AccountHeader`] of all accounts stored in the database along with their
543    /// statuses.
544    ///
545    /// Said accounts' state is the state after the last performed sync.
546    pub async fn get_account_headers(
547        &self,
548    ) -> Result<Vec<(AccountHeader, AccountStatus)>, ClientError> {
549        self.store.get_account_headers().await.map_err(Into::into)
550    }
551
552    /// Retrieves the full [`Account`] object from the store, returning `None` if not found.
553    ///
554    /// This method loads the complete account state including vault, storage, and code.
555    ///
556    /// For lazy access that fetches only the data you need, use
557    /// [`Client::account_reader`] instead.
558    ///
559    /// Use [`Client::try_get_account`] if you want to error when the account is not found.
560    pub async fn get_account(&self, account_id: AccountId) -> Result<Option<Account>, ClientError> {
561        match self.store.get_account(account_id).await? {
562            Some(record) => Ok(Some(record.try_into()?)),
563            None => Ok(None),
564        }
565    }
566
567    /// Retrieves the full [`Account`] object from the store, erroring if not found.
568    ///
569    /// This method loads the complete account state including vault, storage, and code.
570    ///
571    /// Use [`Client::get_account`] if you want to handle missing accounts gracefully.
572    pub async fn try_get_account(&self, account_id: AccountId) -> Result<Account, ClientError> {
573        self.get_account(account_id)
574            .await?
575            .ok_or(ClientError::AccountDataNotFound(account_id))
576    }
577
578    /// Creates an [`AccountReader`] for lazy access to account data.
579    ///
580    /// The `AccountReader` provides lazy access to account state - each method call
581    /// fetches fresh data from storage, ensuring you always see the current state.
582    ///
583    /// For loading the full [`Account`] object, use [`Client::get_account`] instead.
584    ///
585    /// # Example
586    /// ```ignore
587    /// let reader = client.account_reader(account_id);
588    ///
589    /// // Each call fetches fresh data
590    /// let nonce = reader.nonce().await?;
591    /// let balance = reader.get_balance(faucet_id).await?;
592    ///
593    /// // Storage access is integrated
594    /// let value = reader.get_storage_item("my_slot").await?;
595    /// let (map_value, witness) = reader.get_storage_map_witness("balances", key).await?;
596    /// ```
597    pub fn account_reader(&self, account_id: AccountId) -> AccountReader {
598        AccountReader::new(self.store.clone(), account_id)
599    }
600
601    /// Prunes historical account states for the specified account up to the given nonce.
602    ///
603    /// Deletes all historical entries with `replaced_at_nonce <= up_to_nonce` and any
604    /// orphaned account code.
605    ///
606    /// Returns the total number of rows deleted, including historical entries and orphaned
607    /// account code.
608    pub async fn prune_account_history(
609        &self,
610        account_id: AccountId,
611        up_to_nonce: Felt,
612    ) -> Result<usize, ClientError> {
613        Ok(self.store.prune_account_history(account_id, up_to_nonce).await?)
614    }
615}
616
617// UTILITY FUNCTIONS
618// ================================================================================================
619
620/// Builds an regular account ID from the provided parameters. The ID may be used along
621/// `Client::import_account_by_id` to import a public account from the network (provided that the
622/// used seed is known).
623///
624/// This function currently supports accounts composed of the [`BasicWallet`] component and one of
625/// the supported authentication schemes ([`AuthSingleSig`]).
626///
627/// # Arguments
628/// - `init_seed`: Initial seed used to create the account. This is the seed passed to
629///   [`AccountBuilder::new`].
630/// - `public_key`: Public key of the account used for the authentication component.
631/// - `account_visibility`: Public/private visibility of the account.
632///
633/// # Errors
634/// - If the account cannot be built.
635pub fn build_wallet_id(
636    init_seed: [u8; 32],
637    public_key: &PublicKey,
638    account_visibility: AccountType,
639) -> Result<AccountId, ClientError> {
640    let auth_scheme = public_key.auth_scheme();
641    let auth_component: AccountComponent =
642        AuthSingleSig::new(Approver::new(public_key.to_commitment(), auth_scheme)).into();
643
644    let account = AccountBuilder::new(init_seed)
645        .account_type(account_visibility)
646        .with_auth_component(auth_component)
647        .with_component(BasicWallet)
648        .build_with_schema_commitment()?;
649
650    Ok(account.id())
651}
652
653#[cfg(test)]
654mod schema_commitment_tests {
655    use miden_protocol::EMPTY_WORD;
656    use miden_protocol::account::auth::AuthSecretKey;
657    use miden_standards::account::inspection::AccountSchemaCommitment;
658
659    use super::{
660        AccountBuilder,
661        AccountBuilderSchemaCommitmentExt,
662        AccountType,
663        Approver,
664        AuthSingleSig,
665        BasicWallet,
666    };
667    use crate::auth::AuthSchemeId;
668
669    #[test]
670    fn wallet_build_includes_schema_commitment_metadata_slot() {
671        let key = AuthSecretKey::new_falcon512_poseidon2();
672        let account = AccountBuilder::new([2u8; 32])
673            .account_type(AccountType::Private)
674            .with_auth_component(AuthSingleSig::new(Approver::new(
675                key.public_key().to_commitment(),
676                AuthSchemeId::Falcon512Poseidon2,
677            )))
678            .with_component(BasicWallet)
679            .build_with_schema_commitment()
680            .expect("build_with_schema_commitment");
681
682        let commitment = account
683            .storage()
684            .get_item(AccountSchemaCommitment::schema_commitment_slot())
685            .expect("schema commitment slot");
686        assert_ne!(commitment, EMPTY_WORD);
687    }
688}