1use 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 target: AccountId,
76 serial_number: Word,
77 #[builder(default)] note_type: NoteType,
78 #[builder(default)] salt: [Felt; 2],
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 storage = P2idNoteStorage::new(target).with_salt(salt);
85 let assets = NoteAssets::new(assets)?;
86 let attachments = NoteAttachments::new(attachments)?;
87
88 Ok(Self {
89 sender,
90 storage,
91 serial_number,
92 note_type,
93 assets,
94 attachments,
95 })
96 }
97}
98
99impl P2idNote {
100 pub const NUM_STORAGE_ITEMS: usize = P2idNoteStorage::NUM_ITEMS;
105
106 pub fn script() -> NoteScript {
111 P2ID_SCRIPT.clone()
112 }
113
114 pub fn script_root() -> NoteScriptRoot {
116 P2ID_SCRIPT.root()
117 }
118
119 pub fn sender(&self) -> AccountId {
121 self.sender
122 }
123
124 pub fn storage(&self) -> P2idNoteStorage {
126 self.storage
127 }
128
129 pub fn target(&self) -> AccountId {
131 self.storage.target()
132 }
133
134 pub fn serial_number(&self) -> Word {
136 self.serial_number
137 }
138
139 pub fn note_type(&self) -> NoteType {
141 self.note_type
142 }
143
144 pub fn assets(&self) -> &NoteAssets {
146 &self.assets
147 }
148
149 pub fn attachments(&self) -> &NoteAttachments {
151 &self.attachments
152 }
153}
154
155impl<S: p2id_note_builder::State> P2idNoteBuilder<S> {
159 pub fn asset(mut self, asset: impl Into<Asset>) -> Self {
161 self.assets.push(asset.into());
162 self
163 }
164
165 pub fn assets(mut self, assets: impl IntoIterator<Item = impl Into<Asset>>) -> Self {
167 self.assets.extend(assets.into_iter().map(Into::into));
168 self
169 }
170
171 pub fn attachment(mut self, attachment: impl Into<NoteAttachment>) -> Self {
173 self.attachments.push(attachment.into());
174 self
175 }
176
177 pub fn attachments(
179 mut self,
180 attachments: impl IntoIterator<Item = impl Into<NoteAttachment>>,
181 ) -> Self {
182 self.attachments.extend(attachments.into_iter().map(Into::into));
183 self
184 }
185}
186
187impl<S: p2id_note_builder::State> P2idNoteBuilder<S>
188where
189 S::SerialNumber: p2id_note_builder::IsUnset,
190{
191 pub fn generate_serial_number(
193 self,
194 rng: &mut impl FeltRng,
195 ) -> P2idNoteBuilder<p2id_note_builder::SetSerialNumber<S>> {
196 self.serial_number(rng.draw_word())
197 }
198}
199
200impl From<P2idNote> for Note {
204 fn from(note: P2idNote) -> Self {
205 let recipient = note.storage.into_recipient(note.serial_number);
206 let tag = NoteTag::with_account_target(note.storage.target());
207 let metadata = PartialNoteMetadata::new(note.sender, note.note_type).with_tag(tag);
208
209 Note::with_attachments(note.assets, metadata, recipient, note.attachments)
210 }
211}
212
213#[derive(Debug, Clone, Copy, PartialEq, Eq)]
225pub struct P2idNoteStorage {
226 target: AccountId,
227 salt: [Felt; 2],
228}
229
230impl P2idNoteStorage {
231 pub const NUM_ITEMS: usize = 4;
236
237 pub fn new(target: AccountId) -> Self {
239 Self { target, salt: [Felt::ZERO; 2] }
240 }
241
242 pub fn with_salt(mut self, salt: [Felt; 2]) -> Self {
249 self.salt = salt;
250 self
251 }
252
253 pub fn into_recipient(self, serial_num: Word) -> NoteRecipient {
258 NoteRecipient::new(serial_num, P2idNote::script(), NoteStorage::from(self))
259 }
260
261 pub fn target(&self) -> AccountId {
263 self.target
264 }
265
266 pub fn salt(&self) -> [Felt; 2] {
268 self.salt
269 }
270}
271
272impl From<P2idNoteStorage> for NoteStorage {
273 fn from(storage: P2idNoteStorage) -> Self {
274 NoteStorage::new(vec![
277 storage.target.suffix(),
278 storage.target.prefix().as_felt(),
279 storage.salt[0],
280 storage.salt[1],
281 ])
282 .expect("number of storage items should not exceed max storage items")
283 }
284}
285
286impl TryFrom<&[Felt]> for P2idNoteStorage {
287 type Error = NoteError;
288
289 fn try_from(note_storage: &[Felt]) -> Result<Self, Self::Error> {
290 if note_storage.len() != P2idNote::NUM_STORAGE_ITEMS {
291 return Err(NoteError::InvalidNoteStorageLength {
292 expected: P2idNote::NUM_STORAGE_ITEMS,
293 actual: note_storage.len(),
294 });
295 }
296
297 let target = AccountId::try_from_elements(note_storage[0], note_storage[1])
298 .map_err(|err| NoteError::other_with_source("failed to create account id", err))?;
299
300 Ok(Self {
301 target,
302 salt: [note_storage[2], note_storage[3]],
303 })
304 }
305}
306
307impl NoteConsumptionCost for P2idNote {
311 fn consumption_cycles() -> u32 {
312 P2ID_CONSUMPTION_CYCLES
313 }
314}
315
316#[cfg(test)]
320mod tests {
321 use assert_matches::assert_matches;
322 use miden_protocol::account::{AccountId, AccountType};
323 use miden_protocol::asset::FungibleAsset;
324 use miden_protocol::crypto::rand::RandomCoin;
325 use miden_protocol::errors::NoteError;
326 use miden_protocol::{Felt, Word};
327
328 use super::*;
329
330 #[test]
334 fn try_from_valid_storage_succeeds() {
335 let target = AccountId::builder()
336 .account_type(AccountType::Private)
337 .build_with_seed([1u8; 32]);
338
339 let salt = [Felt::ONE, Felt::from(2u32)];
340 let storage = vec![target.suffix(), target.prefix().as_felt(), salt[0], salt[1]];
341
342 let parsed =
343 P2idNoteStorage::try_from(storage.as_slice()).expect("storage should be valid");
344
345 assert_eq!(parsed.target(), target);
346 assert_eq!(parsed.salt(), salt);
347 assert_eq!(NoteStorage::from(parsed).items(), storage.as_slice());
348 }
349
350 #[test]
351 fn try_from_invalid_length_returns_error() {
352 for len in [0, 1, 2, 3, 5] {
353 let storage = vec![Felt::ZERO; len];
354 let err = P2idNoteStorage::try_from(storage.as_slice())
355 .expect_err("should fail due to invalid length");
356
357 assert_matches!(err, NoteError::InvalidNoteStorageLength {
358 expected: P2idNote::NUM_STORAGE_ITEMS,
359 actual,
360 } => assert_eq!(actual, len));
361 }
362 }
363
364 #[test]
365 fn try_from_invalid_storage_contents_returns_error() {
366 let storage = vec![
367 Felt::new_unchecked(999_u64),
368 Felt::new_unchecked(888_u64),
369 Felt::ZERO,
370 Felt::ZERO,
371 ];
372
373 let err = P2idNoteStorage::try_from(storage.as_slice())
374 .expect_err("should fail due to invalid account id encoding");
375
376 assert!(matches!(err, NoteError::Other { source: Some(_), .. }));
377 }
378
379 fn sender() -> AccountId {
383 AccountId::builder()
384 .account_type(AccountType::Private)
385 .build_with_seed([1u8; 32])
386 }
387
388 fn target() -> AccountId {
389 AccountId::builder()
390 .account_type(AccountType::Private)
391 .build_with_seed([2u8; 32])
392 }
393
394 fn faucet_a() -> AccountId {
395 AccountId::builder()
396 .account_type(AccountType::Public)
397 .build_with_seed([3u8; 32])
398 }
399
400 fn faucet_b() -> AccountId {
401 AccountId::builder()
402 .account_type(AccountType::Public)
403 .build_with_seed([4u8; 32])
404 }
405
406 #[test]
408 fn builder_minimal_uses_defaults() {
409 let note = P2idNote::builder()
410 .sender(sender())
411 .target(target())
412 .serial_number(Word::empty())
413 .asset(FungibleAsset::new(faucet_a(), 1).unwrap())
414 .build()
415 .unwrap();
416
417 assert_eq!(note.sender(), sender());
418 assert_eq!(note.target(), target());
419 assert_eq!(note.storage().salt(), [Felt::ZERO; 2]);
420 assert_eq!(note.note_type(), NoteType::default());
421 assert_eq!(note.assets().num_assets(), 1);
422 assert_eq!(note.attachments().num_attachments(), 0);
423 }
424
425 #[test]
426 fn salt_changes_storage_and_recipient_commitments() {
427 let storage = P2idNoteStorage::new(target());
428 let recipient = storage.into_recipient(Word::empty());
429
430 for salt in [[Felt::ONE, Felt::ZERO], [Felt::ZERO, Felt::ONE]] {
431 let note: Note = P2idNote::builder()
432 .sender(sender())
433 .target(target())
434 .salt(salt)
435 .serial_number(Word::empty())
436 .asset(FungibleAsset::new(faucet_a(), 1).unwrap())
437 .build()
438 .unwrap()
439 .into();
440
441 assert_eq!(note.recipient(), &storage.with_salt(salt).into_recipient(Word::empty()));
442 assert_ne!(note.recipient().storage().commitment(), recipient.storage().commitment());
443 assert_ne!(note.recipient().digest(), recipient.digest());
444 }
445 }
446
447 #[test]
449 fn builder_accumulates_assets() {
450 let mut rng = RandomCoin::new(Word::empty());
451 let note = P2idNote::builder()
452 .sender(sender())
453 .target(target())
454 .asset(FungibleAsset::new(faucet_a(), 100).unwrap())
455 .assets([Asset::from(FungibleAsset::new(faucet_b(), 200).unwrap())])
456 .generate_serial_number(&mut rng)
457 .build()
458 .unwrap();
459
460 assert_eq!(note.assets().num_assets(), 2);
461 assert_ne!(note.serial_number(), Word::empty());
462 }
463
464 #[test]
466 fn builder_rejects_empty_assets() {
467 let err = P2idNote::builder()
468 .sender(sender())
469 .target(target())
470 .serial_number(Word::empty())
471 .build()
472 .expect_err("a note without assets must be rejected");
473
474 assert_matches!(err, NoteError::Other { error_msg, .. } => {
475 assert!(error_msg.contains("note must contain at least one asset"))
476 });
477 }
478}