miden_standards/note/
p2ide.rs1use alloc::vec::Vec;
2
3use miden_protocol::account::AccountId;
4use miden_protocol::assembly::Path;
5use miden_protocol::asset::Asset;
6use miden_protocol::block::BlockNumber;
7use miden_protocol::crypto::rand::FeltRng;
8use miden_protocol::errors::NoteError;
9use miden_protocol::note::{
10 Note,
11 NoteAssets,
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 P2IDE_SCRIPT_PATH: &str = "::miden::standards::notes::p2ide::main";
30
31static P2IDE_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
33 let standards_lib = StandardsLib::default();
34 let path = Path::new(P2IDE_SCRIPT_PATH);
35 NoteScript::from_library_reference(standards_lib.as_ref(), path)
36 .expect("Standards library contains P2IDE note script procedure")
37});
38
39pub struct P2ideNote;
52
53impl P2ideNote {
54 pub const NUM_STORAGE_ITEMS: usize = P2ideNoteStorage::NUM_ITEMS;
59
60 pub fn script() -> NoteScript {
65 P2IDE_SCRIPT.clone()
66 }
67
68 pub fn script_root() -> NoteScriptRoot {
70 P2IDE_SCRIPT.root()
71 }
72
73 pub fn create<R: FeltRng>(
85 sender: AccountId,
86 storage: P2ideNoteStorage,
87 assets: Vec<Asset>,
88 note_type: NoteType,
89 attachments: NoteAttachments,
90 rng: &mut R,
91 ) -> Result<Note, NoteError> {
92 let serial_num = rng.draw_word();
93 let recipient = storage.into_recipient(serial_num)?;
94 let tag = NoteTag::with_account_target(storage.target());
95
96 let metadata = PartialNoteMetadata::new(sender, note_type).with_tag(tag);
97 let vault = NoteAssets::new(assets)?;
98
99 Ok(Note::with_attachments(vault, metadata, recipient, attachments))
100 }
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
112pub struct P2ideNoteStorage {
113 pub target: AccountId,
114 pub reclaim_height: Option<BlockNumber>,
115 pub timelock_height: Option<BlockNumber>,
116}
117
118impl P2ideNoteStorage {
119 pub const NUM_ITEMS: usize = 4;
124
125 pub fn new(
127 target: AccountId,
128 reclaim_height: Option<BlockNumber>,
129 timelock_height: Option<BlockNumber>,
130 ) -> Self {
131 Self { target, reclaim_height, timelock_height }
132 }
133
134 pub fn into_recipient(self, serial_num: Word) -> Result<NoteRecipient, NoteError> {
136 let note_script = P2ideNote::script();
137 Ok(NoteRecipient::new(serial_num, note_script, self.into()))
138 }
139
140 pub fn target(&self) -> AccountId {
142 self.target
143 }
144
145 pub fn reclaim_height(&self) -> Option<BlockNumber> {
147 self.reclaim_height
148 }
149
150 pub fn timelock_height(&self) -> Option<BlockNumber> {
152 self.timelock_height
153 }
154}
155
156impl From<P2ideNoteStorage> for NoteStorage {
157 fn from(storage: P2ideNoteStorage) -> Self {
158 let reclaim = storage.reclaim_height.map(Felt::from).unwrap_or(Felt::ZERO);
159 let timelock = storage.timelock_height.map(Felt::from).unwrap_or(Felt::ZERO);
160
161 NoteStorage::new(vec![
162 storage.target.suffix(),
163 storage.target.prefix().as_felt(),
164 reclaim,
165 timelock,
166 ])
167 .expect("number of storage items should not exceed max storage items")
168 }
169}
170
171impl TryFrom<&[Felt]> for P2ideNoteStorage {
172 type Error = NoteError;
173
174 fn try_from(note_storage: &[Felt]) -> Result<Self, Self::Error> {
175 if note_storage.len() != P2ideNote::NUM_STORAGE_ITEMS {
176 return Err(NoteError::InvalidNoteStorageLength {
177 expected: P2ideNote::NUM_STORAGE_ITEMS,
178 actual: note_storage.len(),
179 });
180 }
181
182 let target = AccountId::try_from_elements(note_storage[0], note_storage[1])
183 .map_err(|err| NoteError::other_with_source("failed to create account id", err))?;
184
185 let reclaim_height = if note_storage[2] == Felt::ZERO {
186 None
187 } else {
188 let height: u32 = note_storage[2]
189 .as_canonical_u64()
190 .try_into()
191 .map_err(|e| NoteError::other_with_source("invalid note storage", e))?;
192
193 Some(BlockNumber::from(height))
194 };
195
196 let timelock_height = if note_storage[3] == Felt::ZERO {
197 None
198 } else {
199 let height: u32 = note_storage[3]
200 .as_canonical_u64()
201 .try_into()
202 .map_err(|e| NoteError::other_with_source("invalid note storage", e))?;
203
204 Some(BlockNumber::from(height))
205 };
206
207 Ok(Self { target, reclaim_height, timelock_height })
208 }
209}
210
211#[cfg(test)]
215mod tests {
216 use miden_protocol::Felt;
217 use miden_protocol::account::{AccountId, AccountIdVersion, AccountType};
218 use miden_protocol::block::BlockNumber;
219 use miden_protocol::errors::NoteError;
220
221 use super::*;
222
223 fn dummy_account() -> AccountId {
224 AccountId::dummy([3u8; 15], AccountIdVersion::Version1, AccountType::Private)
225 }
226
227 #[test]
228 fn try_from_valid_storage_with_all_fields_succeeds() {
229 let target = dummy_account();
230
231 let storage = vec![
232 target.suffix(),
233 target.prefix().as_felt(),
234 Felt::from(42u32),
235 Felt::from(100u32),
236 ];
237
238 let decoded = P2ideNoteStorage::try_from(storage.as_slice())
239 .expect("valid P2IDE storage should decode");
240
241 assert_eq!(decoded.target(), target);
242 assert_eq!(decoded.reclaim_height(), Some(BlockNumber::from(42u32)));
243 assert_eq!(decoded.timelock_height(), Some(BlockNumber::from(100u32)));
244 }
245
246 #[test]
247 fn try_from_zero_heights_map_to_none() {
248 let target = dummy_account();
249
250 let storage = vec![target.suffix(), target.prefix().as_felt(), Felt::ZERO, Felt::ZERO];
251
252 let decoded = P2ideNoteStorage::try_from(storage.as_slice()).unwrap();
253
254 assert_eq!(decoded.reclaim_height(), None);
255 assert_eq!(decoded.timelock_height(), None);
256 }
257
258 #[test]
259 fn try_from_invalid_length_fails() {
260 let storage = vec![Felt::ZERO; 3];
261
262 let err =
263 P2ideNoteStorage::try_from(storage.as_slice()).expect_err("wrong length must fail");
264
265 assert!(matches!(
266 err,
267 NoteError::InvalidNoteStorageLength {
268 expected: P2ideNote::NUM_STORAGE_ITEMS,
269 actual: 3
270 }
271 ));
272 }
273
274 #[test]
275 fn try_from_invalid_account_id_fails() {
276 let storage = vec![Felt::from(999_u32), Felt::from(888_u32), Felt::ZERO, Felt::ZERO];
277
278 let err = P2ideNoteStorage::try_from(storage.as_slice())
279 .expect_err("invalid account id encoding must fail");
280
281 assert!(matches!(err, NoteError::Other { source: Some(_), .. }));
282 }
283
284 #[test]
285 fn try_from_reclaim_height_overflow_fails() {
286 let target = dummy_account();
287
288 let overflow = Felt::new_unchecked(u64::from(u32::MAX) + 1);
290
291 let storage = vec![target.suffix(), target.prefix().as_felt(), overflow, Felt::ZERO];
292
293 let err = P2ideNoteStorage::try_from(storage.as_slice())
294 .expect_err("overflow reclaim height must fail");
295
296 assert!(matches!(err, NoteError::Other { source: Some(_), .. }));
297 }
298
299 #[test]
300 fn try_from_timelock_height_overflow_fails() {
301 let target = dummy_account();
302
303 let overflow = Felt::new_unchecked(u64::from(u32::MAX) + 10);
304
305 let storage = vec![target.suffix(), target.prefix().as_felt(), Felt::ZERO, overflow];
306
307 let err = P2ideNoteStorage::try_from(storage.as_slice())
308 .expect_err("overflow timelock height must fail");
309
310 assert!(matches!(err, NoteError::Other { source: Some(_), .. }));
311 }
312}