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 NoteDetails,
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, ONE, Word};
23
24use crate::StandardsLib;
25use crate::note::P2idNoteStorage;
26
27const SWAP_SCRIPT_PATH: &str = "::miden::standards::notes::swap::main";
32
33static SWAP_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
35 let standards_lib = StandardsLib::default();
36 let path = Path::new(SWAP_SCRIPT_PATH);
37 NoteScript::from_library_reference(standards_lib.as_ref(), path)
38 .expect("Standards library contains SWAP note script procedure")
39});
40
41pub struct SwapNote;
46
47impl SwapNote {
48 pub const NUM_STORAGE_ITEMS: usize = SwapNoteStorage::NUM_ITEMS;
53
54 pub fn script() -> NoteScript {
59 SWAP_SCRIPT.clone()
60 }
61
62 pub fn script_root() -> NoteScriptRoot {
64 SWAP_SCRIPT.root()
65 }
66
67 pub fn create<R: FeltRng>(
82 sender: AccountId,
83 offered_asset: Asset,
84 requested_asset: Asset,
85 swap_note_type: NoteType,
86 swap_note_attachments: NoteAttachments,
87 payback_note_type: NoteType,
88 rng: &mut R,
89 ) -> Result<(Note, NoteDetails), NoteError> {
90 if requested_asset == offered_asset {
91 return Err(NoteError::other("requested asset same as offered asset"));
92 }
93
94 let serial_num = rng.draw_word();
95
96 let payback_serial_num = payback_serial_from_swap(serial_num);
100 let payback_recipient = P2idNoteStorage::new(sender).into_recipient(payback_serial_num);
101 let payback_assets = NoteAssets::new(vec![requested_asset])?;
102 let payback_note = NoteDetails::new(payback_assets, payback_recipient.clone());
103
104 let payback_tag = NoteTag::with_account_target(sender);
105 let swap_storage = match payback_note_type {
106 NoteType::Private => SwapNoteStorage::new_private(
107 requested_asset,
108 payback_recipient.digest(),
109 payback_tag,
110 ),
111 NoteType::Public => SwapNoteStorage::new_public(requested_asset, sender, payback_tag),
112 };
113
114 let recipient = swap_storage.into_recipient(serial_num);
115
116 let tag = Self::build_tag(swap_note_type, &offered_asset, &requested_asset);
118
119 let metadata = PartialNoteMetadata::new(sender, swap_note_type).with_tag(tag);
121 let assets = NoteAssets::new(vec![offered_asset])?;
122 let note = Note::with_attachments(assets, metadata, recipient, swap_note_attachments);
123
124 Ok((note, payback_note))
125 }
126
127 pub fn build_tag(
140 note_type: NoteType,
141 offered_asset: &Asset,
142 requested_asset: &Asset,
143 ) -> NoteTag {
144 let swap_root_bytes = Self::script().root().as_bytes();
145 let mut swap_use_case_id = (swap_root_bytes[0] as u16) << 7;
148 swap_use_case_id |= (swap_root_bytes[1] >> 1) as u16;
149
150 let offered_asset_id: u64 = offered_asset.faucet_id().prefix().into();
152 let offered_asset_tag = (offered_asset_id >> 56) as u8;
153
154 let requested_asset_id: u64 = requested_asset.faucet_id().prefix().into();
155 let requested_asset_tag = (requested_asset_id >> 56) as u8;
156
157 let asset_pair = ((offered_asset_tag as u16) << 8) | (requested_asset_tag as u16);
158
159 let tag = ((note_type as u8 as u32) << 31)
160 | ((swap_use_case_id as u32) << 16)
161 | asset_pair as u32;
162
163 NoteTag::new(tag)
164 }
165}
166
167#[derive(Debug, Clone, PartialEq, Eq)]
185pub struct SwapNoteStorage {
186 requested_asset: Asset,
187 payback_tag: NoteTag,
188 payback: SwapPayback,
189}
190
191#[derive(Debug, Clone, PartialEq, Eq)]
199pub enum SwapPayback {
200 Private {
201 recipient: Word,
203 },
204 Public {
205 payback_target_id: AccountId,
207 },
208}
209
210impl SwapNoteStorage {
211 pub const NUM_ITEMS: usize = 16;
216
217 pub fn new_private(
222 requested_asset: Asset,
223 payback_recipient: Word,
224 payback_tag: NoteTag,
225 ) -> Self {
226 Self {
227 requested_asset,
228 payback_tag,
229 payback: SwapPayback::Private { recipient: payback_recipient },
230 }
231 }
232
233 pub fn new_public(
235 requested_asset: Asset,
236 payback_target_id: AccountId,
237 payback_tag: NoteTag,
238 ) -> Self {
239 Self {
240 requested_asset,
241 payback_tag,
242 payback: SwapPayback::Public { payback_target_id },
243 }
244 }
245
246 pub fn payback_note_type(&self) -> NoteType {
251 match self.payback {
252 SwapPayback::Private { .. } => NoteType::Private,
253 SwapPayback::Public { .. } => NoteType::Public,
254 }
255 }
256
257 pub fn requested_asset(&self) -> Asset {
259 self.requested_asset
260 }
261
262 pub fn payback_tag(&self) -> NoteTag {
264 self.payback_tag
265 }
266
267 pub fn payback(&self) -> &SwapPayback {
269 &self.payback
270 }
271
272 pub fn into_recipient(self, serial_num: Word) -> NoteRecipient {
277 NoteRecipient::new(serial_num, SwapNote::script(), NoteStorage::from(self))
278 }
279}
280
281impl From<SwapNoteStorage> for NoteStorage {
282 fn from(storage: SwapNoteStorage) -> Self {
283 let mut storage_values = Vec::with_capacity(SwapNoteStorage::NUM_ITEMS);
284
285 storage_values.extend_from_slice(&storage.requested_asset.as_elements());
287
288 match storage.payback {
289 SwapPayback::Private { recipient } => {
290 storage_values.extend_from_slice(recipient.as_elements());
292 storage_values.push(Felt::from(NoteType::Private.as_u8()));
294 storage_values.push(Felt::from(storage.payback_tag.as_u32()));
296 storage_values.extend_from_slice(&[Felt::ZERO; 2]);
298 },
299 SwapPayback::Public { payback_target_id } => {
300 storage_values.extend_from_slice(&[Felt::ZERO; 4]);
302 storage_values.push(Felt::from(NoteType::Public.as_u8()));
304 storage_values.push(Felt::from(storage.payback_tag.as_u32()));
306 storage_values.push(payback_target_id.suffix());
308 storage_values.push(payback_target_id.prefix().as_felt());
309 },
310 }
311
312 NoteStorage::new(storage_values)
313 .expect("number of storage items should not exceed max storage items")
314 }
315}
316
317impl TryFrom<&[Felt]> for SwapNoteStorage {
319 type Error = NoteError;
320
321 fn try_from(note_storage: &[Felt]) -> Result<Self, Self::Error> {
322 if note_storage.len() != Self::NUM_ITEMS {
323 return Err(NoteError::InvalidNoteStorageLength {
324 expected: Self::NUM_ITEMS,
325 actual: note_storage.len(),
326 });
327 }
328
329 let key = Word::new([note_storage[0], note_storage[1], note_storage[2], note_storage[3]]);
331 let value = Word::new([note_storage[4], note_storage[5], note_storage[6], note_storage[7]]);
332 let requested_asset = Asset::from_id_and_value_words(key, value)
333 .map_err(|err| NoteError::other_with_source("failed to parse requested asset", err))?;
334
335 let payback_note_type = NoteType::try_from(
337 u8::try_from(note_storage[12].as_canonical_u64())
338 .map_err(|_| NoteError::other("payback_note_type exceeds u8"))?,
339 )
340 .map_err(|err| NoteError::other_with_source("failed to parse payback note type", err))?;
341
342 let payback_tag_u32 = u32::try_from(note_storage[13].as_canonical_u64())
344 .map_err(|_| NoteError::other("SWAP payback_tag exceeds u32"))?;
345 let payback_tag = NoteTag::new(payback_tag_u32);
346
347 let payback = match payback_note_type {
348 NoteType::Private => {
349 if note_storage[14].as_canonical_u64() != 0
351 || note_storage[15].as_canonical_u64() != 0
352 {
353 return Err(NoteError::other(
354 "SWAP private payback must have payback target id slots cleared",
355 ));
356 }
357
358 let recipient = Word::new([
360 note_storage[8],
361 note_storage[9],
362 note_storage[10],
363 note_storage[11],
364 ]);
365
366 SwapPayback::Private { recipient }
367 },
368 NoteType::Public => {
369 if note_storage[8..=11].iter().any(|felt| felt.as_canonical_u64() != 0) {
371 return Err(NoteError::other(
372 "SWAP public payback must have recipient slots cleared",
373 ));
374 }
375
376 let payback_target_id = AccountId::try_from_elements(
377 note_storage[14],
378 note_storage[15],
379 )
380 .map_err(|err| {
381 NoteError::other_with_source("failed to parse payback target account ID", err)
382 })?;
383
384 SwapPayback::Public { payback_target_id }
385 },
386 };
387
388 Ok(Self { requested_asset, payback_tag, payback })
389 }
390}
391
392pub fn payback_serial_from_swap(swap_serial: Word) -> Word {
398 let elements = swap_serial.as_elements();
399 Word::new([elements[0] + ONE, elements[1], elements[2], elements[3]])
400}
401
402#[cfg(test)]
406mod tests {
407
408 use assert_matches::assert_matches;
409 use miden_protocol::account::{AccountIdVersion, AccountType, AssetCallbackFlag};
410 use miden_protocol::asset::{FungibleAsset, NonFungibleAsset, NonFungibleAssetDetails};
411 use miden_protocol::note::{NoteStorage, NoteType};
412 use miden_protocol::testing::account_id::{
413 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
414 ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET,
415 };
416
417 use super::*;
418
419 fn fungible_faucet() -> AccountId {
420 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET.try_into().unwrap()
421 }
422
423 fn non_fungible_faucet() -> AccountId {
424 ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET.try_into().unwrap()
425 }
426
427 fn fungible_asset() -> Asset {
428 Asset::Fungible(FungibleAsset::new(fungible_faucet(), 1000).unwrap())
429 }
430
431 fn non_fungible_asset() -> Asset {
432 let details = NonFungibleAssetDetails::new(non_fungible_faucet(), vec![0xaa, 0xbb]);
433 Asset::NonFungible(NonFungibleAsset::new(&details))
434 }
435
436 fn dummy_target_id() -> AccountId {
437 AccountId::builder().account_type(AccountType::Private).build_with_seed([1; 32])
438 }
439
440 fn dummy_recipient_digest() -> Word {
441 Word::new([Felt::from(7u32), Felt::from(11u32), Felt::from(13u32), Felt::from(17u32)])
442 }
443
444 fn dummy_payback_tag() -> NoteTag {
445 NoteTag::new(0xabcd1234)
446 }
447
448 #[test]
449 fn swap_note_storage_round_trip_fungible_private() {
450 let storage = SwapNoteStorage::new_private(
451 fungible_asset(),
452 dummy_recipient_digest(),
453 dummy_payback_tag(),
454 );
455
456 let note_storage = NoteStorage::from(storage.clone());
457 assert_eq!(note_storage.num_items() as usize, SwapNoteStorage::NUM_ITEMS);
458 assert_eq!(storage.payback_note_type(), NoteType::Private);
459 assert_eq!(storage.requested_asset(), fungible_asset());
460 assert_eq!(storage.payback_tag(), dummy_payback_tag());
461 match storage.payback() {
462 SwapPayback::Private { recipient } => {
463 assert_eq!(*recipient, dummy_recipient_digest());
464 },
465 SwapPayback::Public { .. } => panic!("expected private payback"),
466 }
467
468 let parsed =
469 SwapNoteStorage::try_from(note_storage.items()).expect("round trip should succeed");
470 assert_eq!(parsed, storage);
471 }
472
473 #[test]
474 fn swap_note_storage_round_trip_non_fungible_public() {
475 let target = dummy_target_id();
476 let storage =
477 SwapNoteStorage::new_public(non_fungible_asset(), target, dummy_payback_tag());
478
479 let note_storage = NoteStorage::from(storage.clone());
480 assert_eq!(note_storage.num_items() as usize, SwapNoteStorage::NUM_ITEMS);
481 assert_eq!(storage.payback_note_type(), NoteType::Public);
482 assert_eq!(storage.requested_asset(), non_fungible_asset());
483 assert_eq!(storage.payback_tag(), dummy_payback_tag());
484 match storage.payback() {
485 SwapPayback::Public { payback_target_id } => {
486 assert_eq!(*payback_target_id, target);
487 },
488 SwapPayback::Private { .. } => panic!("expected public payback"),
489 }
490
491 let parsed =
492 SwapNoteStorage::try_from(note_storage.items()).expect("round trip should succeed");
493 assert_eq!(parsed, storage);
494 }
495
496 #[test]
497 fn swap_note_storage_private_rejects_dirty_target_slots() {
498 let mut items: Vec<Felt> = NoteStorage::from(SwapNoteStorage::new_private(
499 fungible_asset(),
500 dummy_recipient_digest(),
501 dummy_payback_tag(),
502 ))
503 .items()
504 .to_vec();
505
506 items[14] = Felt::from(1u32);
508 let err = SwapNoteStorage::try_from(items.as_slice())
509 .expect_err("private payback with a dirty target slot must be rejected");
510 assert_matches!(
511 err,
512 NoteError::Other { error_msg, .. }
513 if error_msg == "SWAP private payback must have payback target id slots cleared".into()
514 );
515 }
516
517 #[test]
518 fn swap_note_storage_public_rejects_dirty_private_slots() {
519 let mut items: Vec<Felt> = NoteStorage::from(SwapNoteStorage::new_public(
520 fungible_asset(),
521 dummy_target_id(),
522 dummy_payback_tag(),
523 ))
524 .items()
525 .to_vec();
526
527 items[8] = Felt::from(1u32);
529 let err = SwapNoteStorage::try_from(items.as_slice())
530 .expect_err("public payback with a dirty recipient slot must be rejected");
531 assert_matches!(
532 err,
533 NoteError::Other { error_msg, .. }
534 if error_msg == "SWAP public payback must have recipient slots cleared".into()
535 );
536 }
537
538 #[test]
539 fn swap_tag() {
540 let mut fungible_faucet_id_bytes = [0; 15];
542 fungible_faucet_id_bytes[0] = 0xcd;
543 fungible_faucet_id_bytes[1] = 0xb1;
544
545 let mut non_fungible_faucet_id_bytes = [0; 15];
547 non_fungible_faucet_id_bytes[0] = 0xab;
548 non_fungible_faucet_id_bytes[1] = 0xec;
549
550 let offered_asset = Asset::Fungible(
551 FungibleAsset::new(
552 AccountId::dummy(
553 fungible_faucet_id_bytes,
554 AccountIdVersion::Version1,
555 AccountType::Public,
556 AssetCallbackFlag::Disabled,
557 ),
558 2500,
559 )
560 .unwrap(),
561 );
562
563 let requested_asset =
564 Asset::NonFungible(NonFungibleAsset::new(&NonFungibleAssetDetails::new(
565 AccountId::dummy(
566 non_fungible_faucet_id_bytes,
567 AccountIdVersion::Version1,
568 AccountType::Public,
569 AssetCallbackFlag::Disabled,
570 ),
571 vec![0xaa, 0xbb, 0xcc, 0xdd],
572 )));
573
574 let expected_asset_pair = 0xcdab;
578
579 let note_type = NoteType::Public;
580 let actual_tag = SwapNote::build_tag(note_type, &offered_asset, &requested_asset);
581
582 assert_eq!(actual_tag.as_u32() as u16, expected_asset_pair, "asset pair should match");
583 assert_eq!((actual_tag.as_u32() >> 31) as u8, note_type as u8, "note type should match");
584 assert_eq!(
586 (actual_tag.as_u32() >> 23) as u8,
587 SwapNote::script_root().as_bytes()[0],
588 "swap script root byte 0 should match"
589 );
590 assert_eq!(
592 ((actual_tag.as_u32() & 0b00000000_01111111_00000000_00000000) >> 16) as u8,
593 SwapNote::script_root().as_bytes()[1] >> 1,
594 "swap script root byte 1 should match with the highest bit set to zero"
595 );
596 }
597}