miden_standards/note/
p2id.rs1use alloc::vec::Vec;
2
3use miden_protocol::account::AccountId;
4use miden_protocol::assembly::Path;
5use miden_protocol::asset::Asset;
6use miden_protocol::crypto::rand::FeltRng;
7use miden_protocol::errors::NoteError;
8use miden_protocol::note::{
9 Note,
10 NoteAssets,
11 NoteAttachment,
12 NoteAttachments,
13 NoteRecipient,
14 NoteScript,
15 NoteScriptRoot,
16 NoteStorage,
17 NoteTag,
18 NoteType,
19 PartialNoteMetadata,
20};
21use miden_protocol::utils::sync::LazyLock;
22use miden_protocol::{Felt, Word};
23
24use crate::StandardsLib;
25use crate::note::costs::{NoteConsumptionCost, P2ID_CONSUMPTION_CYCLES};
26const P2ID_SCRIPT_PATH: &str = "::miden::standards::notes::p2id::main";
31
32static P2ID_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
34 let standards_lib = StandardsLib::default();
35 let path = Path::new(P2ID_SCRIPT_PATH);
36 NoteScript::from_package_reference(standards_lib.as_ref(), path)
37 .expect("Standards library contains P2ID note script procedure")
38});
39
40#[derive(Debug, Clone)]
51pub struct P2idNote {
52 sender: AccountId,
53 storage: P2idNoteStorage,
54 serial_number: Word,
55 note_type: NoteType,
56 assets: NoteAssets,
57 attachments: NoteAttachments,
58}
59
60#[bon::bon]
61impl P2idNote {
62 #[builder]
71 pub fn new(
72 #[builder(field)] assets: Vec<Asset>,
73 #[builder(field)] attachments: Vec<NoteAttachment>,
74 sender: AccountId,
75 #[builder(name = target, with = |target: AccountId| P2idNoteStorage::new(target))]
76 storage: P2idNoteStorage,
77 serial_number: Word,
78 #[builder(default)] note_type: NoteType,
79 ) -> Result<Self, NoteError> {
80 if assets.is_empty() {
81 return Err(NoteError::other("a P2ID note must contain at least one asset"));
82 }
83
84 let assets = NoteAssets::new(assets)?;
85 let attachments = NoteAttachments::new(attachments)?;
86
87 Ok(Self {
88 sender,
89 storage,
90 serial_number,
91 note_type,
92 assets,
93 attachments,
94 })
95 }
96}
97
98impl P2idNote {
99 pub const NUM_STORAGE_ITEMS: usize = P2idNoteStorage::NUM_ITEMS;
104
105 pub fn script() -> NoteScript {
110 P2ID_SCRIPT.clone()
111 }
112
113 pub fn script_root() -> NoteScriptRoot {
115 P2ID_SCRIPT.root()
116 }
117
118 pub fn sender(&self) -> AccountId {
120 self.sender
121 }
122
123 pub fn storage(&self) -> P2idNoteStorage {
125 self.storage
126 }
127
128 pub fn target(&self) -> AccountId {
130 self.storage.target()
131 }
132
133 pub fn serial_number(&self) -> Word {
135 self.serial_number
136 }
137
138 pub fn note_type(&self) -> NoteType {
140 self.note_type
141 }
142
143 pub fn assets(&self) -> &NoteAssets {
145 &self.assets
146 }
147
148 pub fn attachments(&self) -> &NoteAttachments {
150 &self.attachments
151 }
152}
153
154impl<S: p2id_note_builder::State> P2idNoteBuilder<S> {
158 pub fn asset(mut self, asset: impl Into<Asset>) -> Self {
160 self.assets.push(asset.into());
161 self
162 }
163
164 pub fn assets(mut self, assets: impl IntoIterator<Item = impl Into<Asset>>) -> Self {
166 self.assets.extend(assets.into_iter().map(Into::into));
167 self
168 }
169
170 pub fn attachment(mut self, attachment: impl Into<NoteAttachment>) -> Self {
172 self.attachments.push(attachment.into());
173 self
174 }
175
176 pub fn attachments(
178 mut self,
179 attachments: impl IntoIterator<Item = impl Into<NoteAttachment>>,
180 ) -> Self {
181 self.attachments.extend(attachments.into_iter().map(Into::into));
182 self
183 }
184}
185
186impl<S: p2id_note_builder::State> P2idNoteBuilder<S>
187where
188 S::SerialNumber: p2id_note_builder::IsUnset,
189{
190 pub fn generate_serial_number(
192 self,
193 rng: &mut impl FeltRng,
194 ) -> P2idNoteBuilder<p2id_note_builder::SetSerialNumber<S>> {
195 self.serial_number(rng.draw_word())
196 }
197}
198
199impl From<P2idNote> for Note {
203 fn from(note: P2idNote) -> Self {
204 let recipient = note.storage.into_recipient(note.serial_number);
205 let tag = NoteTag::with_account_target(note.storage.target());
206 let metadata = PartialNoteMetadata::new(note.sender, note.note_type).with_tag(tag);
207
208 Note::with_attachments(note.assets, metadata, recipient, note.attachments)
209 }
210}
211
212#[derive(Debug, Clone, Copy, PartialEq, Eq)]
221pub struct P2idNoteStorage {
222 target: AccountId,
223}
224
225impl P2idNoteStorage {
226 pub const NUM_ITEMS: usize = 2;
231
232 pub fn new(target: AccountId) -> Self {
234 Self { target }
235 }
236
237 pub fn into_recipient(self, serial_num: Word) -> NoteRecipient {
242 NoteRecipient::new(serial_num, P2idNote::script(), NoteStorage::from(self))
243 }
244
245 pub fn target(&self) -> AccountId {
247 self.target
248 }
249}
250
251impl From<P2idNoteStorage> for NoteStorage {
252 fn from(storage: P2idNoteStorage) -> Self {
253 NoteStorage::new(vec![storage.target.suffix(), storage.target.prefix().as_felt()])
256 .expect("number of storage items should not exceed max storage items")
257 }
258}
259
260impl TryFrom<&[Felt]> for P2idNoteStorage {
261 type Error = NoteError;
262
263 fn try_from(note_storage: &[Felt]) -> Result<Self, Self::Error> {
264 if note_storage.len() != P2idNote::NUM_STORAGE_ITEMS {
265 return Err(NoteError::InvalidNoteStorageLength {
266 expected: P2idNote::NUM_STORAGE_ITEMS,
267 actual: note_storage.len(),
268 });
269 }
270
271 let target = AccountId::try_from_elements(note_storage[0], note_storage[1])
272 .map_err(|err| NoteError::other_with_source("failed to create account id", err))?;
273
274 Ok(Self { target })
275 }
276}
277
278impl NoteConsumptionCost for P2idNote {
282 fn consumption_cycles() -> u32 {
283 P2ID_CONSUMPTION_CYCLES
284 }
285}
286
287#[cfg(test)]
291mod tests {
292 use assert_matches::assert_matches;
293 use miden_protocol::account::{AccountId, AccountType};
294 use miden_protocol::asset::FungibleAsset;
295 use miden_protocol::crypto::rand::RandomCoin;
296 use miden_protocol::errors::NoteError;
297 use miden_protocol::{Felt, Word};
298
299 use super::*;
300
301 #[test]
305 fn try_from_valid_storage_succeeds() {
306 let target = AccountId::builder()
307 .account_type(AccountType::Private)
308 .build_with_seed([1u8; 32]);
309
310 let storage = vec![target.suffix(), target.prefix().as_felt()];
311
312 let parsed =
313 P2idNoteStorage::try_from(storage.as_slice()).expect("storage should be valid");
314
315 assert_eq!(parsed.target(), target);
316 }
317
318 #[test]
319 fn try_from_invalid_length_returns_error() {
320 let storage = vec![Felt::ZERO];
321
322 let err = P2idNoteStorage::try_from(storage.as_slice())
323 .expect_err("should fail due to invalid length");
324
325 assert!(matches!(
326 err,
327 NoteError::InvalidNoteStorageLength {
328 expected: P2idNote::NUM_STORAGE_ITEMS,
329 actual: 1
330 }
331 ));
332 }
333
334 #[test]
335 fn try_from_invalid_storage_contents_returns_error() {
336 let storage = vec![Felt::new_unchecked(999_u64), Felt::new_unchecked(888_u64)];
337
338 let err = P2idNoteStorage::try_from(storage.as_slice())
339 .expect_err("should fail due to invalid account id encoding");
340
341 assert!(matches!(err, NoteError::Other { source: Some(_), .. }));
342 }
343
344 fn sender() -> AccountId {
348 AccountId::builder()
349 .account_type(AccountType::Private)
350 .build_with_seed([1u8; 32])
351 }
352
353 fn target() -> AccountId {
354 AccountId::builder()
355 .account_type(AccountType::Private)
356 .build_with_seed([2u8; 32])
357 }
358
359 fn faucet_a() -> AccountId {
360 AccountId::builder()
361 .account_type(AccountType::Public)
362 .build_with_seed([3u8; 32])
363 }
364
365 fn faucet_b() -> AccountId {
366 AccountId::builder()
367 .account_type(AccountType::Public)
368 .build_with_seed([4u8; 32])
369 }
370
371 #[test]
373 fn builder_minimal_uses_defaults() {
374 let note = P2idNote::builder()
375 .sender(sender())
376 .target(target())
377 .serial_number(Word::empty())
378 .asset(FungibleAsset::new(faucet_a(), 1).unwrap())
379 .build()
380 .unwrap();
381
382 assert_eq!(note.sender(), sender());
383 assert_eq!(note.target(), target());
384 assert_eq!(note.note_type(), NoteType::default());
385 assert_eq!(note.assets().num_assets(), 1);
386 assert_eq!(note.attachments().num_attachments(), 0);
387 }
388
389 #[test]
391 fn builder_accumulates_assets() {
392 let mut rng = RandomCoin::new(Word::empty());
393 let note = P2idNote::builder()
394 .sender(sender())
395 .target(target())
396 .asset(FungibleAsset::new(faucet_a(), 100).unwrap())
397 .assets([Asset::from(FungibleAsset::new(faucet_b(), 200).unwrap())])
398 .generate_serial_number(&mut rng)
399 .build()
400 .unwrap();
401
402 assert_eq!(note.assets().num_assets(), 2);
403 assert_ne!(note.serial_number(), Word::empty());
404 }
405
406 #[test]
408 fn builder_rejects_empty_assets() {
409 let err = P2idNote::builder()
410 .sender(sender())
411 .target(target())
412 .serial_number(Word::empty())
413 .build()
414 .expect_err("a note without assets must be rejected");
415
416 assert_matches!(err, NoteError::Other { error_msg, .. } => {
417 assert!(error_msg.contains("note must contain at least one asset"))
418 });
419 }
420}