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