miden_standards/note/
tx_fee.rs1use alloc::vec::Vec;
2
3use miden_protocol::account::AccountId;
4use miden_protocol::assembly::Path;
5use miden_protocol::asset::Asset;
6use miden_protocol::block::BlockNumber;
7use miden_protocol::crypto::rand::FeltRng;
8use miden_protocol::errors::NoteError;
9use miden_protocol::note::{
10 Note,
11 NoteAssets,
12 NoteRecipient,
13 NoteScript,
14 NoteScriptRoot,
15 NoteStorage,
16 NoteTag,
17 NoteType,
18 PartialNoteMetadata,
19};
20use miden_protocol::utils::sync::LazyLock;
21use miden_protocol::{Felt, Hasher, Word};
22
23use crate::StandardsLib;
24
25const TX_FEE_SCRIPT_PATH: &str = "::miden::standards::notes::tx_fee::main";
30
31static TX_FEE_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
33 let standards_lib = StandardsLib::default();
34 let path = Path::new(TX_FEE_SCRIPT_PATH);
35 NoteScript::from_package_reference(standards_lib.as_ref(), path)
36 .expect("Standards library contains TX_FEE note script procedure")
37});
38
39#[derive(Debug, Clone)]
54pub struct TxFeeNote {
55 sender: AccountId,
56 serial_number: Word,
57 assets: NoteAssets,
58}
59
60#[bon::bon]
61impl TxFeeNote {
62 #[builder]
70 pub fn new(
71 #[builder(field)] assets: Vec<Asset>,
72 sender: AccountId,
73 serial_number: Word,
74 ) -> Result<Self, NoteError> {
75 if assets.is_empty() {
76 return Err(NoteError::other("a TX_FEE note must contain at least one asset"));
77 }
78
79 let assets = NoteAssets::new(assets)?;
80
81 Ok(Self { sender, serial_number, assets })
82 }
83}
84
85impl TxFeeNote {
86 pub const NUM_STORAGE_ITEMS: usize = 0;
91
92 pub const TAG_ID: u32 = 0xfee;
98
99 pub const TAG: NoteTag = NoteTag::new(Self::TAG_ID);
107
108 pub fn script() -> NoteScript {
113 TX_FEE_SCRIPT.clone()
114 }
115
116 pub fn script_root() -> NoteScriptRoot {
118 TX_FEE_SCRIPT.root()
119 }
120
121 pub fn sender(&self) -> AccountId {
123 self.sender
124 }
125
126 pub fn serial_number(&self) -> Word {
128 self.serial_number
129 }
130
131 pub fn assets(&self) -> &NoteAssets {
133 &self.assets
134 }
135
136 pub fn derive_serial_number(
151 sender: AccountId,
152 initial_nonce: Felt,
153 ref_block_num: BlockNumber,
154 ) -> Word {
155 let fee_domain = Word::from([Felt::from(Self::TAG_ID), Felt::ZERO, Felt::ZERO, Felt::ZERO]);
157 let tuple = Word::from([
158 Felt::from(ref_block_num.as_u32()),
159 initial_nonce,
160 sender.suffix(),
161 sender.prefix().as_felt(),
162 ]);
163
164 Hasher::merge(&[fee_domain, tuple])
165 }
166}
167
168impl<S: tx_fee_note_builder::State> TxFeeNoteBuilder<S> {
172 pub fn asset(mut self, asset: impl Into<Asset>) -> Self {
174 self.assets.push(asset.into());
175 self
176 }
177
178 pub fn assets(mut self, assets: impl IntoIterator<Item = impl Into<Asset>>) -> Self {
180 self.assets.extend(assets.into_iter().map(Into::into));
181 self
182 }
183}
184
185impl<S: tx_fee_note_builder::State> TxFeeNoteBuilder<S>
186where
187 S::SerialNumber: tx_fee_note_builder::IsUnset,
188{
189 pub fn generate_serial_number(
191 self,
192 rng: &mut impl FeltRng,
193 ) -> TxFeeNoteBuilder<tx_fee_note_builder::SetSerialNumber<S>> {
194 self.serial_number(rng.draw_word())
195 }
196}
197
198impl From<TxFeeNote> for Note {
202 fn from(note: TxFeeNote) -> Self {
203 let metadata =
206 PartialNoteMetadata::new(note.sender, NoteType::Public).with_tag(TxFeeNote::TAG);
207 let recipient =
208 NoteRecipient::new(note.serial_number, TxFeeNote::script(), NoteStorage::default());
209
210 Note::new(note.assets, metadata, recipient)
211 }
212}
213
214#[cfg(test)]
218mod tests {
219 use assert_matches::assert_matches;
220 use miden_protocol::account::{AccountId, AccountType};
221 use miden_protocol::asset::FungibleAsset;
222 use miden_protocol::block::BlockNumber;
223 use miden_protocol::{Felt, Word};
224
225 use super::*;
226 use crate::note::{NoteConsumptionStatus, StandardNote};
227
228 fn sender() -> AccountId {
229 AccountId::builder()
230 .account_type(AccountType::Private)
231 .build_with_seed([1u8; 32])
232 }
233
234 fn unrelated_consumer() -> AccountId {
235 AccountId::builder()
236 .account_type(AccountType::Public)
237 .build_with_seed([2u8; 32])
238 }
239
240 fn faucet_a() -> AccountId {
241 AccountId::builder()
242 .account_type(AccountType::Public)
243 .build_with_seed([3u8; 32])
244 }
245
246 #[test]
252 fn conversion_produces_public_untargeted_note() {
253 let serial_number = Word::from([1u32, 2, 3, 4]);
254 let note: Note = TxFeeNote::builder()
255 .sender(sender())
256 .serial_number(serial_number)
257 .asset(FungibleAsset::new(faucet_a(), 100).unwrap())
258 .build()
259 .unwrap()
260 .into();
261
262 assert_eq!(note.metadata().note_type(), NoteType::Public);
263 assert_eq!(note.metadata().sender(), sender());
264 assert_eq!(note.metadata().tag(), TxFeeNote::TAG);
265 assert_eq!(usize::from(note.storage().num_items()), TxFeeNote::NUM_STORAGE_ITEMS);
266 assert_eq!(note.attachments().num_attachments(), 0);
267 assert_eq!(
268 *note.recipient(),
269 NoteRecipient::new(serial_number, TxFeeNote::script(), NoteStorage::default())
270 );
271 }
272
273 #[test]
277 fn tag_never_collides_with_default_account_target_tags() {
278 const LOW_18_BITS: u32 = (1 << 18) - 1;
279 assert_ne!(TxFeeNote::TAG.as_u32() & LOW_18_BITS, 0);
280 assert_eq!(Felt::from(TxFeeNote::TAG), Felt::from(TxFeeNote::TAG_ID));
281 }
282
283 #[test]
290 fn is_consumable_validates_storage() {
291 let block_ref = BlockNumber::from(0u32);
292 let asset = FungibleAsset::new(faucet_a(), 100).unwrap();
293
294 let standard_note = StandardNote::from_script_root(TxFeeNote::script_root())
295 .expect("TX_FEE script root should be recognized as a standard note");
296
297 let fee_note: Note = TxFeeNote::builder()
298 .sender(sender())
299 .serial_number(Word::empty())
300 .asset(asset)
301 .build()
302 .unwrap()
303 .into();
304
305 assert_matches!(
306 standard_note.is_consumable(&fee_note, unrelated_consumer(), block_ref),
307 Some(NoteConsumptionStatus::ConsumableWithAuthorization)
308 );
309
310 let malformed_storage = NoteStorage::new(vec![Felt::from(1u32)]).unwrap();
312 let malformed_note = Note::new(
313 NoteAssets::new(vec![asset.into()]).unwrap(),
314 PartialNoteMetadata::new(sender(), NoteType::Public).with_tag(TxFeeNote::TAG),
315 NoteRecipient::new(Word::empty(), TxFeeNote::script(), malformed_storage),
316 );
317
318 assert_matches!(
319 standard_note.is_consumable(&malformed_note, unrelated_consumer(), block_ref),
320 Some(NoteConsumptionStatus::NeverConsumable(_))
321 );
322 }
323}