use alloc::string::{String, ToString};
use alloc::vec::Vec;
use miden_protocol::Felt;
use miden_protocol::account::auth::PublicKey;
pub use miden_protocol::account::delta::AccountUpdateDetails;
pub use miden_protocol::account::{
Account,
AccountBuilder,
AccountCode,
AccountComponent,
AccountComponentCode,
AccountDelta,
AccountFile,
AccountHeader,
AccountId,
AccountIdPrefix,
AccountIdPrefixV1,
AccountIdV1,
AccountIdVersion,
AccountProcedureRoot,
AccountStorage,
AccountType,
PartialAccount,
PartialStorage,
PartialStorageMap,
RoleSymbol,
StorageMap,
StorageMapDelta,
StorageMapKey,
StorageMapKeyHash,
StorageMapWitness,
StorageSlot,
StorageSlotContent,
StorageSlotDelta,
StorageSlotId,
StorageSlotName,
StorageSlotType,
};
pub use miden_protocol::address::{Address, AddressInterface, AddressType, NetworkId};
use miden_protocol::asset::AssetVault;
pub use miden_protocol::errors::{AccountIdError, AddressError, NetworkIdError};
use miden_protocol::note::NoteTag;
use miden_tx::utils::serde::{
ByteReader,
ByteWriter,
Deserializable,
DeserializationError,
Serializable,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FaucetMetadata {
pub symbol: String,
pub decimals: u8,
}
impl Serializable for FaucetMetadata {
fn write_into<W: ByteWriter>(&self, target: &mut W) {
self.symbol.write_into(target);
target.write_u8(self.decimals);
}
}
impl Deserializable for FaucetMetadata {
fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
let symbol = String::read_from(source)?;
let decimals = source.read_u8()?;
Ok(Self { symbol, decimals })
}
}
mod account_reader;
pub use account_reader::AccountReader;
pub use miden_standards::account as standards;
use miden_standards::account::auth::AuthSingleSig;
use miden_standards::account::faucets::FungibleFaucet;
pub use miden_standards::account::interface::{
AccountComponentInterface,
AccountComponentInterfaceExt,
AccountInterface,
AccountInterfaceExt,
};
pub use miden_standards::account::metadata::{
AccountBuilderSchemaCommitmentExt,
AccountSchemaCommitment,
};
use miden_standards::account::wallets::BasicWallet;
use super::Client;
use crate::asset::TokenSymbol;
use crate::errors::ClientError;
use crate::rpc::domain::account::GetAccountRequest;
use crate::rpc::node::{EndpointError, GetAccountError};
use crate::store::{AccountStatus, AccountStorageFilter, ClientAccountType};
use crate::sync::NoteTagRecord;
pub mod component {
pub const MIDEN_PACKAGE_EXTENSION: &str = "masp";
pub use miden_protocol::account::auth::*;
pub use miden_protocol::account::component::{
FeltSchema,
InitStorageData,
InitStorageDataError,
MapSlotSchema,
SchemaRequirement,
SchemaType,
SchemaTypeError,
StorageSchema,
StorageSlotSchema,
StorageValueName,
StorageValueNameError,
ValueSlotSchema,
WordSchema,
WordValue,
};
pub use miden_protocol::account::{
AccountComponent,
AccountComponentMetadata,
AccountComponentName,
AccountProcedureRoot,
RoleSymbol,
};
pub use miden_standards::account::access::{
AccessControl,
Authority,
AuthorityError,
Ownable2Step,
Ownable2StepError,
Pausable,
PausableManager,
PausableStorage,
RoleBasedAccessControl,
};
pub use miden_standards::account::auth::*;
pub use miden_standards::account::components::StandardAccountComponent;
pub use miden_standards::account::faucets::{
Description,
ExternalLink,
FungibleFaucet,
FungibleFaucetBuilder,
FungibleFaucetError,
LogoURI,
TokenMetadata,
TokenMetadataError,
TokenName,
create_fungible_faucet,
};
pub use miden_standards::account::policies::{
AllowlistOwnerControlled,
AllowlistStorage,
BasicAllowlist,
BasicBlocklist,
BlocklistOwnerControlled,
BlocklistStorage,
BurnAllowAll,
BurnOwnerOnly,
BurnPolicyConfig,
MintAllowAll,
MintOwnerOnly,
MintPolicyConfig,
PolicyRegistration,
TokenPolicyManager,
TokenPolicyManagerError,
TransferAllowAll,
TransferPolicy,
};
pub use miden_standards::account::wallets::BasicWallet;
}
impl<AUTH> Client<AUTH> {
pub async fn add_account(
&mut self,
account: &Account,
overwrite: bool,
) -> Result<(), ClientError> {
self.add_account_inner(account, ClientAccountType::Native, overwrite).await
}
async fn add_account_inner(
&mut self,
account: &Account,
client_account_type: ClientAccountType,
overwrite: bool,
) -> Result<(), ClientError> {
if account.is_new() {
if account.seed().is_none() {
return Err(ClientError::AddNewAccountWithoutSeed);
}
} else {
if account.seed().is_some() {
tracing::warn!(
"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."
);
}
}
let tracked_account = self.store.get_account(account.id()).await?;
match tracked_account {
None => {
let default_address = Address::new(account.id());
self.store
.insert_account(account, default_address.clone(), client_account_type)
.await
.map_err(ClientError::StoreError)?;
if matches!(client_account_type, ClientAccountType::Native) {
let default_address_note_tag = default_address.to_note_tag();
let note_tag_record =
NoteTagRecord::with_account_source(default_address_note_tag, account.id());
self.store.add_note_tag(note_tag_record).await?;
}
Ok(())
},
Some(tracked_account) => {
if !overwrite {
return Err(ClientError::AccountAlreadyTracked(account.id()));
}
if client_account_type != tracked_account.client_account_type() {
return Err(ClientError::AccountWatchedMismatch(account.id()));
}
if tracked_account.nonce().as_canonical_u64() > account.nonce().as_canonical_u64() {
return Err(ClientError::AccountNonceTooLow);
}
if tracked_account.is_locked() {
let network_account_commitment = self
.rpc_api
.get_account(account.id(), GetAccountRequest::new())
.await?
.1
.account_commitment();
if network_account_commitment != account.to_commitment() {
return Err(ClientError::AccountCommitmentMismatch(
network_account_commitment,
));
}
}
self.store.update_account(account).await?;
Ok(())
},
}
}
pub async fn import_account_by_id(&mut self, account_id: AccountId) -> Result<(), ClientError> {
let account = self.fetch_public_account(account_id).await?;
self.add_account_inner(&account, ClientAccountType::Native, true).await
}
pub async fn import_watched_account_by_id(
&mut self,
account_id: AccountId,
) -> Result<(), ClientError> {
let account = self.fetch_public_account(account_id).await?;
self.add_account_inner(&account, ClientAccountType::Watched, true).await
}
async fn fetch_public_account(&self, account_id: AccountId) -> Result<Account, ClientError> {
let fetched_account =
self.rpc_api.get_account_details(account_id).await.map_err(|err| {
match err.endpoint_error() {
Some(EndpointError::GetAccount(GetAccountError::AccountNotFound)) => {
ClientError::AccountNotFoundOnChain(account_id)
},
_ => ClientError::RpcError(err),
}
})?;
fetched_account.ok_or(ClientError::AccountIsPrivate(account_id))
}
pub async fn fetch_remote_token_metadata(
&self,
faucet_id: AccountId,
) -> Result<Option<FaucetMetadata>, ClientError> {
let proof = match self.rpc_api.get_account(faucet_id, GetAccountRequest::new()).await {
Ok((_, proof)) => proof,
Err(err) => match err.endpoint_error() {
Some(EndpointError::GetAccount(
GetAccountError::AccountNotFound | GetAccountError::AccountNotPublic,
)) => return Ok(None),
_ => return Err(ClientError::RpcError(err)),
},
};
let Some(storage_header) = proof.storage_header() else {
return Ok(None);
};
let Some(slot_header) =
storage_header.find_slot_header_by_name(FungibleFaucet::token_config_slot())
else {
return Ok(None);
};
let [_token_supply, _max_supply, decimals, symbol] = *slot_header.value();
let Ok(symbol) = TokenSymbol::try_from(symbol) else {
return Ok(None);
};
let Ok(decimals) = u8::try_from(decimals.as_canonical_u64()) else {
return Ok(None);
};
Ok(Some(FaucetMetadata { symbol: symbol.to_string(), decimals }))
}
pub async fn add_address(
&mut self,
address: Address,
account_id: AccountId,
) -> Result<(), ClientError> {
let network_id = self.rpc_api.get_network_id().await?;
let address_bench32 = address.encode(network_id);
if self.store.get_addresses_by_account_id(account_id).await?.contains(&address) {
return Err(ClientError::AddressAlreadyTracked(address_bench32));
}
let tracked_account = self.store.get_account(account_id).await?;
match tracked_account {
None => Err(ClientError::AccountDataNotFound(account_id)),
Some(tracked_account) => {
self.store.insert_address(address.clone(), account_id).await?;
if !tracked_account.is_watched() {
let derived_note_tag: NoteTag = address.to_note_tag();
let note_tag_record =
NoteTagRecord::with_account_source(derived_note_tag, account_id);
self.store.add_note_tag(note_tag_record).await?;
}
Ok(())
},
}
}
pub async fn remove_address(
&mut self,
address: Address,
account_id: AccountId,
) -> Result<(), ClientError> {
let derived_note_tag = address.to_note_tag();
let note_tag_record = NoteTagRecord::with_account_source(derived_note_tag, account_id);
self.store.remove_address(address).await?;
let addresses = self.store.get_addresses_by_account_id(account_id).await?;
if addresses.iter().all(|address| address.to_note_tag() != derived_note_tag) {
self.store.remove_note_tag(note_tag_record).await?;
}
Ok(())
}
pub async fn get_account_vault(
&self,
account_id: AccountId,
) -> Result<AssetVault, ClientError> {
self.store.get_account_vault(account_id).await.map_err(ClientError::StoreError)
}
pub async fn get_account_storage(
&self,
account_id: AccountId,
) -> Result<AccountStorage, ClientError> {
self.store
.get_account_storage(account_id, AccountStorageFilter::All)
.await
.map_err(ClientError::StoreError)
}
pub async fn get_account_code(
&self,
account_id: AccountId,
) -> Result<Option<AccountCode>, ClientError> {
self.store.get_account_code(account_id).await.map_err(ClientError::StoreError)
}
pub async fn get_account_headers(
&self,
) -> Result<Vec<(AccountHeader, AccountStatus)>, ClientError> {
self.store.get_account_headers().await.map_err(Into::into)
}
pub async fn get_account(&self, account_id: AccountId) -> Result<Option<Account>, ClientError> {
match self.store.get_account(account_id).await? {
Some(record) => Ok(Some(record.try_into()?)),
None => Ok(None),
}
}
pub async fn try_get_account(&self, account_id: AccountId) -> Result<Account, ClientError> {
self.get_account(account_id)
.await?
.ok_or(ClientError::AccountDataNotFound(account_id))
}
pub fn account_reader(&self, account_id: AccountId) -> AccountReader {
AccountReader::new(self.store.clone(), account_id)
}
pub async fn prune_account_history(
&self,
account_id: AccountId,
up_to_nonce: Felt,
) -> Result<usize, ClientError> {
Ok(self.store.prune_account_history(account_id, up_to_nonce).await?)
}
}
pub fn build_wallet_id(
init_seed: [u8; 32],
public_key: &PublicKey,
account_visibility: AccountType,
) -> Result<AccountId, ClientError> {
let auth_scheme = public_key.auth_scheme();
let auth_component: AccountComponent =
AuthSingleSig::new(public_key.to_commitment(), auth_scheme).into();
let account = AccountBuilder::new(init_seed)
.account_type(account_visibility)
.with_auth_component(auth_component)
.with_component(BasicWallet)
.build_with_schema_commitment()?;
Ok(account.id())
}
#[cfg(test)]
mod schema_commitment_tests {
use miden_protocol::EMPTY_WORD;
use miden_protocol::account::auth::AuthSecretKey;
use miden_standards::account::metadata::AccountSchemaCommitment;
use super::{
AccountBuilder,
AccountBuilderSchemaCommitmentExt,
AccountType,
AuthSingleSig,
BasicWallet,
};
use crate::auth::AuthSchemeId;
#[test]
fn wallet_build_includes_schema_commitment_metadata_slot() {
let key = AuthSecretKey::new_falcon512_poseidon2();
let account = AccountBuilder::new([2u8; 32])
.account_type(AccountType::Private)
.with_auth_component(AuthSingleSig::new(
key.public_key().to_commitment(),
AuthSchemeId::Falcon512Poseidon2,
))
.with_component(BasicWallet)
.build_with_schema_commitment()
.expect("build_with_schema_commitment");
let commitment = account
.storage()
.get_item(AccountSchemaCommitment::schema_commitment_slot())
.expect("schema commitment slot");
assert_ne!(commitment, EMPTY_WORD);
}
}