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