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