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