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}