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