1use alloc::string::{String, ToString};
36use alloc::vec::Vec;
37
38use miden_protocol::Felt;
39use miden_protocol::account::auth::PublicKey;
40pub use miden_protocol::account::{
41 Account,
42 AccountBuilder,
43 AccountCode,
44 AccountComponent,
45 AccountComponentCode,
46 AccountDelta,
47 AccountFile,
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#[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
116fn 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;
136pub use miden_standards::account as standards;
138use miden_standards::account::auth::{Approver, AuthSingleSig};
139use miden_standards::account::faucets::FungibleFaucet;
140pub use miden_standards::account::inspection::{
141 AccountBuilderSchemaCommitmentExt,
142 AccountSchemaCommitment,
143};
144pub 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
248impl<AUTH> Client<AUTH> {
266 pub async fn add_account(
284 &mut self,
285 account: &Account,
286 overwrite: bool,
287 ) -> Result<(), ClientError> {
288 self.add_account_inner(account, ClientAccountType::Native, overwrite).await
289 }
290
291 async fn add_account_inner(
297 &mut self,
298 account: &Account,
299 client_account_type: ClientAccountType,
300 overwrite: bool,
301 ) -> Result<(), ClientError> {
302 if account.is_new() {
303 if account.seed().is_none() {
304 return Err(ClientError::AddNewAccountWithoutSeed);
305 }
306 } else {
307 if account.seed().is_some() {
309 tracing::warn!(
310 "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."
311 );
312 }
313 }
314
315 let tracked_account = self.store.get_minimal_partial_account(account.id()).await?;
316
317 match tracked_account {
318 None => {
319 let default_address = Address::new(account.id());
320
321 self.store
322 .insert_account(account, default_address.clone(), client_account_type)
323 .await
324 .map_err(ClientError::StoreError)?;
325
326 if matches!(client_account_type, ClientAccountType::Native) {
327 let default_address_note_tag = default_address.to_note_tag();
329 let note_tag_record =
330 NoteTagRecord::with_account_source(default_address_note_tag, account.id());
331 self.store.add_note_tag(note_tag_record).await?;
332 }
333
334 Ok(())
335 },
336 Some(tracked_account) => {
337 if !overwrite {
338 return Err(ClientError::AccountAlreadyTracked(account.id()));
340 }
341
342 if client_account_type != tracked_account.client_account_type() {
343 return Err(ClientError::AccountWatchedMismatch(account.id()));
347 }
348
349 if tracked_account.nonce().as_canonical_u64() > account.nonce().as_canonical_u64() {
350 return Err(ClientError::AccountNonceTooLow);
352 }
353
354 if tracked_account.is_locked() {
355 let network_account_commitment = self
358 .rpc_api
359 .get_account(account.id(), GetAccountRequest::new())
360 .await?
361 .1
362 .account_commitment();
363 if network_account_commitment != account.to_commitment() {
364 return Err(ClientError::AccountCommitmentMismatch(
365 network_account_commitment,
366 ));
367 }
368 }
369
370 self.store.update_account(account).await?;
371
372 Ok(())
373 },
374 }
375 }
376
377 pub async fn import_account_by_id(&mut self, account_id: AccountId) -> Result<(), ClientError> {
391 let account = self.fetch_public_account(account_id).await?;
392 self.add_account_inner(&account, ClientAccountType::Native, true).await
393 }
394
395 pub async fn import_watched_account_by_id(
411 &mut self,
412 account_id: AccountId,
413 ) -> Result<(), ClientError> {
414 let account = self.fetch_public_account(account_id).await?;
415 self.add_account_inner(&account, ClientAccountType::Watched, true).await
416 }
417
418 async fn fetch_public_account(&self, account_id: AccountId) -> Result<Account, ClientError> {
421 let fetched_account =
422 self.rpc_api.get_account_details(account_id).await.map_err(|err| {
423 match err.endpoint_error() {
424 Some(EndpointError::GetAccount(GetAccountError::AccountNotFound)) => {
425 ClientError::AccountNotFoundOnChain(account_id)
426 },
427 _ => ClientError::RpcError(err),
428 }
429 })?;
430
431 fetched_account.ok_or(ClientError::AccountIsPrivate(account_id))
432 }
433
434 pub async fn fetch_remote_token_metadata(
446 &self,
447 faucet_id: AccountId,
448 ) -> Result<Option<FaucetMetadata>, ClientError> {
449 let proof = match self.rpc_api.get_account(faucet_id, GetAccountRequest::new()).await {
450 Ok((_, proof)) => proof,
451 Err(err) => match err.endpoint_error() {
452 Some(EndpointError::GetAccount(
453 GetAccountError::AccountNotFound | GetAccountError::AccountNotPublic,
454 )) => return Ok(None),
455 _ => return Err(ClientError::RpcError(err)),
456 },
457 };
458
459 let Some(storage_header) = proof.storage_header() else {
460 return Ok(None);
461 };
462
463 let Some(slot_header) =
464 storage_header.find_slot_header_by_name(FungibleFaucet::token_config_slot())
465 else {
466 return Ok(None);
467 };
468
469 Ok(faucet_metadata_from_token_config(*slot_header.value()))
470 }
471
472 pub async fn add_address(
479 &mut self,
480 address: Address,
481 account_id: AccountId,
482 ) -> Result<(), ClientError> {
483 let network_id = self.rpc_api.get_network_id().await?;
484 let address_bench32 = address.encode(network_id);
485 if self.store.get_addresses_by_account_id(account_id).await?.contains(&address) {
486 return Err(ClientError::AddressAlreadyTracked(address_bench32));
487 }
488
489 let tracked_account = self.store.get_minimal_partial_account(account_id).await?;
490 match tracked_account {
491 None => Err(ClientError::AccountDataNotFound(account_id)),
492 Some(tracked_account) => {
493 self.store.insert_address(address.clone(), account_id).await?;
494 if !tracked_account.is_watched() {
497 let derived_note_tag: NoteTag = address.to_note_tag();
498 let note_tag_record =
499 NoteTagRecord::with_account_source(derived_note_tag, account_id);
500 self.store.add_note_tag(note_tag_record).await?;
501 }
502 Ok(())
503 },
504 }
505 }
506
507 pub async fn remove_address(
512 &mut self,
513 address: Address,
514 account_id: AccountId,
515 ) -> Result<bool, ClientError> {
516 let derived_note_tag = address.to_note_tag();
517 let note_tag_record = NoteTagRecord::with_account_source(derived_note_tag, account_id);
518 if !self.store.remove_address(address).await? {
519 return Ok(false);
520 }
521 let addresses = self.store.get_addresses_by_account_id(account_id).await?;
523 if addresses.iter().all(|address| address.to_note_tag() != derived_note_tag) {
524 self.store.remove_note_tag(note_tag_record).await?;
525 }
526 Ok(true)
527 }
528
529 pub async fn get_account_vault(
536 &self,
537 account_id: AccountId,
538 ) -> Result<AssetVault, ClientError> {
539 self.store.get_account_vault(account_id).await.map_err(ClientError::StoreError)
540 }
541
542 pub async fn get_account_storage(
546 &self,
547 account_id: AccountId,
548 ) -> Result<AccountStorage, ClientError> {
549 self.store
550 .get_account_storage(account_id, AccountStorageFilter::All)
551 .await
552 .map_err(ClientError::StoreError)
553 }
554
555 pub async fn get_account_code(
559 &self,
560 account_id: AccountId,
561 ) -> Result<Option<AccountCode>, ClientError> {
562 self.store.get_account_code(account_id).await.map_err(ClientError::StoreError)
563 }
564
565 pub async fn get_account_headers(
570 &self,
571 ) -> Result<Vec<(AccountHeader, AccountStatus)>, ClientError> {
572 self.store.get_account_headers().await.map_err(Into::into)
573 }
574
575 pub async fn get_account_header(
580 &self,
581 account_id: AccountId,
582 ) -> Result<Option<(AccountHeader, AccountStatus)>, ClientError> {
583 self.store.get_account_header(account_id).await.map_err(Into::into)
584 }
585
586 pub async fn get_account(&self, account_id: AccountId) -> Result<Option<Account>, ClientError> {
592 match self.store.get_account(account_id).await? {
593 Some(record) => Ok(Some(record.try_into()?)),
594 None => Ok(None),
595 }
596 }
597
598 pub fn account_reader(&self, account_id: AccountId) -> AccountReader {
618 AccountReader::new(self.store.clone(), account_id)
619 }
620
621 pub async fn prune_account_history(
629 &self,
630 account_id: AccountId,
631 up_to_nonce: Felt,
632 ) -> Result<usize, ClientError> {
633 Ok(self.store.prune_account_history(account_id, up_to_nonce).await?)
634 }
635}
636
637pub fn build_wallet_id(
656 init_seed: [u8; 32],
657 public_key: &PublicKey,
658 account_visibility: AccountType,
659) -> Result<AccountId, ClientError> {
660 let auth_scheme = public_key.auth_scheme();
661 let auth_component: AccountComponent =
662 AuthSingleSig::new(Approver::new(public_key.to_commitment(), auth_scheme)).into();
663
664 let account = AccountBuilder::new(init_seed)
665 .account_type(account_visibility)
666 .with_component(auth_component)
667 .with_component(BasicWallet)
668 .build_with_schema_commitment()?;
669
670 Ok(account.id())
671}
672
673#[cfg(test)]
674mod schema_commitment_tests {
675 use miden_protocol::EMPTY_WORD;
676 use miden_protocol::account::auth::AuthSecretKey;
677 use miden_standards::account::inspection::AccountSchemaCommitment;
678
679 use super::{
680 AccountBuilder,
681 AccountBuilderSchemaCommitmentExt,
682 AccountType,
683 Approver,
684 AuthSingleSig,
685 BasicWallet,
686 };
687 use crate::auth::AuthSchemeId;
688
689 #[test]
690 fn wallet_build_includes_schema_commitment_metadata_slot() {
691 let key = AuthSecretKey::new_falcon512_poseidon2();
692 let account = AccountBuilder::new([2u8; 32])
693 .account_type(AccountType::Private)
694 .with_component(AuthSingleSig::new(Approver::new(
695 key.public_key().to_commitment(),
696 AuthSchemeId::Falcon512Poseidon2,
697 )))
698 .with_component(BasicWallet)
699 .build_with_schema_commitment()
700 .expect("build_with_schema_commitment");
701
702 let commitment = account
703 .storage()
704 .get_item(AccountSchemaCommitment::schema_commitment_slot())
705 .expect("schema commitment slot");
706 assert_ne!(commitment, EMPTY_WORD);
707 }
708}
709
710#[cfg(test)]
711mod faucet_metadata_tests {
712 use miden_protocol::Felt;
713
714 use super::{FungibleFaucet, TokenSymbol, faucet_metadata_from_token_config};
715
716 fn token_config(decimals: u32) -> [Felt; 4] {
718 [
719 Felt::from(0u32),
720 Felt::from(0u32),
721 Felt::from(decimals),
722 TokenSymbol::new("TST").unwrap().as_element(),
723 ]
724 }
725
726 #[test]
727 fn decodes_a_config_within_the_protocol_bounds() {
728 let metadata = faucet_metadata_from_token_config(token_config(8)).unwrap();
729
730 assert_eq!(metadata.symbol, "TST");
731 assert_eq!(metadata.decimals, 8);
732 }
733
734 #[test]
735 fn accepts_the_maximum_supported_decimals() {
736 let max = u32::from(FungibleFaucet::MAX_DECIMALS);
737 let metadata = faucet_metadata_from_token_config(token_config(max)).unwrap();
738
739 assert_eq!(metadata.decimals, FungibleFaucet::MAX_DECIMALS);
740 }
741
742 #[test]
743 fn rejects_decimals_above_the_maximum() {
744 let above_max = u32::from(FungibleFaucet::MAX_DECIMALS) + 1;
745
746 assert!(faucet_metadata_from_token_config(token_config(above_max)).is_none());
747 assert!(faucet_metadata_from_token_config(token_config(200)).is_none());
748 }
749
750 #[test]
751 fn rejects_decimals_that_do_not_fit_a_u8() {
752 assert!(faucet_metadata_from_token_config(token_config(300)).is_none());
753 }
754
755 #[test]
756 fn rejects_a_symbol_that_is_not_a_token_symbol() {
757 let mut config = token_config(8);
758 config[3] = Felt::from(0u32);
759
760 assert!(faucet_metadata_from_token_config(config).is_none());
761 }
762}