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 rules for
5//! manipulating them. Once an account is registered with the client, its state will be updated
6//! 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, AccountBuilderSchemaCommitmentExt, AccountType, component::BasicWallet},
16//! # crypto::FeltRng
17//! # };
18//! # async fn add_new_account_example<AUTH>(
19//! # client: &mut miden_client::Client<AUTH>
20//! # ) -> Result<(), miden_client::ClientError> {
21//! # let random_seed = Default::default();
22//! let account = AccountBuilder::new(random_seed)
23//! .account_type(AccountType::Private)
24//! .with_component(BasicWallet)
25//! .build_with_schema_commitment()?;
26//!
27//! // Add the account to the client. The account already embeds its seed information.
28//! client.add_account(&account, false).await?;
29//! # Ok(())
30//! # }
31//! ```
32//!
33//! For more details on accounts, refer to the [Account] documentation.
34
35use alloc::string::{String, ToString};
36use alloc::vec::Vec;
37
38pub use miden_objects::account_file::{AccountFile, AccountFileError};
39use miden_protocol::Felt;
40use miden_protocol::account::auth::PublicKey;
41pub use miden_protocol::account::{
42 Account,
43 AccountBuilder,
44 AccountCode,
45 AccountComponent,
46 AccountComponentCode,
47 AccountDelta,
48 AccountHeader,
49 AccountId,
50 AccountIdPrefix,
51 AccountIdPrefixV1,
52 AccountIdV1,
53 AccountIdVersion,
54 AccountPatch,
55 AccountProcedureRoot,
56 AccountStorage,
57 AccountStoragePatch,
58 AccountType,
59 AccountUpdateDetails,
60 AccountVaultPatch,
61 PartialAccount,
62 PartialStorage,
63 PartialStorageMap,
64 RoleSymbol,
65 StorageMap,
66 StorageMapKey,
67 StorageMapKeyHash,
68 StorageMapPatch,
69 StorageMapPatchEntries,
70 StorageMapWitness,
71 StorageSlot,
72 StorageSlotContent,
73 StorageSlotId,
74 StorageSlotName,
75 StorageSlotPatch,
76 StorageSlotType,
77 StorageValuePatch,
78};
79pub use miden_protocol::address::{Address, AddressInterface, AddressType, NetworkId};
80use miden_protocol::asset::AssetVault;
81pub use miden_protocol::errors::{AccountIdError, AddressError, NetworkIdError};
82use miden_protocol::note::NoteTag;
83use miden_tx::utils::serde::{
84 ByteReader,
85 ByteWriter,
86 Deserializable,
87 DeserializationError,
88 Serializable,
89};
90
91/// Display-only metadata for a faucet account, persisted in the client's settings store.
92///
93/// Populated lazily by the CLI resolver from the on-chain token config of a public faucet and
94/// persisted under a `faucet_metadata:<faucet-id>` key.
95#[derive(Debug, Clone, PartialEq, Eq)]
96pub struct FaucetMetadata {
97 pub symbol: String,
98 pub decimals: u8,
99}
100
101impl Serializable for FaucetMetadata {
102 fn write_into<W: ByteWriter>(&self, target: &mut W) {
103 self.symbol.write_into(target);
104 target.write_u8(self.decimals);
105 }
106}
107
108impl Deserializable for FaucetMetadata {
109 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
110 let symbol = String::read_from(source)?;
111 let decimals = source.read_u8()?;
112 Ok(Self { symbol, decimals })
113 }
114}
115
116/// Decodes a fungible faucet token config slot value into display metadata.
117///
118/// Returns `None` when the value does not describe a fungible faucet config the protocol would
119/// accept: the symbol must decode as a [`TokenSymbol`], and the decimals must be within
120/// [`FungibleFaucet::MAX_DECIMALS`], which is what [`FungibleFaucet`] enforces when the component
121/// is built.
122fn faucet_metadata_from_token_config(token_config: [Felt; 4]) -> Option<FaucetMetadata> {
123 let [_token_supply, _max_supply, decimals, symbol] = token_config;
124
125 let symbol = TokenSymbol::try_from(symbol).ok()?;
126 let decimals = u8::try_from(decimals.as_canonical_u64()).ok()?;
127 if decimals > FungibleFaucet::MAX_DECIMALS {
128 return None;
129 }
130
131 Some(FaucetMetadata { symbol: symbol.to_string(), decimals })
132}
133
134mod account_reader;
135pub use account_reader::AccountReader;
136/// Raw access to `miden-standards` account modules for items not curated by `miden-client`.
137pub use miden_standards::account as standards;
138use miden_standards::account::auth::{Approver, AuthSingleSig, NetworkAccount};
139use miden_standards::account::faucets::FungibleFaucet;
140pub use miden_standards::account::inspection::{
141 AccountBuilderSchemaCommitmentExt,
142 AccountSchemaCommitment,
143};
144// RE-EXPORTS
145// ================================================================================================
146pub use miden_standards::account::interface::{
147 AccountComponentInterface,
148 AccountComponentInterfaceExt,
149 AccountInterface,
150 AccountInterfaceExt,
151};
152use miden_standards::account::wallets::BasicWallet;
153
154use super::Client;
155use crate::asset::TokenSymbol;
156use crate::errors::ClientError;
157use crate::rpc::domain::account::GetAccountRequest;
158use crate::rpc::node::{EndpointError, GetAccountError};
159use crate::store::{AccountStatus, AccountStorageFilter, ClientAccountType};
160use crate::sync::NoteTagRecord;
161
162pub mod component {
163 pub const MIDEN_PACKAGE_EXTENSION: &str = "masp";
164
165 pub use miden_protocol::account::auth::*;
166 pub use miden_protocol::account::component::{
167 FeltSchema,
168 InitStorageData,
169 InitStorageDataError,
170 MapSlotSchema,
171 SchemaRequirement,
172 SchemaType,
173 SchemaTypeError,
174 StorageSchema,
175 StorageSlotSchema,
176 StorageValueName,
177 StorageValueNameError,
178 ValueSlotSchema,
179 WordSchema,
180 WordValue,
181 };
182 pub use miden_protocol::account::{
183 AccountComponent,
184 AccountComponentMetadata,
185 AccountComponentName,
186 AccountProcedureRoot,
187 RoleSymbol,
188 };
189 pub use miden_standards::account::access::{
190 AccessControl,
191 Authority,
192 AuthorityError,
193 Ownable2Step,
194 Ownable2StepError,
195 Pausable,
196 PausableManager,
197 PausableStorage,
198 RoleBasedAccessControl,
199 };
200 pub use miden_standards::account::auth::*;
201 pub use miden_standards::account::components::StandardAccountComponent;
202 pub use miden_standards::account::faucets::{
203 Description,
204 ExternalLink,
205 FungibleFaucet,
206 FungibleFaucetBuilder,
207 FungibleFaucetError,
208 LogoURI,
209 NonFungibleFaucet,
210 TokenMetadata,
211 TokenMetadataError,
212 TokenName,
213 create_network_fungible_faucet,
214 create_singlesig_user_fungible_faucet,
215 };
216 pub use miden_standards::account::fees::{
217 BasicConstantFeePolicy,
218 FeePolicy,
219 FeePolicyError,
220 FeePolicyManager,
221 FeePolicyManagerBuilder,
222 };
223 pub use miden_standards::account::policies::{
224 AllowlistManager,
225 AllowlistStorage,
226 BasicAllowlist,
227 BasicBlocklist,
228 BlocklistManager,
229 BlocklistStorage,
230 BurnAllowAll,
231 BurnOwnerOnly,
232 BurnPolicy,
233 BurnPolicyError,
234 MinBurnAmount,
235 MintAllowAll,
236 MintOwnerOnly,
237 MintPolicy,
238 MintPolicyError,
239 TokenPolicyManager,
240 TokenPolicyManagerBuilder,
241 TransferAllowAll,
242 TransferPolicy,
243 TransferPolicyError,
244 };
245 pub use miden_standards::account::wallets::BasicWallet;
246}
247
248// CLIENT METHODS
249// ================================================================================================
250
251/// This section of the [Client] contains methods for:
252///
253/// - **Account creation:** Use the [`AccountBuilder`] to construct new accounts, specifying account
254/// visibility (`AccountType::Public` / `AccountType::Private`) and attaching necessary components
255/// (e.g., basic wallet or fungible faucet). Prefer
256/// [`AccountBuilderSchemaCommitmentExt::build_with_schema_commitment`] so the account includes
257/// merged storage schema commitment metadata; use plain [`AccountBuilder::build`] only when you
258/// need to opt out. After creation, accounts can be added to the client.
259///
260/// - **Account tracking:** Accounts added via the client are persisted to the local store, where
261/// their state (including nonce, balance, and metadata) is updated upon every synchronization
262/// with the network.
263///
264/// - **Account registration:** On a network that enforces an account allowlist,
265/// [`Client::register_account`] binds an invitation code to a new account before its first
266/// transaction creates it on chain, and [`Client::is_account_allowed`] asks whether the network
267/// accepts the creation of an account.
268///
269/// - **Data retrieval:** The module also provides methods to fetch account-related data.
270impl<AUTH> Client<AUTH> {
271 // ACCOUNT CREATION
272 // --------------------------------------------------------------------------------------------
273
274 /// Adds the provided [Account] in the store so it can start being tracked by the client.
275 ///
276 /// If the account is already being tracked and `overwrite` is set to `true`, the account will
277 /// be overwritten. Newly created accounts must embed their seed (`account.seed()` must return
278 /// `Some(_)`).
279 ///
280 /// # Errors
281 ///
282 /// - If the account is new but it does not contain the seed.
283 /// - If the account is already tracked and `overwrite` is set to `false`.
284 /// - If `overwrite` is set to `true` and the `account_data` nonce is lower than the one already
285 /// being tracked.
286 /// - If `overwrite` is set to `true` and the `account_data` commitment doesn't match the
287 /// network's account commitment.
288 pub async fn add_account(
289 &mut self,
290 account: &Account,
291 overwrite: bool,
292 ) -> Result<(), ClientError> {
293 self.add_account_inner(account, ClientAccountType::Native, overwrite).await
294 }
295
296 // ACCOUNT REGISTRATION
297 // --------------------------------------------------------------------------------------------
298
299 /// Binds an invitation code to a tracked account on the network allowlist.
300 ///
301 /// A network that enforces an account allowlist creates an account on chain only when the
302 /// account is registered. The first transaction of an account is what creates it, so the
303 /// account must be registered before that transaction is submitted.
304 /// [`Client::submit_new_transaction`] and [`BatchBuilder::submit`] ask the node first, and fail
305 /// with [`ClientError::AccountNotAllowlisted`] for an account the network does not accept. Only
306 /// account creation is gated: an account that already exists on chain is never checked, and
307 /// network accounts are exempt.
308 ///
309 /// The account must be tracked by the client, must not be deployed on chain yet, and must not
310 /// be a network account. The invitation code must exist on the node and must not be bound to
311 /// another account. A registration consumes the code, so the client asks the node first and
312 /// does not send the code for an account the node already allows.
313 ///
314 /// When the network operator runs a funding service, the node pays the registered account a
315 /// public P2ID note with the native asset. The node answers once the funding service queues the
316 /// note, before the note is committed. The note is not part of the response, and the client
317 /// does not see it until a [`Client::sync_state`] runs after the note is committed. The client
318 /// tracks the note tag of every account it owns, so that sync imports the note and
319 /// [`Client::get_consumable_notes`] lists it. Sync again until the note arrives. The account
320 /// then consumes the note in its first transaction. That transaction creates the account on
321 /// chain and pays its fee out of the received funds.
322 ///
323 /// # Errors
324 ///
325 /// - [`ClientError::AccountDataNotFound`] if the client does not track the account.
326 /// - [`ClientError::AccountIsNotNew`] if the account already exists on chain.
327 /// - [`ClientError::AccountIsNetworkAccount`] if the account is a network account. The node
328 /// admits network accounts without a code.
329 /// - [`ClientError::AccountAlreadyAllowed`] if the node already allows the account, because it
330 /// is registered or because the network does not enforce an allowlist. The code is not sent.
331 /// - [`ClientError::RpcError`] carrying a [`RegisterAccountError`] if the node rejects the
332 /// code or the account, or an `Unavailable` status if the funding failed. In the second
333 /// case the account stays registered, so a retry fails with
334 /// [`ClientError::AccountAlreadyAllowed`] and the account has to be funded another way.
335 ///
336 /// [`BatchBuilder::submit`]: crate::transaction::BatchBuilder::submit
337 /// [`RegisterAccountError`]: crate::rpc::RegisterAccountError
338 pub async fn register_account(
339 &self,
340 account_id: AccountId,
341 invitation_code: &str,
342 ) -> Result<(), ClientError> {
343 let (_, status) = self
344 .store
345 .get_account_header(account_id)
346 .await?
347 .ok_or(ClientError::AccountDataNotFound(account_id))?;
348 if !status.is_new() {
349 return Err(ClientError::AccountIsNotNew(account_id));
350 }
351
352 let account = self
353 .get_account(account_id)
354 .await?
355 .ok_or(ClientError::AccountDataNotFound(account_id))?;
356 // The node admits a network account without a code.
357 if NetworkAccount::new(account).is_ok() {
358 return Err(ClientError::AccountIsNetworkAccount(account_id));
359 }
360 // A registration consumes the code, so do not send it when the node already allows the
361 // account.
362 if self.is_account_allowed(account_id).await? {
363 return Err(ClientError::AccountAlreadyAllowed(account_id));
364 }
365
366 self.rpc_api.register_account(invitation_code, account_id).await?;
367
368 Ok(())
369 }
370
371 /// Returns whether the network lets `account_id` be created on chain.
372 ///
373 /// The node answers `true` when it does not enforce an account allowlist, or when the account
374 /// is registered. See [`Client::register_account`] for how an account gets registered.
375 pub async fn is_account_allowed(&self, account_id: AccountId) -> Result<bool, ClientError> {
376 Ok(self.rpc_api.is_account_allowed(account_id).await?)
377 }
378
379 /// Returns whether a transaction against `account_id` creates an account that the network
380 /// allowlist gates.
381 ///
382 /// Only a new account is gated, and a network account is exempt. The answer is `false` for an
383 /// account that the client does not track.
384 pub(crate) async fn is_allowlist_gated(
385 &self,
386 account_id: AccountId,
387 ) -> Result<bool, ClientError> {
388 let Some((_, status)) = self.store.get_account_header(account_id).await? else {
389 return Ok(false);
390 };
391 if !status.is_new() {
392 return Ok(false);
393 }
394
395 let Some(account) = self.get_account(account_id).await? else {
396 return Ok(false);
397 };
398
399 Ok(NetworkAccount::new(account).is_err())
400 }
401
402 /// Inserts `account` into the store (or overwrites it if `overwrite` is true) and registers the
403 /// per-account note tag if `client_account_type` is [`ClientAccountType::Native`].
404 ///
405 /// Switching the [`ClientAccountType`] of an already-tracked account is not supported and
406 /// returns [`ClientError::AccountWatchedMismatch`].
407 async fn add_account_inner(
408 &mut self,
409 account: &Account,
410 client_account_type: ClientAccountType,
411 overwrite: bool,
412 ) -> Result<(), ClientError> {
413 if account.is_new() {
414 if account.seed().is_none() {
415 return Err(ClientError::AddNewAccountWithoutSeed);
416 }
417 } else {
418 // Ignore the seed since it's not a new account
419 if account.seed().is_some() {
420 tracing::warn!(
421 "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."
422 );
423 }
424 }
425
426 let tracked_account = self.store.get_minimal_partial_account(account.id()).await?;
427
428 match tracked_account {
429 None => {
430 let default_address = Address::new(account.id());
431
432 self.store
433 .insert_account(account, default_address.clone(), client_account_type)
434 .await
435 .map_err(ClientError::StoreError)?;
436
437 if matches!(client_account_type, ClientAccountType::Native) {
438 // Set the default address note tag so sync pulls notes.
439 let default_address_note_tag = default_address.to_note_tag();
440 let note_tag_record =
441 NoteTagRecord::with_account_source(default_address_note_tag, account.id());
442 self.store.add_note_tag(note_tag_record).await?;
443 }
444
445 Ok(())
446 },
447 Some(tracked_account) => {
448 if !overwrite {
449 // Only overwrite the account if the flag is set to `true`
450 return Err(ClientError::AccountAlreadyTracked(account.id()));
451 }
452
453 if client_account_type != tracked_account.client_account_type() {
454 // Switching between Watched and Native after the account is tracked is not
455 // supported: the per-account note tag and any client-side state derived from
456 // that mode are set up at insertion time and not migrated on the fly.
457 return Err(ClientError::AccountWatchedMismatch(account.id()));
458 }
459
460 if tracked_account.nonce().as_canonical_u64() > account.nonce().as_canonical_u64() {
461 // If the new account is older than the one being tracked, return an error
462 return Err(ClientError::AccountNonceTooLow);
463 }
464
465 if tracked_account.is_locked() {
466 // If the tracked account is locked, check that the account commitment matches
467 // the one in the network
468 let network_account_commitment = self
469 .rpc_api
470 .get_account(account.id(), GetAccountRequest::new())
471 .await?
472 .1
473 .account_commitment();
474 if network_account_commitment != account.to_commitment() {
475 return Err(ClientError::AccountCommitmentMismatch(
476 network_account_commitment,
477 ));
478 }
479 }
480
481 self.store.update_account(account).await?;
482
483 Ok(())
484 },
485 }
486 }
487
488 /// Imports an account from the network to the client's store. The account needs to be public
489 /// and be tracked by the network, it will be fetched by its ID. If the account was already
490 /// being tracked by the client, its state will be overwritten.
491 ///
492 /// To import an account as watched (state-tracking only, no note sync), use
493 /// [`Self::import_watched_account_by_id`] instead. Switching an already-tracked account between
494 /// Native and Watched is not supported.
495 ///
496 /// # Errors
497 /// - If the account is not found on the network.
498 /// - If the account is private.
499 /// - If the account is already tracked as watched.
500 /// - There was an error sending the request to the network.
501 pub async fn import_account_by_id(&mut self, account_id: AccountId) -> Result<(), ClientError> {
502 let account = self.fetch_public_account(account_id).await?;
503 self.add_account_inner(&account, ClientAccountType::Native, true).await
504 }
505
506 /// Starts watching an on-chain account ([`ClientAccountType::Watched`]).
507 ///
508 /// Like [`Self::import_account_by_id`], the account is fetched from the network by its ID.
509 /// Unlike `import_account_by_id`, the account is added without registering its derived note
510 /// tag: `sync_state` will keep the account's commitment, nonce and storage up to date but will
511 /// **not** pull notes targeted at it.
512 ///
513 /// If the account is already being tracked as watched its state is overwritten. Switching an
514 /// already-tracked native account to watched is not supported.
515 ///
516 /// # Errors
517 /// - If the account is not found on the network.
518 /// - If the account is private.
519 /// - If the account is already tracked as native.
520 /// - There was an error sending the request to the network.
521 pub async fn import_watched_account_by_id(
522 &mut self,
523 account_id: AccountId,
524 ) -> Result<(), ClientError> {
525 let account = self.fetch_public_account(account_id).await?;
526 self.add_account_inner(&account, ClientAccountType::Watched, true).await
527 }
528
529 /// Fetches a public [`Account`] from the network, returning a typed error when the account
530 /// doesn't exist on chain or is private.
531 async fn fetch_public_account(&self, account_id: AccountId) -> Result<Account, ClientError> {
532 let fetched_account =
533 self.rpc_api.get_account_details(account_id).await.map_err(|err| {
534 match err.endpoint_error() {
535 Some(EndpointError::GetAccount(GetAccountError::AccountNotFound)) => {
536 ClientError::AccountNotFoundOnChain(account_id)
537 },
538 _ => ClientError::RpcError(err),
539 }
540 })?;
541
542 fetched_account.ok_or(ClientError::AccountIsPrivate(account_id))
543 }
544
545 /// Fetches a public faucet's display metadata from the network.
546 ///
547 /// Uses [`get_account`](crate::rpc::NodeRpcClient::get_account) with a minimal request so that
548 /// the node does not return vault data. The faucet's token config lives in a single value slot,
549 /// which is always present in the returned storage header.
550 ///
551 /// Returns:
552 /// - `Ok(Some(_))` — the account is public and its token config storage slot decoded.
553 /// - `Ok(None)` — the account is private, not on chain, or the storage slot does not parse
554 /// as a token config. Caller should fall back to a raw display.
555 /// - `Err(_)` — transport-level RPC error.
556 pub async fn fetch_remote_token_metadata(
557 &self,
558 faucet_id: AccountId,
559 ) -> Result<Option<FaucetMetadata>, ClientError> {
560 let proof = match self.rpc_api.get_account(faucet_id, GetAccountRequest::new()).await {
561 Ok((_, proof)) => proof,
562 Err(err) => match err.endpoint_error() {
563 Some(EndpointError::GetAccount(
564 GetAccountError::AccountNotFound | GetAccountError::AccountNotPublic,
565 )) => return Ok(None),
566 _ => return Err(ClientError::RpcError(err)),
567 },
568 };
569
570 let Some(storage_header) = proof.storage_header() else {
571 return Ok(None);
572 };
573
574 let Some(slot_header) =
575 storage_header.find_slot_header_by_name(FungibleFaucet::token_config_slot())
576 else {
577 return Ok(None);
578 };
579
580 Ok(faucet_metadata_from_token_config(*slot_header.value()))
581 }
582
583 /// Adds an [`Address`] to the associated [`AccountId`], alongside its derived [`NoteTag`]. If
584 /// the account is tracked as watched, the note tag is not registered.
585 ///
586 /// # Errors
587 /// - If the account is not found on the network.
588 /// - If the address is already being tracked.
589 pub async fn add_address(
590 &mut self,
591 address: Address,
592 account_id: AccountId,
593 ) -> Result<(), ClientError> {
594 let network_id = self.rpc_api.get_network_id().await?;
595 let address_bench32 = address.encode(network_id);
596 if self.store.get_addresses_by_account_id(account_id).await?.contains(&address) {
597 return Err(ClientError::AddressAlreadyTracked(address_bench32));
598 }
599
600 let tracked_account = self.store.get_minimal_partial_account(account_id).await?;
601 match tracked_account {
602 None => Err(ClientError::AccountDataNotFound(account_id)),
603 Some(tracked_account) => {
604 self.store.insert_address(address.clone(), account_id).await?;
605 // Watched accounts intentionally have no derived note tag registered to avoid sync
606 // state pulling notes for them.
607 if !tracked_account.is_watched() {
608 let derived_note_tag: NoteTag = address.to_note_tag();
609 let note_tag_record =
610 NoteTagRecord::with_account_source(derived_note_tag, account_id);
611 self.store.add_note_tag(note_tag_record).await?;
612 }
613 Ok(())
614 },
615 }
616 }
617
618 /// Removes an [`Address`] from the associated [`AccountId`], alongside its derived [`NoteTag`].
619 ///
620 /// Returns `true` if the address was tracked. If it wasn't, this is a no-op: the derived tag is
621 /// left in place, since it may have been registered by something other than this address.
622 pub async fn remove_address(
623 &mut self,
624 address: Address,
625 account_id: AccountId,
626 ) -> Result<bool, ClientError> {
627 let derived_note_tag = address.to_note_tag();
628 let note_tag_record = NoteTagRecord::with_account_source(derived_note_tag, account_id);
629 if !self.store.remove_address(address).await? {
630 return Ok(false);
631 }
632 // Remove the note tag if no other address are associated with it.
633 let addresses = self.store.get_addresses_by_account_id(account_id).await?;
634 if addresses.iter().all(|address| address.to_note_tag() != derived_note_tag) {
635 self.store.remove_note_tag(note_tag_record).await?;
636 }
637 Ok(true)
638 }
639
640 // ACCOUNT DATA RETRIEVAL
641 // --------------------------------------------------------------------------------------------
642
643 /// Retrieves the asset vault for a specific account.
644 ///
645 /// To check the balance for a single asset, use [`Client::account_reader`] instead.
646 pub async fn get_account_vault(
647 &self,
648 account_id: AccountId,
649 ) -> Result<AssetVault, ClientError> {
650 self.store.get_account_vault(account_id).await.map_err(ClientError::StoreError)
651 }
652
653 /// Retrieves the whole account storage for a specific account.
654 ///
655 /// To only load a specific slot, use [`Client::account_reader`] instead.
656 pub async fn get_account_storage(
657 &self,
658 account_id: AccountId,
659 ) -> Result<AccountStorage, ClientError> {
660 self.store
661 .get_account_storage(account_id, AccountStorageFilter::All)
662 .await
663 .map_err(ClientError::StoreError)
664 }
665
666 /// Retrieves the account code for a specific account.
667 ///
668 /// Returns `None` if the account is not found.
669 pub async fn get_account_code(
670 &self,
671 account_id: AccountId,
672 ) -> Result<Option<AccountCode>, ClientError> {
673 self.store.get_account_code(account_id).await.map_err(ClientError::StoreError)
674 }
675
676 /// Returns a list of [`AccountHeader`] of all accounts stored in the database along with their
677 /// statuses.
678 ///
679 /// Said accounts' state is the state after the last performed sync.
680 pub async fn get_account_headers(
681 &self,
682 ) -> Result<Vec<(AccountHeader, AccountStatus)>, ClientError> {
683 self.store.get_account_headers().await.map_err(Into::into)
684 }
685
686 /// Returns the [`AccountHeader`] of the account with the specified ID along with its status, or
687 /// `None` if the account isn't tracked by the client.
688 ///
689 /// Said account's state is the state after the last performed sync.
690 pub async fn get_account_header(
691 &self,
692 account_id: AccountId,
693 ) -> Result<Option<(AccountHeader, AccountStatus)>, ClientError> {
694 self.store.get_account_header(account_id).await.map_err(Into::into)
695 }
696
697 /// Retrieves the full [`Account`] object from the store, returning `None` if not found.
698 ///
699 /// This method loads the complete account state including vault, storage, and code — including
700 /// building the vault's Merkle tree. For lazy access that fetches only the data you need
701 /// (existence checks, single fields, storage items), use [`Client::account_reader`] instead.
702 pub async fn get_account(&self, account_id: AccountId) -> Result<Option<Account>, ClientError> {
703 match self.store.get_account(account_id).await? {
704 Some(record) => Ok(Some(record.try_into()?)),
705 None => Ok(None),
706 }
707 }
708
709 /// Creates an [`AccountReader`] for lazy access to account data.
710 ///
711 /// The `AccountReader` provides lazy access to account state - each method call fetches fresh
712 /// data from storage, ensuring you always see the current state.
713 ///
714 /// For loading the full [`Account`] object, use [`Client::get_account`] instead.
715 ///
716 /// # Example
717 /// ```ignore
718 /// let reader = client.account_reader(account_id);
719 ///
720 /// // Each call fetches fresh data
721 /// let nonce = reader.nonce().await?;
722 /// let balance = reader.get_balance(faucet_id).await?;
723 ///
724 /// // Storage access is integrated
725 /// let value = reader.get_storage_item("my_slot").await?;
726 /// let (map_value, witness) = reader.get_storage_map_witness("balances", key).await?;
727 /// ```
728 pub fn account_reader(&self, account_id: AccountId) -> AccountReader {
729 AccountReader::new(self.store.clone(), account_id)
730 }
731
732 /// Prunes historical account states for the specified account up to the given nonce.
733 ///
734 /// Deletes all historical entries with `replaced_at_nonce <= up_to_nonce` and any orphaned
735 /// account code.
736 ///
737 /// Returns the total number of rows deleted, including historical entries and orphaned account
738 /// code.
739 pub async fn prune_account_history(
740 &self,
741 account_id: AccountId,
742 up_to_nonce: Felt,
743 ) -> Result<usize, ClientError> {
744 Ok(self.store.prune_account_history(account_id, up_to_nonce).await?)
745 }
746}
747
748// UTILITY FUNCTIONS
749// ================================================================================================
750
751/// Builds an regular account ID from the provided parameters. The ID may be used along
752/// `Client::import_account_by_id` to import a public account from the network (provided that the
753/// used seed is known).
754///
755/// This function currently supports accounts composed of the [`BasicWallet`] component and one of
756/// the supported authentication schemes ([`AuthSingleSig`]).
757///
758/// # Arguments
759/// - `init_seed`: Initial seed used to create the account. This is the seed passed to
760/// [`AccountBuilder::new`].
761/// - `public_key`: Public key of the account used for the authentication component.
762/// - `account_visibility`: Public/private visibility of the account.
763///
764/// # Errors
765/// - If the account cannot be built.
766pub fn build_wallet_id(
767 init_seed: [u8; 32],
768 public_key: &PublicKey,
769 account_visibility: AccountType,
770) -> Result<AccountId, ClientError> {
771 let auth_scheme = public_key.auth_scheme();
772 let auth_component: AccountComponent =
773 AuthSingleSig::new(Approver::new(public_key.to_commitment(), auth_scheme)).into();
774
775 let account = AccountBuilder::new(init_seed)
776 .account_type(account_visibility)
777 .with_component(auth_component)
778 .with_component(BasicWallet)
779 .build_with_schema_commitment()?;
780
781 Ok(account.id())
782}
783
784#[cfg(test)]
785mod schema_commitment_tests {
786 use miden_protocol::EMPTY_WORD;
787 use miden_protocol::account::auth::AuthSecretKey;
788 use miden_standards::account::inspection::AccountSchemaCommitment;
789
790 use super::{
791 AccountBuilder,
792 AccountBuilderSchemaCommitmentExt,
793 AccountType,
794 Approver,
795 AuthSingleSig,
796 BasicWallet,
797 };
798 use crate::auth::AuthSchemeId;
799
800 #[test]
801 fn wallet_build_includes_schema_commitment_metadata_slot() {
802 let key = AuthSecretKey::new_falcon512_poseidon2();
803 let account = AccountBuilder::new([2u8; 32])
804 .account_type(AccountType::Private)
805 .with_component(AuthSingleSig::new(Approver::new(
806 key.public_key().to_commitment(),
807 AuthSchemeId::Falcon512Poseidon2,
808 )))
809 .with_component(BasicWallet)
810 .build_with_schema_commitment()
811 .expect("build_with_schema_commitment");
812
813 let commitment = account
814 .storage()
815 .get_item(AccountSchemaCommitment::schema_commitment_slot())
816 .expect("schema commitment slot");
817 assert_ne!(commitment, EMPTY_WORD);
818 }
819}
820
821#[cfg(test)]
822mod faucet_metadata_tests {
823 use miden_protocol::Felt;
824
825 use super::{FungibleFaucet, TokenSymbol, faucet_metadata_from_token_config};
826
827 /// Builds a token config slot value carrying the given decimals and the symbol "TST".
828 fn token_config(decimals: u32) -> [Felt; 4] {
829 [
830 Felt::from(0u32),
831 Felt::from(0u32),
832 Felt::from(decimals),
833 TokenSymbol::new("TST").unwrap().as_element(),
834 ]
835 }
836
837 #[test]
838 fn decodes_a_config_within_the_protocol_bounds() {
839 let metadata = faucet_metadata_from_token_config(token_config(8)).unwrap();
840
841 assert_eq!(metadata.symbol, "TST");
842 assert_eq!(metadata.decimals, 8);
843 }
844
845 #[test]
846 fn accepts_the_maximum_supported_decimals() {
847 let max = u32::from(FungibleFaucet::MAX_DECIMALS);
848 let metadata = faucet_metadata_from_token_config(token_config(max)).unwrap();
849
850 assert_eq!(metadata.decimals, FungibleFaucet::MAX_DECIMALS);
851 }
852
853 #[test]
854 fn rejects_decimals_above_the_maximum() {
855 let above_max = u32::from(FungibleFaucet::MAX_DECIMALS) + 1;
856
857 assert!(faucet_metadata_from_token_config(token_config(above_max)).is_none());
858 assert!(faucet_metadata_from_token_config(token_config(200)).is_none());
859 }
860
861 #[test]
862 fn rejects_decimals_that_do_not_fit_a_u8() {
863 assert!(faucet_metadata_from_token_config(token_config(300)).is_none());
864 }
865
866 #[test]
867 fn rejects_a_symbol_that_is_not_a_token_symbol() {
868 let mut config = token_config(8);
869 config[3] = Felt::from(0u32);
870
871 assert!(faucet_metadata_from_token_config(config).is_none());
872 }
873}