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}