Skip to main content

miden_client/account/
mod.rs

1//! The `account` module provides types and client APIs for managing accounts within the Miden
2//! network.
3//!
4//! Accounts are foundational entities of the Miden protocol. They store assets and define
5//! rules for manipulating them. Once an account is registered with the client, its state will
6//! be updated accordingly, and validated against the network state on every sync.
7//!
8//! # Example
9//!
10//! To add a new account to the client's store, you might use the [`Client::add_account`] method as
11//! follows:
12//!
13//! ```rust
14//! # use miden_client::{
15//! #   account::{Account, AccountBuilder, AccountType, component::BasicWallet},
16//! #   crypto::FeltRng
17//! # };
18//! # use miden_protocol::account::AccountStorageMode;
19//! # async fn add_new_account_example<AUTH>(
20//! #     client: &mut miden_client::Client<AUTH>
21//! # ) -> Result<(), miden_client::ClientError> {
22//! #   let random_seed = Default::default();
23//! let account = AccountBuilder::new(random_seed)
24//!     .account_type(AccountType::RegularAccountImmutableCode)
25//!     .storage_mode(AccountStorageMode::Private)
26//!     .with_component(BasicWallet)
27//!     .build()?;
28//!
29//! // Add the account to the client. The account already embeds its seed information.
30//! client.add_account(&account, false).await?;
31//! #   Ok(())
32//! # }
33//! ```
34//!
35//! For more details on accounts, refer to the [Account] documentation.
36
37use alloc::vec::Vec;
38
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    AccountFile,
49    AccountHeader,
50    AccountId,
51    AccountIdPrefix,
52    AccountStorage,
53    AccountStorageMode,
54    AccountType,
55    PartialAccount,
56    PartialStorage,
57    PartialStorageMap,
58    StorageMap,
59    StorageMapKey,
60    StorageMapWitness,
61    StorageSlot,
62    StorageSlotContent,
63    StorageSlotId,
64    StorageSlotName,
65    StorageSlotType,
66};
67pub use miden_protocol::address::{Address, AddressInterface, AddressType, NetworkId};
68use miden_protocol::asset::AssetVault;
69pub use miden_protocol::errors::{AccountIdError, AddressError, NetworkIdError};
70use miden_protocol::note::NoteTag;
71
72mod account_reader;
73pub use account_reader::AccountReader;
74use miden_standards::account::auth::AuthSingleSig;
75// RE-EXPORTS
76// ================================================================================================
77pub use miden_standards::account::interface::AccountInterfaceExt;
78use miden_standards::account::wallets::BasicWallet;
79
80use super::Client;
81use crate::errors::ClientError;
82use crate::rpc::domain::account::FetchedAccount;
83use crate::rpc::node::{EndpointError, GetAccountError};
84use crate::store::{AccountStatus, AccountStorageFilter};
85use crate::sync::NoteTagRecord;
86
87pub mod component {
88    pub const MIDEN_PACKAGE_EXTENSION: &str = "masp";
89
90    pub use miden_protocol::account::auth::*;
91    pub use miden_protocol::account::component::{
92        FeltSchema,
93        InitStorageData,
94        SchemaType,
95        StorageSchema,
96        StorageSlotSchema,
97        StorageValueName,
98    };
99    pub use miden_protocol::account::{AccountComponent, AccountComponentMetadata};
100    pub use miden_standards::account::auth::*;
101    pub use miden_standards::account::components::{
102        basic_fungible_faucet_library,
103        basic_wallet_library,
104        multisig_library,
105        network_fungible_faucet_library,
106        no_auth_library,
107        singlesig_acl_library,
108        singlesig_library,
109    };
110    pub use miden_standards::account::faucets::{BasicFungibleFaucet, NetworkFungibleFaucet};
111    pub use miden_standards::account::mint_policies::{
112        AuthControlled,
113        AuthControlledInitConfig,
114        OwnerControlled,
115        OwnerControlledInitConfig,
116    };
117    pub use miden_standards::account::wallets::BasicWallet;
118}
119
120// CLIENT METHODS
121// ================================================================================================
122
123/// This section of the [Client] contains methods for:
124///
125/// - **Account creation:** Use the [`AccountBuilder`] to construct new accounts, specifying account
126///   type, storage mode (public/private), and attaching necessary components (e.g., basic wallet or
127///   fungible faucet). After creation, they can be added to the client.
128///
129/// - **Account tracking:** Accounts added via the client are persisted to the local store, where
130///   their state (including nonce, balance, and metadata) is updated upon every synchronization
131///   with the network.
132///
133/// - **Data retrieval:** The module also provides methods to fetch account-related data.
134impl<AUTH> Client<AUTH> {
135    // ACCOUNT CREATION
136    // --------------------------------------------------------------------------------------------
137
138    /// Adds the provided [Account] in the store so it can start being tracked by the client.
139    ///
140    /// If the account is already being tracked and `overwrite` is set to `true`, the account will
141    /// be overwritten. Newly created accounts must embed their seed (`account.seed()` must return
142    /// `Some(_)`).
143    ///
144    /// # Errors
145    ///
146    /// - If the account is new but it does not contain the seed.
147    /// - If the account is already tracked and `overwrite` is set to `false`.
148    /// - If `overwrite` is set to `true` and the `account_data` nonce is lower than the one already
149    ///   being tracked.
150    /// - If `overwrite` is set to `true` and the `account_data` commitment doesn't match the
151    ///   network's account commitment.
152    /// - If the client has reached the accounts limit.
153    /// - If the client has reached the note tags limit.
154    pub async fn add_account(
155        &mut self,
156        account: &Account,
157        overwrite: bool,
158    ) -> Result<(), ClientError> {
159        if account.is_new() {
160            if account.seed().is_none() {
161                return Err(ClientError::AddNewAccountWithoutSeed);
162            }
163        } else {
164            // Ignore the seed since it's not a new account
165            if account.seed().is_some() {
166                tracing::warn!(
167                    "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."
168                );
169            }
170        }
171
172        let tracked_account = self.store.get_account(account.id()).await?;
173
174        match tracked_account {
175            None => {
176                // Check limits since it's a non-tracked account
177                self.check_account_limit().await?;
178                self.check_note_tag_limit().await?;
179
180                let default_address = Address::new(account.id());
181
182                // If the account is not being tracked, insert it into the store regardless of the
183                // `overwrite` flag
184                let default_address_note_tag = default_address.to_note_tag();
185                let note_tag_record =
186                    NoteTagRecord::with_account_source(default_address_note_tag, account.id());
187                self.store.add_note_tag(note_tag_record).await?;
188
189                self.store
190                    .insert_account(account, default_address)
191                    .await
192                    .map_err(ClientError::StoreError)
193            },
194            Some(tracked_account) => {
195                if !overwrite {
196                    // Only overwrite the account if the flag is set to `true`
197                    return Err(ClientError::AccountAlreadyTracked(account.id()));
198                }
199
200                if tracked_account.nonce().as_canonical_u64() > account.nonce().as_canonical_u64() {
201                    // If the new account is older than the one being tracked, return an error
202                    return Err(ClientError::AccountNonceTooLow);
203                }
204
205                if tracked_account.is_locked() {
206                    // If the tracked account is locked, check that the account commitment matches
207                    // the one in the network
208                    let network_account_commitment =
209                        self.rpc_api.get_account_details(account.id()).await?.commitment();
210                    if network_account_commitment != account.to_commitment() {
211                        return Err(ClientError::AccountCommitmentMismatch(
212                            network_account_commitment,
213                        ));
214                    }
215                }
216
217                self.store.update_account(account).await.map_err(ClientError::StoreError)
218            },
219        }
220    }
221
222    /// Imports an account from the network to the client's store. The account needs to be public
223    /// and be tracked by the network, it will be fetched by its ID. If the account was already
224    /// being tracked by the client, it's state will be overwritten.
225    ///
226    /// # Errors
227    /// - If the account is not found on the network.
228    /// - If the account is private.
229    /// - There was an error sending the request to the network.
230    pub async fn import_account_by_id(&mut self, account_id: AccountId) -> Result<(), ClientError> {
231        let fetched_account =
232            self.rpc_api.get_account_details(account_id).await.map_err(|err| {
233                match err.endpoint_error() {
234                    Some(EndpointError::GetAccount(GetAccountError::AccountNotFound)) => {
235                        ClientError::AccountNotFoundOnChain(account_id)
236                    },
237                    _ => ClientError::RpcError(err),
238                }
239            })?;
240
241        let account = match fetched_account {
242            FetchedAccount::Private(..) => {
243                return Err(ClientError::AccountIsPrivate(account_id));
244            },
245            FetchedAccount::Public(account, ..) => *account,
246        };
247
248        self.add_account(&account, true).await
249    }
250
251    /// Adds an [`Address`] to the associated [`AccountId`], alongside its derived [`NoteTag`].
252    ///
253    /// # Errors
254    /// - If the account is not found on the network.
255    /// - If the address is already being tracked.
256    /// - If the client has reached the note tags limit.
257    pub async fn add_address(
258        &mut self,
259        address: Address,
260        account_id: AccountId,
261    ) -> Result<(), ClientError> {
262        let network_id = self.rpc_api.get_network_id().await?;
263        let address_bench32 = address.encode(network_id);
264        if self.store.get_addresses_by_account_id(account_id).await?.contains(&address) {
265            return Err(ClientError::AddressAlreadyTracked(address_bench32));
266        }
267
268        let tracked_account = self.store.get_account(account_id).await?;
269        match tracked_account {
270            None => Err(ClientError::AccountDataNotFound(account_id)),
271            Some(_tracked_account) => {
272                // Check that the Address is not already tracked
273                let derived_note_tag: NoteTag = address.to_note_tag();
274                let note_tag_record =
275                    NoteTagRecord::with_account_source(derived_note_tag, account_id);
276                if self.store.get_note_tags().await?.contains(&note_tag_record) {
277                    return Err(ClientError::NoteTagDerivedAddressAlreadyTracked(
278                        address_bench32,
279                        derived_note_tag,
280                    ));
281                }
282
283                self.check_note_tag_limit().await?;
284                self.store.insert_address(address, account_id).await?;
285                Ok(())
286            },
287        }
288    }
289
290    /// Removes an [`Address`] from the associated [`AccountId`], alongside its derived [`NoteTag`].
291    ///
292    /// # Errors
293    /// - If the account is not found on the network.
294    /// - If the address is not being tracked.
295    pub async fn remove_address(
296        &mut self,
297        address: Address,
298        account_id: AccountId,
299    ) -> Result<(), ClientError> {
300        self.store.remove_address(address, account_id).await?;
301        Ok(())
302    }
303
304    // ACCOUNT DATA RETRIEVAL
305    // --------------------------------------------------------------------------------------------
306
307    /// Retrieves the asset vault for a specific account.
308    ///
309    /// To check the balance for a single asset, use [`Client::account_reader`] instead.
310    pub async fn get_account_vault(
311        &self,
312        account_id: AccountId,
313    ) -> Result<AssetVault, ClientError> {
314        self.store.get_account_vault(account_id).await.map_err(ClientError::StoreError)
315    }
316
317    /// Retrieves the whole account storage for a specific account.
318    ///
319    /// To only load a specific slot, use [`Client::account_reader`] instead.
320    pub async fn get_account_storage(
321        &self,
322        account_id: AccountId,
323    ) -> Result<AccountStorage, ClientError> {
324        self.store
325            .get_account_storage(account_id, AccountStorageFilter::All)
326            .await
327            .map_err(ClientError::StoreError)
328    }
329
330    /// Retrieves the account code for a specific account.
331    ///
332    /// Returns `None` if the account is not found.
333    pub async fn get_account_code(
334        &self,
335        account_id: AccountId,
336    ) -> Result<Option<AccountCode>, ClientError> {
337        self.store.get_account_code(account_id).await.map_err(ClientError::StoreError)
338    }
339
340    /// Returns a list of [`AccountHeader`] of all accounts stored in the database along with their
341    /// statuses.
342    ///
343    /// Said accounts' state is the state after the last performed sync.
344    pub async fn get_account_headers(
345        &self,
346    ) -> Result<Vec<(AccountHeader, AccountStatus)>, ClientError> {
347        self.store.get_account_headers().await.map_err(Into::into)
348    }
349
350    /// Retrieves the full [`Account`] object from the store, returning `None` if not found.
351    ///
352    /// This method loads the complete account state including vault, storage, and code.
353    ///
354    /// For lazy access that fetches only the data you need, use
355    /// [`Client::account_reader`] instead.
356    ///
357    /// Use [`Client::try_get_account`] if you want to error when the account is not found.
358    pub async fn get_account(&self, account_id: AccountId) -> Result<Option<Account>, ClientError> {
359        match self.store.get_account(account_id).await? {
360            Some(record) => Ok(Some(record.try_into()?)),
361            None => Ok(None),
362        }
363    }
364
365    /// Retrieves the full [`Account`] object from the store, erroring if not found.
366    ///
367    /// This method loads the complete account state including vault, storage, and code.
368    ///
369    /// Use [`Client::get_account`] if you want to handle missing accounts gracefully.
370    pub async fn try_get_account(&self, account_id: AccountId) -> Result<Account, ClientError> {
371        self.get_account(account_id)
372            .await?
373            .ok_or(ClientError::AccountDataNotFound(account_id))
374    }
375
376    /// Creates an [`AccountReader`] for lazy access to account data.
377    ///
378    /// The `AccountReader` provides lazy access to account state - each method call
379    /// fetches fresh data from storage, ensuring you always see the current state.
380    ///
381    /// For loading the full [`Account`] object, use [`Client::get_account`] instead.
382    ///
383    /// # Example
384    /// ```ignore
385    /// let reader = client.account_reader(account_id);
386    ///
387    /// // Each call fetches fresh data
388    /// let nonce = reader.nonce().await?;
389    /// let balance = reader.get_balance(faucet_id).await?;
390    ///
391    /// // Storage access is integrated
392    /// let value = reader.get_storage_item("my_slot").await?;
393    /// let (map_value, witness) = reader.get_storage_map_witness("balances", key).await?;
394    /// ```
395    pub fn account_reader(&self, account_id: AccountId) -> AccountReader {
396        AccountReader::new(self.store.clone(), account_id)
397    }
398
399    /// Prunes historical account states for the specified account up to the given nonce.
400    ///
401    /// Deletes all historical entries with `replaced_at_nonce <= up_to_nonce` and any
402    /// orphaned account code.
403    ///
404    /// Returns the total number of rows deleted, including historical entries and orphaned
405    /// account code.
406    pub async fn prune_account_history(
407        &self,
408        account_id: AccountId,
409        up_to_nonce: Felt,
410    ) -> Result<usize, ClientError> {
411        Ok(self.store.prune_account_history(account_id, up_to_nonce).await?)
412    }
413}
414
415// UTILITY FUNCTIONS
416// ================================================================================================
417
418/// Builds an regular account ID from the provided parameters. The ID may be used along
419/// `Client::import_account_by_id` to import a public account from the network (provided that the
420/// used seed is known).
421///
422/// This function currently supports accounts composed of the [`BasicWallet`] component and one of
423/// the supported authentication schemes ([`AuthSingleSig`]).
424///
425/// # Arguments
426/// - `init_seed`: Initial seed used to create the account. This is the seed passed to
427///   [`AccountBuilder::new`].
428/// - `public_key`: Public key of the account used for the authentication component.
429/// - `storage_mode`: Storage mode of the account.
430/// - `is_mutable`: Whether the account is mutable or not.
431///
432/// # Errors
433/// - If the account cannot be built.
434pub fn build_wallet_id(
435    init_seed: [u8; 32],
436    public_key: &PublicKey,
437    storage_mode: AccountStorageMode,
438    is_mutable: bool,
439) -> Result<AccountId, ClientError> {
440    let account_type = if is_mutable {
441        AccountType::RegularAccountUpdatableCode
442    } else {
443        AccountType::RegularAccountImmutableCode
444    };
445
446    let auth_scheme = public_key.auth_scheme();
447    let auth_component: AccountComponent =
448        AuthSingleSig::new(public_key.to_commitment(), auth_scheme).into();
449
450    let account = AccountBuilder::new(init_seed)
451        .account_type(account_type)
452        .storage_mode(storage_mode)
453        .with_auth_component(auth_component)
454        .with_component(BasicWallet)
455        .build()?;
456
457    Ok(account.id())
458}