use alloc::vec::Vec;
use miden_protocol::account::AccountId;
use miden_protocol::assembly::Path;
use miden_protocol::asset::Asset;
use miden_protocol::block::BlockNumber;
use miden_protocol::crypto::rand::FeltRng;
use miden_protocol::errors::NoteError;
use miden_protocol::note::{
Note,
NoteAssets,
NoteRecipient,
NoteScript,
NoteScriptRoot,
NoteStorage,
NoteTag,
NoteType,
PartialNoteMetadata,
};
use miden_protocol::utils::sync::LazyLock;
use miden_protocol::{Felt, Hasher, Word};
use crate::StandardsLib;
const TX_FEE_SCRIPT_PATH: &str = "::miden::standards::notes::tx_fee::main";
static TX_FEE_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
let standards_lib = StandardsLib::default();
let path = Path::new(TX_FEE_SCRIPT_PATH);
NoteScript::from_package_reference(standards_lib.as_ref(), path)
.expect("Standards library contains TX_FEE note script procedure")
});
#[derive(Debug, Clone)]
pub struct TxFeeNote {
sender: AccountId,
serial_number: Word,
assets: NoteAssets,
}
#[bon::bon]
impl TxFeeNote {
#[builder]
pub fn new(
#[builder(field)] assets: Vec<Asset>,
sender: AccountId,
serial_number: Word,
) -> Result<Self, NoteError> {
if assets.is_empty() {
return Err(NoteError::other("a TX_FEE note must contain at least one asset"));
}
let assets = NoteAssets::new(assets)?;
Ok(Self { sender, serial_number, assets })
}
}
impl TxFeeNote {
pub const NUM_STORAGE_ITEMS: usize = 0;
pub const TAG_ID: u32 = 0xfee;
pub const TAG: NoteTag = NoteTag::new(Self::TAG_ID);
pub fn script() -> NoteScript {
TX_FEE_SCRIPT.clone()
}
pub fn script_root() -> NoteScriptRoot {
TX_FEE_SCRIPT.root()
}
pub fn sender(&self) -> AccountId {
self.sender
}
pub fn serial_number(&self) -> Word {
self.serial_number
}
pub fn assets(&self) -> &NoteAssets {
&self.assets
}
pub fn derive_serial_number(
sender: AccountId,
initial_nonce: Felt,
ref_block_num: BlockNumber,
) -> Word {
let fee_domain = Word::from([Felt::from(Self::TAG_ID), Felt::ZERO, Felt::ZERO, Felt::ZERO]);
let tuple = Word::from([
Felt::from(ref_block_num.as_u32()),
initial_nonce,
sender.suffix(),
sender.prefix().as_felt(),
]);
Hasher::merge(&[fee_domain, tuple])
}
}
impl<S: tx_fee_note_builder::State> TxFeeNoteBuilder<S> {
pub fn asset(mut self, asset: impl Into<Asset>) -> Self {
self.assets.push(asset.into());
self
}
pub fn assets(mut self, assets: impl IntoIterator<Item = impl Into<Asset>>) -> Self {
self.assets.extend(assets.into_iter().map(Into::into));
self
}
}
impl<S: tx_fee_note_builder::State> TxFeeNoteBuilder<S>
where
S::SerialNumber: tx_fee_note_builder::IsUnset,
{
pub fn generate_serial_number(
self,
rng: &mut impl FeltRng,
) -> TxFeeNoteBuilder<tx_fee_note_builder::SetSerialNumber<S>> {
self.serial_number(rng.draw_word())
}
}
impl From<TxFeeNote> for Note {
fn from(note: TxFeeNote) -> Self {
let metadata =
PartialNoteMetadata::new(note.sender, NoteType::Public).with_tag(TxFeeNote::TAG);
let recipient =
NoteRecipient::new(note.serial_number, TxFeeNote::script(), NoteStorage::default());
Note::new(note.assets, metadata, recipient)
}
}
#[cfg(test)]
mod tests {
use assert_matches::assert_matches;
use miden_protocol::account::{AccountId, AccountType};
use miden_protocol::asset::FungibleAsset;
use miden_protocol::block::BlockNumber;
use miden_protocol::{Felt, Word};
use super::*;
use crate::note::{NoteConsumptionStatus, StandardNote};
fn sender() -> AccountId {
AccountId::builder()
.account_type(AccountType::Private)
.build_with_seed([1u8; 32])
}
fn unrelated_consumer() -> AccountId {
AccountId::builder()
.account_type(AccountType::Public)
.build_with_seed([2u8; 32])
}
fn faucet_a() -> AccountId {
AccountId::builder()
.account_type(AccountType::Public)
.build_with_seed([3u8; 32])
}
#[test]
fn conversion_produces_public_untargeted_note() {
let serial_number = Word::from([1u32, 2, 3, 4]);
let note: Note = TxFeeNote::builder()
.sender(sender())
.serial_number(serial_number)
.asset(FungibleAsset::new(faucet_a(), 100).unwrap())
.build()
.unwrap()
.into();
assert_eq!(note.metadata().note_type(), NoteType::Public);
assert_eq!(note.metadata().sender(), sender());
assert_eq!(note.metadata().tag(), TxFeeNote::TAG);
assert_eq!(usize::from(note.storage().num_items()), TxFeeNote::NUM_STORAGE_ITEMS);
assert_eq!(note.attachments().num_attachments(), 0);
assert_eq!(
*note.recipient(),
NoteRecipient::new(serial_number, TxFeeNote::script(), NoteStorage::default())
);
}
#[test]
fn tag_never_collides_with_default_account_target_tags() {
const LOW_18_BITS: u32 = (1 << 18) - 1;
assert_ne!(TxFeeNote::TAG.as_u32() & LOW_18_BITS, 0);
assert_eq!(Felt::from(TxFeeNote::TAG), Felt::from(TxFeeNote::TAG_ID));
}
#[test]
fn is_consumable_validates_storage() {
let block_ref = BlockNumber::from(0u32);
let asset = FungibleAsset::new(faucet_a(), 100).unwrap();
let standard_note = StandardNote::from_script_root(TxFeeNote::script_root())
.expect("TX_FEE script root should be recognized as a standard note");
let fee_note: Note = TxFeeNote::builder()
.sender(sender())
.serial_number(Word::empty())
.asset(asset)
.build()
.unwrap()
.into();
assert_matches!(
standard_note.is_consumable(&fee_note, unrelated_consumer(), block_ref),
Some(NoteConsumptionStatus::ConsumableWithAuthorization)
);
let malformed_storage = NoteStorage::new(vec![Felt::from(1u32)]).unwrap();
let malformed_note = Note::new(
NoteAssets::new(vec![asset.into()]).unwrap(),
PartialNoteMetadata::new(sender(), NoteType::Public).with_tag(TxFeeNote::TAG),
NoteRecipient::new(Word::empty(), TxFeeNote::script(), malformed_storage),
);
assert_matches!(
standard_note.is_consumable(&malformed_note, unrelated_consumer(), block_ref),
Some(NoteConsumptionStatus::NeverConsumable(_))
);
}
}