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::collections::BTreeSet;
38use alloc::vec::Vec;
39
40use miden_protocol::account::auth::{PublicKey, PublicKeyCommitment};
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 StorageSlot,
60 StorageSlotContent,
61 StorageSlotId,
62 StorageSlotName,
63 StorageSlotType,
64};
65pub use miden_protocol::address::{Address, AddressInterface, AddressType, NetworkId};
66pub use miden_protocol::errors::{AccountIdError, AddressError, NetworkIdError};
67use miden_protocol::note::NoteTag;
68use miden_standards::account::auth::{AuthEcdsaK256Keccak, AuthFalcon512Rpo};
69// RE-EXPORTS
70// ================================================================================================
71pub use miden_standards::account::interface::AccountInterfaceExt;
72use miden_standards::account::wallets::BasicWallet;
73use miden_tx::utils::{Deserializable, Serializable};
74
75use super::Client;
76use crate::Word;
77use crate::auth::AuthSchemeId;
78use crate::errors::ClientError;
79use crate::rpc::domain::account::FetchedAccount;
80use crate::store::{AccountRecord, AccountStatus};
81use crate::sync::NoteTagRecord;
82
83const PUBLIC_KEY_COMMITMENT_SETTING_SUFFIX: &str = "_public_key_commitments";
84
85pub mod component {
86 pub const MIDEN_PACKAGE_EXTENSION: &str = "masp";
87
88 pub use miden_protocol::account::auth::*;
89 pub use miden_protocol::account::component::{
90 InitStorageData,
91 StorageSlotSchema,
92 StorageValueName,
93 };
94 pub use miden_protocol::account::{AccountComponent, AccountComponentMetadata};
95 pub use miden_standards::account::auth::*;
96 pub use miden_standards::account::components::{
97 basic_fungible_faucet_library,
98 basic_wallet_library,
99 ecdsa_k256_keccak_library,
100 falcon_512_rpo_acl_library,
101 falcon_512_rpo_library,
102 falcon_512_rpo_multisig_library,
103 network_fungible_faucet_library,
104 no_auth_library,
105 };
106 pub use miden_standards::account::faucets::{
107 BasicFungibleFaucet,
108 FungibleFaucetExt,
109 NetworkFungibleFaucet,
110 };
111 pub use miden_standards::account::wallets::BasicWallet;
112}
113
114// CLIENT METHODS
115// ================================================================================================
116
117/// This section of the [Client] contains methods for:
118///
119/// - **Account creation:** Use the [`AccountBuilder`] to construct new accounts, specifying account
120/// type, storage mode (public/private), and attaching necessary components (e.g., basic wallet or
121/// fungible faucet). After creation, they can be added to the client.
122///
123/// - **Account tracking:** Accounts added via the client are persisted to the local store, where
124/// their state (including nonce, balance, and metadata) is updated upon every synchronization
125/// with the network.
126///
127/// - **Data retrieval:** The module also provides methods to fetch account-related data.
128impl<AUTH> Client<AUTH> {
129 // ACCOUNT CREATION
130 // --------------------------------------------------------------------------------------------
131
132 /// Adds the provided [Account] in the store so it can start being tracked by the client.
133 ///
134 /// If the account is already being tracked and `overwrite` is set to `true`, the account will
135 /// be overwritten. Newly created accounts must embed their seed (`account.seed()` must return
136 /// `Some(_)`).
137 ///
138 /// # Errors
139 ///
140 /// - If the account is new but it does not contain the seed.
141 /// - If the account is already tracked and `overwrite` is set to `false`.
142 /// - If `overwrite` is set to `true` and the `account_data` nonce is lower than the one already
143 /// being tracked.
144 /// - If `overwrite` is set to `true` and the `account_data` commitment doesn't match the
145 /// network's account commitment.
146 /// - If the client has reached the accounts limit
147 /// ([`ACCOUNT_ID_LIMIT`](crate::rpc::ACCOUNT_ID_LIMIT)).
148 /// - If the client has reached the note tags limit
149 /// ([`NOTE_TAG_LIMIT`](crate::rpc::NOTE_TAG_LIMIT)).
150 pub async fn add_account(
151 &mut self,
152 account: &Account,
153 overwrite: bool,
154 ) -> Result<(), ClientError> {
155 if account.is_new() {
156 if account.seed().is_none() {
157 return Err(ClientError::AddNewAccountWithoutSeed);
158 }
159 } else {
160 // Ignore the seed since it's not a new account
161 if account.seed().is_some() {
162 tracing::warn!(
163 "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."
164 );
165 }
166 }
167
168 let tracked_account = self.store.get_account(account.id()).await?;
169
170 match tracked_account {
171 None => {
172 // Check limits since it's a non-tracked account
173 self.check_account_limit().await?;
174 self.check_note_tag_limit().await?;
175
176 let default_address = Address::new(account.id());
177
178 // If the account is not being tracked, insert it into the store regardless of the
179 // `overwrite` flag
180 let default_address_note_tag = default_address.to_note_tag();
181 let note_tag_record =
182 NoteTagRecord::with_account_source(default_address_note_tag, account.id());
183 self.store.add_note_tag(note_tag_record).await?;
184
185 self.store
186 .insert_account(account, default_address)
187 .await
188 .map_err(ClientError::StoreError)
189 },
190 Some(tracked_account) => {
191 if !overwrite {
192 // Only overwrite the account if the flag is set to `true`
193 return Err(ClientError::AccountAlreadyTracked(account.id()));
194 }
195
196 if tracked_account.nonce().as_int() > account.nonce().as_int() {
197 // If the new account is older than the one being tracked, return an error
198 return Err(ClientError::AccountNonceTooLow);
199 }
200
201 if tracked_account.is_locked() {
202 // If the tracked account is locked, check that the account commitment matches
203 // the one in the network
204 let network_account_commitment =
205 self.rpc_api.get_account_details(account.id()).await?.commitment();
206 if network_account_commitment != account.commitment() {
207 return Err(ClientError::AccountCommitmentMismatch(
208 network_account_commitment,
209 ));
210 }
211 }
212
213 self.store.update_account(account).await.map_err(ClientError::StoreError)
214 },
215 }
216 }
217
218 /// Imports an account from the network to the client's store. The account needs to be public
219 /// and be tracked by the network, it will be fetched by its ID. If the account was already
220 /// being tracked by the client, it's state will be overwritten.
221 ///
222 /// # Errors
223 /// - If the account is not found on the network.
224 /// - If the account is private.
225 /// - There was an error sending the request to the network.
226 pub async fn import_account_by_id(&mut self, account_id: AccountId) -> Result<(), ClientError> {
227 let fetched_account = self.rpc_api.get_account_details(account_id).await?;
228
229 let account = match fetched_account {
230 FetchedAccount::Private(..) => {
231 return Err(ClientError::AccountIsPrivate(account_id));
232 },
233 FetchedAccount::Public(account, ..) => *account,
234 };
235
236 self.add_account(&account, true).await
237 }
238
239 /// Adds an [`Address`] to the associated [`AccountId`], alongside its derived [`NoteTag`].
240 ///
241 /// # Errors
242 /// - If the account is not found on the network.
243 /// - If the address is already being tracked.
244 /// - If the client has reached the note tags limit
245 /// ([`NOTE_TAG_LIMIT`](crate::rpc::NOTE_TAG_LIMIT)).
246 pub async fn add_address(
247 &mut self,
248 address: Address,
249 account_id: AccountId,
250 ) -> Result<(), ClientError> {
251 let network_id = self.rpc_api.get_network_id().await?;
252 let address_bench32 = address.encode(network_id);
253 if self.store.get_addresses_by_account_id(account_id).await?.contains(&address) {
254 return Err(ClientError::AddressAlreadyTracked(address_bench32));
255 }
256
257 let tracked_account = self.store.get_account(account_id).await?;
258 match tracked_account {
259 None => Err(ClientError::AccountDataNotFound(account_id)),
260 Some(_tracked_account) => {
261 // Check that the Address is not already tracked
262 let derived_note_tag: NoteTag = address.to_note_tag();
263 let note_tag_record =
264 NoteTagRecord::with_account_source(derived_note_tag, account_id);
265 if self.store.get_note_tags().await?.contains(¬e_tag_record) {
266 return Err(ClientError::NoteTagDerivedAddressAlreadyTracked(
267 address_bench32,
268 derived_note_tag,
269 ));
270 }
271
272 self.check_note_tag_limit().await?;
273 self.store.insert_address(address, account_id).await?;
274 Ok(())
275 },
276 }
277 }
278
279 /// Removes an [`Address`] from the associated [`AccountId`], alongside its derived [`NoteTag`].
280 ///
281 /// # Errors
282 /// - If the account is not found on the network.
283 /// - If the address is not being tracked.
284 pub async fn remove_address(
285 &mut self,
286 address: Address,
287 account_id: AccountId,
288 ) -> Result<(), ClientError> {
289 self.store.remove_address(address, account_id).await?;
290 Ok(())
291 }
292
293 // ACCOUNT DATA RETRIEVAL
294 // --------------------------------------------------------------------------------------------
295
296 /// Returns a list of [`AccountHeader`] of all accounts stored in the database along with their
297 /// statuses.
298 ///
299 /// Said accounts' state is the state after the last performed sync.
300 pub async fn get_account_headers(
301 &self,
302 ) -> Result<Vec<(AccountHeader, AccountStatus)>, ClientError> {
303 self.store.get_account_headers().await.map_err(Into::into)
304 }
305
306 /// Retrieves a full [`AccountRecord`] object for the specified `account_id`. This result
307 /// represents data for the latest state known to the client, alongside its status. Returns
308 /// `None` if the account ID is not found.
309 pub async fn get_account(
310 &self,
311 account_id: AccountId,
312 ) -> Result<Option<AccountRecord>, ClientError> {
313 self.store.get_account(account_id).await.map_err(Into::into)
314 }
315
316 /// Retrieves an [`AccountHeader`] object for the specified [`AccountId`] along with its status.
317 /// Returns `None` if the account ID is not found.
318 ///
319 /// Said account's state is the state according to the last sync performed.
320 pub async fn get_account_header_by_id(
321 &self,
322 account_id: AccountId,
323 ) -> Result<Option<(AccountHeader, AccountStatus)>, ClientError> {
324 self.store.get_account_header(account_id).await.map_err(Into::into)
325 }
326
327 /// Attempts to retrieve an [`AccountRecord`] by its [`AccountId`].
328 ///
329 /// # Errors
330 ///
331 /// - If the account record is not found.
332 /// - If the underlying store operation fails.
333 pub async fn try_get_account(
334 &self,
335 account_id: AccountId,
336 ) -> Result<AccountRecord, ClientError> {
337 self.get_account(account_id)
338 .await?
339 .ok_or(ClientError::AccountDataNotFound(account_id))
340 }
341
342 /// Attempts to retrieve an [`AccountHeader`] by its [`AccountId`].
343 ///
344 /// # Errors
345 ///
346 /// - If the account header is not found.
347 /// - If the underlying store operation fails.
348 pub async fn try_get_account_header(
349 &self,
350 account_id: AccountId,
351 ) -> Result<(AccountHeader, AccountStatus), ClientError> {
352 self.get_account_header_by_id(account_id)
353 .await?
354 .ok_or(ClientError::AccountDataNotFound(account_id))
355 }
356
357 /// Adds a list of public key commitments associated with the given account ID.
358 ///
359 /// Commitments are stored as a `BTreeSet`, so duplicates are ignored. If the account already
360 /// has known commitments, the new ones are merged into the existing set.
361 ///
362 /// This is useful because with a public key commitment, we can retrieve its corresponding
363 /// secret key using, for example,
364 /// [`FilesystemKeyStore::get_key`](crate::keystore::FilesystemKeyStore::get_key). This yields
365 /// an indirect mapping from account ID to its secret keys: account ID -> public key commitments
366 /// -> secret keys (via keystore).
367 ///
368 /// To identify these keys and avoid collisions, the account ID is turned into its hex
369 /// representation and a suffix is added. If the resulting set is empty, any existing settings
370 /// entry is removed.
371 pub async fn register_account_public_key_commitments(
372 &self,
373 account_id: &AccountId,
374 pub_keys: &[PublicKey],
375 ) -> Result<(), ClientError> {
376 let setting_key =
377 format!("{}{}", account_id.to_hex(), PUBLIC_KEY_COMMITMENT_SETTING_SUFFIX);
378 // Store commitments as Words because PublicKeyCommitment doesn't implement
379 // (De)Serializable.
380 let (had_setting, mut commitments): (bool, BTreeSet<Word>) =
381 match self.store.get_setting(setting_key.clone()).await? {
382 Some(known) => {
383 let known: BTreeSet<Word> = Deserializable::read_from_bytes(&known)
384 .map_err(ClientError::DataDeserializationError)?;
385 (true, known)
386 },
387 None => (false, BTreeSet::new()),
388 };
389
390 commitments.extend(pub_keys.iter().map(|pk| Word::from(pk.to_commitment())));
391
392 if commitments.is_empty() {
393 if had_setting {
394 self.store.remove_setting(setting_key).await.map_err(ClientError::StoreError)?;
395 }
396 return Ok(());
397 }
398
399 self.store
400 .set_setting(setting_key, Serializable::to_bytes(&commitments))
401 .await
402 .map_err(ClientError::StoreError)
403 }
404
405 /// Removes a list of public key commitments associated with the given account ID.
406 ///
407 /// Commitments are stored as a `BTreeSet`, so duplicates in `pub_key_commitments` are ignored
408 /// and missing commitments are skipped. If the account is not registered or has no stored
409 /// commitments, this is a no-op.
410 ///
411 /// If the resulting set is empty, the settings entry is removed. Returns `true` if at least
412 /// one commitment was removed, or `false` otherwise.
413 pub async fn deregister_account_public_key_commitment(
414 &self,
415 account_id: &AccountId,
416 pub_key_commitments: &[PublicKeyCommitment],
417 ) -> Result<bool, ClientError> {
418 let setting_key =
419 format!("{}{}", account_id.to_hex(), PUBLIC_KEY_COMMITMENT_SETTING_SUFFIX);
420 let Some(known) = self.store.get_setting(setting_key.clone()).await? else {
421 return Ok(false);
422 };
423 let mut commitments: BTreeSet<Word> = Deserializable::read_from_bytes(&known)
424 .map_err(ClientError::DataDeserializationError)?;
425
426 if commitments.is_empty() {
427 self.store.remove_setting(setting_key).await.map_err(ClientError::StoreError)?;
428 return Ok(false);
429 }
430
431 let mut removed_any = false;
432 for commitment in pub_key_commitments {
433 let word = Word::from(*commitment);
434 if commitments.remove(&word) {
435 removed_any = true;
436 }
437 }
438
439 if !removed_any {
440 return Ok(false);
441 }
442
443 if commitments.is_empty() {
444 self.store.remove_setting(setting_key).await.map_err(ClientError::StoreError)?;
445 return Ok(true);
446 }
447
448 self.store
449 .set_setting(setting_key, Serializable::to_bytes(&commitments))
450 .await
451 .map_err(ClientError::StoreError)?;
452 Ok(true)
453 }
454
455 /// Returns the previously stored public key commitments associated with the given
456 /// [`AccountId`], if any.
457 ///
458 /// Once retrieved, this list of public key commitments can be used in conjunction with
459 /// [`FilesystemKeyStore::get_key`](crate::keystore::FilesystemKeyStore::get_key) to retrieve
460 /// secret keys.
461 ///
462 /// Commitments are stored as a `BTreeSet`, so the returned list is deduplicated. Returns an
463 /// empty vector if the account is not registered or no commitments are stored.
464 pub async fn get_account_public_key_commitments(
465 &self,
466 account_id: &AccountId,
467 ) -> Result<Vec<PublicKeyCommitment>, ClientError> {
468 let setting_key =
469 format!("{}{}", account_id.to_hex(), PUBLIC_KEY_COMMITMENT_SETTING_SUFFIX);
470 match self.store.get_setting(setting_key).await? {
471 Some(known) => {
472 let commitments: BTreeSet<Word> = Deserializable::read_from_bytes(&known)
473 .map_err(ClientError::DataDeserializationError)?;
474 Ok(commitments.into_iter().map(PublicKeyCommitment::from).collect())
475 },
476 None => Ok(vec![]),
477 }
478 }
479}
480
481// UTILITY FUNCTIONS
482// ================================================================================================
483
484/// Builds an regular account ID from the provided parameters. The ID may be used along
485/// `Client::import_account_by_id` to import a public account from the network (provided that the
486/// used seed is known).
487///
488/// This function currently supports accounts composed of the [`BasicWallet`] component and one of
489/// the supported authentication schemes ([`AuthFalcon512Rpo`] or [`AuthEcdsaK256Keccak`]).
490///
491/// # Arguments
492/// - `init_seed`: Initial seed used to create the account. This is the seed passed to
493/// [`AccountBuilder::new`].
494/// - `public_key`: Public key of the account used for the authentication component.
495/// - `storage_mode`: Storage mode of the account.
496/// - `is_mutable`: Whether the account is mutable or not.
497///
498/// # Errors
499/// - If the account cannot be built.
500pub fn build_wallet_id(
501 init_seed: [u8; 32],
502 public_key: &PublicKey,
503 storage_mode: AccountStorageMode,
504 is_mutable: bool,
505) -> Result<AccountId, ClientError> {
506 let account_type = if is_mutable {
507 AccountType::RegularAccountUpdatableCode
508 } else {
509 AccountType::RegularAccountImmutableCode
510 };
511
512 let auth_scheme = public_key.auth_scheme();
513 let auth_component = match auth_scheme {
514 AuthSchemeId::Falcon512Rpo => {
515 let auth_component: AccountComponent =
516 AuthFalcon512Rpo::new(public_key.to_commitment()).into();
517 auth_component
518 },
519 AuthSchemeId::EcdsaK256Keccak => {
520 let auth_component: AccountComponent =
521 AuthEcdsaK256Keccak::new(public_key.to_commitment()).into();
522 auth_component
523 },
524 auth_scheme => {
525 return Err(ClientError::UnsupportedAuthSchemeId(auth_scheme.as_u8()));
526 },
527 };
528
529 let account = AccountBuilder::new(init_seed)
530 .account_type(account_type)
531 .storage_mode(storage_mode)
532 .with_auth_component(auth_component)
533 .with_component(BasicWallet)
534 .build()?;
535
536 Ok(account.id())
537}