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    NoteMetadata,
13    NoteRecipient,
14    NoteScript,
15    NoteStorage,
16    NoteTag,
17    NoteType,
18};
19use miden_protocol::utils::sync::LazyLock;
20use miden_protocol::{Felt, Word};
21
22use crate::StandardsLib;
23// NOTE SCRIPT
24// ================================================================================================
25
26/// Path to the P2ID note script procedure in the standards library.
27const P2ID_SCRIPT_PATH: &str = "::miden::standards::notes::p2id::main";
28
29// Initialize the P2ID note script only once
30static P2ID_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
31    let standards_lib = StandardsLib::default();
32    let path = Path::new(P2ID_SCRIPT_PATH);
33    NoteScript::from_library_reference(standards_lib.as_ref(), path)
34        .expect("Standards library contains P2ID note script procedure")
35});
36
37// P2ID NOTE
38// ================================================================================================
39
40/// TODO: add docs
41pub struct P2idNote;
42
43impl P2idNote {
44    // CONSTANTS
45    // --------------------------------------------------------------------------------------------
46
47    /// Expected number of storage items of the P2ID note.
48    pub const NUM_STORAGE_ITEMS: usize = P2idNoteStorage::NUM_ITEMS;
49
50    // PUBLIC ACCESSORS
51    // --------------------------------------------------------------------------------------------
52
53    /// Returns the script of the P2ID (Pay-to-ID) note.
54    pub fn script() -> NoteScript {
55        P2ID_SCRIPT.clone()
56    }
57
58    /// Returns the P2ID (Pay-to-ID) note script root.
59    pub fn script_root() -> Word {
60        P2ID_SCRIPT.root()
61    }
62
63    // BUILDERS
64    // --------------------------------------------------------------------------------------------
65
66    /// Generates a P2ID note - Pay-to-ID note.
67    ///
68    /// This script enables the transfer of assets from the `sender` account to the `target` account
69    /// by specifying the target's account ID.
70    ///
71    /// The passed-in `rng` is used to generate a serial number for the note. The returned note's
72    /// tag is set to the target's account ID.
73    ///
74    /// # Errors
75    /// Returns an error if deserialization or compilation of the `P2ID` script fails.
76    pub fn create<R: FeltRng>(
77        sender: AccountId,
78        target: AccountId,
79        assets: Vec<Asset>,
80        note_type: NoteType,
81        attachment: NoteAttachment,
82        rng: &mut R,
83    ) -> Result<Note, NoteError> {
84        let serial_num = rng.draw_word();
85        let recipient = P2idNoteStorage::new(target).into_recipient(serial_num);
86
87        let tag = NoteTag::with_account_target(target);
88
89        let metadata =
90            NoteMetadata::new(sender, note_type).with_tag(tag).with_attachment(attachment);
91        let vault = NoteAssets::new(assets)?;
92
93        Ok(Note::new(vault, metadata, recipient))
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([note_storage[1], note_storage[0]])
154            .map_err(|e| NoteError::other_with_source("failed to create account id", e))?;
155
156        Ok(Self { target })
157    }
158}
159
160// TESTS
161// ================================================================================================
162
163#[cfg(test)]
164mod tests {
165    use miden_protocol::account::{AccountId, AccountIdVersion, AccountStorageMode, AccountType};
166    use miden_protocol::errors::NoteError;
167    use miden_protocol::{Felt, FieldElement};
168
169    use super::*;
170
171    #[test]
172    fn try_from_valid_storage_succeeds() {
173        let target = AccountId::dummy(
174            [1u8; 15],
175            AccountIdVersion::Version0,
176            AccountType::FungibleFaucet,
177            AccountStorageMode::Private,
178        );
179
180        let storage = vec![target.suffix(), target.prefix().as_felt()];
181
182        let parsed =
183            P2idNoteStorage::try_from(storage.as_slice()).expect("storage should be valid");
184
185        assert_eq!(parsed.target(), target);
186    }
187
188    #[test]
189    fn try_from_invalid_length_returns_error() {
190        let storage = vec![Felt::ZERO];
191
192        let err = P2idNoteStorage::try_from(storage.as_slice())
193            .expect_err("should fail due to invalid length");
194
195        assert!(matches!(
196            err,
197            NoteError::InvalidNoteStorageLength {
198                expected: P2idNote::NUM_STORAGE_ITEMS,
199                actual: 1
200            }
201        ));
202    }
203
204    #[test]
205    fn try_from_invalid_storage_contents_returns_error() {
206        let storage = vec![Felt::new(999u64), Felt::new(888u64)];
207
208        let err = P2idNoteStorage::try_from(storage.as_slice())
209            .expect_err("should fail due to invalid account id encoding");
210
211        assert!(matches!(err, NoteError::Other { source: Some(_), .. }));
212    }
213}