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
38pub use miden_objects::account_file::{AccountFile, AccountFileError};
39use miden_protocol::Felt;
40use miden_protocol::account::auth::PublicKey;
41pub use miden_protocol::account::{
42    Account,
43    AccountBuilder,
44    AccountCode,
45    AccountComponent,
46    AccountComponentCode,
47    AccountDelta,
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, NetworkAccount};
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/// - **Account registration:** On a network that enforces an account allowlist,
265///   [`Client::register_account`] binds an invitation code to a new account before its first
266///   transaction creates it on chain, and [`Client::is_account_allowed`] asks whether the network
267///   accepts the creation of an account.
268///
269/// - **Data retrieval:** The module also provides methods to fetch account-related data.
270impl<AUTH> Client<AUTH> {
271    // ACCOUNT CREATION
272    // --------------------------------------------------------------------------------------------
273
274    /// Adds the provided [Account] in the store so it can start being tracked by the client.
275    ///
276    /// If the account is already being tracked and `overwrite` is set to `true`, the account will
277    /// be overwritten. Newly created accounts must embed their seed (`account.seed()` must return
278    /// `Some(_)`).
279    ///
280    /// # Errors
281    ///
282    /// - If the account is new but it does not contain the seed.
283    /// - If the account is already tracked and `overwrite` is set to `false`.
284    /// - If `overwrite` is set to `true` and the `account_data` nonce is lower than the one already
285    ///   being tracked.
286    /// - If `overwrite` is set to `true` and the `account_data` commitment doesn't match the
287    ///   network's account commitment.
288    pub async fn add_account(
289        &mut self,
290        account: &Account,
291        overwrite: bool,
292    ) -> Result<(), ClientError> {
293        self.add_account_inner(account, ClientAccountType::Native, overwrite).await
294    }
295
296    // ACCOUNT REGISTRATION
297    // --------------------------------------------------------------------------------------------
298
299    /// Binds an invitation code to a tracked account on the network allowlist.
300    ///
301    /// A network that enforces an account allowlist creates an account on chain only when the
302    /// account is registered. The first transaction of an account is what creates it, so the
303    /// account must be registered before that transaction is submitted.
304    /// [`Client::submit_new_transaction`] and [`BatchBuilder::submit`] ask the node first, and fail
305    /// with [`ClientError::AccountNotAllowlisted`] for an account the network does not accept. Only
306    /// account creation is gated: an account that already exists on chain is never checked, and
307    /// network accounts are exempt.
308    ///
309    /// The account must be tracked by the client, must not be deployed on chain yet, and must not
310    /// be a network account. The invitation code must exist on the node and must not be bound to
311    /// another account. A registration consumes the code, so the client asks the node first and
312    /// does not send the code for an account the node already allows.
313    ///
314    /// When the network operator runs a funding service, the node pays the registered account a
315    /// public P2ID note with the native asset. The node answers once the funding service queues the
316    /// note, before the note is committed. The note is not part of the response, and the client
317    /// does not see it until a [`Client::sync_state`] runs after the note is committed. The client
318    /// tracks the note tag of every account it owns, so that sync imports the note and
319    /// [`Client::get_consumable_notes`] lists it. Sync again until the note arrives. The account
320    /// then consumes the note in its first transaction. That transaction creates the account on
321    /// chain and pays its fee out of the received funds.
322    ///
323    /// # Errors
324    ///
325    /// - [`ClientError::AccountDataNotFound`] if the client does not track the account.
326    /// - [`ClientError::AccountIsNotNew`] if the account already exists on chain.
327    /// - [`ClientError::AccountIsNetworkAccount`] if the account is a network account. The node
328    ///   admits network accounts without a code.
329    /// - [`ClientError::AccountAlreadyAllowed`] if the node already allows the account, because it
330    ///   is registered or because the network does not enforce an allowlist. The code is not sent.
331    /// - [`ClientError::RpcError`] carrying a [`RegisterAccountError`] if the node rejects the
332    ///   code or the account, or an `Unavailable` status if the funding failed. In the second
333    ///   case the account stays registered, so a retry fails with
334    ///   [`ClientError::AccountAlreadyAllowed`] and the account has to be funded another way.
335    ///
336    /// [`BatchBuilder::submit`]: crate::transaction::BatchBuilder::submit
337    /// [`RegisterAccountError`]: crate::rpc::RegisterAccountError
338    pub async fn register_account(
339        &self,
340        account_id: AccountId,
341        invitation_code: &str,
342    ) -> Result<(), ClientError> {
343        let (_, status) = self
344            .store
345            .get_account_header(account_id)
346            .await?
347            .ok_or(ClientError::AccountDataNotFound(account_id))?;
348        if !status.is_new() {
349            return Err(ClientError::AccountIsNotNew(account_id));
350        }
351
352        let account = self
353            .get_account(account_id)
354            .await?
355            .ok_or(ClientError::AccountDataNotFound(account_id))?;
356        // The node admits a network account without a code.
357        if NetworkAccount::new(account).is_ok() {
358            return Err(ClientError::AccountIsNetworkAccount(account_id));
359        }
360        // A registration consumes the code, so do not send it when the node already allows the
361        // account.
362        if self.is_account_allowed(account_id).await? {
363            return Err(ClientError::AccountAlreadyAllowed(account_id));
364        }
365
366        self.rpc_api.register_account(invitation_code, account_id).await?;
367
368        Ok(())
369    }
370
371    /// Returns whether the network lets `account_id` be created on chain.
372    ///
373    /// The node answers `true` when it does not enforce an account allowlist, or when the account
374    /// is registered. See [`Client::register_account`] for how an account gets registered.
375    pub async fn is_account_allowed(&self, account_id: AccountId) -> Result<bool, ClientError> {
376        Ok(self.rpc_api.is_account_allowed(account_id).await?)
377    }
378
379    /// Returns whether a transaction against `account_id` creates an account that the network
380    /// allowlist gates.
381    ///
382    /// Only a new account is gated, and a network account is exempt. The answer is `false` for an
383    /// account that the client does not track.
384    pub(crate) async fn is_allowlist_gated(
385        &self,
386        account_id: AccountId,
387    ) -> Result<bool, ClientError> {
388        let Some((_, status)) = self.store.get_account_header(account_id).await? else {
389            return Ok(false);
390        };
391        if !status.is_new() {
392            return Ok(false);
393        }
394
395        let Some(account) = self.get_account(account_id).await? else {
396            return Ok(false);
397        };
398
399        Ok(NetworkAccount::new(account).is_err())
400    }
401
402    /// Inserts `account` into the store (or overwrites it if `overwrite` is true) and registers the
403    /// per-account note tag if `client_account_type` is [`ClientAccountType::Native`].
404    ///
405    /// Switching the [`ClientAccountType`] of an already-tracked account is not supported and
406    /// returns [`ClientError::AccountWatchedMismatch`].
407    async fn add_account_inner(
408        &mut self,
409        account: &Account,
410        client_account_type: ClientAccountType,
411        overwrite: bool,
412    ) -> Result<(), ClientError> {
413        if account.is_new() {
414            if account.seed().is_none() {
415                return Err(ClientError::AddNewAccountWithoutSeed);
416            }
417        } else {
418            // Ignore the seed since it's not a new account
419            if account.seed().is_some() {
420                tracing::warn!(
421                    "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."
422                );
423            }
424        }
425
426        let tracked_account = self.store.get_minimal_partial_account(account.id()).await?;
427
428        match tracked_account {
429            None => {
430                let default_address = Address::new(account.id());
431
432                self.store
433                    .insert_account(account, default_address.clone(), client_account_type)
434                    .await
435                    .map_err(ClientError::StoreError)?;
436
437                if matches!(client_account_type, ClientAccountType::Native) {
438                    // Set the default address note tag so sync pulls notes.
439                    let default_address_note_tag = default_address.to_note_tag();
440                    let note_tag_record =
441                        NoteTagRecord::with_account_source(default_address_note_tag, account.id());
442                    self.store.add_note_tag(note_tag_record).await?;
443                }
444
445                Ok(())
446            },
447            Some(tracked_account) => {
448                if !overwrite {
449                    // Only overwrite the account if the flag is set to `true`
450                    return Err(ClientError::AccountAlreadyTracked(account.id()));
451                }
452
453                if client_account_type != tracked_account.client_account_type() {
454                    // Switching between Watched and Native after the account is tracked is not
455                    // supported: the per-account note tag and any client-side state derived from
456                    // that mode are set up at insertion time and not migrated on the fly.
457                    return Err(ClientError::AccountWatchedMismatch(account.id()));
458                }
459
460                if tracked_account.nonce().as_canonical_u64() > account.nonce().as_canonical_u64() {
461                    // If the new account is older than the one being tracked, return an error
462                    return Err(ClientError::AccountNonceTooLow);
463                }
464
465                if tracked_account.is_locked() {
466                    // If the tracked account is locked, check that the account commitment matches
467                    // the one in the network
468                    let network_account_commitment = self
469                        .rpc_api
470                        .get_account(account.id(), GetAccountRequest::new())
471                        .await?
472                        .1
473                        .account_commitment();
474                    if network_account_commitment != account.to_commitment() {
475                        return Err(ClientError::AccountCommitmentMismatch(
476                            network_account_commitment,
477                        ));
478                    }
479                }
480
481                self.store.update_account(account).await?;
482
483                Ok(())
484            },
485        }
486    }
487
488    /// Imports an account from the network to the client's store. The account needs to be public
489    /// and be tracked by the network, it will be fetched by its ID. If the account was already
490    /// being tracked by the client, its state will be overwritten.
491    ///
492    /// To import an account as watched (state-tracking only, no note sync), use
493    /// [`Self::import_watched_account_by_id`] instead. Switching an already-tracked account between
494    /// Native and Watched is not supported.
495    ///
496    /// # Errors
497    /// - If the account is not found on the network.
498    /// - If the account is private.
499    /// - If the account is already tracked as watched.
500    /// - There was an error sending the request to the network.
501    pub async fn import_account_by_id(&mut self, account_id: AccountId) -> Result<(), ClientError> {
502        let account = self.fetch_public_account(account_id).await?;
503        self.add_account_inner(&account, ClientAccountType::Native, true).await
504    }
505
506    /// Starts watching an on-chain account ([`ClientAccountType::Watched`]).
507    ///
508    /// Like [`Self::import_account_by_id`], the account is fetched from the network by its ID.
509    /// Unlike `import_account_by_id`, the account is added without registering its derived note
510    /// tag: `sync_state` will keep the account's commitment, nonce and storage up to date but will
511    /// **not** pull notes targeted at it.
512    ///
513    /// If the account is already being tracked as watched its state is overwritten. Switching an
514    /// already-tracked native account to watched is not supported.
515    ///
516    /// # Errors
517    /// - If the account is not found on the network.
518    /// - If the account is private.
519    /// - If the account is already tracked as native.
520    /// - There was an error sending the request to the network.
521    pub async fn import_watched_account_by_id(
522        &mut self,
523        account_id: AccountId,
524    ) -> Result<(), ClientError> {
525        let account = self.fetch_public_account(account_id).await?;
526        self.add_account_inner(&account, ClientAccountType::Watched, true).await
527    }
528
529    /// Fetches a public [`Account`] from the network, returning a typed error when the account
530    /// doesn't exist on chain or is private.
531    async fn fetch_public_account(&self, account_id: AccountId) -> Result<Account, ClientError> {
532        let fetched_account =
533            self.rpc_api.get_account_details(account_id).await.map_err(|err| {
534                match err.endpoint_error() {
535                    Some(EndpointError::GetAccount(GetAccountError::AccountNotFound)) => {
536                        ClientError::AccountNotFoundOnChain(account_id)
537                    },
538                    _ => ClientError::RpcError(err),
539                }
540            })?;
541
542        fetched_account.ok_or(ClientError::AccountIsPrivate(account_id))
543    }
544
545    /// Fetches a public faucet's display metadata from the network.
546    ///
547    /// Uses [`get_account`](crate::rpc::NodeRpcClient::get_account) with a minimal request so that
548    /// the node does not return vault data. The faucet's token config lives in a single value slot,
549    /// which is always present in the returned storage header.
550    ///
551    /// Returns:
552    /// - `Ok(Some(_))` — the account is public and its token config storage slot decoded.
553    /// - `Ok(None)`    — the account is private, not on chain, or the storage slot does not parse
554    ///   as a token config. Caller should fall back to a raw display.
555    /// - `Err(_)`      — transport-level RPC error.
556    pub async fn fetch_remote_token_metadata(
557        &self,
558        faucet_id: AccountId,
559    ) -> Result<Option<FaucetMetadata>, ClientError> {
560        let proof = match self.rpc_api.get_account(faucet_id, GetAccountRequest::new()).await {
561            Ok((_, proof)) => proof,
562            Err(err) => match err.endpoint_error() {
563                Some(EndpointError::GetAccount(
564                    GetAccountError::AccountNotFound | GetAccountError::AccountNotPublic,
565                )) => return Ok(None),
566                _ => return Err(ClientError::RpcError(err)),
567            },
568        };
569
570        let Some(storage_header) = proof.storage_header() else {
571            return Ok(None);
572        };
573
574        let Some(slot_header) =
575            storage_header.find_slot_header_by_name(FungibleFaucet::token_config_slot())
576        else {
577            return Ok(None);
578        };
579
580        Ok(faucet_metadata_from_token_config(*slot_header.value()))
581    }
582
583    /// Adds an [`Address`] to the associated [`AccountId`], alongside its derived [`NoteTag`]. If
584    /// the account is tracked as watched, the note tag is not registered.
585    ///
586    /// # Errors
587    /// - If the account is not found on the network.
588    /// - If the address is already being tracked.
589    pub async fn add_address(
590        &mut self,
591        address: Address,
592        account_id: AccountId,
593    ) -> Result<(), ClientError> {
594        let network_id = self.rpc_api.get_network_id().await?;
595        let address_bench32 = address.encode(network_id);
596        if self.store.get_addresses_by_account_id(account_id).await?.contains(&address) {
597            return Err(ClientError::AddressAlreadyTracked(address_bench32));
598        }
599
600        let tracked_account = self.store.get_minimal_partial_account(account_id).await?;
601        match tracked_account {
602            None => Err(ClientError::AccountDataNotFound(account_id)),
603            Some(tracked_account) => {
604                self.store.insert_address(address.clone(), account_id).await?;
605                // Watched accounts intentionally have no derived note tag registered to avoid sync
606                // state pulling notes for them.
607                if !tracked_account.is_watched() {
608                    let derived_note_tag: NoteTag = address.to_note_tag();
609                    let note_tag_record =
610                        NoteTagRecord::with_account_source(derived_note_tag, account_id);
611                    self.store.add_note_tag(note_tag_record).await?;
612                }
613                Ok(())
614            },
615        }
616    }
617
618    /// Removes an [`Address`] from the associated [`AccountId`], alongside its derived [`NoteTag`].
619    ///
620    /// Returns `true` if the address was tracked. If it wasn't, this is a no-op: the derived tag is
621    /// left in place, since it may have been registered by something other than this address.
622    pub async fn remove_address(
623        &mut self,
624        address: Address,
625        account_id: AccountId,
626    ) -> Result<bool, ClientError> {
627        let derived_note_tag = address.to_note_tag();
628        let note_tag_record = NoteTagRecord::with_account_source(derived_note_tag, account_id);
629        if !self.store.remove_address(address).await? {
630            return Ok(false);
631        }
632        // Remove the note tag if no other address are associated with it.
633        let addresses = self.store.get_addresses_by_account_id(account_id).await?;
634        if addresses.iter().all(|address| address.to_note_tag() != derived_note_tag) {
635            self.store.remove_note_tag(note_tag_record).await?;
636        }
637        Ok(true)
638    }
639
640    // ACCOUNT DATA RETRIEVAL
641    // --------------------------------------------------------------------------------------------
642
643    /// Retrieves the asset vault for a specific account.
644    ///
645    /// To check the balance for a single asset, use [`Client::account_reader`] instead.
646    pub async fn get_account_vault(
647        &self,
648        account_id: AccountId,
649    ) -> Result<AssetVault, ClientError> {
650        self.store.get_account_vault(account_id).await.map_err(ClientError::StoreError)
651    }
652
653    /// Retrieves the whole account storage for a specific account.
654    ///
655    /// To only load a specific slot, use [`Client::account_reader`] instead.
656    pub async fn get_account_storage(
657        &self,
658        account_id: AccountId,
659    ) -> Result<AccountStorage, ClientError> {
660        self.store
661            .get_account_storage(account_id, AccountStorageFilter::All)
662            .await
663            .map_err(ClientError::StoreError)
664    }
665
666    /// Retrieves the account code for a specific account.
667    ///
668    /// Returns `None` if the account is not found.
669    pub async fn get_account_code(
670        &self,
671        account_id: AccountId,
672    ) -> Result<Option<AccountCode>, ClientError> {
673        self.store.get_account_code(account_id).await.map_err(ClientError::StoreError)
674    }
675
676    /// Returns a list of [`AccountHeader`] of all accounts stored in the database along with their
677    /// statuses.
678    ///
679    /// Said accounts' state is the state after the last performed sync.
680    pub async fn get_account_headers(
681        &self,
682    ) -> Result<Vec<(AccountHeader, AccountStatus)>, ClientError> {
683        self.store.get_account_headers().await.map_err(Into::into)
684    }
685
686    /// Returns the [`AccountHeader`] of the account with the specified ID along with its status, or
687    /// `None` if the account isn't tracked by the client.
688    ///
689    /// Said account's state is the state after the last performed sync.
690    pub async fn get_account_header(
691        &self,
692        account_id: AccountId,
693    ) -> Result<Option<(AccountHeader, AccountStatus)>, ClientError> {
694        self.store.get_account_header(account_id).await.map_err(Into::into)
695    }
696
697    /// Retrieves the full [`Account`] object from the store, returning `None` if not found.
698    ///
699    /// This method loads the complete account state including vault, storage, and code — including
700    /// building the vault's Merkle tree. For lazy access that fetches only the data you need
701    /// (existence checks, single fields, storage items), use [`Client::account_reader`] instead.
702    pub async fn get_account(&self, account_id: AccountId) -> Result<Option<Account>, ClientError> {
703        match self.store.get_account(account_id).await? {
704            Some(record) => Ok(Some(record.try_into()?)),
705            None => Ok(None),
706        }
707    }
708
709    /// Creates an [`AccountReader`] for lazy access to account data.
710    ///
711    /// The `AccountReader` provides lazy access to account state - each method call fetches fresh
712    /// data from storage, ensuring you always see the current state.
713    ///
714    /// For loading the full [`Account`] object, use [`Client::get_account`] instead.
715    ///
716    /// # Example
717    /// ```ignore
718    /// let reader = client.account_reader(account_id);
719    ///
720    /// // Each call fetches fresh data
721    /// let nonce = reader.nonce().await?;
722    /// let balance = reader.get_balance(faucet_id).await?;
723    ///
724    /// // Storage access is integrated
725    /// let value = reader.get_storage_item("my_slot").await?;
726    /// let (map_value, witness) = reader.get_storage_map_witness("balances", key).await?;
727    /// ```
728    pub fn account_reader(&self, account_id: AccountId) -> AccountReader {
729        AccountReader::new(self.store.clone(), account_id)
730    }
731
732    /// Prunes historical account states for the specified account up to the given nonce.
733    ///
734    /// Deletes all historical entries with `replaced_at_nonce <= up_to_nonce` and any orphaned
735    /// account code.
736    ///
737    /// Returns the total number of rows deleted, including historical entries and orphaned account
738    /// code.
739    pub async fn prune_account_history(
740        &self,
741        account_id: AccountId,
742        up_to_nonce: Felt,
743    ) -> Result<usize, ClientError> {
744        Ok(self.store.prune_account_history(account_id, up_to_nonce).await?)
745    }
746}
747
748// UTILITY FUNCTIONS
749// ================================================================================================
750
751/// Builds an regular account ID from the provided parameters. The ID may be used along
752/// `Client::import_account_by_id` to import a public account from the network (provided that the
753/// used seed is known).
754///
755/// This function currently supports accounts composed of the [`BasicWallet`] component and one of
756/// the supported authentication schemes ([`AuthSingleSig`]).
757///
758/// # Arguments
759/// - `init_seed`: Initial seed used to create the account. This is the seed passed to
760///   [`AccountBuilder::new`].
761/// - `public_key`: Public key of the account used for the authentication component.
762/// - `account_visibility`: Public/private visibility of the account.
763///
764/// # Errors
765/// - If the account cannot be built.
766pub fn build_wallet_id(
767    init_seed: [u8; 32],
768    public_key: &PublicKey,
769    account_visibility: AccountType,
770) -> Result<AccountId, ClientError> {
771    let auth_scheme = public_key.auth_scheme();
772    let auth_component: AccountComponent =
773        AuthSingleSig::new(Approver::new(public_key.to_commitment(), auth_scheme)).into();
774
775    let account = AccountBuilder::new(init_seed)
776        .account_type(account_visibility)
777        .with_component(auth_component)
778        .with_component(BasicWallet)
779        .build_with_schema_commitment()?;
780
781    Ok(account.id())
782}
783
784#[cfg(test)]
785mod schema_commitment_tests {
786    use miden_protocol::EMPTY_WORD;
787    use miden_protocol::account::auth::AuthSecretKey;
788    use miden_standards::account::inspection::AccountSchemaCommitment;
789
790    use super::{
791        AccountBuilder,
792        AccountBuilderSchemaCommitmentExt,
793        AccountType,
794        Approver,
795        AuthSingleSig,
796        BasicWallet,
797    };
798    use crate::auth::AuthSchemeId;
799
800    #[test]
801    fn wallet_build_includes_schema_commitment_metadata_slot() {
802        let key = AuthSecretKey::new_falcon512_poseidon2();
803        let account = AccountBuilder::new([2u8; 32])
804            .account_type(AccountType::Private)
805            .with_component(AuthSingleSig::new(Approver::new(
806                key.public_key().to_commitment(),
807                AuthSchemeId::Falcon512Poseidon2,
808            )))
809            .with_component(BasicWallet)
810            .build_with_schema_commitment()
811            .expect("build_with_schema_commitment");
812
813        let commitment = account
814            .storage()
815            .get_item(AccountSchemaCommitment::schema_commitment_slot())
816            .expect("schema commitment slot");
817        assert_ne!(commitment, EMPTY_WORD);
818    }
819}
820
821#[cfg(test)]
822mod faucet_metadata_tests {
823    use miden_protocol::Felt;
824
825    use super::{FungibleFaucet, TokenSymbol, faucet_metadata_from_token_config};
826
827    /// Builds a token config slot value carrying the given decimals and the symbol "TST".
828    fn token_config(decimals: u32) -> [Felt; 4] {
829        [
830            Felt::from(0u32),
831            Felt::from(0u32),
832            Felt::from(decimals),
833            TokenSymbol::new("TST").unwrap().as_element(),
834        ]
835    }
836
837    #[test]
838    fn decodes_a_config_within_the_protocol_bounds() {
839        let metadata = faucet_metadata_from_token_config(token_config(8)).unwrap();
840
841        assert_eq!(metadata.symbol, "TST");
842        assert_eq!(metadata.decimals, 8);
843    }
844
845    #[test]
846    fn accepts_the_maximum_supported_decimals() {
847        let max = u32::from(FungibleFaucet::MAX_DECIMALS);
848        let metadata = faucet_metadata_from_token_config(token_config(max)).unwrap();
849
850        assert_eq!(metadata.decimals, FungibleFaucet::MAX_DECIMALS);
851    }
852
853    #[test]
854    fn rejects_decimals_above_the_maximum() {
855        let above_max = u32::from(FungibleFaucet::MAX_DECIMALS) + 1;
856
857        assert!(faucet_metadata_from_token_config(token_config(above_max)).is_none());
858        assert!(faucet_metadata_from_token_config(token_config(200)).is_none());
859    }
860
861    #[test]
862    fn rejects_decimals_that_do_not_fit_a_u8() {
863        assert!(faucet_metadata_from_token_config(token_config(300)).is_none());
864    }
865
866    #[test]
867    fn rejects_a_symbol_that_is_not_a_token_symbol() {
868        let mut config = token_config(8);
869        config[3] = Felt::from(0u32);
870
871        assert!(faucet_metadata_from_token_config(config).is_none());
872    }
873}