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 the invitation code can be used to register an account.
383 ///
384 /// The node answers `true` when it does not enforce an account allowlist, or when the code
385 /// exists and is not registered to an account. Unknown and registered codes return `false`. An
386 /// empty code returns an error when enforcement is enabled. This query does not consume the
387 /// code or identify the account that holds it. See [`Client::register_account`] to register an
388 /// account.
389 pub async fn is_invitation_code_valid(
390 &self,
391 invitation_code: &str,
392 ) -> Result<bool, ClientError> {
393 Ok(self.rpc_api.is_invitation_code_valid(invitation_code).await?)
394 }
395
396 /// Returns whether a transaction against `account_id` creates an account that the network
397 /// allowlist gates.
398 ///
399 /// Only a new account is gated, and a network account is exempt. The answer is `false` for an
400 /// account that the client does not track.
401 pub(crate) async fn is_allowlist_gated(
402 &self,
403 account_id: AccountId,
404 ) -> Result<bool, ClientError> {
405 let Some((_, status)) = self.store.get_account_header(account_id).await? else {
406 return Ok(false);
407 };
408 if !status.is_new() {
409 return Ok(false);
410 }
411
412 let Some(account) = self.get_account(account_id).await? else {
413 return Ok(false);
414 };
415
416 Ok(NetworkAccount::new(account).is_err())
417 }
418
419 /// Inserts `account` into the store (or overwrites it if `overwrite` is true) and registers the
420 /// per-account note tag if `client_account_type` is [`ClientAccountType::Native`].
421 ///
422 /// Switching the [`ClientAccountType`] of an already-tracked account is not supported and
423 /// returns [`ClientError::AccountWatchedMismatch`].
424 async fn add_account_inner(
425 &mut self,
426 account: &Account,
427 client_account_type: ClientAccountType,
428 overwrite: bool,
429 ) -> Result<(), ClientError> {
430 if account.is_new() {
431 if account.seed().is_none() {
432 return Err(ClientError::AddNewAccountWithoutSeed);
433 }
434 } else {
435 // Ignore the seed since it's not a new account
436 if account.seed().is_some() {
437 tracing::warn!(
438 "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."
439 );
440 }
441 }
442
443 let tracked_account = self.store.get_minimal_partial_account(account.id()).await?;
444
445 match tracked_account {
446 None => {
447 let default_address = Address::new(account.id());
448
449 self.store
450 .insert_account(account, default_address.clone(), client_account_type)
451 .await
452 .map_err(ClientError::StoreError)?;
453
454 if matches!(client_account_type, ClientAccountType::Native) {
455 // Set the default address note tag so sync pulls notes.
456 let default_address_note_tag = default_address.to_note_tag();
457 let note_tag_record =
458 NoteTagRecord::with_account_source(default_address_note_tag, account.id());
459 self.store.add_note_tag(note_tag_record).await?;
460 }
461
462 Ok(())
463 },
464 Some(tracked_account) => {
465 if !overwrite {
466 // Only overwrite the account if the flag is set to `true`
467 return Err(ClientError::AccountAlreadyTracked(account.id()));
468 }
469
470 if client_account_type != tracked_account.client_account_type() {
471 // Switching between Watched and Native after the account is tracked is not
472 // supported: the per-account note tag and any client-side state derived from
473 // that mode are set up at insertion time and not migrated on the fly.
474 return Err(ClientError::AccountWatchedMismatch(account.id()));
475 }
476
477 if tracked_account.nonce().as_canonical_u64() > account.nonce().as_canonical_u64() {
478 // If the new account is older than the one being tracked, return an error
479 return Err(ClientError::AccountNonceTooLow);
480 }
481
482 if tracked_account.is_locked() {
483 // If the tracked account is locked, check that the account commitment matches
484 // the one in the network
485 let network_account_commitment = self
486 .rpc_api
487 .get_account(account.id(), GetAccountRequest::new())
488 .await?
489 .1
490 .account_commitment();
491 if network_account_commitment != account.to_commitment() {
492 return Err(ClientError::AccountCommitmentMismatch(
493 network_account_commitment,
494 ));
495 }
496 }
497
498 self.store.update_account(account).await?;
499
500 Ok(())
501 },
502 }
503 }
504
505 /// Imports an account from the network to the client's store. The account needs to be public
506 /// and be tracked by the network, it will be fetched by its ID. If the account was already
507 /// being tracked by the client, its state will be overwritten.
508 ///
509 /// To import an account as watched (state-tracking only, no note sync), use
510 /// [`Self::import_watched_account_by_id`] instead. Switching an already-tracked account between
511 /// Native and Watched is not supported.
512 ///
513 /// # Errors
514 /// - If the account is not found on the network.
515 /// - If the account is private.
516 /// - If the account is already tracked as watched.
517 /// - There was an error sending the request to the network.
518 pub async fn import_account_by_id(&mut self, account_id: AccountId) -> Result<(), ClientError> {
519 let account = self.fetch_public_account(account_id).await?;
520 self.add_account_inner(&account, ClientAccountType::Native, true).await
521 }
522
523 /// Starts watching an on-chain account ([`ClientAccountType::Watched`]).
524 ///
525 /// Like [`Self::import_account_by_id`], the account is fetched from the network by its ID.
526 /// Unlike `import_account_by_id`, the account is added without registering its derived note
527 /// tag: `sync_state` will keep the account's commitment, nonce and storage up to date but will
528 /// **not** pull notes targeted at it.
529 ///
530 /// If the account is already being tracked as watched its state is overwritten. Switching an
531 /// already-tracked native account to watched is not supported.
532 ///
533 /// # Errors
534 /// - If the account is not found on the network.
535 /// - If the account is private.
536 /// - If the account is already tracked as native.
537 /// - There was an error sending the request to the network.
538 pub async fn import_watched_account_by_id(
539 &mut self,
540 account_id: AccountId,
541 ) -> Result<(), ClientError> {
542 let account = self.fetch_public_account(account_id).await?;
543 self.add_account_inner(&account, ClientAccountType::Watched, true).await
544 }
545
546 // ACCOUNT WITNESS PREFETCHING
547 // --------------------------------------------------------------------------------------------
548
549 /// Registers an account whose account witness [`Client::sync_chain`] keeps up to date, so that
550 /// transactions using it as a foreign account resolve the witness locally. This trades one
551 /// request per transaction for one per sync.
552 ///
553 /// A [`ForeignAccount::Private`](crate::transaction::ForeignAccount) needs nothing else, since
554 /// the caller supplies the account data. A
555 /// [`ForeignAccount::Public`](crate::transaction::ForeignAccount) additionally has to be
556 /// tracked by this client, so that its code, storage and vault come from the store as well;
557 /// registering an untracked public account costs a request per sync and saves none.
558 ///
559 /// The witness is fetched by the next sync, not by this call. Transactions assume that a sync
560 /// ran after the account was registered.
561 ///
562 /// The account is not validated against the network here. The sync fails while a registered
563 /// account has no witness that the node can return, so an account that is not in the account
564 /// tree blocks the sync until it is unregistered.
565 ///
566 /// Registering an already registered account is a no-op and keeps any cached witness.
567 ///
568 /// Returns `true` if the account was not registered before this call.
569 pub async fn track_account_witness(&self, account_id: AccountId) -> Result<bool, ClientError> {
570 self.store.track_account_witness(account_id).await.map_err(Into::into)
571 }
572
573 /// Stops keeping the account's witness up to date and drops the cached one.
574 ///
575 /// Returns `true` if the account was registered. Transactions using it keep working, falling
576 /// back to fetching the witness from the node.
577 pub async fn untrack_account_witness(
578 &self,
579 account_id: AccountId,
580 ) -> Result<bool, ClientError> {
581 self.store.untrack_account_witness(account_id).await.map_err(Into::into)
582 }
583
584 /// Returns the IDs of every account registered via [`Client::track_account_witness`].
585 pub async fn tracked_account_witnesses(&self) -> Result<Vec<AccountId>, ClientError> {
586 self.store.tracked_account_witnesses().await.map_err(Into::into)
587 }
588
589 /// Fetches a public [`Account`] from the network, returning a typed error when the account
590 /// doesn't exist on chain or is private.
591 async fn fetch_public_account(&self, account_id: AccountId) -> Result<Account, ClientError> {
592 let fetched_account =
593 self.rpc_api.get_account_details(account_id).await.map_err(|err| {
594 match err.endpoint_error() {
595 Some(EndpointError::GetAccount(GetAccountError::AccountNotFound)) => {
596 ClientError::AccountNotFoundOnChain(account_id)
597 },
598 _ => ClientError::RpcError(err),
599 }
600 })?;
601
602 fetched_account.ok_or(ClientError::AccountIsPrivate(account_id))
603 }
604
605 /// Fetches a public faucet's display metadata from the network.
606 ///
607 /// Uses [`get_account`](crate::rpc::NodeRpcClient::get_account) with a minimal request so that
608 /// the node does not return vault data. The faucet's token config lives in a single value slot,
609 /// which is always present in the returned storage header.
610 ///
611 /// Returns:
612 /// - `Ok(Some(_))` — the account is public and its token config storage slot decoded.
613 /// - `Ok(None)` — the account is private, not on chain, or the storage slot does not parse
614 /// as a token config. Caller should fall back to a raw display.
615 /// - `Err(_)` — transport-level RPC error.
616 pub async fn fetch_remote_token_metadata(
617 &self,
618 faucet_id: AccountId,
619 ) -> Result<Option<FaucetMetadata>, ClientError> {
620 let proof = match self.rpc_api.get_account(faucet_id, GetAccountRequest::new()).await {
621 Ok((_, proof)) => proof,
622 Err(err) => match err.endpoint_error() {
623 Some(EndpointError::GetAccount(
624 GetAccountError::AccountNotFound | GetAccountError::AccountNotPublic,
625 )) => return Ok(None),
626 _ => return Err(ClientError::RpcError(err)),
627 },
628 };
629
630 let Some(storage_header) = proof.storage_header() else {
631 return Ok(None);
632 };
633
634 let Some(slot_header) =
635 storage_header.find_slot_header_by_name(FungibleFaucet::token_config_slot())
636 else {
637 return Ok(None);
638 };
639
640 Ok(faucet_metadata_from_token_config(*slot_header.value()))
641 }
642
643 /// Adds an [`Address`] to the associated [`AccountId`], alongside its derived [`NoteTag`]. If
644 /// the account is tracked as watched, the note tag is not registered.
645 ///
646 /// # Errors
647 /// - If the account is not found on the network.
648 /// - If the address is already being tracked.
649 pub async fn add_address(
650 &mut self,
651 address: Address,
652 account_id: AccountId,
653 ) -> Result<(), ClientError> {
654 let network_id = self.rpc_api.get_network_id().await?;
655 let address_bench32 = address.encode(network_id);
656 if self.store.get_addresses_by_account_id(account_id).await?.contains(&address) {
657 return Err(ClientError::AddressAlreadyTracked(address_bench32));
658 }
659
660 let tracked_account = self.store.get_minimal_partial_account(account_id).await?;
661 match tracked_account {
662 None => Err(ClientError::AccountDataNotFound(account_id)),
663 Some(tracked_account) => {
664 self.store.insert_address(address.clone(), account_id).await?;
665 // Watched accounts intentionally have no derived note tag registered to avoid sync
666 // state pulling notes for them.
667 if !tracked_account.is_watched() {
668 let derived_note_tag: NoteTag = address.to_note_tag();
669 let note_tag_record =
670 NoteTagRecord::with_account_source(derived_note_tag, account_id);
671 self.store.add_note_tag(note_tag_record).await?;
672 }
673 Ok(())
674 },
675 }
676 }
677
678 /// Removes an [`Address`] from the associated [`AccountId`], alongside its derived [`NoteTag`].
679 ///
680 /// Returns `true` if the address was tracked. If it wasn't, this is a no-op: the derived tag is
681 /// left in place, since it may have been registered by something other than this address.
682 pub async fn remove_address(
683 &mut self,
684 address: Address,
685 account_id: AccountId,
686 ) -> Result<bool, ClientError> {
687 let derived_note_tag = address.to_note_tag();
688 let note_tag_record = NoteTagRecord::with_account_source(derived_note_tag, account_id);
689 if !self.store.remove_address(address).await? {
690 return Ok(false);
691 }
692 // Remove the note tag if no other address are associated with it.
693 let addresses = self.store.get_addresses_by_account_id(account_id).await?;
694 if addresses.iter().all(|address| address.to_note_tag() != derived_note_tag) {
695 self.store.remove_note_tag(note_tag_record).await?;
696 }
697 Ok(true)
698 }
699
700 // ACCOUNT DATA RETRIEVAL
701 // --------------------------------------------------------------------------------------------
702
703 /// Retrieves the asset vault for a specific account.
704 ///
705 /// To check the balance for a single asset, use [`Client::account_reader`] instead.
706 pub async fn get_account_vault(
707 &self,
708 account_id: AccountId,
709 ) -> Result<AssetVault, ClientError> {
710 self.store.get_account_vault(account_id).await.map_err(ClientError::StoreError)
711 }
712
713 /// Retrieves the whole account storage for a specific account.
714 ///
715 /// To only load a specific slot, use [`Client::account_reader`] instead.
716 pub async fn get_account_storage(
717 &self,
718 account_id: AccountId,
719 ) -> Result<AccountStorage, ClientError> {
720 self.store
721 .get_account_storage(account_id, AccountStorageFilter::All)
722 .await
723 .map_err(ClientError::StoreError)
724 }
725
726 /// Retrieves the account code for a specific account.
727 ///
728 /// Returns `None` if the account is not found.
729 pub async fn get_account_code(
730 &self,
731 account_id: AccountId,
732 ) -> Result<Option<AccountCode>, ClientError> {
733 self.store.get_account_code(account_id).await.map_err(ClientError::StoreError)
734 }
735
736 /// Returns a list of [`AccountHeader`] of all accounts stored in the database along with their
737 /// statuses.
738 ///
739 /// Said accounts' state is the state after the last performed sync.
740 pub async fn get_account_headers(
741 &self,
742 ) -> Result<Vec<(AccountHeader, AccountStatus)>, ClientError> {
743 self.store.get_account_headers().await.map_err(Into::into)
744 }
745
746 /// Returns the [`AccountHeader`] of the account with the specified ID along with its status, or
747 /// `None` if the account isn't tracked by the client.
748 ///
749 /// Said account's state is the state after the last performed sync.
750 pub async fn get_account_header(
751 &self,
752 account_id: AccountId,
753 ) -> Result<Option<(AccountHeader, AccountStatus)>, ClientError> {
754 self.store.get_account_header(account_id).await.map_err(Into::into)
755 }
756
757 /// Retrieves the full [`Account`] object from the store, returning `None` if not found.
758 ///
759 /// This method loads the complete account state including vault, storage, and code — including
760 /// building the vault's Merkle tree. For lazy access that fetches only the data you need
761 /// (existence checks, single fields, storage items), use [`Client::account_reader`] instead.
762 pub async fn get_account(&self, account_id: AccountId) -> Result<Option<Account>, ClientError> {
763 match self.store.get_account(account_id).await? {
764 Some(record) => Ok(Some(record.try_into()?)),
765 None => Ok(None),
766 }
767 }
768
769 /// Creates an [`AccountReader`] for lazy access to account data.
770 ///
771 /// The `AccountReader` provides lazy access to account state - each method call fetches fresh
772 /// data from storage, ensuring you always see the current state.
773 ///
774 /// For loading the full [`Account`] object, use [`Client::get_account`] instead.
775 ///
776 /// # Example
777 /// ```ignore
778 /// let reader = client.account_reader(account_id);
779 ///
780 /// // Each call fetches fresh data
781 /// let nonce = reader.nonce().await?;
782 /// let balance = reader.get_balance(faucet_id).await?;
783 ///
784 /// // Storage access is integrated
785 /// let value = reader.get_storage_item("my_slot").await?;
786 /// let (map_value, witness) = reader.get_storage_map_witness("balances", key).await?;
787 /// ```
788 pub fn account_reader(&self, account_id: AccountId) -> AccountReader {
789 AccountReader::new(self.store.clone(), account_id)
790 }
791
792 /// Prunes historical account states for the specified account up to the given nonce.
793 ///
794 /// Deletes all historical entries with `replaced_at_nonce <= up_to_nonce` and any orphaned
795 /// account code.
796 ///
797 /// Returns the total number of rows deleted, including historical entries and orphaned account
798 /// code.
799 pub async fn prune_account_history(
800 &self,
801 account_id: AccountId,
802 up_to_nonce: Felt,
803 ) -> Result<usize, ClientError> {
804 Ok(self.store.prune_account_history(account_id, up_to_nonce).await?)
805 }
806}
807
808// UTILITY FUNCTIONS
809// ================================================================================================
810
811/// Builds an regular account ID from the provided parameters. The ID may be used along
812/// `Client::import_account_by_id` to import a public account from the network (provided that the
813/// used seed is known).
814///
815/// This function currently supports accounts composed of the [`BasicWallet`] component and one of
816/// the supported authentication schemes ([`AuthSingleSig`]).
817///
818/// # Arguments
819/// - `init_seed`: Initial seed used to create the account. This is the seed passed to
820/// [`AccountBuilder::new`].
821/// - `public_key`: Public key of the account used for the authentication component.
822/// - `account_visibility`: Public/private visibility of the account.
823///
824/// # Errors
825/// - If the account cannot be built.
826pub fn build_wallet_id(
827 init_seed: [u8; 32],
828 public_key: &PublicKey,
829 account_visibility: AccountType,
830) -> Result<AccountId, ClientError> {
831 let auth_scheme = public_key.auth_scheme();
832 let auth_component: AccountComponent =
833 AuthSingleSig::new(Approver::new(public_key.to_commitment(), auth_scheme)).into();
834
835 let account = AccountBuilder::new(init_seed)
836 .account_type(account_visibility)
837 .with_component(auth_component)
838 .with_component(BasicWallet)
839 .build_with_schema_commitment()?;
840
841 Ok(account.id())
842}
843
844#[cfg(test)]
845mod schema_commitment_tests {
846 use miden_protocol::EMPTY_WORD;
847 use miden_protocol::account::auth::AuthSecretKey;
848 use miden_standards::account::inspection::AccountSchemaCommitment;
849
850 use super::{
851 AccountBuilder,
852 AccountBuilderSchemaCommitmentExt,
853 AccountType,
854 Approver,
855 AuthSingleSig,
856 BasicWallet,
857 };
858 use crate::auth::AuthSchemeId;
859
860 #[test]
861 fn wallet_build_includes_schema_commitment_metadata_slot() {
862 let key = AuthSecretKey::new_falcon512_poseidon2();
863 let account = AccountBuilder::new([2u8; 32])
864 .account_type(AccountType::Private)
865 .with_component(AuthSingleSig::new(Approver::new(
866 key.public_key().to_commitment(),
867 AuthSchemeId::Falcon512Poseidon2,
868 )))
869 .with_component(BasicWallet)
870 .build_with_schema_commitment()
871 .expect("build_with_schema_commitment");
872
873 let commitment = account
874 .storage()
875 .get_item(AccountSchemaCommitment::schema_commitment_slot())
876 .expect("schema commitment slot");
877 assert_ne!(commitment, EMPTY_WORD);
878 }
879}
880
881#[cfg(test)]
882mod faucet_metadata_tests {
883 use miden_protocol::Felt;
884
885 use super::{FungibleFaucet, TokenSymbol, faucet_metadata_from_token_config};
886
887 /// Builds a token config slot value carrying the given decimals and the symbol "TST".
888 fn token_config(decimals: u32) -> [Felt; 4] {
889 [
890 Felt::from(0u32),
891 Felt::from(0u32),
892 Felt::from(decimals),
893 TokenSymbol::new("TST").unwrap().as_element(),
894 ]
895 }
896
897 #[test]
898 fn decodes_a_config_within_the_protocol_bounds() {
899 let metadata = faucet_metadata_from_token_config(token_config(8)).unwrap();
900
901 assert_eq!(metadata.symbol, "TST");
902 assert_eq!(metadata.decimals, 8);
903 }
904
905 #[test]
906 fn accepts_the_maximum_supported_decimals() {
907 let max = u32::from(FungibleFaucet::MAX_DECIMALS);
908 let metadata = faucet_metadata_from_token_config(token_config(max)).unwrap();
909
910 assert_eq!(metadata.decimals, FungibleFaucet::MAX_DECIMALS);
911 }
912
913 #[test]
914 fn rejects_decimals_above_the_maximum() {
915 let above_max = u32::from(FungibleFaucet::MAX_DECIMALS) + 1;
916
917 assert!(faucet_metadata_from_token_config(token_config(above_max)).is_none());
918 assert!(faucet_metadata_from_token_config(token_config(200)).is_none());
919 }
920
921 #[test]
922 fn rejects_decimals_that_do_not_fit_a_u8() {
923 assert!(faucet_metadata_from_token_config(token_config(300)).is_none());
924 }
925
926 #[test]
927 fn rejects_a_symbol_that_is_not_a_token_symbol() {
928 let mut config = token_config(8);
929 config[3] = Felt::from(0u32);
930
931 assert!(faucet_metadata_from_token_config(config).is_none());
932 }
933}