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