1use 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 NoteAttachment,
13 NoteMetadata,
14 NoteRecipient,
15 NoteScript,
16 NoteStorage,
17 NoteTag,
18 NoteType,
19};
20use miden_protocol::utils::sync::LazyLock;
21use miden_protocol::{Felt, FieldElement, Word};
22
23use crate::StandardsLib;
24const P2IDE_SCRIPT_PATH: &str = "::miden::standards::notes::p2ide::main";
29
30static P2IDE_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
32 let standards_lib = StandardsLib::default();
33 let path = Path::new(P2IDE_SCRIPT_PATH);
34 NoteScript::from_library_reference(standards_lib.as_ref(), path)
35 .expect("Standards library contains P2IDE note script procedure")
36});
37
38pub struct P2ideNote;
51
52impl P2ideNote {
53 pub const NUM_STORAGE_ITEMS: usize = P2ideNoteStorage::NUM_ITEMS;
58
59 pub fn script() -> NoteScript {
64 P2IDE_SCRIPT.clone()
65 }
66
67 pub fn script_root() -> Word {
69 P2IDE_SCRIPT.root()
70 }
71
72 pub fn create<R: FeltRng>(
84 sender: AccountId,
85 storage: P2ideNoteStorage,
86 assets: Vec<Asset>,
87 note_type: NoteType,
88 attachment: NoteAttachment,
89 rng: &mut R,
90 ) -> Result<Note, NoteError> {
91 let serial_num = rng.draw_word();
92 let recipient = storage.into_recipient(serial_num)?;
93 let tag = NoteTag::with_account_target(storage.target());
94
95 let metadata =
96 NoteMetadata::new(sender, note_type).with_tag(tag).with_attachment(attachment);
97 let vault = NoteAssets::new(assets)?;
98
99 Ok(Note::new(vault, metadata, recipient))
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([note_storage[1], note_storage[0]])
183 .map_err(|e| NoteError::other_with_source("failed to create account id", e))?;
184
185 let reclaim_height = if note_storage[2] == Felt::ZERO {
186 None
187 } else {
188 let height: u32 = note_storage[2]
189 .as_int()
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_int()
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::account::{AccountId, AccountIdVersion, AccountStorageMode, AccountType};
217 use miden_protocol::block::BlockNumber;
218 use miden_protocol::errors::NoteError;
219 use miden_protocol::{Felt, FieldElement};
220
221 use super::*;
222
223 fn dummy_account() -> AccountId {
224 AccountId::dummy(
225 [3u8; 15],
226 AccountIdVersion::Version0,
227 AccountType::FungibleFaucet,
228 AccountStorageMode::Private,
229 )
230 }
231
232 #[test]
233 fn try_from_valid_storage_with_all_fields_succeeds() {
234 let target = dummy_account();
235
236 let storage = vec![
237 target.suffix(),
238 target.prefix().as_felt(),
239 Felt::from(42u32),
240 Felt::from(100u32),
241 ];
242
243 let decoded = P2ideNoteStorage::try_from(storage.as_slice())
244 .expect("valid P2IDE storage should decode");
245
246 assert_eq!(decoded.target(), target);
247 assert_eq!(decoded.reclaim_height(), Some(BlockNumber::from(42u32)));
248 assert_eq!(decoded.timelock_height(), Some(BlockNumber::from(100u32)));
249 }
250
251 #[test]
252 fn try_from_zero_heights_map_to_none() {
253 let target = dummy_account();
254
255 let storage = vec![target.suffix(), target.prefix().as_felt(), Felt::ZERO, Felt::ZERO];
256
257 let decoded = P2ideNoteStorage::try_from(storage.as_slice()).unwrap();
258
259 assert_eq!(decoded.reclaim_height(), None);
260 assert_eq!(decoded.timelock_height(), None);
261 }
262
263 #[test]
264 fn try_from_invalid_length_fails() {
265 let storage = vec![Felt::ZERO; 3];
266
267 let err =
268 P2ideNoteStorage::try_from(storage.as_slice()).expect_err("wrong length must fail");
269
270 assert!(matches!(
271 err,
272 NoteError::InvalidNoteStorageLength {
273 expected: P2ideNote::NUM_STORAGE_ITEMS,
274 actual: 3
275 }
276 ));
277 }
278
279 #[test]
280 fn try_from_invalid_account_id_fails() {
281 let storage = vec![Felt::new(999u64), Felt::new(888u64), Felt::ZERO, Felt::ZERO];
282
283 let err = P2ideNoteStorage::try_from(storage.as_slice())
284 .expect_err("invalid account id encoding must fail");
285
286 assert!(matches!(err, NoteError::Other { source: Some(_), .. }));
287 }
288
289 #[test]
290 fn try_from_reclaim_height_overflow_fails() {
291 let target = dummy_account();
292
293 let overflow = Felt::new(u64::from(u32::MAX) + 1);
295
296 let storage = vec![target.suffix(), target.prefix().as_felt(), overflow, Felt::ZERO];
297
298 let err = P2ideNoteStorage::try_from(storage.as_slice())
299 .expect_err("overflow reclaim height must fail");
300
301 assert!(matches!(err, NoteError::Other { source: Some(_), .. }));
302 }
303
304 #[test]
305 fn try_from_timelock_height_overflow_fails() {
306 let target = dummy_account();
307
308 let overflow = Felt::new(u64::from(u32::MAX) + 10);
309
310 let storage = vec![target.suffix(), target.prefix().as_felt(), Felt::ZERO, overflow];
311
312 let err = P2ideNoteStorage::try_from(storage.as_slice())
313 .expect_err("overflow timelock height must fail");
314
315 assert!(matches!(err, NoteError::Other { source: Some(_), .. }));
316 }
317}