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