use std::collections::BTreeSet;
use miden_protocol::account::{Account, AccountId, StorageMapKey};
use miden_protocol::asset::{AssetAmount, FungibleAsset};
use miden_protocol::note::{Note, NoteScriptRoot};
use miden_protocol::testing::account_id::ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1;
use miden_protocol::transaction::RawOutputNote;
use miden_protocol::{Felt, Word};
use miden_standards::account::access::{Authority, Ownable2Step};
use miden_standards::account::auth::NetworkAccount;
use miden_standards::account::fees::{
BasicConstantFeePolicy,
ConstantFeeManager,
FeePolicyManager,
};
use miden_standards::account::wallets::BasicWallet;
use miden_standards::errors::standards::{
ERR_FEE_ASSET_ID_MISMATCH,
ERR_FUNGIBLE_ASSET_AMOUNT_EXCEEDS_MAX_ALLOWED_AMOUNT,
ERR_FUNGIBLE_ASSET_VALUE_MALFORMED,
};
use miden_standards::note::ConstantFeePolicyConfigNote;
use miden_testing::{MockChain, assert_transaction_executor_error};
use rstest::rstest;
use super::constant_fee_policy_config::build_config_note;
use super::fee_manager::{FEE_AMOUNT, fee_faucet_id, priced_root};
use super::rbac::{build_note, test_account_id};
use crate::consume_note;
pub(super) fn owner_id() -> AccountId {
test_account_id(70)
}
pub(super) fn non_owner_id() -> AccountId {
test_account_id(71)
}
pub(super) fn fee_asset(amount: u64) -> anyhow::Result<FungibleAsset> {
Ok(FungibleAsset::new(fee_faucet_id()?, amount)?)
}
fn build_set_note_fee_note_raw(
sender: AccountId,
lookup_key: NoteScriptRoot,
fee_asset_id: Word,
fee_asset_value: Word,
) -> anyhow::Result<Note> {
build_note(
sender,
format!(
r#"
use miden::standards::fees::policies::constant_fee_manager
@note_script
pub proc main
# set_note_fee inputs: [NOTE_LOOKUP_KEY, FEE_ASSET_ID, FEE_ASSET_VALUE, pad(4)]
push.{fee_asset_value}
push.{fee_asset_id}
push.{lookup_key}
call.constant_fee_manager::set_note_fee
dropw dropw dropw
end
"#,
lookup_key = lookup_key.as_word(),
),
)
}
pub(super) fn build_manageable_fee_account(
owner: AccountId,
admin_note_roots: BTreeSet<NoteScriptRoot>,
) -> anyhow::Result<Account> {
let mut policy = BasicConstantFeePolicy::new();
for root in &admin_note_roots {
policy = policy.with_fee(*root, AssetAmount::ZERO);
}
let fee_policy_manager = FeePolicyManager::builder()
.fee_faucet_id(fee_faucet_id()?)
.active_fee_policy(policy.into())
.build();
Ok(NetworkAccount::builder([7; 32], admin_note_roots, fee_policy_manager)?
.with_component(BasicWallet)
.with_component(Ownable2Step::new(owner))
.with_component(Authority::OwnerControlled)
.with_component(ConstantFeeManager::for_basic_constant_fee_policy())
.build_existing()?)
}
pub(super) fn committed_fee_schedule_entry(
mock_chain: &MockChain,
account_id: AccountId,
lookup_key: NoteScriptRoot,
) -> anyhow::Result<Word> {
let account = mock_chain.committed_account(account_id)?;
let entry = account.storage().get_map_item(
BasicConstantFeePolicy::fee_schedule_slot_name(),
StorageMapKey::new(lookup_key.as_word()),
)?;
Ok(entry)
}
fn fee_asset_id_word() -> anyhow::Result<Word> {
Ok(fee_asset(0)?.to_id_word())
}
#[tokio::test]
async fn owner_set_note_fee_zero_schedules_free_note() -> anyhow::Result<()> {
let owner = test_account_id(70);
let account = build_manageable_fee_account(
owner,
BTreeSet::from([ConstantFeePolicyConfigNote::script_root()]),
)?;
let config_note = build_config_note(owner, account.id(), fee_asset(0)?, 1)?;
let mut builder = MockChain::builder();
builder.add_account(account.clone())?;
builder.add_output_note(RawOutputNote::Full(config_note.clone()));
let mut mock_chain = builder.build()?;
mock_chain.prove_next_block()?;
consume_note(&mut mock_chain, account.id(), &config_note).await?;
let entry = committed_fee_schedule_entry(&mock_chain, account.id(), priced_root())?;
assert_eq!(entry, Word::from([0u32, 0, 0, 1]));
Ok(())
}
#[tokio::test]
async fn owner_set_note_fee_overwrites_existing_entry() -> anyhow::Result<()> {
let owner = test_account_id(70);
let updated_fee = FEE_AMOUNT + 123;
let account = build_manageable_fee_account(
owner,
BTreeSet::from([ConstantFeePolicyConfigNote::script_root()]),
)?;
let first = build_config_note(owner, account.id(), fee_asset(FEE_AMOUNT)?, 1)?;
let second = build_config_note(owner, account.id(), fee_asset(updated_fee)?, 2)?;
let mut builder = MockChain::builder();
builder.add_account(account.clone())?;
builder.add_output_note(RawOutputNote::Full(first.clone()));
builder.add_output_note(RawOutputNote::Full(second.clone()));
let mut mock_chain = builder.build()?;
mock_chain.prove_next_block()?;
consume_note(&mut mock_chain, account.id(), &first).await?;
consume_note(&mut mock_chain, account.id(), &second).await?;
let entry = committed_fee_schedule_entry(&mock_chain, account.id(), priced_root())?;
assert_eq!(entry, Word::from([updated_fee as u32, 0, 0, 1]));
Ok(())
}
#[tokio::test]
async fn set_note_fee_with_wrong_fee_asset_is_rejected() -> anyhow::Result<()> {
let owner = test_account_id(70);
let wrong_asset =
FungibleAsset::new(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1.try_into()?, FEE_AMOUNT)?;
let account = build_manageable_fee_account(
owner,
BTreeSet::from([ConstantFeePolicyConfigNote::script_root()]),
)?;
let config_note = build_config_note(owner, account.id(), wrong_asset, 1)?;
let mut builder = MockChain::builder();
builder.add_account(account.clone())?;
builder.add_output_note(RawOutputNote::Full(config_note.clone()));
let mut mock_chain = builder.build()?;
mock_chain.prove_next_block()?;
let result = mock_chain
.build_transaction(account.id())
.authenticated_input_note(config_note.id())
.build()?
.execute()
.await;
assert_transaction_executor_error!(result, ERR_FEE_ASSET_ID_MISMATCH);
Ok(())
}
#[rstest]
#[case::element_1(1)]
#[case::element_2(2)]
#[case::element_3(3)]
#[tokio::test]
async fn set_note_fee_with_malformed_fee_asset_value_is_rejected(
#[case] dirty_element: usize,
) -> anyhow::Result<()> {
let owner = test_account_id(70);
let mut value = [FEE_AMOUNT as u32, 0, 0, 0];
value[dirty_element] = 1;
let set_note =
build_set_note_fee_note_raw(owner, priced_root(), fee_asset_id_word()?, Word::from(value))?;
let account = build_manageable_fee_account(owner, BTreeSet::from([set_note.script().root()]))?;
let mut builder = MockChain::builder();
builder.add_account(account.clone())?;
builder.add_output_note(RawOutputNote::Full(set_note.clone()));
let mut mock_chain = builder.build()?;
mock_chain.prove_next_block()?;
let result = mock_chain
.build_transaction(account.id())
.authenticated_input_note(set_note.id())
.build()?
.execute()
.await;
assert_transaction_executor_error!(result, ERR_FUNGIBLE_ASSET_VALUE_MALFORMED);
Ok(())
}
#[tokio::test]
async fn set_note_fee_with_amount_over_max_is_rejected() -> anyhow::Result<()> {
let owner = test_account_id(70);
let over_max =
Word::from([Felt::from(AssetAmount::MAX) + Felt::ONE, Felt::ZERO, Felt::ZERO, Felt::ZERO]);
let set_note =
build_set_note_fee_note_raw(owner, priced_root(), fee_asset_id_word()?, over_max)?;
let account = build_manageable_fee_account(owner, BTreeSet::from([set_note.script().root()]))?;
let mut builder = MockChain::builder();
builder.add_account(account.clone())?;
builder.add_output_note(RawOutputNote::Full(set_note.clone()));
let mut mock_chain = builder.build()?;
mock_chain.prove_next_block()?;
let result = mock_chain
.build_transaction(account.id())
.authenticated_input_note(set_note.id())
.build()?
.execute()
.await;
assert_transaction_executor_error!(
result,
ERR_FUNGIBLE_ASSET_AMOUNT_EXCEEDS_MAX_ALLOWED_AMOUNT
);
Ok(())
}
#[tokio::test]
async fn owner_set_note_fee_at_max_amount_is_accepted() -> anyhow::Result<()> {
let owner = test_account_id(70);
let max_value = Word::from([Felt::from(AssetAmount::MAX), Felt::ZERO, Felt::ZERO, Felt::ZERO]);
let set_note =
build_set_note_fee_note_raw(owner, priced_root(), fee_asset_id_word()?, max_value)?;
let account = build_manageable_fee_account(owner, BTreeSet::from([set_note.script().root()]))?;
let mut builder = MockChain::builder();
builder.add_account(account.clone())?;
builder.add_output_note(RawOutputNote::Full(set_note.clone()));
let mut mock_chain = builder.build()?;
mock_chain.prove_next_block()?;
consume_note(&mut mock_chain, account.id(), &set_note).await?;
let entry = committed_fee_schedule_entry(&mock_chain, account.id(), priced_root())?;
assert_eq!(
entry,
Word::from([Felt::from(AssetAmount::MAX), Felt::ZERO, Felt::ZERO, Felt::ONE])
);
Ok(())
}