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