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_protocol::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 = 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 already embeds its seed information.
30//! client.add_account(&account, false).await?;
31//! # Ok(())
32//! # }
33//! ```
34//!
35//! For more details on accounts, refer to the [Account] documentation.
36
37use alloc::vec::Vec;
38
39use miden_protocol::account::auth::PublicKey;
40pub use miden_protocol::account::{
41 Account,
42 AccountBuilder,
43 AccountCode,
44 AccountComponent,
45 AccountComponentCode,
46 AccountDelta,
47 AccountFile,
48 AccountHeader,
49 AccountId,
50 AccountIdPrefix,
51 AccountStorage,
52 AccountStorageMode,
53 AccountType,
54 PartialAccount,
55 PartialStorage,
56 PartialStorageMap,
57 StorageMap,
58 StorageMapKey,
59 StorageMapWitness,
60 StorageSlot,
61 StorageSlotContent,
62 StorageSlotId,
63 StorageSlotName,
64 StorageSlotType,
65};
66pub use miden_protocol::address::{Address, AddressInterface, AddressType, NetworkId};
67use miden_protocol::asset::AssetVault;
68pub use miden_protocol::errors::{AccountIdError, AddressError, NetworkIdError};
69use miden_protocol::note::NoteTag;
70
71mod account_reader;
72pub use account_reader::AccountReader;
73use miden_standards::account::auth::AuthSingleSig;
74// RE-EXPORTS
75// ================================================================================================
76pub use miden_standards::account::interface::AccountInterfaceExt;
77use miden_standards::account::wallets::BasicWallet;
78
79use super::Client;
80use crate::auth::AuthSchemeId;
81use crate::errors::ClientError;
82use crate::rpc::domain::account::FetchedAccount;
83use crate::rpc::node::{EndpointError, GetAccountError};
84use crate::store::{AccountStatus, AccountStorageFilter};
85use crate::sync::NoteTagRecord;
86
87pub mod component {
88 pub const MIDEN_PACKAGE_EXTENSION: &str = "masp";
89
90 pub use miden_protocol::account::auth::*;
91 pub use miden_protocol::account::component::{
92 InitStorageData,
93 StorageSlotSchema,
94 StorageValueName,
95 };
96 pub use miden_protocol::account::{AccountComponent, AccountComponentMetadata};
97 pub use miden_standards::account::auth::*;
98 pub use miden_standards::account::components::{
99 basic_fungible_faucet_library,
100 basic_wallet_library,
101 multisig_library,
102 network_fungible_faucet_library,
103 no_auth_library,
104 singlesig_acl_library,
105 singlesig_library,
106 storage_schema_library,
107 };
108 pub use miden_standards::account::faucets::{BasicFungibleFaucet, NetworkFungibleFaucet};
109 pub use miden_standards::account::wallets::BasicWallet;
110}
111
112// CLIENT METHODS
113// ================================================================================================
114
115/// This section of the [Client] contains methods for:
116///
117/// - **Account creation:** Use the [`AccountBuilder`] to construct new accounts, specifying account
118/// type, storage mode (public/private), and attaching necessary components (e.g., basic wallet or
119/// fungible faucet). After creation, they can be added to the client.
120///
121/// - **Account tracking:** Accounts added via the client are persisted to the local store, where
122/// their state (including nonce, balance, and metadata) is updated upon every synchronization
123/// with the network.
124///
125/// - **Data retrieval:** The module also provides methods to fetch account-related data.
126impl<AUTH> Client<AUTH> {
127 // ACCOUNT CREATION
128 // --------------------------------------------------------------------------------------------
129
130 /// Adds the provided [Account] in the store so it can start being tracked by the client.
131 ///
132 /// If the account is already being tracked and `overwrite` is set to `true`, the account will
133 /// be overwritten. Newly created accounts must embed their seed (`account.seed()` must return
134 /// `Some(_)`).
135 ///
136 /// # Errors
137 ///
138 /// - If the account is new but it does not contain the seed.
139 /// - If the account is already tracked and `overwrite` is set to `false`.
140 /// - If `overwrite` is set to `true` and the `account_data` nonce is lower than the one already
141 /// being tracked.
142 /// - If `overwrite` is set to `true` and the `account_data` commitment doesn't match the
143 /// network's account commitment.
144 /// - If the client has reached the accounts limit.
145 /// - If the client has reached the note tags limit.
146 pub async fn add_account(
147 &mut self,
148 account: &Account,
149 overwrite: bool,
150 ) -> Result<(), ClientError> {
151 if account.is_new() {
152 if account.seed().is_none() {
153 return Err(ClientError::AddNewAccountWithoutSeed);
154 }
155 } else {
156 // Ignore the seed since it's not a new account
157 if account.seed().is_some() {
158 tracing::warn!(
159 "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."
160 );
161 }
162 }
163
164 let tracked_account = self.store.get_account(account.id()).await?;
165
166 match tracked_account {
167 None => {
168 // Check limits since it's a non-tracked account
169 self.check_account_limit().await?;
170 self.check_note_tag_limit().await?;
171
172 let default_address = Address::new(account.id());
173
174 // If the account is not being tracked, insert it into the store regardless of the
175 // `overwrite` flag
176 let default_address_note_tag = default_address.to_note_tag();
177 let note_tag_record =
178 NoteTagRecord::with_account_source(default_address_note_tag, account.id());
179 self.store.add_note_tag(note_tag_record).await?;
180
181 self.store
182 .insert_account(account, default_address)
183 .await
184 .map_err(ClientError::StoreError)
185 },
186 Some(tracked_account) => {
187 if !overwrite {
188 // Only overwrite the account if the flag is set to `true`
189 return Err(ClientError::AccountAlreadyTracked(account.id()));
190 }
191
192 if tracked_account.nonce().as_int() > account.nonce().as_int() {
193 // If the new account is older than the one being tracked, return an error
194 return Err(ClientError::AccountNonceTooLow);
195 }
196
197 if tracked_account.is_locked() {
198 // If the tracked account is locked, check that the account commitment matches
199 // the one in the network
200 let network_account_commitment =
201 self.rpc_api.get_account_details(account.id()).await?.commitment();
202 if network_account_commitment != account.to_commitment() {
203 return Err(ClientError::AccountCommitmentMismatch(
204 network_account_commitment,
205 ));
206 }
207 }
208
209 self.store.update_account(account).await.map_err(ClientError::StoreError)
210 },
211 }
212 }
213
214 /// Imports an account from the network to the client's store. The account needs to be public
215 /// and be tracked by the network, it will be fetched by its ID. If the account was already
216 /// being tracked by the client, it's state will be overwritten.
217 ///
218 /// # Errors
219 /// - If the account is not found on the network.
220 /// - If the account is private.
221 /// - There was an error sending the request to the network.
222 pub async fn import_account_by_id(&mut self, account_id: AccountId) -> Result<(), ClientError> {
223 let fetched_account =
224 self.rpc_api.get_account_details(account_id).await.map_err(|err| {
225 match err.endpoint_error() {
226 Some(EndpointError::GetAccount(GetAccountError::AccountNotFound)) => {
227 ClientError::AccountNotFoundOnChain(account_id)
228 },
229 _ => ClientError::RpcError(err),
230 }
231 })?;
232
233 let account = match fetched_account {
234 FetchedAccount::Private(..) => {
235 return Err(ClientError::AccountIsPrivate(account_id));
236 },
237 FetchedAccount::Public(account, ..) => *account,
238 };
239
240 self.add_account(&account, true).await
241 }
242
243 /// Adds an [`Address`] to the associated [`AccountId`], alongside its derived [`NoteTag`].
244 ///
245 /// # Errors
246 /// - If the account is not found on the network.
247 /// - If the address is already being tracked.
248 /// - If the client has reached the note tags limit.
249 pub async fn add_address(
250 &mut self,
251 address: Address,
252 account_id: AccountId,
253 ) -> Result<(), ClientError> {
254 let network_id = self.rpc_api.get_network_id().await?;
255 let address_bench32 = address.encode(network_id);
256 if self.store.get_addresses_by_account_id(account_id).await?.contains(&address) {
257 return Err(ClientError::AddressAlreadyTracked(address_bench32));
258 }
259
260 let tracked_account = self.store.get_account(account_id).await?;
261 match tracked_account {
262 None => Err(ClientError::AccountDataNotFound(account_id)),
263 Some(_tracked_account) => {
264 // Check that the Address is not already tracked
265 let derived_note_tag: NoteTag = address.to_note_tag();
266 let note_tag_record =
267 NoteTagRecord::with_account_source(derived_note_tag, account_id);
268 if self.store.get_note_tags().await?.contains(¬e_tag_record) {
269 return Err(ClientError::NoteTagDerivedAddressAlreadyTracked(
270 address_bench32,
271 derived_note_tag,
272 ));
273 }
274
275 self.check_note_tag_limit().await?;
276 self.store.insert_address(address, account_id).await?;
277 Ok(())
278 },
279 }
280 }
281
282 /// Removes an [`Address`] from the associated [`AccountId`], alongside its derived [`NoteTag`].
283 ///
284 /// # Errors
285 /// - If the account is not found on the network.
286 /// - If the address is not being tracked.
287 pub async fn remove_address(
288 &mut self,
289 address: Address,
290 account_id: AccountId,
291 ) -> Result<(), ClientError> {
292 self.store.remove_address(address, account_id).await?;
293 Ok(())
294 }
295
296 // ACCOUNT DATA RETRIEVAL
297 // --------------------------------------------------------------------------------------------
298
299 /// Retrieves the asset vault for a specific account.
300 ///
301 /// To check the balance for a single asset, use [`Client::account_reader`] instead.
302 pub async fn get_account_vault(
303 &self,
304 account_id: AccountId,
305 ) -> Result<AssetVault, ClientError> {
306 self.store.get_account_vault(account_id).await.map_err(ClientError::StoreError)
307 }
308
309 /// Retrieves the whole account storage for a specific account.
310 ///
311 /// To only load a specific slot, use [`Client::account_reader`] instead.
312 pub async fn get_account_storage(
313 &self,
314 account_id: AccountId,
315 ) -> Result<AccountStorage, ClientError> {
316 self.store
317 .get_account_storage(account_id, AccountStorageFilter::All)
318 .await
319 .map_err(ClientError::StoreError)
320 }
321
322 /// Retrieves the account code for a specific account.
323 ///
324 /// Returns `None` if the account is not found.
325 pub async fn get_account_code(
326 &self,
327 account_id: AccountId,
328 ) -> Result<Option<AccountCode>, ClientError> {
329 self.store.get_account_code(account_id).await.map_err(ClientError::StoreError)
330 }
331
332 /// Returns a list of [`AccountHeader`] of all accounts stored in the database along with their
333 /// statuses.
334 ///
335 /// Said accounts' state is the state after the last performed sync.
336 pub async fn get_account_headers(
337 &self,
338 ) -> Result<Vec<(AccountHeader, AccountStatus)>, ClientError> {
339 self.store.get_account_headers().await.map_err(Into::into)
340 }
341
342 /// Retrieves the full [`Account`] object from the store, returning `None` if not found.
343 ///
344 /// This method loads the complete account state including vault, storage, and code.
345 ///
346 /// For lazy access that fetches only the data you need, use
347 /// [`Client::account_reader`] instead.
348 ///
349 /// Use [`Client::try_get_account`] if you want to error when the account is not found.
350 pub async fn get_account(&self, account_id: AccountId) -> Result<Option<Account>, ClientError> {
351 match self.store.get_account(account_id).await? {
352 Some(record) => Ok(Some(record.try_into()?)),
353 None => Ok(None),
354 }
355 }
356
357 /// Retrieves the full [`Account`] object from the store, erroring if not found.
358 ///
359 /// This method loads the complete account state including vault, storage, and code.
360 ///
361 /// Use [`Client::get_account`] if you want to handle missing accounts gracefully.
362 pub async fn try_get_account(&self, account_id: AccountId) -> Result<Account, ClientError> {
363 self.get_account(account_id)
364 .await?
365 .ok_or(ClientError::AccountDataNotFound(account_id))
366 }
367
368 /// Creates an [`AccountReader`] for lazy access to account data.
369 ///
370 /// The `AccountReader` provides lazy access to account state - each method call
371 /// fetches fresh data from storage, ensuring you always see the current state.
372 ///
373 /// For loading the full [`Account`] object, use [`Client::get_account`] instead.
374 ///
375 /// # Example
376 /// ```ignore
377 /// let reader = client.account_reader(account_id);
378 ///
379 /// // Each call fetches fresh data
380 /// let nonce = reader.nonce().await?;
381 /// let balance = reader.get_balance(faucet_id).await?;
382 ///
383 /// // Storage access is integrated
384 /// let value = reader.get_storage_item("my_slot").await?;
385 /// let (map_value, witness) = reader.get_storage_map_witness("balances", key).await?;
386 /// ```
387 pub fn account_reader(&self, account_id: AccountId) -> AccountReader {
388 AccountReader::new(self.store.clone(), account_id)
389 }
390}
391
392// UTILITY FUNCTIONS
393// ================================================================================================
394
395/// Builds an regular account ID from the provided parameters. The ID may be used along
396/// `Client::import_account_by_id` to import a public account from the network (provided that the
397/// used seed is known).
398///
399/// This function currently supports accounts composed of the [`BasicWallet`] component and one of
400/// the supported authentication schemes ([`AuthSingleSig`]).
401///
402/// # Arguments
403/// - `init_seed`: Initial seed used to create the account. This is the seed passed to
404/// [`AccountBuilder::new`].
405/// - `public_key`: Public key of the account used for the authentication component.
406/// - `storage_mode`: Storage mode of the account.
407/// - `is_mutable`: Whether the account is mutable or not.
408///
409/// # Errors
410/// - If the account cannot be built.
411pub fn build_wallet_id(
412 init_seed: [u8; 32],
413 public_key: &PublicKey,
414 storage_mode: AccountStorageMode,
415 is_mutable: bool,
416) -> Result<AccountId, ClientError> {
417 let account_type = if is_mutable {
418 AccountType::RegularAccountUpdatableCode
419 } else {
420 AccountType::RegularAccountImmutableCode
421 };
422
423 let auth_scheme = public_key.auth_scheme();
424 let auth_component = match auth_scheme {
425 AuthSchemeId::Falcon512Rpo => {
426 let auth_component: AccountComponent =
427 AuthSingleSig::new(public_key.to_commitment(), AuthSchemeId::Falcon512Rpo).into();
428 auth_component
429 },
430 AuthSchemeId::EcdsaK256Keccak => {
431 let auth_component: AccountComponent =
432 AuthSingleSig::new(public_key.to_commitment(), AuthSchemeId::EcdsaK256Keccak)
433 .into();
434 auth_component
435 },
436 auth_scheme => {
437 return Err(ClientError::UnsupportedAuthSchemeId(auth_scheme.as_u8()));
438 },
439 };
440
441 let account = AccountBuilder::new(init_seed)
442 .account_type(account_type)
443 .storage_mode(storage_mode)
444 .with_auth_component(auth_component)
445 .with_component(BasicWallet)
446 .build()?;
447
448 Ok(account.id())
449}