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    NoteAttachments,
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, Word};
22
23use crate::StandardsLib;
24// NOTE SCRIPT
25// ================================================================================================
26
27/// Path to the P2ID note script procedure in the standards library.
28const P2ID_SCRIPT_PATH: &str = "::miden::standards::notes::p2id::main";
29
30// Initialize the P2ID note script only once
31static P2ID_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
32    let standards_lib = StandardsLib::default();
33    let path = Path::new(P2ID_SCRIPT_PATH);
34    NoteScript::from_library_reference(standards_lib.as_ref(), path)
35        .expect("Standards library contains P2ID note script procedure")
36});
37
38// P2ID NOTE
39// ================================================================================================
40
41/// TODO: add docs
42pub struct P2idNote;
43
44impl P2idNote {
45    // CONSTANTS
46    // --------------------------------------------------------------------------------------------
47
48    /// Expected number of storage items of the P2ID note.
49    pub const NUM_STORAGE_ITEMS: usize = P2idNoteStorage::NUM_ITEMS;
50
51    // PUBLIC ACCESSORS
52    // --------------------------------------------------------------------------------------------
53
54    /// Returns the script of the P2ID (Pay-to-ID) note.
55    pub fn script() -> NoteScript {
56        P2ID_SCRIPT.clone()
57    }
58
59    /// Returns the P2ID (Pay-to-ID) note script root.
60    pub fn script_root() -> NoteScriptRoot {
61        P2ID_SCRIPT.root()
62    }
63
64    // BUILDERS
65    // --------------------------------------------------------------------------------------------
66
67    /// Generates a P2ID note - Pay-to-ID note.
68    ///
69    /// This script enables the transfer of assets from the `sender` account to the `target` account
70    /// by specifying the target's account ID.
71    ///
72    /// The passed-in `rng` is used to generate a serial number for the note. The returned note's
73    /// tag is set to the target's account ID.
74    ///
75    /// # Errors
76    /// Returns an error if deserialization or compilation of the `P2ID` script fails.
77    pub fn create<R: FeltRng>(
78        sender: AccountId,
79        target: AccountId,
80        assets: Vec<Asset>,
81        note_type: NoteType,
82        attachments: NoteAttachments,
83        rng: &mut R,
84    ) -> Result<Note, NoteError> {
85        let serial_num = rng.draw_word();
86        let recipient = P2idNoteStorage::new(target).into_recipient(serial_num);
87
88        let tag = NoteTag::with_account_target(target);
89
90        let metadata = PartialNoteMetadata::new(sender, note_type).with_tag(tag);
91        let vault = NoteAssets::new(assets)?;
92
93        Ok(Note::with_attachments(vault, metadata, recipient, attachments))
94    }
95}
96
97/// Canonical storage representation for a P2ID note.
98///
99/// Contains the identifier of the target account that is authorized
100/// to consume the note. Only the account matching this ID can execute
101/// the note and claim its assets.
102#[derive(Debug, Clone, Copy, PartialEq, Eq)]
103pub struct P2idNoteStorage {
104    target: AccountId,
105}
106
107impl P2idNoteStorage {
108    // CONSTANTS
109    // --------------------------------------------------------------------------------------------
110
111    /// Expected number of storage items of the P2ID note.
112    pub const NUM_ITEMS: usize = 2;
113
114    /// Creates new P2ID note storage targeting the given account.
115    pub fn new(target: AccountId) -> Self {
116        Self { target }
117    }
118
119    /// Consumes the storage and returns a P2ID [`NoteRecipient`] with the provided serial number.
120    ///
121    /// Notes created with this recipient will be P2ID notes consumable by the specified target
122    /// account stored in this [`P2idNoteStorage`].
123    pub fn into_recipient(self, serial_num: Word) -> NoteRecipient {
124        NoteRecipient::new(serial_num, P2idNote::script(), NoteStorage::from(self))
125    }
126
127    /// Returns the target account ID.
128    pub fn target(&self) -> AccountId {
129        self.target
130    }
131}
132
133impl From<P2idNoteStorage> for NoteStorage {
134    fn from(storage: P2idNoteStorage) -> Self {
135        // Storage layout:
136        // [ account_id_suffix, account_id_prefix ]
137        NoteStorage::new(vec![storage.target.suffix(), storage.target.prefix().as_felt()])
138            .expect("number of storage items should not exceed max storage items")
139    }
140}
141
142impl TryFrom<&[Felt]> for P2idNoteStorage {
143    type Error = NoteError;
144
145    fn try_from(note_storage: &[Felt]) -> Result<Self, Self::Error> {
146        if note_storage.len() != P2idNote::NUM_STORAGE_ITEMS {
147            return Err(NoteError::InvalidNoteStorageLength {
148                expected: P2idNote::NUM_STORAGE_ITEMS,
149                actual: note_storage.len(),
150            });
151        }
152
153        let target = AccountId::try_from_elements(note_storage[0], note_storage[1])
154            .map_err(|err| NoteError::other_with_source("failed to create account id", err))?;
155
156        Ok(Self { target })
157    }
158}
159
160// TESTS
161// ================================================================================================
162
163#[cfg(test)]
164mod tests {
165    use miden_protocol::Felt;
166    use miden_protocol::account::{AccountId, AccountIdVersion, AccountType};
167    use miden_protocol::errors::NoteError;
168
169    use super::*;
170
171    #[test]
172    fn try_from_valid_storage_succeeds() {
173        let target = AccountId::dummy([1u8; 15], AccountIdVersion::Version1, AccountType::Private);
174
175        let storage = vec![target.suffix(), target.prefix().as_felt()];
176
177        let parsed =
178            P2idNoteStorage::try_from(storage.as_slice()).expect("storage should be valid");
179
180        assert_eq!(parsed.target(), target);
181    }
182
183    #[test]
184    fn try_from_invalid_length_returns_error() {
185        let storage = vec![Felt::ZERO];
186
187        let err = P2idNoteStorage::try_from(storage.as_slice())
188            .expect_err("should fail due to invalid length");
189
190        assert!(matches!(
191            err,
192            NoteError::InvalidNoteStorageLength {
193                expected: P2idNote::NUM_STORAGE_ITEMS,
194                actual: 1
195            }
196        ));
197    }
198
199    #[test]
200    fn try_from_invalid_storage_contents_returns_error() {
201        let storage = vec![Felt::new_unchecked(999_u64), Felt::new_unchecked(888_u64)];
202
203        let err = P2idNoteStorage::try_from(storage.as_slice())
204            .expect_err("should fail due to invalid account id encoding");
205
206        assert!(matches!(err, NoteError::Other { source: Some(_), .. }));
207    }
208}