use alloc::collections::BTreeSet;
use miden_protocol::account::{Account, AccountBuilder, AccountId, AccountStorage, AccountType};
use miden_protocol::note::NoteScriptRoot;
use miden_protocol::transaction::TransactionScriptRoot;
use crate::account::auth::network_account::{
AuthNetworkAccount,
NetworkAccountNoteAllowlist,
NetworkAccountNoteAllowlistError,
NetworkAccountTxScriptAllowlist,
NetworkAccountTxScriptAllowlistError,
};
use crate::account::fees::FeePolicyManager;
use crate::tx_script::ExpirationTransactionScript;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct NetworkAccount {
account: Account,
note_allowlist: NetworkAccountNoteAllowlist,
tx_script_allowlist: NetworkAccountTxScriptAllowlist,
}
impl NetworkAccount {
pub fn new(account: Account) -> Result<Self, NetworkAccountError> {
if !account.is_public() {
return Err(NetworkAccountError::AccountNotPublic(account.id()));
}
let note_allowlist = NetworkAccountNoteAllowlist::try_from(account.storage())
.map_err(NetworkAccountError::NoteAllowlist)?;
let tx_script_allowlist = NetworkAccountTxScriptAllowlist::try_from(account.storage())
.map_err(NetworkAccountError::TxScriptAllowlist)?;
if !tx_script_allowlist
.allowed_script_roots()
.contains(&ExpirationTransactionScript::script_root())
{
return Err(NetworkAccountError::ExpirationScriptNotAllowlisted);
}
Ok(Self {
account,
note_allowlist,
tx_script_allowlist,
})
}
pub fn builder(
init_seed: [u8; 32],
allowed_notes: BTreeSet<NoteScriptRoot>,
fee_policy_manager: FeePolicyManager,
) -> Result<AccountBuilder, NetworkAccountNoteAllowlistError> {
let auth_component = AuthNetworkAccount::new(allowed_notes, fee_policy_manager)?;
Ok(AccountBuilder::new(init_seed)
.account_type(AccountType::Public)
.with_components(auth_component))
}
pub fn into_account(self) -> Account {
self.account
}
pub fn as_account(&self) -> &Account {
&self.account
}
pub fn id(&self) -> AccountId {
self.account.id()
}
pub fn storage(&self) -> &AccountStorage {
self.account.storage()
}
pub fn allowed_notes(&self) -> &NetworkAccountNoteAllowlist {
&self.note_allowlist
}
pub fn allowed_tx_scripts(&self) -> &NetworkAccountTxScriptAllowlist {
&self.tx_script_allowlist
}
pub fn allows_tx_script(&self, root: &TransactionScriptRoot) -> bool {
self.tx_script_allowlist.allowed_script_roots().contains(root)
}
}
impl TryFrom<Account> for NetworkAccount {
type Error = NetworkAccountError;
fn try_from(account: Account) -> Result<Self, Self::Error> {
Self::new(account)
}
}
#[derive(Debug, thiserror::Error)]
pub enum NetworkAccountError {
#[error("network account must have public account type, but account {0} does not")]
AccountNotPublic(AccountId),
#[error("failed to decode the note-script allowlist from account storage")]
NoteAllowlist(#[source] NetworkAccountNoteAllowlistError),
#[error("failed to decode the tx-script allowlist from account storage")]
TxScriptAllowlist(#[source] NetworkAccountTxScriptAllowlistError),
#[error(
"network account tx-script allowlist must contain the canonical expiration transaction \
script root"
)]
ExpirationScriptNotAllowlisted,
}
#[cfg(test)]
mod tests {
use alloc::collections::BTreeSet;
use miden_protocol::Word;
use miden_protocol::account::{AccountBuilder, AccountType};
use miden_protocol::asset::FungibleAsset;
use miden_protocol::note::NoteScriptRoot;
use super::*;
use crate::account::auth::network_account::AuthNetworkAccount;
use crate::account::wallets::BasicWallet;
use crate::note::FeeSponsorshipNote;
fn build_account(account_type: AccountType, roots: BTreeSet<NoteScriptRoot>) -> Account {
AccountBuilder::new([0; 32])
.account_type(account_type)
.with_components(
AuthNetworkAccount::new(
roots,
FeePolicyManager::mock(FungibleAsset::mock_issuer()),
)
.expect("non-empty allowlist"),
)
.with_component(BasicWallet)
.build()
.expect("account building should succeed")
}
#[test]
fn public_account_with_allowlist_is_a_network_account() {
let root = NoteScriptRoot::from_array([1, 2, 3, 4]);
let roots = BTreeSet::from_iter([root]);
let account = build_account(AccountType::Public, roots.clone());
let network_account = NetworkAccount::new(account).expect("should be a network account");
let actual: BTreeSet<NoteScriptRoot> =
network_account.allowed_notes().allowed_script_roots().iter().copied().collect();
let mut expected = roots;
expected.insert(crate::note::NetworkAccountConfigNote::script_root());
expected.insert(FeeSponsorshipNote::script_root());
assert_eq!(actual, expected);
}
#[test]
fn private_account_is_rejected_even_with_allowlist() {
let root = NoteScriptRoot::from_array([1, 2, 3, 4]);
let account = build_account(AccountType::Private, BTreeSet::from_iter([root]));
let id = account.id();
let err = NetworkAccount::new(account).expect_err("private account must be rejected");
assert!(matches!(
err,
NetworkAccountError::AccountNotPublic(account_id) if account_id == id
));
}
#[test]
fn public_account_without_allowlist_is_not_a_network_account() {
let account = AccountBuilder::new([0; 32])
.account_type(AccountType::Public)
.with_component(crate::account::auth::NoAuth)
.with_component(BasicWallet)
.build()
.expect("account building should succeed");
let err = NetworkAccount::new(account).expect_err("missing allowlist must be rejected");
assert!(matches!(
err,
NetworkAccountError::NoteAllowlist(NetworkAccountNoteAllowlistError::SlotNotFound)
));
}
#[test]
fn account_without_expiration_script_is_rejected() {
let note_root = NoteScriptRoot::from_array([1, 2, 3, 4]);
let account = AccountBuilder::new([0; 32])
.account_type(AccountType::Public)
.with_components(
AuthNetworkAccount::custom(
BTreeSet::from_iter([note_root]),
FeePolicyManager::mock(FungibleAsset::mock_issuer()),
)
.expect("non-empty allowlist"),
)
.with_component(BasicWallet)
.build()
.expect("account building should succeed");
let err = NetworkAccount::new(account).expect_err("missing expiration root");
assert!(matches!(err, NetworkAccountError::ExpirationScriptNotAllowlisted));
}
#[test]
fn builder_produces_network_account_with_expiration_script_allowlisted() {
let note_root = NoteScriptRoot::from_array([1, 2, 3, 4]);
let account = NetworkAccount::builder(
[0; 32],
BTreeSet::from_iter([note_root]),
FeePolicyManager::mock(FungibleAsset::mock_issuer()),
)
.expect("non-empty allowlist")
.with_component(BasicWallet)
.build()
.expect("account building should succeed");
let network_account = NetworkAccount::new(account).expect("should be a network account");
assert!(network_account.allows_tx_script(&ExpirationTransactionScript::script_root()));
let other_root = TransactionScriptRoot::from_raw(Word::from([9u32, 10, 11, 12]));
assert!(!network_account.allows_tx_script(&other_root));
assert!(
network_account
.allowed_notes()
.allowed_script_roots()
.contains(&FeeSponsorshipNote::script_root())
);
}
}