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