use std::collections::BTreeSet;
use miden_processor::crypto::random::RandomCoin;
use miden_protocol::account::{AccountId, AccountType};
use miden_protocol::asset::FungibleAsset;
use miden_protocol::note::Note;
use miden_protocol::transaction::RawOutputNote;
use miden_protocol::{Felt, Word};
use miden_standards::errors::standards::{
ERR_CONSTANT_FEE_POLICY_CONFIG_ACCOUNT_MISMATCH,
ERR_CONSTANT_FEE_POLICY_CONFIG_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS,
ERR_NETWORK_ACCOUNT_TARGET_MISSING,
ERR_SENDER_NOT_OWNER,
};
use miden_standards::note::{ConstantFeePolicyConfigNote, NetworkAccountTarget, NoteExecutionHint};
use miden_standards::testing::note::NoteBuilder;
use miden_testing::{MockChain, assert_transaction_executor_error};
use rstest::rstest;
use super::constant_fee_manager::{
build_manageable_fee_account,
committed_fee_schedule_entry,
fee_asset,
non_owner_id,
owner_id,
};
use super::fee_manager::{FEE_AMOUNT, priced_root};
use crate::consume_note;
pub(super) fn build_config_note(
sender: AccountId,
account: AccountId,
fee_asset: FungibleAsset,
serial_seed: u32,
) -> anyhow::Result<Note> {
let note = ConstantFeePolicyConfigNote::builder()
.sender(sender)
.target(account)
.note_script_root(priced_root())
.fee_asset(fee_asset)
.serial_number(Word::from([serial_seed, 0, 0, 0]))
.build()?;
Ok(Note::from(note))
}
fn build_wrong_storage_config_note(
sender: AccountId,
account: AccountId,
num_items: usize,
) -> anyhow::Result<Note> {
let mut rng = RandomCoin::new(Word::from([7u32, 0, 0, 0]));
let target = NetworkAccountTarget::new(account, NoteExecutionHint::Always)?;
let note = NoteBuilder::new(sender, &mut rng)
.script(ConstantFeePolicyConfigNote::script())
.note_storage(vec![Felt::from(1u32); num_items])?
.attachment(target)
.build()?;
Ok(note)
}
#[tokio::test]
async fn config_note_schedules_fee() -> anyhow::Result<()> {
let owner = owner_id();
let account = build_manageable_fee_account(
owner,
BTreeSet::from([ConstantFeePolicyConfigNote::script_root()]),
)?;
let config_note = build_config_note(owner, account.id(), fee_asset(FEE_AMOUNT)?, 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([FEE_AMOUNT as u32, 0, 0, 1]));
Ok(())
}
#[tokio::test]
async fn non_owner_config_note_is_rejected() -> anyhow::Result<()> {
let owner = owner_id();
let account = build_manageable_fee_account(
owner,
BTreeSet::from([ConstantFeePolicyConfigNote::script_root()]),
)?;
let attacker_note = build_config_note(non_owner_id(), account.id(), fee_asset(FEE_AMOUNT)?, 2)?;
let mut builder = MockChain::builder();
builder.add_account(account.clone())?;
builder.add_output_note(RawOutputNote::Full(attacker_note.clone()));
let mut mock_chain = builder.build()?;
mock_chain.prove_next_block()?;
let result = mock_chain
.build_transaction(account.id())
.authenticated_input_note(attacker_note.id())
.build()?
.execute()
.await;
assert_transaction_executor_error!(result, ERR_SENDER_NOT_OWNER);
Ok(())
}
#[rstest]
#[case::empty(0)]
#[case::too_few(ConstantFeePolicyConfigNote::NUM_STORAGE_ITEMS - 4)]
#[case::too_many(ConstantFeePolicyConfigNote::NUM_STORAGE_ITEMS + 4)]
#[tokio::test]
async fn config_note_with_wrong_storage_item_count_is_rejected(
#[case] num_items: usize,
) -> anyhow::Result<()> {
let owner = owner_id();
let account = build_manageable_fee_account(
owner,
BTreeSet::from([ConstantFeePolicyConfigNote::script_root()]),
)?;
let malformed_note = build_wrong_storage_config_note(owner, account.id(), num_items)?;
let mut builder = MockChain::builder();
builder.add_account(account.clone())?;
builder.add_output_note(RawOutputNote::Full(malformed_note.clone()));
let mut mock_chain = builder.build()?;
mock_chain.prove_next_block()?;
let result = mock_chain
.build_transaction(account.id())
.authenticated_input_note(malformed_note.id())
.build()?
.execute()
.await;
assert_transaction_executor_error!(
result,
ERR_CONSTANT_FEE_POLICY_CONFIG_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS
);
Ok(())
}
#[tokio::test]
async fn config_note_for_another_account_is_rejected() -> anyhow::Result<()> {
let owner = owner_id();
let target_account =
AccountId::builder().account_type(AccountType::Public).build_with_seed([80; 32]);
let consuming_account = build_manageable_fee_account(
owner,
BTreeSet::from([ConstantFeePolicyConfigNote::script_root()]),
)?;
let config_note = build_config_note(owner, target_account, fee_asset(FEE_AMOUNT)?, 3)?;
let mut builder = MockChain::builder();
builder.add_account(consuming_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(consuming_account.id())
.authenticated_input_note(config_note.id())
.build()?
.execute()
.await;
assert_transaction_executor_error!(result, ERR_CONSTANT_FEE_POLICY_CONFIG_ACCOUNT_MISMATCH);
Ok(())
}
#[tokio::test]
async fn config_note_without_target_attachment_is_rejected() -> anyhow::Result<()> {
let owner = owner_id();
let account = build_manageable_fee_account(
owner,
BTreeSet::from([ConstantFeePolicyConfigNote::script_root()]),
)?;
let mut rng = RandomCoin::new(Word::from([9u32, 0, 0, 0]));
let note = NoteBuilder::new(owner, &mut rng)
.script(ConstantFeePolicyConfigNote::script())
.note_storage(vec![Felt::from(1u32); ConstantFeePolicyConfigNote::NUM_STORAGE_ITEMS])?
.build()?;
let mut builder = MockChain::builder();
builder.add_account(account.clone())?;
builder.add_output_note(RawOutputNote::Full(note.clone()));
let mut mock_chain = builder.build()?;
mock_chain.prove_next_block()?;
let result = mock_chain
.build_transaction(account.id())
.authenticated_input_note(note.id())
.build()?
.execute()
.await;
assert_transaction_executor_error!(result, ERR_NETWORK_ACCOUNT_TARGET_MISSING);
Ok(())
}