Skip to main content

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(&note_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}