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<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_lib::account::auth::{AuthEcdsaK256Keccak, AuthRpoFalcon512};
40use miden_lib::account::wallets::BasicWallet;
41use miden_objects::account::auth::PublicKey;
42use miden_objects::note::NoteTag;
43// RE-EXPORTS
44// ================================================================================================
45pub use miden_objects::{
46 AccountIdError,
47 AddressError,
48 NetworkIdError,
49 account::{
50 Account,
51 AccountBuilder,
52 AccountCode,
53 AccountComponent,
54 AccountDelta,
55 AccountFile,
56 AccountHeader,
57 AccountId,
58 AccountIdPrefix,
59 AccountStorage,
60 AccountStorageMode,
61 AccountType,
62 PartialAccount,
63 PartialStorage,
64 PartialStorageMap,
65 StorageMap,
66 StorageSlot,
67 StorageSlotType,
68 },
69 address::{Address, AddressInterface, AddressType, NetworkId},
70};
71
72use super::Client;
73use crate::errors::ClientError;
74use crate::rpc::domain::account::FetchedAccount;
75use crate::store::{AccountRecord, AccountStatus};
76use crate::sync::NoteTagRecord;
77
78pub mod component {
79 pub const MIDEN_PACKAGE_EXTENSION: &str = "masp";
80
81 pub use miden_lib::account::auth::*;
82 pub use miden_lib::account::components::{
83 basic_fungible_faucet_library,
84 basic_wallet_library,
85 ecdsa_k256_keccak_library,
86 network_fungible_faucet_library,
87 no_auth_library,
88 rpo_falcon_512_acl_library,
89 rpo_falcon_512_library,
90 rpo_falcon_512_multisig_library,
91 };
92 pub use miden_lib::account::faucets::{
93 BasicFungibleFaucet,
94 FungibleFaucetExt,
95 NetworkFungibleFaucet,
96 };
97 pub use miden_lib::account::wallets::BasicWallet;
98 pub use miden_objects::account::{
99 AccountComponent,
100 AccountComponentMetadata,
101 FeltRepresentation,
102 InitStorageData,
103 StorageEntry,
104 StorageValueName,
105 TemplateType,
106 WordRepresentation,
107 };
108}
109
110// CLIENT METHODS
111// ================================================================================================
112
113/// This section of the [Client] contains methods for:
114///
115/// - **Account creation:** Use the [`AccountBuilder`] to construct new accounts, specifying account
116/// type, storage mode (public/private), and attaching necessary components (e.g., basic wallet or
117/// fungible faucet). After creation, they can be added to the client.
118///
119/// - **Account tracking:** Accounts added via the client are persisted to the local store, where
120/// their state (including nonce, balance, and metadata) is updated upon every synchronization
121/// with the network.
122///
123/// - **Data retrieval:** The module also provides methods to fetch account-related data.
124impl<AUTH> Client<AUTH> {
125 // ACCOUNT CREATION
126 // --------------------------------------------------------------------------------------------
127
128 /// Adds the provided [Account] in the store so it can start being tracked by the client.
129 ///
130 /// If the account is already being tracked and `overwrite` is set to `true`, the account will
131 /// be overwritten. Newly created accounts must embed their seed (`account.seed()` must return
132 /// `Some(_)`).
133 ///
134 /// # Errors
135 ///
136 /// - If the account is new but it does not contain the seed.
137 /// - If the account is already tracked and `overwrite` is set to `false`.
138 /// - If `overwrite` is set to `true` and the `account_data` nonce is lower than the one already
139 /// being tracked.
140 /// - If `overwrite` is set to `true` and the `account_data` commitment doesn't match the
141 /// network's account commitment.
142 pub async fn add_account(
143 &mut self,
144 account: &Account,
145 overwrite: bool,
146 ) -> Result<(), ClientError> {
147 if account.is_new() {
148 if account.seed().is_none() {
149 return Err(ClientError::AddNewAccountWithoutSeed);
150 }
151 } else {
152 // Ignore the seed since it's not a new account
153
154 // TODO: The alternative approach to this is to store the seed anyway, but
155 // ignore it at the point of executing against this transaction, but that
156 // approach seems a little bit more incorrect
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 let default_address = Address::new(account.id());
169
170 // If the account is not being tracked, insert it into the store regardless of the
171 // `overwrite` flag
172 let default_address_note_tag = default_address.to_note_tag();
173 let note_tag_record =
174 NoteTagRecord::with_account_source(default_address_note_tag, account.id());
175 self.store.add_note_tag(note_tag_record).await?;
176
177 self.store
178 .insert_account(account, default_address)
179 .await
180 .map_err(ClientError::StoreError)
181 },
182 Some(tracked_account) => {
183 if !overwrite {
184 // Only overwrite the account if the flag is set to `true`
185 return Err(ClientError::AccountAlreadyTracked(account.id()));
186 }
187
188 if tracked_account.account().nonce().as_int() > account.nonce().as_int() {
189 // If the new account is older than the one being tracked, return an error
190 return Err(ClientError::AccountNonceTooLow);
191 }
192
193 if tracked_account.is_locked() {
194 // If the tracked account is locked, check that the account commitment matches
195 // the one in the network
196 let network_account_commitment =
197 self.rpc_api.get_account_details(account.id()).await?.commitment();
198 if network_account_commitment != account.commitment() {
199 return Err(ClientError::AccountCommitmentMismatch(
200 network_account_commitment,
201 ));
202 }
203 }
204
205 self.store.update_account(account).await.map_err(ClientError::StoreError)
206 },
207 }
208 }
209
210 /// Imports an account from the network to the client's store. The account needs to be public
211 /// and be tracked by the network, it will be fetched by its ID. If the account was already
212 /// being tracked by the client, it's state will be overwritten.
213 ///
214 /// # Errors
215 /// - If the account is not found on the network.
216 /// - If the account is private.
217 /// - There was an error sending the request to the network.
218 pub async fn import_account_by_id(&mut self, account_id: AccountId) -> Result<(), ClientError> {
219 let fetched_account = self.rpc_api.get_account_details(account_id).await?;
220
221 let account = match fetched_account {
222 FetchedAccount::Private(..) => {
223 return Err(ClientError::AccountIsPrivate(account_id));
224 },
225 FetchedAccount::Public(account, ..) => *account,
226 };
227
228 self.add_account(&account, true).await
229 }
230
231 /// Adds an [`Address`] to the associated [`AccountId`], alongside its derived [`NoteTag`].
232 ///
233 /// # Errors
234 /// - If the account is not found on the network.
235 /// - If the address is already being tracked.
236 pub async fn add_address(
237 &mut self,
238 address: Address,
239 account_id: AccountId,
240 ) -> Result<(), ClientError> {
241 let network_id = self.rpc_api.get_network_id().await?;
242 let address_bench32 = address.encode(network_id);
243 if self.store.get_addresses_by_account_id(account_id).await?.contains(&address) {
244 return Err(ClientError::AddressAlreadyTracked(address_bench32));
245 }
246
247 let tracked_account = self.store.get_account(account_id).await?;
248 match tracked_account {
249 None => Err(ClientError::AccountDataNotFound(account_id)),
250 Some(_tracked_account) => {
251 // Check that the Address is not already tracked
252 let derived_note_tag: NoteTag = address.to_note_tag();
253 let note_tag_record =
254 NoteTagRecord::with_account_source(derived_note_tag, account_id);
255 if self.store.get_note_tags().await?.contains(¬e_tag_record) {
256 return Err(ClientError::NoteTagDerivedAddressAlreadyTracked(
257 address_bench32,
258 derived_note_tag,
259 ));
260 }
261
262 self.store.insert_address(address, account_id).await?;
263 Ok(())
264 },
265 }
266 }
267
268 /// Removes an [`Address`] from the associated [`AccountId`], alongside its derived [`NoteTag`].
269 ///
270 /// # Errors
271 /// - If the account is not found on the network.
272 /// - If the address is not being tracked.
273 pub async fn remove_address(
274 &mut self,
275 address: Address,
276 account_id: AccountId,
277 ) -> Result<(), ClientError> {
278 self.store.remove_address(address, account_id).await?;
279 Ok(())
280 }
281
282 // ACCOUNT DATA RETRIEVAL
283 // --------------------------------------------------------------------------------------------
284
285 /// Returns a list of [`AccountHeader`] of all accounts stored in the database along with their
286 /// statuses.
287 ///
288 /// Said accounts' state is the state after the last performed sync.
289 pub async fn get_account_headers(
290 &self,
291 ) -> Result<Vec<(AccountHeader, AccountStatus)>, ClientError> {
292 self.store.get_account_headers().await.map_err(Into::into)
293 }
294
295 /// Retrieves a full [`AccountRecord`] object for the specified `account_id`. This result
296 /// represents data for the latest state known to the client, alongside its status. Returns
297 /// `None` if the account ID is not found.
298 pub async fn get_account(
299 &self,
300 account_id: AccountId,
301 ) -> Result<Option<AccountRecord>, ClientError> {
302 self.store.get_account(account_id).await.map_err(Into::into)
303 }
304
305 /// Retrieves an [`AccountHeader`] object for the specified [`AccountId`] along with its status.
306 /// Returns `None` if the account ID is not found.
307 ///
308 /// Said account's state is the state according to the last sync performed.
309 pub async fn get_account_header_by_id(
310 &self,
311 account_id: AccountId,
312 ) -> Result<Option<(AccountHeader, AccountStatus)>, ClientError> {
313 self.store.get_account_header(account_id).await.map_err(Into::into)
314 }
315
316 /// Attempts to retrieve an [`AccountRecord`] by its [`AccountId`].
317 ///
318 /// # Errors
319 ///
320 /// - If the account record is not found.
321 /// - If the underlying store operation fails.
322 pub async fn try_get_account(
323 &self,
324 account_id: AccountId,
325 ) -> Result<AccountRecord, ClientError> {
326 self.get_account(account_id)
327 .await?
328 .ok_or(ClientError::AccountDataNotFound(account_id))
329 }
330
331 /// Attempts to retrieve an [`AccountHeader`] by its [`AccountId`].
332 ///
333 /// # Errors
334 ///
335 /// - If the account header is not found.
336 /// - If the underlying store operation fails.
337 pub async fn try_get_account_header(
338 &self,
339 account_id: AccountId,
340 ) -> Result<(AccountHeader, AccountStatus), ClientError> {
341 self.get_account_header_by_id(account_id)
342 .await?
343 .ok_or(ClientError::AccountDataNotFound(account_id))
344 }
345}
346
347// UTILITY FUNCTIONS
348// ================================================================================================
349
350/// Builds an regular account ID from the provided parameters. The ID may be used along
351/// `Client::import_account_by_id` to import a public account from the network (provided that the
352/// used seed is known).
353///
354/// This function will only work for accounts with the [`BasicWallet`] and [`AuthRpoFalcon512`]
355/// components.
356///
357/// # Arguments
358/// - `init_seed`: Initial seed used to create the account. This is the seed passed to
359/// [`AccountBuilder::new`].
360/// - `public_key`: Public key of the account used in the [`AuthRpoFalcon512`] component.
361/// - `storage_mode`: Storage mode of the account.
362/// - `is_mutable`: Whether the account is mutable or not.
363///
364/// # Errors
365/// - If the account cannot be built.
366pub fn build_wallet_id(
367 init_seed: [u8; 32],
368 public_key: &PublicKey,
369 storage_mode: AccountStorageMode,
370 is_mutable: bool,
371 auth_scheme_id: u8,
372) -> Result<AccountId, ClientError> {
373 let account_type = if is_mutable {
374 AccountType::RegularAccountUpdatableCode
375 } else {
376 AccountType::RegularAccountImmutableCode
377 };
378
379 let auth_component = match auth_scheme_id {
380 0 => {
381 let auth_component: AccountComponent =
382 AuthRpoFalcon512::new(public_key.to_commitment()).into();
383 auth_component
384 },
385 1 => {
386 let auth_component: AccountComponent =
387 AuthEcdsaK256Keccak::new(public_key.to_commitment()).into();
388 auth_component
389 },
390 _ => {
391 return Err(ClientError::UnsupportedAuthSchemeId(auth_scheme_id));
392 },
393 };
394
395 let account = AccountBuilder::new(init_seed)
396 .account_type(account_type)
397 .storage_mode(storage_mode)
398 .with_auth_component(auth_component)
399 .with_component(BasicWallet)
400 .build()?;
401
402 Ok(account.id())
403}