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}