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    AccountCodePatch,
46    AccountCodeUpgrade,
47    AccountComponent,
48    AccountComponentCode,
49    AccountDelta,
50    AccountHeader,
51    AccountId,
52    AccountIdPrefix,
53    AccountIdPrefixV1,
54    AccountIdV1,
55    AccountIdVersion,
56    AccountPatch,
57    AccountProcedureRoot,
58    AccountStorage,
59    AccountStoragePatch,
60    AccountType,
61    AccountUpdateDetails,
62    AccountVaultPatch,
63    PartialAccount,
64    PartialStorage,
65    PartialStorageMap,
66    RoleSymbol,
67    StorageMap,
68    StorageMapKey,
69    StorageMapKeyHash,
70    StorageMapPatch,
71    StorageMapPatchEntries,
72    StorageMapWitness,
73    StorageSlot,
74    StorageSlotContent,
75    StorageSlotId,
76    StorageSlotName,
77    StorageSlotPatch,
78    StorageSlotType,
79    StorageValuePatch,
80};
81pub use miden_protocol::address::{Address, AddressInterface, AddressType, NetworkId};
82use miden_protocol::asset::AssetVault;
83pub use miden_protocol::errors::{AccountIdError, AddressError, NetworkIdError};
84use miden_protocol::note::NoteTag;
85use miden_tx::utils::serde::{
86    ByteReader,
87    ByteWriter,
88    Deserializable,
89    DeserializationError,
90    Serializable,
91};
92
93/// Display-only metadata for a faucet account, persisted in the client's settings store.
94///
95/// Populated lazily by the CLI resolver from the on-chain token config of a public faucet and
96/// persisted under a `faucet_metadata:<faucet-id>` key.
97#[derive(Debug, Clone, PartialEq, Eq)]
98pub struct FaucetMetadata {
99    pub symbol: String,
100    pub decimals: u8,
101}
102
103impl Serializable for FaucetMetadata {
104    fn write_into<W: ByteWriter>(&self, target: &mut W) {
105        self.symbol.write_into(target);
106        target.write_u8(self.decimals);
107    }
108}
109
110impl Deserializable for FaucetMetadata {
111    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
112        let symbol = String::read_from(source)?;
113        let decimals = source.read_u8()?;
114        Ok(Self { symbol, decimals })
115    }
116}
117
118/// Decodes a fungible faucet token config slot value into display metadata.
119///
120/// Returns `None` when the value does not describe a fungible faucet config the protocol would
121/// accept: the symbol must decode as a [`TokenSymbol`], and the decimals must be within
122/// [`FungibleFaucet::MAX_DECIMALS`], which is what [`FungibleFaucet`] enforces when the component
123/// is built.
124fn faucet_metadata_from_token_config(token_config: [Felt; 4]) -> Option<FaucetMetadata> {
125    let [_token_supply, _max_supply, decimals, symbol] = token_config;
126
127    let symbol = TokenSymbol::try_from(symbol).ok()?;
128    let decimals = u8::try_from(decimals.as_canonical_u64()).ok()?;
129    if decimals > FungibleFaucet::MAX_DECIMALS {
130        return None;
131    }
132
133    Some(FaucetMetadata { symbol: symbol.to_string(), decimals })
134}
135
136mod account_reader;
137pub use account_reader::AccountReader;
138/// Raw access to `miden-standards` account modules for items not curated by `miden-client`.
139pub use miden_standards::account as standards;
140use miden_standards::account::auth::{Approver, AuthSingleSig, NetworkAccount};
141use miden_standards::account::faucets::FungibleFaucet;
142pub use miden_standards::account::inspection::{
143    AccountBuilderSchemaCommitmentExt,
144    AccountSchemaCommitment,
145};
146// RE-EXPORTS
147// ================================================================================================
148pub use miden_standards::account::interface::{
149    AccountComponentInterface,
150    AccountComponentInterfaceExt,
151    AccountInterface,
152    AccountInterfaceExt,
153};
154use miden_standards::account::wallets::BasicWallet;
155
156use super::Client;
157use crate::asset::TokenSymbol;
158use crate::errors::ClientError;
159use crate::rpc::domain::account::GetAccountRequest;
160use crate::rpc::node::{EndpointError, GetAccountError};
161use crate::store::{AccountStatus, AccountStorageFilter, ClientAccountType};
162use crate::sync::NoteTagRecord;
163
164pub mod component {
165    pub const MIDEN_PACKAGE_EXTENSION: &str = "masp";
166
167    pub use miden_protocol::account::auth::*;
168    pub use miden_protocol::account::component::{
169        FeltSchema,
170        InitStorageData,
171        InitStorageDataError,
172        MapSlotSchema,
173        SchemaRequirement,
174        SchemaType,
175        SchemaTypeError,
176        StorageSchema,
177        StorageSlotSchema,
178        StorageValueName,
179        StorageValueNameError,
180        ValueSlotSchema,
181        WordSchema,
182        WordValue,
183    };
184    pub use miden_protocol::account::{
185        AccountComponent,
186        AccountComponentMetadata,
187        AccountComponentName,
188        AccountProcedureRoot,
189        RoleSymbol,
190    };
191    pub use miden_standards::account::access::{
192        AccessControl,
193        Authority,
194        AuthorityError,
195        Ownable2Step,
196        Ownable2StepError,
197        Pausable,
198        PausableManager,
199        PausableStorage,
200        RoleBasedAccessControl,
201    };
202    pub use miden_standards::account::auth::*;
203    pub use miden_standards::account::components::StandardAccountComponent;
204    pub use miden_standards::account::faucets::{
205        Description,
206        ExternalLink,
207        FungibleFaucet,
208        FungibleFaucetBuilder,
209        FungibleFaucetError,
210        LogoURI,
211        NonFungibleFaucet,
212        TokenMetadata,
213        TokenMetadataError,
214        TokenName,
215        create_network_fungible_faucet,
216        create_singlesig_user_fungible_faucet,
217    };
218    pub use miden_standards::account::fees::{
219        BasicConstantFeePolicy,
220        FeePolicy,
221        FeePolicyError,
222        FeePolicyManager,
223        FeePolicyManagerBuilder,
224    };
225    pub use miden_standards::account::policies::{
226        AllowlistManager,
227        AllowlistStorage,
228        BasicAllowlist,
229        BasicBlocklist,
230        BlocklistManager,
231        BlocklistStorage,
232        BurnAllowAll,
233        BurnOwnerOnly,
234        BurnPolicy,
235        BurnPolicyError,
236        MinBurnAmount,
237        MintAllowAll,
238        MintOwnerOnly,
239        MintPolicy,
240        MintPolicyError,
241        TokenPolicyManager,
242        TokenPolicyManagerBuilder,
243        TransferAllowAll,
244        TransferPolicy,
245        TransferPolicyError,
246    };
247    pub use miden_standards::account::upgrade::UpgradeManager;
248    pub use miden_standards::account::wallets::BasicWallet;
249}
250
251// CLIENT METHODS
252// ================================================================================================
253
254/// This section of the [Client] contains methods for:
255///
256/// - **Account creation:** Use the [`AccountBuilder`] to construct new accounts, specifying account
257///   visibility (`AccountType::Public` / `AccountType::Private`) and attaching necessary components
258///   (e.g., basic wallet or fungible faucet). Prefer
259///   [`AccountBuilderSchemaCommitmentExt::build_with_schema_commitment`] so the account includes
260///   merged storage schema commitment metadata; use plain [`AccountBuilder::build`] only when you
261///   need to opt out. After creation, accounts can be added to the client.
262///
263/// - **Account tracking:** Accounts added via the client are persisted to the local store, where
264///   their state (including nonce, balance, and metadata) is updated upon every synchronization
265///   with the network.
266///
267/// - **Account registration:** On a network that enforces an account allowlist,
268///   [`Client::register_account`] binds an invitation code to a new account before its first
269///   transaction creates it on chain, and [`Client::is_account_allowed`] asks whether the network
270///   accepts the creation of an account.
271///
272/// - **Data retrieval:** The module also provides methods to fetch account-related data.
273impl<AUTH> Client<AUTH> {
274    // ACCOUNT CREATION
275    // --------------------------------------------------------------------------------------------
276
277    /// Adds the provided [Account] in the store so it can start being tracked by the client.
278    ///
279    /// If the account is already being tracked and `overwrite` is set to `true`, the account will
280    /// be overwritten. Newly created accounts must embed their seed (`account.seed()` must return
281    /// `Some(_)`).
282    ///
283    /// # Errors
284    ///
285    /// - If the account is new but it does not contain the seed.
286    /// - If the account is already tracked and `overwrite` is set to `false`.
287    /// - If `overwrite` is set to `true` and the `account_data` nonce is lower than the one already
288    ///   being tracked.
289    /// - If `overwrite` is set to `true` and the `account_data` commitment doesn't match the
290    ///   network's account commitment.
291    pub async fn add_account(
292        &mut self,
293        account: &Account,
294        overwrite: bool,
295    ) -> Result<(), ClientError> {
296        self.add_account_inner(account, ClientAccountType::Native, overwrite).await
297    }
298
299    // ACCOUNT REGISTRATION
300    // --------------------------------------------------------------------------------------------
301
302    /// Binds an invitation code to a tracked account on the network allowlist.
303    ///
304    /// A network that enforces an account allowlist creates an account on chain only when the
305    /// account is registered. The first transaction of an account is what creates it, so the
306    /// account must be registered before that transaction is submitted.
307    /// [`Client::submit_new_transaction`] and [`BatchBuilder::submit`] ask the node first, and fail
308    /// with [`ClientError::AccountNotAllowlisted`] for an account the network does not accept. Only
309    /// account creation is gated: an account that already exists on chain is never checked, and
310    /// network accounts are exempt.
311    ///
312    /// The account must be tracked by the client, must not be deployed on chain yet, and must not
313    /// be a network account. The invitation code must exist on the node and must not be bound to
314    /// another account. A registration consumes the code, so the client asks the node first and
315    /// does not send the code for an account the node already allows.
316    ///
317    /// When the network operator runs a funding service, the node pays the registered account a
318    /// public P2ID note with the native asset. The node answers once the funding service queues the
319    /// note, before the note is committed. The note is not part of the response, and the client
320    /// does not see it until a [`Client::sync_state`] runs after the note is committed. The client
321    /// tracks the note tag of every account it owns, so that sync imports the note and
322    /// [`Client::get_consumable_notes`] lists it. Sync again until the note arrives. The account
323    /// then consumes the note in its first transaction. That transaction creates the account on
324    /// chain and pays its fee out of the received funds.
325    ///
326    /// # Errors
327    ///
328    /// - [`ClientError::AccountDataNotFound`] if the client does not track the account.
329    /// - [`ClientError::AccountIsNotNew`] if the account already exists on chain.
330    /// - [`ClientError::AccountIsNetworkAccount`] if the account is a network account. The node
331    ///   admits network accounts without a code.
332    /// - [`ClientError::AccountAlreadyAllowed`] if the node already allows the account, because it
333    ///   is registered or because the network does not enforce an allowlist. The code is not sent.
334    /// - [`ClientError::RpcError`] carrying a [`RegisterAccountError`] if the node rejects the
335    ///   code or the account, or an `Unavailable` status if the funding failed. In the second
336    ///   case the account stays registered, so a retry fails with
337    ///   [`ClientError::AccountAlreadyAllowed`] and the account has to be funded another way.
338    ///
339    /// [`BatchBuilder::submit`]: crate::transaction::BatchBuilder::submit
340    /// [`RegisterAccountError`]: crate::rpc::RegisterAccountError
341    pub async fn register_account(
342        &self,
343        account_id: AccountId,
344        invitation_code: &str,
345    ) -> Result<(), ClientError> {
346        let (_, status) = self
347            .store
348            .get_account_header(account_id)
349            .await?
350            .ok_or(ClientError::AccountDataNotFound(account_id))?;
351        if !status.is_new() {
352            return Err(ClientError::AccountIsNotNew(account_id));
353        }
354
355        let account = self
356            .get_account(account_id)
357            .await?
358            .ok_or(ClientError::AccountDataNotFound(account_id))?;
359        // The node admits a network account without a code.
360        if NetworkAccount::new(account).is_ok() {
361            return Err(ClientError::AccountIsNetworkAccount(account_id));
362        }
363        // A registration consumes the code, so do not send it when the node already allows the
364        // account.
365        if self.is_account_allowed(account_id).await? {
366            return Err(ClientError::AccountAlreadyAllowed(account_id));
367        }
368
369        self.rpc_api.register_account(invitation_code, account_id).await?;
370
371        Ok(())
372    }
373
374    /// Returns whether the network lets `account_id` be created on chain.
375    ///
376    /// The node answers `true` when it does not enforce an account allowlist, or when the account
377    /// is registered. See [`Client::register_account`] for how an account gets registered.
378    pub async fn is_account_allowed(&self, account_id: AccountId) -> Result<bool, ClientError> {
379        Ok(self.rpc_api.is_account_allowed(account_id).await?)
380    }
381
382    /// Returns whether a transaction against `account_id` creates an account that the network
383    /// allowlist gates.
384    ///
385    /// Only a new account is gated, and a network account is exempt. The answer is `false` for an
386    /// account that the client does not track.
387    pub(crate) async fn is_allowlist_gated(
388        &self,
389        account_id: AccountId,
390    ) -> Result<bool, ClientError> {
391        let Some((_, status)) = self.store.get_account_header(account_id).await? else {
392            return Ok(false);
393        };
394        if !status.is_new() {
395            return Ok(false);
396        }
397
398        let Some(account) = self.get_account(account_id).await? else {
399            return Ok(false);
400        };
401
402        Ok(NetworkAccount::new(account).is_err())
403    }
404
405    /// Inserts `account` into the store (or overwrites it if `overwrite` is true) and registers the
406    /// per-account note tag if `client_account_type` is [`ClientAccountType::Native`].
407    ///
408    /// Switching the [`ClientAccountType`] of an already-tracked account is not supported and
409    /// returns [`ClientError::AccountWatchedMismatch`].
410    async fn add_account_inner(
411        &mut self,
412        account: &Account,
413        client_account_type: ClientAccountType,
414        overwrite: bool,
415    ) -> Result<(), ClientError> {
416        if account.is_new() {
417            if account.seed().is_none() {
418                return Err(ClientError::AddNewAccountWithoutSeed);
419            }
420        } else {
421            // Ignore the seed since it's not a new account
422            if account.seed().is_some() {
423                tracing::warn!(
424                    "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."
425                );
426            }
427        }
428
429        let tracked_account = self.store.get_minimal_partial_account(account.id()).await?;
430
431        match tracked_account {
432            None => {
433                let default_address = Address::new(account.id());
434
435                self.store
436                    .insert_account(account, default_address.clone(), client_account_type)
437                    .await
438                    .map_err(ClientError::StoreError)?;
439
440                if matches!(client_account_type, ClientAccountType::Native) {
441                    // Set the default address note tag so sync pulls notes.
442                    let default_address_note_tag = default_address.to_note_tag();
443                    let note_tag_record =
444                        NoteTagRecord::with_account_source(default_address_note_tag, account.id());
445                    self.store.add_note_tag(note_tag_record).await?;
446                }
447
448                Ok(())
449            },
450            Some(tracked_account) => {
451                if !overwrite {
452                    // Only overwrite the account if the flag is set to `true`
453                    return Err(ClientError::AccountAlreadyTracked(account.id()));
454                }
455
456                if client_account_type != tracked_account.client_account_type() {
457                    // Switching between Watched and Native after the account is tracked is not
458                    // supported: the per-account note tag and any client-side state derived from
459                    // that mode are set up at insertion time and not migrated on the fly.
460                    return Err(ClientError::AccountWatchedMismatch(account.id()));
461                }
462
463                if tracked_account.nonce().as_canonical_u64() > account.nonce().as_canonical_u64() {
464                    // If the new account is older than the one being tracked, return an error
465                    return Err(ClientError::AccountNonceTooLow);
466                }
467
468                if tracked_account.is_locked() {
469                    // If the tracked account is locked, check that the account commitment matches
470                    // the one in the network
471                    let network_account_commitment = self
472                        .rpc_api
473                        .get_account(account.id(), GetAccountRequest::new())
474                        .await?
475                        .1
476                        .account_commitment();
477                    if network_account_commitment != account.to_commitment() {
478                        return Err(ClientError::AccountCommitmentMismatch(
479                            network_account_commitment,
480                        ));
481                    }
482                }
483
484                self.store.update_account(account).await?;
485
486                Ok(())
487            },
488        }
489    }
490
491    /// Imports an account from the network to the client's store. The account needs to be public
492    /// and be tracked by the network, it will be fetched by its ID. If the account was already
493    /// being tracked by the client, its state will be overwritten.
494    ///
495    /// To import an account as watched (state-tracking only, no note sync), use
496    /// [`Self::import_watched_account_by_id`] instead. Switching an already-tracked account between
497    /// Native and Watched is not supported.
498    ///
499    /// # Errors
500    /// - If the account is not found on the network.
501    /// - If the account is private.
502    /// - If the account is already tracked as watched.
503    /// - There was an error sending the request to the network.
504    pub async fn import_account_by_id(&mut self, account_id: AccountId) -> Result<(), ClientError> {
505        let account = self.fetch_public_account(account_id).await?;
506        self.add_account_inner(&account, ClientAccountType::Native, true).await
507    }
508
509    /// Starts watching an on-chain account ([`ClientAccountType::Watched`]).
510    ///
511    /// Like [`Self::import_account_by_id`], the account is fetched from the network by its ID.
512    /// Unlike `import_account_by_id`, the account is added without registering its derived note
513    /// tag: `sync_state` will keep the account's commitment, nonce and storage up to date but will
514    /// **not** pull notes targeted at it.
515    ///
516    /// If the account is already being tracked as watched its state is overwritten. Switching an
517    /// already-tracked native account to watched is not supported.
518    ///
519    /// # Errors
520    /// - If the account is not found on the network.
521    /// - If the account is private.
522    /// - If the account is already tracked as native.
523    /// - There was an error sending the request to the network.
524    pub async fn import_watched_account_by_id(
525        &mut self,
526        account_id: AccountId,
527    ) -> Result<(), ClientError> {
528        let account = self.fetch_public_account(account_id).await?;
529        self.add_account_inner(&account, ClientAccountType::Watched, true).await
530    }
531
532    // ACCOUNT WITNESS PREFETCHING
533    // --------------------------------------------------------------------------------------------
534
535    /// Registers an account whose account witness [`Client::sync_chain`] keeps up to date, so that
536    /// transactions using it as a foreign account resolve the witness locally. This trades one
537    /// request per transaction for one per sync.
538    ///
539    /// A [`ForeignAccount::Private`](crate::transaction::ForeignAccount) needs nothing else, since
540    /// the caller supplies the account data. A
541    /// [`ForeignAccount::Public`](crate::transaction::ForeignAccount) additionally has to be
542    /// tracked by this client, so that its code, storage and vault come from the store as well;
543    /// registering an untracked public account costs a request per sync and saves none.
544    ///
545    /// The witness is fetched by the next sync, not by this call. Transactions assume that a sync
546    /// ran after the account was registered.
547    ///
548    /// The account is not validated against the network here. The sync fails while a registered
549    /// account has no witness that the node can return, so an account that is not in the account
550    /// tree blocks the sync until it is unregistered.
551    ///
552    /// Registering an already registered account is a no-op and keeps any cached witness.
553    ///
554    /// Returns `true` if the account was not registered before this call.
555    pub async fn track_account_witness(&self, account_id: AccountId) -> Result<bool, ClientError> {
556        self.store.track_account_witness(account_id).await.map_err(Into::into)
557    }
558
559    /// Stops keeping the account's witness up to date and drops the cached one.
560    ///
561    /// Returns `true` if the account was registered. Transactions using it keep working, falling
562    /// back to fetching the witness from the node.
563    pub async fn untrack_account_witness(
564        &self,
565        account_id: AccountId,
566    ) -> Result<bool, ClientError> {
567        self.store.untrack_account_witness(account_id).await.map_err(Into::into)
568    }
569
570    /// Returns the IDs of every account registered via [`Client::track_account_witness`].
571    pub async fn tracked_account_witnesses(&self) -> Result<Vec<AccountId>, ClientError> {
572        self.store.tracked_account_witnesses().await.map_err(Into::into)
573    }
574
575    /// Fetches a public [`Account`] from the network, returning a typed error when the account
576    /// doesn't exist on chain or is private.
577    async fn fetch_public_account(&self, account_id: AccountId) -> Result<Account, ClientError> {
578        let fetched_account =
579            self.rpc_api.get_account_details(account_id).await.map_err(|err| {
580                match err.endpoint_error() {
581                    Some(EndpointError::GetAccount(GetAccountError::AccountNotFound)) => {
582                        ClientError::AccountNotFoundOnChain(account_id)
583                    },
584                    _ => ClientError::RpcError(err),
585                }
586            })?;
587
588        fetched_account.ok_or(ClientError::AccountIsPrivate(account_id))
589    }
590
591    /// Fetches a public faucet's display metadata from the network.
592    ///
593    /// Uses [`get_account`](crate::rpc::NodeRpcClient::get_account) with a minimal request so that
594    /// the node does not return vault data. The faucet's token config lives in a single value slot,
595    /// which is always present in the returned storage header.
596    ///
597    /// Returns:
598    /// - `Ok(Some(_))` — the account is public and its token config storage slot decoded.
599    /// - `Ok(None)`    — the account is private, not on chain, or the storage slot does not parse
600    ///   as a token config. Caller should fall back to a raw display.
601    /// - `Err(_)`      — transport-level RPC error.
602    pub async fn fetch_remote_token_metadata(
603        &self,
604        faucet_id: AccountId,
605    ) -> Result<Option<FaucetMetadata>, ClientError> {
606        let proof = match self.rpc_api.get_account(faucet_id, GetAccountRequest::new()).await {
607            Ok((_, proof)) => proof,
608            Err(err) => match err.endpoint_error() {
609                Some(EndpointError::GetAccount(
610                    GetAccountError::AccountNotFound | GetAccountError::AccountNotPublic,
611                )) => return Ok(None),
612                _ => return Err(ClientError::RpcError(err)),
613            },
614        };
615
616        let Some(storage_header) = proof.storage_header() else {
617            return Ok(None);
618        };
619
620        let Some(slot_header) =
621            storage_header.find_slot_header_by_name(FungibleFaucet::token_config_slot())
622        else {
623            return Ok(None);
624        };
625
626        Ok(faucet_metadata_from_token_config(*slot_header.value()))
627    }
628
629    /// Adds an [`Address`] to the associated [`AccountId`], alongside its derived [`NoteTag`]. If
630    /// the account is tracked as watched, the note tag is not registered.
631    ///
632    /// # Errors
633    /// - If the account is not found on the network.
634    /// - If the address is already being tracked.
635    pub async fn add_address(
636        &mut self,
637        address: Address,
638        account_id: AccountId,
639    ) -> Result<(), ClientError> {
640        let network_id = self.rpc_api.get_network_id().await?;
641        let address_bench32 = address.encode(network_id);
642        if self.store.get_addresses_by_account_id(account_id).await?.contains(&address) {
643            return Err(ClientError::AddressAlreadyTracked(address_bench32));
644        }
645
646        let tracked_account = self.store.get_minimal_partial_account(account_id).await?;
647        match tracked_account {
648            None => Err(ClientError::AccountDataNotFound(account_id)),
649            Some(tracked_account) => {
650                self.store.insert_address(address.clone(), account_id).await?;
651                // Watched accounts intentionally have no derived note tag registered to avoid sync
652                // state pulling notes for them.
653                if !tracked_account.is_watched() {
654                    let derived_note_tag: NoteTag = address.to_note_tag();
655                    let note_tag_record =
656                        NoteTagRecord::with_account_source(derived_note_tag, account_id);
657                    self.store.add_note_tag(note_tag_record).await?;
658                }
659                Ok(())
660            },
661        }
662    }
663
664    /// Removes an [`Address`] from the associated [`AccountId`], alongside its derived [`NoteTag`].
665    ///
666    /// Returns `true` if the address was tracked. If it wasn't, this is a no-op: the derived tag is
667    /// left in place, since it may have been registered by something other than this address.
668    pub async fn remove_address(
669        &mut self,
670        address: Address,
671        account_id: AccountId,
672    ) -> Result<bool, ClientError> {
673        let derived_note_tag = address.to_note_tag();
674        let note_tag_record = NoteTagRecord::with_account_source(derived_note_tag, account_id);
675        if !self.store.remove_address(address).await? {
676            return Ok(false);
677        }
678        // Remove the note tag if no other address are associated with it.
679        let addresses = self.store.get_addresses_by_account_id(account_id).await?;
680        if addresses.iter().all(|address| address.to_note_tag() != derived_note_tag) {
681            self.store.remove_note_tag(note_tag_record).await?;
682        }
683        Ok(true)
684    }
685
686    // ACCOUNT DATA RETRIEVAL
687    // --------------------------------------------------------------------------------------------
688
689    /// Retrieves the asset vault for a specific account.
690    ///
691    /// To check the balance for a single asset, use [`Client::account_reader`] instead.
692    pub async fn get_account_vault(
693        &self,
694        account_id: AccountId,
695    ) -> Result<AssetVault, ClientError> {
696        self.store.get_account_vault(account_id).await.map_err(ClientError::StoreError)
697    }
698
699    /// Retrieves the whole account storage for a specific account.
700    ///
701    /// To only load a specific slot, use [`Client::account_reader`] instead.
702    pub async fn get_account_storage(
703        &self,
704        account_id: AccountId,
705    ) -> Result<AccountStorage, ClientError> {
706        self.store
707            .get_account_storage(account_id, AccountStorageFilter::All)
708            .await
709            .map_err(ClientError::StoreError)
710    }
711
712    /// Retrieves the account code for a specific account.
713    ///
714    /// Returns `None` if the account is not found.
715    pub async fn get_account_code(
716        &self,
717        account_id: AccountId,
718    ) -> Result<Option<AccountCode>, ClientError> {
719        self.store.get_account_code(account_id).await.map_err(ClientError::StoreError)
720    }
721
722    /// Returns a list of [`AccountHeader`] of all accounts stored in the database along with their
723    /// statuses.
724    ///
725    /// Said accounts' state is the state after the last performed sync.
726    pub async fn get_account_headers(
727        &self,
728    ) -> Result<Vec<(AccountHeader, AccountStatus)>, ClientError> {
729        self.store.get_account_headers().await.map_err(Into::into)
730    }
731
732    /// Returns the [`AccountHeader`] of the account with the specified ID along with its status, or
733    /// `None` if the account isn't tracked by the client.
734    ///
735    /// Said account's state is the state after the last performed sync.
736    pub async fn get_account_header(
737        &self,
738        account_id: AccountId,
739    ) -> Result<Option<(AccountHeader, AccountStatus)>, ClientError> {
740        self.store.get_account_header(account_id).await.map_err(Into::into)
741    }
742
743    /// Retrieves the full [`Account`] object from the store, returning `None` if not found.
744    ///
745    /// This method loads the complete account state including vault, storage, and code — including
746    /// building the vault's Merkle tree. For lazy access that fetches only the data you need
747    /// (existence checks, single fields, storage items), use [`Client::account_reader`] instead.
748    pub async fn get_account(&self, account_id: AccountId) -> Result<Option<Account>, ClientError> {
749        match self.store.get_account(account_id).await? {
750            Some(record) => Ok(Some(record.try_into()?)),
751            None => Ok(None),
752        }
753    }
754
755    /// Creates an [`AccountReader`] for lazy access to account data.
756    ///
757    /// The `AccountReader` provides lazy access to account state - each method call fetches fresh
758    /// data from storage, ensuring you always see the current state.
759    ///
760    /// For loading the full [`Account`] object, use [`Client::get_account`] instead.
761    ///
762    /// # Example
763    /// ```ignore
764    /// let reader = client.account_reader(account_id);
765    ///
766    /// // Each call fetches fresh data
767    /// let nonce = reader.nonce().await?;
768    /// let balance = reader.get_balance(faucet_id).await?;
769    ///
770    /// // Storage access is integrated
771    /// let value = reader.get_storage_item("my_slot").await?;
772    /// let (map_value, witness) = reader.get_storage_map_witness("balances", key).await?;
773    /// ```
774    pub fn account_reader(&self, account_id: AccountId) -> AccountReader {
775        AccountReader::new(self.store.clone(), account_id)
776    }
777
778    /// Prunes historical account states for the specified account up to the given nonce.
779    ///
780    /// Deletes all historical entries with `replaced_at_nonce <= up_to_nonce` and any orphaned
781    /// account code.
782    ///
783    /// Returns the total number of rows deleted, including historical entries and orphaned account
784    /// code.
785    pub async fn prune_account_history(
786        &self,
787        account_id: AccountId,
788        up_to_nonce: Felt,
789    ) -> Result<usize, ClientError> {
790        Ok(self.store.prune_account_history(account_id, up_to_nonce).await?)
791    }
792}
793
794// UTILITY FUNCTIONS
795// ================================================================================================
796
797/// Builds an regular account ID from the provided parameters. The ID may be used along
798/// `Client::import_account_by_id` to import a public account from the network (provided that the
799/// used seed is known).
800///
801/// This function currently supports accounts composed of the [`BasicWallet`] component and one of
802/// the supported authentication schemes ([`AuthSingleSig`]).
803///
804/// # Arguments
805/// - `init_seed`: Initial seed used to create the account. This is the seed passed to
806///   [`AccountBuilder::new`].
807/// - `public_key`: Public key of the account used for the authentication component.
808/// - `account_visibility`: Public/private visibility of the account.
809///
810/// # Errors
811/// - If the account cannot be built.
812pub fn build_wallet_id(
813    init_seed: [u8; 32],
814    public_key: &PublicKey,
815    account_visibility: AccountType,
816) -> Result<AccountId, ClientError> {
817    let auth_scheme = public_key.auth_scheme();
818    let auth_component: AccountComponent =
819        AuthSingleSig::new(Approver::new(public_key.to_commitment(), auth_scheme)).into();
820
821    let account = AccountBuilder::new(init_seed)
822        .account_type(account_visibility)
823        .with_component(auth_component)
824        .with_component(BasicWallet)
825        .build_with_schema_commitment()?;
826
827    Ok(account.id())
828}
829
830#[cfg(test)]
831mod schema_commitment_tests {
832    use miden_protocol::EMPTY_WORD;
833    use miden_protocol::account::auth::AuthSecretKey;
834    use miden_standards::account::inspection::AccountSchemaCommitment;
835
836    use super::{
837        AccountBuilder,
838        AccountBuilderSchemaCommitmentExt,
839        AccountType,
840        Approver,
841        AuthSingleSig,
842        BasicWallet,
843    };
844    use crate::auth::AuthSchemeId;
845
846    #[test]
847    fn wallet_build_includes_schema_commitment_metadata_slot() {
848        let key = AuthSecretKey::new_falcon512_poseidon2();
849        let account = AccountBuilder::new([2u8; 32])
850            .account_type(AccountType::Private)
851            .with_component(AuthSingleSig::new(Approver::new(
852                key.public_key().to_commitment(),
853                AuthSchemeId::Falcon512Poseidon2,
854            )))
855            .with_component(BasicWallet)
856            .build_with_schema_commitment()
857            .expect("build_with_schema_commitment");
858
859        let commitment = account
860            .storage()
861            .get_item(AccountSchemaCommitment::schema_commitment_slot())
862            .expect("schema commitment slot");
863        assert_ne!(commitment, EMPTY_WORD);
864    }
865}
866
867#[cfg(test)]
868mod faucet_metadata_tests {
869    use miden_protocol::Felt;
870
871    use super::{FungibleFaucet, TokenSymbol, faucet_metadata_from_token_config};
872
873    /// Builds a token config slot value carrying the given decimals and the symbol "TST".
874    fn token_config(decimals: u32) -> [Felt; 4] {
875        [
876            Felt::from(0u32),
877            Felt::from(0u32),
878            Felt::from(decimals),
879            TokenSymbol::new("TST").unwrap().as_element(),
880        ]
881    }
882
883    #[test]
884    fn decodes_a_config_within_the_protocol_bounds() {
885        let metadata = faucet_metadata_from_token_config(token_config(8)).unwrap();
886
887        assert_eq!(metadata.symbol, "TST");
888        assert_eq!(metadata.decimals, 8);
889    }
890
891    #[test]
892    fn accepts_the_maximum_supported_decimals() {
893        let max = u32::from(FungibleFaucet::MAX_DECIMALS);
894        let metadata = faucet_metadata_from_token_config(token_config(max)).unwrap();
895
896        assert_eq!(metadata.decimals, FungibleFaucet::MAX_DECIMALS);
897    }
898
899    #[test]
900    fn rejects_decimals_above_the_maximum() {
901        let above_max = u32::from(FungibleFaucet::MAX_DECIMALS) + 1;
902
903        assert!(faucet_metadata_from_token_config(token_config(above_max)).is_none());
904        assert!(faucet_metadata_from_token_config(token_config(200)).is_none());
905    }
906
907    #[test]
908    fn rejects_decimals_that_do_not_fit_a_u8() {
909        assert!(faucet_metadata_from_token_config(token_config(300)).is_none());
910    }
911
912    #[test]
913    fn rejects_a_symbol_that_is_not_a_token_symbol() {
914        let mut config = token_config(8);
915        config[3] = Felt::from(0u32);
916
917        assert!(faucet_metadata_from_token_config(config).is_none());
918    }
919}