use alloc::vec::Vec;
use miden_protocol::account::AccountId;
use miden_protocol::assembly::Path;
use miden_protocol::asset::FungibleAsset;
use miden_protocol::crypto::rand::FeltRng;
use miden_protocol::errors::NoteError;
use miden_protocol::note::{
Note,
NoteAssets,
NoteAttachment,
NoteAttachments,
NoteRecipient,
NoteScript,
NoteScriptRoot,
NoteStorage,
NoteTag,
NoteType,
PartialNoteMetadata,
};
use miden_protocol::utils::sync::LazyLock;
use miden_protocol::{Felt, Word};
use crate::StandardsLib;
use crate::note::NetworkAccountTarget;
use crate::note::costs::{CONSTANT_FEE_POLICY_CONFIG_CONSUMPTION_CYCLES, NoteConsumptionCost};
const CONSTANT_FEE_POLICY_CONFIG_SCRIPT_PATH: &str =
"::miden::standards::notes::constant_fee_policy_config::main";
static CONSTANT_FEE_POLICY_CONFIG_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
let standards_lib = StandardsLib::default();
let path = Path::new(CONSTANT_FEE_POLICY_CONFIG_SCRIPT_PATH);
NoteScript::from_package_reference(standards_lib.as_ref(), path)
.expect("Standards library contains CONSTANT_FEE_POLICY_CONFIG note script procedure")
});
#[derive(Debug, Clone)]
pub struct ConstantFeePolicyConfigNote {
sender: AccountId,
target: AccountId,
note_script_root: NoteScriptRoot,
fee_asset: FungibleAsset,
serial_number: Word,
attachments: NoteAttachments,
}
#[bon::bon]
impl ConstantFeePolicyConfigNote {
#[builder]
pub fn new(
#[builder(field)] mut attachments: Vec<NoteAttachment>,
sender: AccountId,
target: AccountId,
note_script_root: NoteScriptRoot,
fee_asset: FungibleAsset,
serial_number: Word,
) -> Result<Self, NoteError> {
NetworkAccountTarget::ensure_presence(&mut attachments, target).map_err(|err| {
NoteError::other_with_source("failed to bind the note to its target account", err)
})?;
let attachments = NoteAttachments::new(attachments)?;
Ok(Self {
sender,
target,
note_script_root,
fee_asset,
serial_number,
attachments,
})
}
}
impl ConstantFeePolicyConfigNote {
pub const NUM_STORAGE_ITEMS: usize = 12;
pub fn script() -> NoteScript {
CONSTANT_FEE_POLICY_CONFIG_SCRIPT.clone()
}
pub fn script_root() -> NoteScriptRoot {
CONSTANT_FEE_POLICY_CONFIG_SCRIPT.root()
}
pub fn sender(&self) -> AccountId {
self.sender
}
pub fn account(&self) -> AccountId {
self.target
}
pub fn note_script_root(&self) -> NoteScriptRoot {
self.note_script_root
}
pub fn fee_asset(&self) -> FungibleAsset {
self.fee_asset
}
pub fn serial_number(&self) -> Word {
self.serial_number
}
pub fn attachments(&self) -> &NoteAttachments {
&self.attachments
}
fn to_storage_values(&self) -> Vec<Felt> {
let mut values = Vec::with_capacity(Self::NUM_STORAGE_ITEMS);
values.extend_from_slice(self.note_script_root.as_word().as_elements());
values.extend_from_slice(self.fee_asset.to_id_word().as_elements());
values.extend_from_slice(self.fee_asset.to_value_word().as_elements());
values
}
}
impl<S: constant_fee_policy_config_note_builder::State> ConstantFeePolicyConfigNoteBuilder<S> {
pub fn attachment(mut self, attachment: impl Into<NoteAttachment>) -> Self {
self.attachments.push(attachment.into());
self
}
pub fn attachments(
mut self,
attachments: impl IntoIterator<Item = impl Into<NoteAttachment>>,
) -> Self {
self.attachments.extend(attachments.into_iter().map(Into::into));
self
}
}
impl<S: constant_fee_policy_config_note_builder::State> ConstantFeePolicyConfigNoteBuilder<S>
where
S::SerialNumber: constant_fee_policy_config_note_builder::IsUnset,
{
pub fn generate_serial_number(
self,
rng: &mut impl FeltRng,
) -> ConstantFeePolicyConfigNoteBuilder<
constant_fee_policy_config_note_builder::SetSerialNumber<S>,
> {
self.serial_number(rng.draw_word())
}
}
impl From<ConstantFeePolicyConfigNote> for Note {
fn from(note: ConstantFeePolicyConfigNote) -> Self {
let metadata = PartialNoteMetadata::new(note.sender, NoteType::Public)
.with_tag(NoteTag::with_account_target(note.target));
let storage = NoteStorage::new(note.to_storage_values())
.expect("number of storage items should not exceed max storage items");
let recipient =
NoteRecipient::new(note.serial_number, ConstantFeePolicyConfigNote::script(), storage);
Note::with_attachments(NoteAssets::default(), metadata, recipient, note.attachments)
}
}
impl NoteConsumptionCost for ConstantFeePolicyConfigNote {
fn consumption_cycles() -> u32 {
CONSTANT_FEE_POLICY_CONFIG_CONSUMPTION_CYCLES
}
}
#[cfg(test)]
mod tests {
use alloc::vec::Vec;
use assert_matches::assert_matches;
use miden_protocol::account::AccountType;
use miden_protocol::crypto::rand::RandomCoin;
use miden_protocol::note::NoteAttachmentScheme;
use miden_protocol::testing::account_id::ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET;
use super::*;
use crate::note::{NetworkAccountTargetError, NoteExecutionHint};
fn account_id(seed: u8) -> AccountId {
AccountId::builder()
.account_type(AccountType::Public)
.build_with_seed([seed; 32])
}
fn note_root(seed: u32) -> NoteScriptRoot {
NoteScriptRoot::from_array([seed, seed + 1, seed + 2, seed + 3])
}
fn fee_asset(amount: u64) -> FungibleAsset {
FungibleAsset::new(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET.try_into().unwrap(), amount).unwrap()
}
#[test]
fn builder_builds_constant_fee_policy_config_note() {
let mut rng = RandomCoin::new(Word::empty());
let account = account_id(1);
let sender = account_id(2);
let note = ConstantFeePolicyConfigNote::builder()
.sender(sender)
.target(account)
.note_script_root(note_root(10))
.fee_asset(fee_asset(500))
.generate_serial_number(&mut rng)
.build()
.unwrap();
assert_eq!(note.sender(), sender);
assert_eq!(note.account(), account);
let note = Note::from(note);
assert_eq!(note.metadata().note_type(), NoteType::Public);
assert_eq!(note.metadata().tag(), NoteTag::with_account_target(account));
assert_eq!(note.assets().num_assets(), 0);
}
#[test]
fn note_is_bound_to_target_account() {
let account = account_id(1);
let note = ConstantFeePolicyConfigNote::builder()
.sender(account_id(2))
.target(account)
.note_script_root(note_root(10))
.fee_asset(fee_asset(500))
.serial_number(Word::empty())
.build()
.unwrap();
let built = Note::from(note);
let target = NetworkAccountTarget::try_from(built.attachments())
.expect("note should carry a network account target attachment");
assert_eq!(target.target_id(), account);
}
#[test]
fn caller_supplied_target_for_other_account_is_rejected() {
let rogue_target =
NetworkAccountTarget::new(account_id(3), NoteExecutionHint::Always).unwrap();
let err = ConstantFeePolicyConfigNote::builder()
.sender(account_id(2))
.target(account_id(1))
.note_script_root(note_root(10))
.fee_asset(fee_asset(500))
.serial_number(Word::empty())
.attachment(rogue_target)
.build()
.unwrap_err();
assert_matches!(err, NoteError::Other { source, .. } => {
assert_matches!(
*source.unwrap().downcast().unwrap(),
NetworkAccountTargetError::TargetMismatch { .. }
)
});
}
#[test]
fn private_target_account_is_rejected() {
let private_account =
AccountId::builder().account_type(AccountType::Private).build_with_seed([9; 32]);
let err = ConstantFeePolicyConfigNote::builder()
.sender(account_id(2))
.target(private_account)
.note_script_root(note_root(10))
.fee_asset(fee_asset(500))
.serial_number(Word::empty())
.build()
.unwrap_err();
assert_matches!(err, NoteError::Other { source, .. } => {
assert_matches!(
*source.unwrap().downcast().unwrap(),
NetworkAccountTargetError::TargetNotPublic { .. }
)
});
}
#[test]
fn caller_attachments_beyond_limit_are_rejected() {
let mut builder = ConstantFeePolicyConfigNote::builder()
.sender(account_id(2))
.target(account_id(1))
.note_script_root(note_root(10))
.fee_asset(fee_asset(500))
.serial_number(Word::empty());
for scheme in 0..NoteAttachments::MAX_COUNT as u16 {
let extra = NoteAttachment::with_word(
NoteAttachmentScheme::new(64 + scheme).unwrap(),
Word::empty(),
);
builder = builder.attachment(extra);
}
assert!(matches!(builder.build(), Err(NoteError::TooManyAttachments(_))));
}
#[test]
fn storage_layout() {
let root = note_root(10);
let asset = fee_asset(777);
let note = ConstantFeePolicyConfigNote::builder()
.sender(account_id(2))
.target(account_id(1))
.note_script_root(root)
.fee_asset(asset)
.serial_number(Word::empty())
.build()
.unwrap();
let built = Note::from(note);
let mut expected = Vec::from(root.as_word().as_elements());
expected.extend_from_slice(asset.to_id_word().as_elements());
expected.extend_from_slice(asset.to_value_word().as_elements());
assert_eq!(built.storage().items(), expected.as_slice());
assert_eq!(built.storage().items().len(), ConstantFeePolicyConfigNote::NUM_STORAGE_ITEMS);
}
#[test]
fn script_root_is_registered_standard_note() {
use crate::note::StandardNote;
let standard = StandardNote::from_script_root(ConstantFeePolicyConfigNote::script_root())
.expect("config note script root should be a registered standard note");
assert_eq!(standard.name(), "CONSTANT_FEE_POLICY_CONFIG");
}
}