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