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 NoteDetails,
13 NoteMetadata,
14 NoteRecipient,
15 NoteScript,
16 NoteStorage,
17 NoteTag,
18 NoteType,
19};
20use miden_protocol::utils::sync::LazyLock;
21use miden_protocol::{Felt, Word};
22
23use crate::StandardsLib;
24use crate::note::P2idNoteStorage;
25
26const SWAP_SCRIPT_PATH: &str = "::miden::standards::notes::swap::main";
31
32static SWAP_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
34 let standards_lib = StandardsLib::default();
35 let path = Path::new(SWAP_SCRIPT_PATH);
36 NoteScript::from_library_reference(standards_lib.as_ref(), path)
37 .expect("Standards library contains SWAP note script procedure")
38});
39
40pub struct SwapNote;
45
46impl SwapNote {
47 pub const NUM_STORAGE_ITEMS: usize = SwapNoteStorage::NUM_ITEMS;
52
53 pub fn script() -> NoteScript {
58 SWAP_SCRIPT.clone()
59 }
60
61 pub fn script_root() -> Word {
63 SWAP_SCRIPT.root()
64 }
65
66 pub fn create<R: FeltRng>(
79 sender: AccountId,
80 offered_asset: Asset,
81 requested_asset: Asset,
82 swap_note_type: NoteType,
83 swap_note_attachment: NoteAttachment,
84 payback_note_type: NoteType,
85 payback_note_attachment: NoteAttachment,
86 rng: &mut R,
87 ) -> Result<(Note, NoteDetails), NoteError> {
88 if requested_asset == offered_asset {
89 return Err(NoteError::other("requested asset same as offered asset"));
90 }
91
92 let payback_serial_num = rng.draw_word();
93
94 let swap_storage = SwapNoteStorage::new(
95 sender,
96 requested_asset,
97 payback_note_type,
98 payback_note_attachment,
99 payback_serial_num,
100 );
101
102 let serial_num = rng.draw_word();
103 let recipient = swap_storage.into_recipient(serial_num);
104
105 let tag = Self::build_tag(swap_note_type, &offered_asset, &requested_asset);
107
108 let metadata = NoteMetadata::new(sender, swap_note_type)
110 .with_tag(tag)
111 .with_attachment(swap_note_attachment);
112 let assets = NoteAssets::new(vec![offered_asset])?;
113 let note = Note::new(assets, metadata, recipient);
114
115 let payback_recipient = P2idNoteStorage::new(sender).into_recipient(payback_serial_num);
117 let payback_assets = NoteAssets::new(vec![requested_asset])?;
118 let payback_note = NoteDetails::new(payback_assets, payback_recipient);
119
120 Ok((note, payback_note))
121 }
122
123 pub fn build_tag(
136 note_type: NoteType,
137 offered_asset: &Asset,
138 requested_asset: &Asset,
139 ) -> NoteTag {
140 let swap_root_bytes = Self::script().root().as_bytes();
141 let mut swap_use_case_id = (swap_root_bytes[0] as u16) << 6;
144 swap_use_case_id |= (swap_root_bytes[1] >> 2) as u16;
145
146 let offered_asset_id: u64 = offered_asset.faucet_id().prefix().into();
148 let offered_asset_tag = (offered_asset_id >> 56) as u8;
149
150 let requested_asset_id: u64 = requested_asset.faucet_id().prefix().into();
151 let requested_asset_tag = (requested_asset_id >> 56) as u8;
152
153 let asset_pair = ((offered_asset_tag as u16) << 8) | (requested_asset_tag as u16);
154
155 let tag = ((note_type as u8 as u32) << 30)
156 | ((swap_use_case_id as u32) << 16)
157 | asset_pair as u32;
158
159 NoteTag::new(tag)
160 }
161}
162
163#[derive(Debug, Clone, PartialEq, Eq)]
172pub struct SwapNoteStorage {
173 payback_note_type: NoteType,
174 payback_tag: NoteTag,
175 payback_attachment: NoteAttachment,
176 requested_asset: Asset,
177 payback_recipient_digest: Word,
178}
179
180impl SwapNoteStorage {
181 pub const NUM_ITEMS: usize = 20;
186
187 pub fn new(
192 sender: AccountId,
193 requested_asset: Asset,
194 payback_note_type: NoteType,
195 payback_attachment: NoteAttachment,
196 payback_serial_number: Word,
197 ) -> Self {
198 let payback_recipient = P2idNoteStorage::new(sender).into_recipient(payback_serial_number);
199 let payback_tag = NoteTag::with_account_target(sender);
200
201 Self::from_parts(
202 payback_note_type,
203 payback_tag,
204 payback_attachment,
205 requested_asset,
206 payback_recipient.digest(),
207 )
208 }
209
210 pub fn from_parts(
212 payback_note_type: NoteType,
213 payback_tag: NoteTag,
214 payback_attachment: NoteAttachment,
215 requested_asset: Asset,
216 payback_recipient_digest: Word,
217 ) -> Self {
218 Self {
219 payback_note_type,
220 payback_tag,
221 payback_attachment,
222 requested_asset,
223 payback_recipient_digest,
224 }
225 }
226
227 pub fn payback_note_type(&self) -> NoteType {
229 self.payback_note_type
230 }
231
232 pub fn payback_tag(&self) -> NoteTag {
234 self.payback_tag
235 }
236
237 pub fn payback_attachment(&self) -> &NoteAttachment {
239 &self.payback_attachment
240 }
241
242 pub fn requested_asset(&self) -> Asset {
244 self.requested_asset
245 }
246
247 pub fn payback_recipient_digest(&self) -> Word {
249 self.payback_recipient_digest
250 }
251
252 pub fn into_recipient(self, serial_num: Word) -> NoteRecipient {
257 NoteRecipient::new(serial_num, SwapNote::script(), NoteStorage::from(self))
258 }
259}
260
261impl From<SwapNoteStorage> for NoteStorage {
262 fn from(storage: SwapNoteStorage) -> Self {
263 let attachment_scheme = Felt::from(storage.payback_attachment.attachment_scheme().as_u32());
264 let attachment_kind = Felt::from(storage.payback_attachment.attachment_kind().as_u8());
265 let attachment = storage.payback_attachment.content().to_word();
266
267 let mut storage_values = Vec::with_capacity(SwapNoteStorage::NUM_ITEMS);
268 storage_values.extend_from_slice(&[
269 storage.payback_note_type.into(),
270 storage.payback_tag.into(),
271 attachment_scheme,
272 attachment_kind,
273 ]);
274 storage_values.extend_from_slice(attachment.as_elements());
275 storage_values.extend_from_slice(&storage.requested_asset.as_elements());
276 storage_values.extend_from_slice(storage.payback_recipient_digest.as_elements());
277
278 NoteStorage::new(storage_values)
279 .expect("number of storage items should not exceed max storage items")
280 }
281}
282
283#[cfg(test)]
290mod tests {
291 use miden_protocol::Felt;
292 use miden_protocol::account::{AccountIdVersion, AccountStorageMode, AccountType};
293 use miden_protocol::asset::{FungibleAsset, NonFungibleAsset, NonFungibleAssetDetails};
294 use miden_protocol::note::{NoteAttachment, NoteStorage, NoteTag, NoteType};
295 use miden_protocol::testing::account_id::{
296 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
297 ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET,
298 };
299
300 use super::*;
301
302 fn fungible_faucet() -> AccountId {
303 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET.try_into().unwrap()
304 }
305
306 fn non_fungible_faucet() -> AccountId {
307 ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET.try_into().unwrap()
308 }
309
310 fn fungible_asset() -> Asset {
311 Asset::Fungible(FungibleAsset::new(fungible_faucet(), 1000).unwrap())
312 }
313
314 fn non_fungible_asset() -> Asset {
315 let details =
316 NonFungibleAssetDetails::new(non_fungible_faucet(), vec![0xaa, 0xbb]).unwrap();
317 Asset::NonFungible(NonFungibleAsset::new(&details).unwrap())
318 }
319
320 #[test]
321 fn swap_note_storage() {
322 let payback_note_type = NoteType::Private;
323 let payback_tag = NoteTag::new(0x12345678);
324 let payback_attachment = NoteAttachment::default();
325 let requested_asset = fungible_asset();
326 let payback_recipient_digest =
327 Word::new([Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)]);
328
329 let storage = SwapNoteStorage::from_parts(
330 payback_note_type,
331 payback_tag,
332 payback_attachment.clone(),
333 requested_asset,
334 payback_recipient_digest,
335 );
336
337 assert_eq!(storage.payback_note_type(), payback_note_type);
338 assert_eq!(storage.payback_tag(), payback_tag);
339 assert_eq!(storage.payback_attachment(), &payback_attachment);
340 assert_eq!(storage.requested_asset(), requested_asset);
341 assert_eq!(storage.payback_recipient_digest(), payback_recipient_digest);
342
343 let note_storage = NoteStorage::from(storage);
345 assert_eq!(note_storage.num_items() as usize, SwapNoteStorage::NUM_ITEMS);
346 }
347
348 #[test]
349 fn swap_note_storage_with_non_fungible_asset() {
350 let payback_note_type = NoteType::Public;
351 let payback_tag = NoteTag::new(0xaabbccdd);
352 let payback_attachment = NoteAttachment::default();
353 let requested_asset = non_fungible_asset();
354 let payback_recipient_digest =
355 Word::new([Felt::new(10), Felt::new(20), Felt::new(30), Felt::new(40)]);
356
357 let storage = SwapNoteStorage::from_parts(
358 payback_note_type,
359 payback_tag,
360 payback_attachment,
361 requested_asset,
362 payback_recipient_digest,
363 );
364
365 assert_eq!(storage.payback_note_type(), payback_note_type);
366 assert_eq!(storage.requested_asset(), requested_asset);
367
368 let note_storage = NoteStorage::from(storage);
369 assert_eq!(note_storage.num_items() as usize, SwapNoteStorage::NUM_ITEMS);
370 }
371
372 #[test]
373 fn swap_tag() {
374 let mut fungible_faucet_id_bytes = [0; 15];
376 fungible_faucet_id_bytes[0] = 0xcd;
377 fungible_faucet_id_bytes[1] = 0xb1;
378
379 let mut non_fungible_faucet_id_bytes = [0; 15];
381 non_fungible_faucet_id_bytes[0] = 0xab;
382 non_fungible_faucet_id_bytes[1] = 0xec;
383
384 let offered_asset = Asset::Fungible(
385 FungibleAsset::new(
386 AccountId::dummy(
387 fungible_faucet_id_bytes,
388 AccountIdVersion::Version0,
389 AccountType::FungibleFaucet,
390 AccountStorageMode::Public,
391 ),
392 2500,
393 )
394 .unwrap(),
395 );
396
397 let requested_asset = Asset::NonFungible(
398 NonFungibleAsset::new(
399 &NonFungibleAssetDetails::new(
400 AccountId::dummy(
401 non_fungible_faucet_id_bytes,
402 AccountIdVersion::Version0,
403 AccountType::NonFungibleFaucet,
404 AccountStorageMode::Public,
405 ),
406 vec![0xaa, 0xbb, 0xcc, 0xdd],
407 )
408 .unwrap(),
409 )
410 .unwrap(),
411 );
412
413 let expected_asset_pair = 0xcdab;
417
418 let note_type = NoteType::Public;
419 let actual_tag = SwapNote::build_tag(note_type, &offered_asset, &requested_asset);
420
421 assert_eq!(actual_tag.as_u32() as u16, expected_asset_pair, "asset pair should match");
422 assert_eq!((actual_tag.as_u32() >> 30) as u8, note_type as u8, "note type should match");
423 assert_eq!(
425 (actual_tag.as_u32() >> 22) as u8,
426 SwapNote::script_root().as_bytes()[0],
427 "swap script root byte 0 should match"
428 );
429 assert_eq!(
431 ((actual_tag.as_u32() & 0b00000000_00111111_00000000_00000000) >> 16) as u8,
432 SwapNote::script_root().as_bytes()[1] >> 2,
433 "swap script root byte 1 should match with the lower two bits set to zero"
434 );
435 }
436}