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_objects::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_lib::account::auth::{AuthEcdsaK256Keccak, AuthRpoFalcon512};
40use miden_lib::account::wallets::BasicWallet;
41use miden_objects::account::auth::PublicKey;
42use miden_objects::note::NoteTag;
43// RE-EXPORTS
44// ================================================================================================
45pub use miden_objects::{
46    AccountIdError,
47    AddressError,
48    NetworkIdError,
49    account::{
50        Account,
51        AccountBuilder,
52        AccountCode,
53        AccountComponent,
54        AccountDelta,
55        AccountFile,
56        AccountHeader,
57        AccountId,
58        AccountIdPrefix,
59        AccountStorage,
60        AccountStorageMode,
61        AccountType,
62        PartialAccount,
63        PartialStorage,
64        PartialStorageMap,
65        StorageMap,
66        StorageSlot,
67        StorageSlotType,
68    },
69    address::{Address, AddressInterface, AddressType, NetworkId},
70};
71
72use super::Client;
73use crate::errors::ClientError;
74use crate::rpc::domain::account::FetchedAccount;
75use crate::store::{AccountRecord, AccountStatus};
76use crate::sync::NoteTagRecord;
77
78pub mod component {
79    pub const MIDEN_PACKAGE_EXTENSION: &str = "masp";
80
81    pub use miden_lib::account::auth::*;
82    pub use miden_lib::account::components::{
83        basic_fungible_faucet_library,
84        basic_wallet_library,
85        ecdsa_k256_keccak_library,
86        network_fungible_faucet_library,
87        no_auth_library,
88        rpo_falcon_512_acl_library,
89        rpo_falcon_512_library,
90        rpo_falcon_512_multisig_library,
91    };
92    pub use miden_lib::account::faucets::{
93        BasicFungibleFaucet,
94        FungibleFaucetExt,
95        NetworkFungibleFaucet,
96    };
97    pub use miden_lib::account::wallets::BasicWallet;
98    pub use miden_objects::account::{
99        AccountComponent,
100        AccountComponentMetadata,
101        FeltRepresentation,
102        InitStorageData,
103        StorageEntry,
104        StorageValueName,
105        TemplateType,
106        WordRepresentation,
107    };
108}
109
110// CLIENT METHODS
111// ================================================================================================
112
113/// This section of the [Client] contains methods for:
114///
115/// - **Account creation:** Use the [`AccountBuilder`] to construct new accounts, specifying account
116///   type, storage mode (public/private), and attaching necessary components (e.g., basic wallet or
117///   fungible faucet). After creation, they can be added to the client.
118///
119/// - **Account tracking:** Accounts added via the client are persisted to the local store, where
120///   their state (including nonce, balance, and metadata) is updated upon every synchronization
121///   with the network.
122///
123/// - **Data retrieval:** The module also provides methods to fetch account-related data.
124impl<AUTH> Client<AUTH> {
125    // ACCOUNT CREATION
126    // --------------------------------------------------------------------------------------------
127
128    /// Adds the provided [Account] in the store so it can start being tracked by the client.
129    ///
130    /// If the account is already being tracked and `overwrite` is set to `true`, the account will
131    /// be overwritten. Newly created accounts must embed their seed (`account.seed()` must return
132    /// `Some(_)`).
133    ///
134    /// # Errors
135    ///
136    /// - If the account is new but it does not contain the seed.
137    /// - If the account is already tracked and `overwrite` is set to `false`.
138    /// - If `overwrite` is set to `true` and the `account_data` nonce is lower than the one already
139    ///   being tracked.
140    /// - If `overwrite` is set to `true` and the `account_data` commitment doesn't match the
141    ///   network's account commitment.
142    pub async fn add_account(
143        &mut self,
144        account: &Account,
145        overwrite: bool,
146    ) -> Result<(), ClientError> {
147        if account.is_new() {
148            if account.seed().is_none() {
149                return Err(ClientError::AddNewAccountWithoutSeed);
150            }
151        } else {
152            // Ignore the seed since it's not a new account
153
154            // TODO: The alternative approach to this is to store the seed anyway, but
155            // ignore it at the point of executing against this transaction, but that
156            // approach seems a little bit more incorrect
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                let default_address = Address::new(account.id());
169
170                // If the account is not being tracked, insert it into the store regardless of the
171                // `overwrite` flag
172                let default_address_note_tag = default_address.to_note_tag();
173                let note_tag_record =
174                    NoteTagRecord::with_account_source(default_address_note_tag, account.id());
175                self.store.add_note_tag(note_tag_record).await?;
176
177                self.store
178                    .insert_account(account, default_address)
179                    .await
180                    .map_err(ClientError::StoreError)
181            },
182            Some(tracked_account) => {
183                if !overwrite {
184                    // Only overwrite the account if the flag is set to `true`
185                    return Err(ClientError::AccountAlreadyTracked(account.id()));
186                }
187
188                if tracked_account.account().nonce().as_int() > account.nonce().as_int() {
189                    // If the new account is older than the one being tracked, return an error
190                    return Err(ClientError::AccountNonceTooLow);
191                }
192
193                if tracked_account.is_locked() {
194                    // If the tracked account is locked, check that the account commitment matches
195                    // the one in the network
196                    let network_account_commitment =
197                        self.rpc_api.get_account_details(account.id()).await?.commitment();
198                    if network_account_commitment != account.commitment() {
199                        return Err(ClientError::AccountCommitmentMismatch(
200                            network_account_commitment,
201                        ));
202                    }
203                }
204
205                self.store.update_account(account).await.map_err(ClientError::StoreError)
206            },
207        }
208    }
209
210    /// Imports an account from the network to the client's store. The account needs to be public
211    /// and be tracked by the network, it will be fetched by its ID. If the account was already
212    /// being tracked by the client, it's state will be overwritten.
213    ///
214    /// # Errors
215    /// - If the account is not found on the network.
216    /// - If the account is private.
217    /// - There was an error sending the request to the network.
218    pub async fn import_account_by_id(&mut self, account_id: AccountId) -> Result<(), ClientError> {
219        let fetched_account = self.rpc_api.get_account_details(account_id).await?;
220
221        let account = match fetched_account {
222            FetchedAccount::Private(..) => {
223                return Err(ClientError::AccountIsPrivate(account_id));
224            },
225            FetchedAccount::Public(account, ..) => *account,
226        };
227
228        self.add_account(&account, true).await
229    }
230
231    /// Adds an [`Address`] to the associated [`AccountId`], alongside its derived [`NoteTag`].
232    ///
233    /// # Errors
234    /// - If the account is not found on the network.
235    /// - If the address is already being tracked.
236    pub async fn add_address(
237        &mut self,
238        address: Address,
239        account_id: AccountId,
240    ) -> Result<(), ClientError> {
241        let network_id = self.rpc_api.get_network_id().await?;
242        let address_bench32 = address.encode(network_id);
243        if self.store.get_addresses_by_account_id(account_id).await?.contains(&address) {
244            return Err(ClientError::AddressAlreadyTracked(address_bench32));
245        }
246
247        let tracked_account = self.store.get_account(account_id).await?;
248        match tracked_account {
249            None => Err(ClientError::AccountDataNotFound(account_id)),
250            Some(_tracked_account) => {
251                // Check that the Address is not already tracked
252                let derived_note_tag: NoteTag = address.to_note_tag();
253                let note_tag_record =
254                    NoteTagRecord::with_account_source(derived_note_tag, account_id);
255                if self.store.get_note_tags().await?.contains(&note_tag_record) {
256                    return Err(ClientError::NoteTagDerivedAddressAlreadyTracked(
257                        address_bench32,
258                        derived_note_tag,
259                    ));
260                }
261
262                self.store.insert_address(address, account_id).await?;
263                Ok(())
264            },
265        }
266    }
267
268    /// Removes an [`Address`] from the associated [`AccountId`], alongside its derived [`NoteTag`].
269    ///
270    /// # Errors
271    /// - If the account is not found on the network.
272    /// - If the address is not being tracked.
273    pub async fn remove_address(
274        &mut self,
275        address: Address,
276        account_id: AccountId,
277    ) -> Result<(), ClientError> {
278        self.store.remove_address(address, account_id).await?;
279        Ok(())
280    }
281
282    // ACCOUNT DATA RETRIEVAL
283    // --------------------------------------------------------------------------------------------
284
285    /// Returns a list of [`AccountHeader`] of all accounts stored in the database along with their
286    /// statuses.
287    ///
288    /// Said accounts' state is the state after the last performed sync.
289    pub async fn get_account_headers(
290        &self,
291    ) -> Result<Vec<(AccountHeader, AccountStatus)>, ClientError> {
292        self.store.get_account_headers().await.map_err(Into::into)
293    }
294
295    /// Retrieves a full [`AccountRecord`] object for the specified `account_id`. This result
296    /// represents data for the latest state known to the client, alongside its status. Returns
297    /// `None` if the account ID is not found.
298    pub async fn get_account(
299        &self,
300        account_id: AccountId,
301    ) -> Result<Option<AccountRecord>, ClientError> {
302        self.store.get_account(account_id).await.map_err(Into::into)
303    }
304
305    /// Retrieves an [`AccountHeader`] object for the specified [`AccountId`] along with its status.
306    /// Returns `None` if the account ID is not found.
307    ///
308    /// Said account's state is the state according to the last sync performed.
309    pub async fn get_account_header_by_id(
310        &self,
311        account_id: AccountId,
312    ) -> Result<Option<(AccountHeader, AccountStatus)>, ClientError> {
313        self.store.get_account_header(account_id).await.map_err(Into::into)
314    }
315
316    /// Attempts to retrieve an [`AccountRecord`] by its [`AccountId`].
317    ///
318    /// # Errors
319    ///
320    /// - If the account record is not found.
321    /// - If the underlying store operation fails.
322    pub async fn try_get_account(
323        &self,
324        account_id: AccountId,
325    ) -> Result<AccountRecord, ClientError> {
326        self.get_account(account_id)
327            .await?
328            .ok_or(ClientError::AccountDataNotFound(account_id))
329    }
330
331    /// Attempts to retrieve an [`AccountHeader`] by its [`AccountId`].
332    ///
333    /// # Errors
334    ///
335    /// - If the account header is not found.
336    /// - If the underlying store operation fails.
337    pub async fn try_get_account_header(
338        &self,
339        account_id: AccountId,
340    ) -> Result<(AccountHeader, AccountStatus), ClientError> {
341        self.get_account_header_by_id(account_id)
342            .await?
343            .ok_or(ClientError::AccountDataNotFound(account_id))
344    }
345}
346
347// UTILITY FUNCTIONS
348// ================================================================================================
349
350/// Builds an regular account ID from the provided parameters. The ID may be used along
351/// `Client::import_account_by_id` to import a public account from the network (provided that the
352/// used seed is known).
353///
354/// This function will only work for accounts with the [`BasicWallet`] and [`AuthRpoFalcon512`]
355/// components.
356///
357/// # Arguments
358/// - `init_seed`: Initial seed used to create the account. This is the seed passed to
359///   [`AccountBuilder::new`].
360/// - `public_key`: Public key of the account used in the [`AuthRpoFalcon512`] component.
361/// - `storage_mode`: Storage mode of the account.
362/// - `is_mutable`: Whether the account is mutable or not.
363///
364/// # Errors
365/// - If the account cannot be built.
366pub fn build_wallet_id(
367    init_seed: [u8; 32],
368    public_key: &PublicKey,
369    storage_mode: AccountStorageMode,
370    is_mutable: bool,
371    auth_scheme_id: u8,
372) -> Result<AccountId, ClientError> {
373    let account_type = if is_mutable {
374        AccountType::RegularAccountUpdatableCode
375    } else {
376        AccountType::RegularAccountImmutableCode
377    };
378
379    let auth_component = match auth_scheme_id {
380        0 => {
381            let auth_component: AccountComponent =
382                AuthRpoFalcon512::new(public_key.to_commitment()).into();
383            auth_component
384        },
385        1 => {
386            let auth_component: AccountComponent =
387                AuthEcdsaK256Keccak::new(public_key.to_commitment()).into();
388            auth_component
389        },
390        _ => {
391            return Err(ClientError::UnsupportedAuthSchemeId(auth_scheme_id));
392        },
393    };
394
395    let account = AccountBuilder::new(init_seed)
396        .account_type(account_type)
397        .storage_mode(storage_mode)
398        .with_auth_component(auth_component)
399        .with_component(BasicWallet)
400        .build()?;
401
402    Ok(account.id())
403}