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