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