Skip to main content

miden_standards/note/
p2id.rs

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;
25// NOTE SCRIPT
26// ================================================================================================
27
28/// Path to the P2ID note script procedure in the standards library.
29const P2ID_SCRIPT_PATH: &str = "::miden::standards::notes::p2id::main";
30
31// Initialize the P2ID note script only once
32static 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// P2ID NOTE
40// ================================================================================================
41
42/// A Pay-to-ID (P2ID) note: transfers `assets` from `sender` to the `target` account.
43///
44/// Only the `target` account can consume the note and claim its assets.
45///
46/// Construct one with the [builder](P2idNote::builder), which sets sensible defaults for the
47/// optional parameters (private note type, no attachments) and requires at least one asset.
48/// Convert a `P2idNote` into a protocol [`Note`] infallibly via `Note::from`.
49#[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    /// Builds a new [`P2idNote`].
62    ///
63    /// # Errors
64    ///
65    /// Returns an error if:
66    /// - No assets were provided.
67    /// - The assets or attachments exceed their protocol limits (see [`NoteAssets::new`] and
68    ///   [`NoteAttachments::new`]).
69    #[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    // CONSTANTS
99    // --------------------------------------------------------------------------------------------
100
101    /// Expected number of storage items of the P2ID note.
102    pub const NUM_STORAGE_ITEMS: usize = P2idNoteStorage::NUM_ITEMS;
103
104    // PUBLIC ACCESSORS
105    // --------------------------------------------------------------------------------------------
106
107    /// Returns the script of the P2ID (Pay-to-ID) note.
108    pub fn script() -> NoteScript {
109        P2ID_SCRIPT.clone()
110    }
111
112    /// Returns the P2ID (Pay-to-ID) note script root.
113    pub fn script_root() -> NoteScriptRoot {
114        P2ID_SCRIPT.root()
115    }
116
117    /// Returns the account ID of the note's sender.
118    pub fn sender(&self) -> AccountId {
119        self.sender
120    }
121
122    /// Returns the note's storage.
123    pub fn storage(&self) -> P2idNoteStorage {
124        self.storage
125    }
126
127    /// Returns the account ID of the note's target (the only account that can consume it).
128    pub fn target(&self) -> AccountId {
129        self.storage.target()
130    }
131
132    /// Returns the note's serial number.
133    pub fn serial_number(&self) -> Word {
134        self.serial_number
135    }
136
137    /// Returns the note's type.
138    pub fn note_type(&self) -> NoteType {
139        self.note_type
140    }
141
142    /// Returns the assets carried by the note.
143    pub fn assets(&self) -> &NoteAssets {
144        &self.assets
145    }
146
147    /// Returns the attachments carried by the note.
148    pub fn attachments(&self) -> &NoteAttachments {
149        &self.attachments
150    }
151}
152
153// BUILDER EXTENSIONS
154// ================================================================================================
155
156impl<S: p2id_note_builder::State> P2idNoteBuilder<S> {
157    /// Adds a single asset to the note. At least one asset is required for `.build()` to succeed.
158    pub fn asset(mut self, asset: impl Into<Asset>) -> Self {
159        self.assets.push(asset.into());
160        self
161    }
162
163    /// Adds multiple assets to the note.
164    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    /// Adds a single attachment to the note.
170    pub fn attachment(mut self, attachment: impl Into<NoteAttachment>) -> Self {
171        self.attachments.push(attachment.into());
172        self
173    }
174
175    /// Adds multiple attachments to the note.
176    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    /// Draws a serial number from `rng` and sets it on the builder.
190    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
198// CONVERSIONS
199// ================================================================================================
200
201impl 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// P2ID NOTE STORAGE
212// ================================================================================================
213
214/// Canonical storage representation for a P2ID note.
215///
216/// Contains the identifier of the target account that is authorized
217/// to consume the note. Only the account matching this ID can execute
218/// the note and claim its assets.
219#[derive(Debug, Clone, Copy, PartialEq, Eq)]
220pub struct P2idNoteStorage {
221    target: AccountId,
222}
223
224impl P2idNoteStorage {
225    // CONSTANTS
226    // --------------------------------------------------------------------------------------------
227
228    /// Expected number of storage items of the P2ID note.
229    pub const NUM_ITEMS: usize = 2;
230
231    /// Creates new P2ID note storage targeting the given account.
232    pub fn new(target: AccountId) -> Self {
233        Self { target }
234    }
235
236    /// Consumes the storage and returns a P2ID [`NoteRecipient`] with the provided serial number.
237    ///
238    /// Notes created with this recipient will be P2ID notes consumable by the specified target
239    /// account stored in this [`P2idNoteStorage`].
240    pub fn into_recipient(self, serial_num: Word) -> NoteRecipient {
241        NoteRecipient::new(serial_num, P2idNote::script(), NoteStorage::from(self))
242    }
243
244    /// Returns the target account ID.
245    pub fn target(&self) -> AccountId {
246        self.target
247    }
248}
249
250impl From<P2idNoteStorage> for NoteStorage {
251    fn from(storage: P2idNoteStorage) -> Self {
252        // Storage layout:
253        // [ account_id_suffix, account_id_prefix ]
254        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// TESTS
278// ================================================================================================
279
280#[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    // STORAGE TESTS
292    // --------------------------------------------------------------------------------------------
293
294    #[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    // BUILDER TESTS
335    // --------------------------------------------------------------------------------------------
336
337    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    /// The minimal builder uses defaults for everything but the required fields.
362    #[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    /// `.asset()` and `.assets()` both append, so they can be combined and called repeatedly.
380    #[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    /// A P2ID note must carry at least one asset.
397    #[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}