1use alloc::vec;
2
3use miden_protocol::account::AccountId;
4use miden_protocol::assembly::Path;
5use miden_protocol::asset::{Asset, AssetAmount, AssetCallbackFlag, FungibleAsset};
6use miden_protocol::errors::NoteError;
7use miden_protocol::note::{
8 Note,
9 NoteAssets,
10 NoteAttachment,
11 NoteAttachmentScheme,
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, ONE, Word, ZERO};
23
24use crate::StandardsLib;
25use crate::note::{P2idNoteStorage, StandardNoteAttachment};
26
27const PSWAP_SCRIPT_PATH: &str = "::miden::standards::notes::pswap::main";
32
33static PSWAP_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
35 let standards_lib = StandardsLib::default();
36 let path = Path::new(PSWAP_SCRIPT_PATH);
37 NoteScript::from_library_reference(standards_lib.as_ref(), path)
38 .expect("Standards library contains PSWAP note script procedure")
39});
40
41#[derive(Debug, Clone, PartialEq, Eq, bon::Builder)]
64pub struct PswapNoteStorage {
65 requested_asset: FungibleAsset,
66
67 creator_account_id: AccountId,
68
69 #[builder(default = NoteType::Private)]
75 payback_note_type: NoteType,
76}
77
78impl PswapNoteStorage {
79 pub const NUM_STORAGE_ITEMS: usize = 7;
84
85 pub fn into_recipient(self, serial_num: Word) -> NoteRecipient {
87 NoteRecipient::new(serial_num, PswapNote::script(), NoteStorage::from(self))
88 }
89
90 pub fn requested_asset(&self) -> &FungibleAsset {
95 &self.requested_asset
96 }
97
98 pub fn payback_note_tag(&self) -> NoteTag {
100 NoteTag::with_account_target(self.creator_account_id)
101 }
102
103 pub fn creator_account_id(&self) -> AccountId {
105 self.creator_account_id
106 }
107
108 pub fn payback_note_type(&self) -> NoteType {
110 self.payback_note_type
111 }
112
113 pub fn requested_faucet_id(&self) -> AccountId {
115 self.requested_asset.faucet_id()
116 }
117
118 pub fn requested_asset_amount(&self) -> u64 {
120 self.requested_asset.amount().as_u64()
121 }
122}
123
124impl From<PswapNoteStorage> for NoteStorage {
126 fn from(storage: PswapNoteStorage) -> Self {
127 let storage_items = vec![
128 Felt::from(storage.requested_asset.callbacks().as_u8()),
130 storage.requested_asset.faucet_id().suffix(),
131 storage.requested_asset.faucet_id().prefix().as_felt(),
132 Felt::from(storage.requested_asset.amount()),
133 Felt::from(storage.payback_note_type.as_u8()),
135 storage.creator_account_id.prefix().as_felt(),
137 storage.creator_account_id.suffix(),
138 ];
139 NoteStorage::new(storage_items)
140 .expect("number of storage items should not exceed max storage items")
141 }
142}
143
144impl TryFrom<&[Felt]> for PswapNoteStorage {
146 type Error = NoteError;
147
148 fn try_from(note_storage: &[Felt]) -> Result<Self, Self::Error> {
149 if note_storage.len() != Self::NUM_STORAGE_ITEMS {
150 return Err(NoteError::InvalidNoteStorageLength {
151 expected: Self::NUM_STORAGE_ITEMS,
152 actual: note_storage.len(),
153 });
154 }
155
156 let callbacks = AssetCallbackFlag::try_from(
159 u8::try_from(note_storage[0].as_canonical_u64())
160 .map_err(|_| NoteError::other("enable_callbacks exceeds u8"))?,
161 )
162 .map_err(|e| NoteError::other_with_source("failed to parse asset callback flag", e))?;
163
164 let faucet_id = AccountId::try_from_elements(note_storage[1], note_storage[2])
165 .map_err(|e| NoteError::other_with_source("failed to parse requested faucet ID", e))?;
166
167 let amount = note_storage[3].as_canonical_u64();
168 let requested_asset = FungibleAsset::new(faucet_id, amount)
169 .map_err(|e| NoteError::other_with_source("failed to create requested asset", e))?
170 .with_callbacks(callbacks);
171
172 let payback_note_type = NoteType::try_from(
174 u8::try_from(note_storage[4].as_canonical_u64())
175 .map_err(|_| NoteError::other("payback_note_type exceeds u8"))?,
176 )
177 .map_err(|e| NoteError::other_with_source("failed to parse payback note type", e))?;
178
179 let creator_account_id = AccountId::try_from_elements(note_storage[6], note_storage[5])
181 .map_err(|e| NoteError::other_with_source("failed to parse creator account ID", e))?;
182
183 Ok(Self {
184 requested_asset,
185 creator_account_id,
186 payback_note_type,
187 })
188 }
189}
190
191#[derive(Debug, Clone, Copy, PartialEq, Eq)]
197pub struct PswapNoteAttachment {
198 amount: AssetAmount,
199 order_id: Felt,
200 depth: u32,
201}
202
203impl PswapNoteAttachment {
204 pub fn new(amount: AssetAmount, order_id: Felt, depth: u32) -> Self {
206 Self { amount, order_id, depth }
207 }
208
209 pub fn amount(&self) -> AssetAmount {
210 self.amount
211 }
212
213 pub fn order_id(&self) -> Felt {
214 self.order_id
215 }
216
217 pub fn depth(&self) -> u32 {
218 self.depth
219 }
220}
221
222impl From<PswapNoteAttachment> for NoteAttachment {
223 fn from(attachment: PswapNoteAttachment) -> Self {
224 let word = Word::from([
225 Felt::from(attachment.amount),
226 attachment.order_id,
227 Felt::from(attachment.depth),
228 ZERO,
229 ]);
230 NoteAttachment::with_word(PswapNote::PSWAP_ATTACHMENT_SCHEME, word)
231 }
232}
233
234#[derive(Debug, Clone, bon::Builder)]
250#[builder(finish_fn(vis = "", name = build_internal))]
251pub struct PswapNote {
252 sender: AccountId,
253 storage: PswapNoteStorage,
254 serial_number: Word,
255
256 #[builder(default = NoteType::Private)]
257 note_type: NoteType,
258
259 offered_asset: FungibleAsset,
260
261 attachment: Option<NoteAttachment>,
262}
263
264impl<S: pswap_note_builder::State> PswapNoteBuilder<S>
265where
266 S: pswap_note_builder::IsComplete,
267{
268 pub fn build(self) -> Result<PswapNote, NoteError> {
274 let note = self.build_internal();
275
276 if note.offered_asset.faucet_id() == note.storage.requested_faucet_id() {
277 return Err(NoteError::other(
278 "offered and requested assets must have different faucets",
279 ));
280 }
281
282 Ok(note)
283 }
284}
285
286impl PswapNote {
287 pub const NUM_STORAGE_ITEMS: usize = PswapNoteStorage::NUM_STORAGE_ITEMS;
292
293 pub const PSWAP_ATTACHMENT_SCHEME: NoteAttachmentScheme =
296 StandardNoteAttachment::PswapAttachment.attachment_scheme();
297
298 const PARENT_ATTACHMENT_DEPTH_OFFSET: usize = 2;
300
301 pub fn script() -> NoteScript {
306 PSWAP_SCRIPT.clone()
307 }
308
309 pub fn script_root() -> NoteScriptRoot {
311 PSWAP_SCRIPT.root()
312 }
313
314 pub fn create_args(account_fill: u64, note_fill: u64) -> Result<Word, NoteError> {
337 let account_fill = Felt::try_from(account_fill)
338 .map_err(|e| NoteError::other_with_source("account_fill is not a valid felt", e))?;
339 let note_fill = Felt::try_from(note_fill)
340 .map_err(|e| NoteError::other_with_source("note_fill is not a valid felt", e))?;
341 Ok(Word::from([account_fill, note_fill, ZERO, ZERO]))
342 }
343
344 pub fn sender(&self) -> AccountId {
346 self.sender
347 }
348
349 pub fn storage(&self) -> &PswapNoteStorage {
351 &self.storage
352 }
353
354 pub fn serial_number(&self) -> Word {
356 self.serial_number
357 }
358
359 pub fn note_type(&self) -> NoteType {
361 self.note_type
362 }
363
364 pub fn offered_asset(&self) -> &FungibleAsset {
366 &self.offered_asset
367 }
368
369 pub fn attachments(&self) -> Option<&NoteAttachment> {
377 self.attachment.as_ref()
378 }
379
380 pub fn order_id(&self) -> Felt {
382 self.serial_number[1]
383 }
384
385 pub fn parent_depth(&self) -> u64 {
392 match self.attachment.as_ref() {
393 Some(att) if att.attachment_scheme() == Self::PSWAP_ATTACHMENT_SCHEME => {
394 let attachment_word = att.content().as_words()[0];
395 attachment_word[Self::PARENT_ATTACHMENT_DEPTH_OFFSET].as_canonical_u64()
396 },
397 _ => 0,
398 }
399 }
400
401 pub fn execute_full_fill(&self, consumer_account_id: AccountId) -> Result<Note, NoteError> {
412 let requested_faucet_id = self.storage.requested_faucet_id();
413 let total_requested_amount = self.storage.requested_asset_amount();
414
415 let fill_asset = FungibleAsset::new(requested_faucet_id, total_requested_amount)
416 .map_err(|e| NoteError::other_with_source("failed to create full fill asset", e))?
417 .with_callbacks(self.storage.requested_asset().callbacks());
418
419 self.create_payback_note(consumer_account_id, fill_asset, total_requested_amount)
420 }
421
422 pub fn execute(
438 &self,
439 consumer_account_id: AccountId,
440 account_fill_asset: Option<FungibleAsset>,
441 note_fill_asset: Option<FungibleAsset>,
442 ) -> Result<(Note, Option<PswapNote>), NoteError> {
443 let payback_asset = match (account_fill_asset, note_fill_asset) {
445 (Some(account_fill), Some(note_fill)) => account_fill.add(note_fill).map_err(|e| {
446 NoteError::other_with_source(
447 "failed to combine account fill and note fill assets",
448 e,
449 )
450 })?,
451 (Some(asset), None) | (None, Some(asset)) => asset,
452 (None, None) => {
453 return Err(NoteError::other(
454 "at least one of account_fill_asset or note_fill_asset must be provided",
455 ));
456 },
457 };
458 let fill_amount = payback_asset.amount().as_u64();
459
460 let total_offered_amount = self.offered_asset.amount().as_u64();
461 let requested_faucet_id = self.storage.requested_faucet_id();
462 let total_requested_amount = self.storage.requested_asset_amount();
463
464 if fill_amount == 0 {
466 return Err(NoteError::other("Fill amount must be greater than 0"));
467 }
468 if fill_amount > total_requested_amount {
469 return Err(NoteError::other(alloc::format!(
470 "Fill amount {} exceeds requested amount {}",
471 fill_amount,
472 total_requested_amount
473 )));
474 }
475
476 let account_fill_amount = account_fill_asset.as_ref().map_or(0, |a| a.amount().as_u64());
481 let note_fill_amount = note_fill_asset.as_ref().map_or(0, |a| a.amount().as_u64());
482 let payout_for_account_fill = Self::calculate_output_amount(
483 total_offered_amount,
484 total_requested_amount,
485 account_fill_amount,
486 )?;
487 let payout_for_note_fill = Self::calculate_output_amount(
488 total_offered_amount,
489 total_requested_amount,
490 note_fill_amount,
491 )?;
492 let offered_amount_for_fill = payout_for_account_fill + payout_for_note_fill;
493
494 let payback_note =
495 self.create_payback_note(consumer_account_id, payback_asset, fill_amount)?;
496
497 let remainder = if fill_amount < total_requested_amount {
499 let remaining_offered = total_offered_amount - offered_amount_for_fill;
500 let remaining_requested = total_requested_amount - fill_amount;
501
502 let remaining_offered_asset =
503 FungibleAsset::new(self.offered_asset.faucet_id(), remaining_offered)
504 .map_err(|e| {
505 NoteError::other_with_source("failed to create remainder asset", e)
506 })?
507 .with_callbacks(self.offered_asset.callbacks());
508
509 let remaining_requested_asset =
510 FungibleAsset::new(requested_faucet_id, remaining_requested)
511 .map_err(|e| {
512 NoteError::other_with_source(
513 "failed to create remaining requested asset",
514 e,
515 )
516 })?
517 .with_callbacks(self.storage.requested_asset().callbacks());
518
519 Some(self.create_remainder_pswap_note(
520 consumer_account_id,
521 remaining_offered_asset,
522 remaining_requested_asset,
523 offered_amount_for_fill,
524 )?)
525 } else {
526 None
527 };
528
529 Ok((payback_note, remainder))
530 }
531
532 pub fn calculate_offered_for_requested(&self, fill_amount: u64) -> Result<u64, NoteError> {
539 let total_requested = self.storage.requested_asset_amount();
540 let total_offered = self.offered_asset.amount().as_u64();
541
542 Self::calculate_output_amount(total_offered, total_requested, fill_amount)
543 }
544
545 pub fn payback_note(
560 &self,
561 consumer_account_id: AccountId,
562 attachment: &PswapNoteAttachment,
563 ) -> Result<Note, NoteError> {
564 let depth = attachment.depth();
565 if depth == 0 {
566 return Err(NoteError::other("depth must be >= 1"));
567 }
568 let parent_depth = Felt::from(depth - 1);
569 let p2id_serial = Word::from([
570 self.serial_number[0] + ONE,
571 self.serial_number[1],
572 self.serial_number[2],
573 self.serial_number[3] + parent_depth,
574 ]);
575
576 let recipient =
577 P2idNoteStorage::new(self.storage.creator_account_id).into_recipient(p2id_serial);
578
579 let fill_asset =
580 FungibleAsset::new(self.storage.requested_faucet_id(), u64::from(attachment.amount()))
581 .map_err(|e| NoteError::other_with_source("invalid fill amount", e))?
582 .with_callbacks(self.storage.requested_asset().callbacks());
583 let assets = NoteAssets::new(vec![fill_asset.into()])?;
584
585 let metadata =
586 PartialNoteMetadata::new(consumer_account_id, self.storage.payback_note_type)
587 .with_tag(self.storage.payback_note_tag());
588
589 Ok(Note::with_attachments(
590 assets,
591 metadata,
592 recipient,
593 NoteAttachments::from(NoteAttachment::from(*attachment)),
594 ))
595 }
596
597 pub fn remainder_note(
615 &self,
616 consumer_account_id: AccountId,
617 attachment: &PswapNoteAttachment,
618 remaining_offered: AssetAmount,
619 remaining_requested: AssetAmount,
620 ) -> Result<Note, NoteError> {
621 let depth = attachment.depth();
622 if depth == 0 {
623 return Err(NoteError::other("depth must be >= 1"));
624 }
625 let remainder_serial = Word::from([
626 self.serial_number[0],
627 self.serial_number[1],
628 self.serial_number[2],
629 self.serial_number[3] + Felt::from(depth),
630 ]);
631
632 let requested_asset =
633 FungibleAsset::new(self.storage.requested_faucet_id(), u64::from(remaining_requested))
634 .map_err(|e| NoteError::other_with_source("invalid remaining_requested amount", e))?
635 .with_callbacks(self.storage.requested_asset().callbacks());
636 let offered_asset =
637 FungibleAsset::new(self.offered_asset.faucet_id(), u64::from(remaining_offered))
638 .map_err(|e| NoteError::other_with_source("invalid remaining_offered amount", e))?
639 .with_callbacks(self.offered_asset.callbacks());
640
641 let new_storage = PswapNoteStorage::builder()
642 .requested_asset(requested_asset)
643 .creator_account_id(self.storage.creator_account_id)
644 .payback_note_type(self.storage.payback_note_type)
645 .build();
646 let recipient = new_storage.into_recipient(remainder_serial);
647
648 let assets = NoteAssets::new(vec![offered_asset.into()])?;
649
650 let tag = Self::create_tag(self.note_type, &offered_asset, &requested_asset);
651 let metadata = PartialNoteMetadata::new(consumer_account_id, self.note_type).with_tag(tag);
652
653 Ok(Note::with_attachments(
654 assets,
655 metadata,
656 recipient,
657 NoteAttachments::from(NoteAttachment::from(*attachment)),
658 ))
659 }
660
661 pub fn create_tag(
673 note_type: NoteType,
674 offered_asset: &FungibleAsset,
675 requested_asset: &FungibleAsset,
676 ) -> NoteTag {
677 let pswap_root_bytes = Self::script().root().as_bytes();
678
679 let mut pswap_use_case_id = (pswap_root_bytes[0] as u16) << 6;
682 pswap_use_case_id |= (pswap_root_bytes[1] >> 2) as u16;
683
684 let offered_asset_id: u64 = offered_asset.faucet_id().prefix().into();
686 let offered_asset_tag = (offered_asset_id >> 56) as u8;
687
688 let requested_asset_id: u64 = requested_asset.faucet_id().prefix().into();
689 let requested_asset_tag = (requested_asset_id >> 56) as u8;
690
691 let asset_pair = ((offered_asset_tag as u16) << 8) | (requested_asset_tag as u16);
692
693 let tag = ((note_type as u8 as u32) << 30)
694 | ((pswap_use_case_id as u32) << 16)
695 | asset_pair as u32;
696
697 NoteTag::new(tag)
698 }
699
700 fn calculate_output_amount(
708 offered_total: u64,
709 requested_total: u64,
710 fill_amount: u64,
711 ) -> Result<u64, NoteError> {
712 let product = (offered_total as u128) * (fill_amount as u128);
713 let quotient = product / (requested_total as u128);
714 let amount = u64::try_from(quotient)
715 .map_err(|_| NoteError::other("payout quotient does not fit in u64"))?;
716 AssetAmount::new(amount).map_err(|e| {
718 NoteError::other_with_source("payout amount exceeds max fungible asset amount", e)
719 })?;
720 Ok(amount)
721 }
722
723 fn pswap_output_attachment(
729 amount: u64,
730 order_id: Felt,
731 depth: u64,
732 ) -> Result<NoteAttachment, NoteError> {
733 let amount = AssetAmount::new(amount)
734 .map_err(|e| NoteError::other_with_source("amount is not a valid asset amount", e))?;
735 let depth = u32::try_from(depth)
736 .map_err(|_| NoteError::other("PSWAP depth does not fit in u32"))?;
737 Ok(PswapNoteAttachment::new(amount, order_id, depth).into())
738 }
739
740 fn create_payback_note(
750 &self,
751 consumer_account_id: AccountId,
752 payback_asset: FungibleAsset,
753 fill_amount: u64,
754 ) -> Result<Note, NoteError> {
755 let payback_note_tag = self.storage.payback_note_tag();
756 let p2id_serial_num = Word::from([
758 self.serial_number[0] + ONE,
759 self.serial_number[1],
760 self.serial_number[2],
761 self.serial_number[3],
762 ]);
763
764 let recipient =
766 P2idNoteStorage::new(self.storage.creator_account_id).into_recipient(p2id_serial_num);
767
768 let current_depth = self.parent_depth() + 1;
769 let attachment =
770 Self::pswap_output_attachment(fill_amount, self.order_id(), current_depth)?;
771
772 let p2id_assets = NoteAssets::new(vec![payback_asset.into()])?;
773 let p2id_metadata =
774 PartialNoteMetadata::new(consumer_account_id, self.storage.payback_note_type)
775 .with_tag(payback_note_tag);
776
777 Ok(Note::with_attachments(
778 p2id_assets,
779 p2id_metadata,
780 recipient,
781 NoteAttachments::from(attachment),
782 ))
783 }
784
785 fn create_remainder_pswap_note(
795 &self,
796 consumer_account_id: AccountId,
797 remaining_offered_asset: FungibleAsset,
798 remaining_requested_asset: FungibleAsset,
799 offered_amount_for_fill: u64,
800 ) -> Result<PswapNote, NoteError> {
801 let new_storage = PswapNoteStorage::builder()
802 .requested_asset(remaining_requested_asset)
803 .creator_account_id(self.storage.creator_account_id)
804 .payback_note_type(self.storage.payback_note_type)
805 .build();
806
807 let remainder_serial_num = Word::from([
810 self.serial_number[0],
811 self.serial_number[1],
812 self.serial_number[2],
813 self.serial_number[3] + ONE,
814 ]);
815
816 let current_depth = self.parent_depth() + 1;
817 let attachment =
818 Self::pswap_output_attachment(offered_amount_for_fill, self.order_id(), current_depth)?;
819
820 PswapNote::builder()
821 .sender(consumer_account_id)
822 .storage(new_storage)
823 .serial_number(remainder_serial_num)
824 .note_type(self.note_type)
825 .offered_asset(remaining_offered_asset)
826 .attachment(attachment)
827 .build()
828 }
829}
830
831impl From<PswapNote> for Note {
836 fn from(pswap: PswapNote) -> Self {
837 let tag = PswapNote::create_tag(
838 pswap.note_type,
839 &pswap.offered_asset,
840 pswap.storage.requested_asset(),
841 );
842
843 let recipient = pswap.storage.into_recipient(pswap.serial_number);
844
845 let assets = NoteAssets::new(vec![pswap.offered_asset.into()])
846 .expect("single fungible asset should be valid");
847
848 let metadata = PartialNoteMetadata::new(pswap.sender, pswap.note_type).with_tag(tag);
849
850 let attachments = pswap.attachment.map(NoteAttachments::from).unwrap_or_default();
851
852 Note::with_attachments(assets, metadata, recipient, attachments)
853 }
854}
855
856impl TryFrom<&Note> for PswapNote {
858 type Error = NoteError;
859
860 fn try_from(note: &Note) -> Result<Self, Self::Error> {
861 if note.recipient().script().root() != PswapNote::script_root() {
862 return Err(NoteError::other("note script root does not match PSWAP script root"));
863 }
864
865 let storage = PswapNoteStorage::try_from(note.recipient().storage().items())?;
866
867 if note.assets().num_assets() != 1 {
868 return Err(NoteError::other("PSWAP note must have exactly one asset"));
869 }
870 let offered_asset = match note.assets().iter().next().unwrap() {
871 Asset::Fungible(fa) => *fa,
872 Asset::NonFungible(_) => {
873 return Err(NoteError::other("PSWAP note asset must be fungible"));
874 },
875 };
876
877 let attachment = match note.attachments().num_attachments() {
878 0 => None,
879 1 => {
880 Some(note.attachments().get(0).expect("length should have been validated").clone())
881 },
882 _ => return Err(NoteError::other("pswap note supports only one attachment")),
883 };
884
885 PswapNote::builder()
886 .sender(note.metadata().sender())
887 .storage(storage)
888 .serial_number(note.recipient().serial_num())
889 .note_type(note.metadata().note_type())
890 .offered_asset(offered_asset)
891 .maybe_attachment(attachment)
892 .build()
893 }
894}
895
896#[cfg(test)]
900mod tests {
901 use miden_protocol::account::{AccountId, AccountIdVersion, AccountType};
902 use miden_protocol::asset::FungibleAsset;
903 use miden_protocol::crypto::rand::{FeltRng, RandomCoin};
904
905 use super::*;
906
907 fn dummy_faucet_id(byte: u8) -> AccountId {
911 let mut bytes = [0; 15];
912 bytes[0] = byte;
913 AccountId::dummy(bytes, AccountIdVersion::Version1, AccountType::Public)
914 }
915
916 fn dummy_creator_id() -> AccountId {
917 AccountId::dummy([1; 15], AccountIdVersion::Version1, AccountType::Public)
918 }
919
920 fn dummy_consumer_id() -> AccountId {
921 AccountId::dummy([2; 15], AccountIdVersion::Version1, AccountType::Public)
922 }
923
924 fn build_pswap_note(
925 offered_asset: FungibleAsset,
926 requested_asset: FungibleAsset,
927 creator_id: AccountId,
928 ) -> (PswapNote, Note) {
929 let mut rng = RandomCoin::new(Word::default());
930 let storage = PswapNoteStorage::builder()
931 .requested_asset(requested_asset)
932 .creator_account_id(creator_id)
933 .build();
934 let pswap = PswapNote::builder()
935 .sender(creator_id)
936 .storage(storage)
937 .serial_number(rng.draw_word())
938 .note_type(NoteType::Public)
939 .offered_asset(offered_asset)
940 .build()
941 .unwrap();
942 let note: Note = pswap.clone().into();
943 (pswap, note)
944 }
945
946 #[test]
950 fn pswap_note_creation_and_script() {
951 let creator_id = dummy_creator_id();
952 let offered_asset = FungibleAsset::new(dummy_faucet_id(0xaa), 1000).unwrap();
953 let requested_asset = FungibleAsset::new(dummy_faucet_id(0xbb), 500).unwrap();
954
955 let (pswap, note) = build_pswap_note(offered_asset, requested_asset, creator_id);
956
957 assert_eq!(pswap.sender(), creator_id);
958 assert_eq!(pswap.note_type(), NoteType::Public);
959
960 let script = PswapNote::script();
961 assert!(Word::from(script.root()) != Word::default(), "Script root should not be zero");
962 assert_eq!(note.metadata().sender(), creator_id);
963 assert_eq!(note.metadata().note_type(), NoteType::Public);
964 assert_eq!(note.assets().num_assets(), 1);
965 assert_eq!(note.recipient().script().root(), script.root());
966 assert_eq!(
967 note.recipient().storage().num_items(),
968 PswapNoteStorage::NUM_STORAGE_ITEMS as u16,
969 );
970 }
971
972 #[test]
973 fn pswap_note_builder() {
974 let creator_id = dummy_creator_id();
975 let offered_asset = FungibleAsset::new(dummy_faucet_id(0xaa), 1000).unwrap();
976 let requested_asset = FungibleAsset::new(dummy_faucet_id(0xbb), 500).unwrap();
977
978 let (pswap, note) = build_pswap_note(offered_asset, requested_asset, creator_id);
979
980 assert_eq!(pswap.sender(), creator_id);
981 assert_eq!(pswap.note_type(), NoteType::Public);
982 assert_eq!(note.metadata().sender(), creator_id);
983 assert_eq!(note.metadata().note_type(), NoteType::Public);
984 assert_eq!(note.assets().num_assets(), 1);
985 assert_eq!(
986 note.recipient().storage().num_items(),
987 PswapNoteStorage::NUM_STORAGE_ITEMS as u16,
988 );
989 }
990
991 #[test]
992 fn pswap_tag() {
993 let mut offered_faucet_bytes = [0; 15];
994 offered_faucet_bytes[0] = 0xcd;
995 offered_faucet_bytes[1] = 0xb1;
996
997 let mut requested_faucet_bytes = [0; 15];
998 requested_faucet_bytes[0] = 0xab;
999 requested_faucet_bytes[1] = 0xec;
1000
1001 let offered_asset = FungibleAsset::new(
1002 AccountId::dummy(offered_faucet_bytes, AccountIdVersion::Version1, AccountType::Public),
1003 100,
1004 )
1005 .unwrap();
1006 let requested_asset = FungibleAsset::new(
1007 AccountId::dummy(
1008 requested_faucet_bytes,
1009 AccountIdVersion::Version1,
1010 AccountType::Public,
1011 ),
1012 200,
1013 )
1014 .unwrap();
1015
1016 let tag = PswapNote::create_tag(NoteType::Public, &offered_asset, &requested_asset);
1017 let tag_u32 = u32::from(tag);
1018
1019 let note_type_bits = tag_u32 >> 30;
1021 assert_eq!(note_type_bits, NoteType::Public as u32);
1022 }
1023
1024 #[test]
1025 fn calculate_output_amount() {
1026 assert_eq!(PswapNote::calculate_output_amount(100, 100, 50).unwrap(), 50); assert_eq!(PswapNote::calculate_output_amount(200, 100, 50).unwrap(), 100); assert_eq!(PswapNote::calculate_output_amount(100, 200, 50).unwrap(), 25); let result = PswapNote::calculate_output_amount(100, 73, 7).unwrap();
1032 assert!(result > 0, "Should produce non-zero output");
1033 }
1034
1035 #[test]
1036 fn pswap_note_storage_try_from() {
1037 let creator_id = dummy_creator_id();
1038 let requested_asset = FungibleAsset::new(dummy_faucet_id(0xaa), 500).unwrap();
1039
1040 let storage_items = vec![
1041 Felt::from(requested_asset.callbacks().as_u8()),
1042 requested_asset.faucet_id().suffix(),
1043 requested_asset.faucet_id().prefix().as_felt(),
1044 Felt::from(requested_asset.amount()),
1045 Felt::from(NoteType::Private.as_u8()), creator_id.prefix().as_felt(),
1047 creator_id.suffix(),
1048 ];
1049
1050 let parsed = PswapNoteStorage::try_from(storage_items.as_slice()).unwrap();
1051 assert_eq!(parsed.creator_account_id(), creator_id);
1052 assert_eq!(parsed.requested_asset_amount(), 500);
1053 }
1054
1055 #[test]
1056 fn pswap_note_storage_roundtrip() {
1057 let creator_id = dummy_creator_id();
1058 let requested_asset = FungibleAsset::new(dummy_faucet_id(0xaa), 500).unwrap();
1059
1060 let storage = PswapNoteStorage::builder()
1061 .requested_asset(requested_asset)
1062 .creator_account_id(creator_id)
1063 .build();
1064
1065 let note_storage = NoteStorage::from(storage.clone());
1066 let parsed = PswapNoteStorage::try_from(note_storage.items()).unwrap();
1067
1068 assert_eq!(parsed.creator_account_id(), creator_id);
1069 assert_eq!(parsed.requested_asset_amount(), 500);
1070 }
1071
1072 #[test]
1077 fn pswap_execute_combined_account_fill_and_note_fill_partial_fill() {
1078 let creator_id = dummy_creator_id();
1079 let consumer_id = dummy_consumer_id();
1080 let offered_faucet = dummy_faucet_id(0xaa);
1081 let requested_faucet = dummy_faucet_id(0xbb);
1082
1083 let offered_asset = FungibleAsset::new(offered_faucet, 100).unwrap();
1085 let requested_asset = FungibleAsset::new(requested_faucet, 50).unwrap();
1086 let (pswap, _) = build_pswap_note(offered_asset, requested_asset, creator_id);
1087
1088 let account_fill = FungibleAsset::new(requested_faucet, 10).unwrap();
1090 let note_fill = FungibleAsset::new(requested_faucet, 20).unwrap();
1091
1092 let (payback, remainder) =
1093 pswap.execute(consumer_id, Some(account_fill), Some(note_fill)).unwrap();
1094
1095 assert_eq!(payback.assets().num_assets(), 1);
1097 let payback_asset = payback.assets().iter().next().unwrap();
1098 let Asset::Fungible(fa) = payback_asset else {
1099 panic!("expected fungible payback asset");
1100 };
1101 assert_eq!(fa.faucet_id(), requested_faucet);
1102 assert_eq!(fa.amount().as_u64(), 30);
1103
1104 let remainder = remainder.expect("partial fill should produce remainder");
1107 assert_eq!(remainder.storage().requested_asset_amount(), 20);
1108 assert_eq!(remainder.offered_asset().amount().as_u64(), 40);
1109 assert_eq!(remainder.storage().creator_account_id(), creator_id);
1110 }
1111
1112 #[test]
1116 fn pswap_execute_combined_account_fill_and_note_fill_full_fill() {
1117 let creator_id = dummy_creator_id();
1118 let consumer_id = dummy_consumer_id();
1119 let offered_faucet = dummy_faucet_id(0xaa);
1120 let requested_faucet = dummy_faucet_id(0xbb);
1121
1122 let offered_asset = FungibleAsset::new(offered_faucet, 100).unwrap();
1123 let requested_asset = FungibleAsset::new(requested_faucet, 50).unwrap();
1124 let (pswap, _) = build_pswap_note(offered_asset, requested_asset, creator_id);
1125
1126 let account_fill = FungibleAsset::new(requested_faucet, 30).unwrap();
1128 let note_fill = FungibleAsset::new(requested_faucet, 20).unwrap();
1129
1130 let (payback, remainder) =
1131 pswap.execute(consumer_id, Some(account_fill), Some(note_fill)).unwrap();
1132
1133 assert_eq!(payback.assets().num_assets(), 1);
1135 let payback_asset = payback.assets().iter().next().unwrap();
1136 let Asset::Fungible(fa) = payback_asset else {
1137 panic!("expected fungible payback asset");
1138 };
1139 assert_eq!(fa.faucet_id(), requested_faucet);
1140 assert_eq!(fa.amount().as_u64(), 50);
1141
1142 assert!(remainder.is_none(), "full fill must not produce a remainder");
1144 }
1145
1146 #[test]
1152 fn pswap_output_assets_preserve_callback_flag() {
1153 let creator_id = dummy_creator_id();
1154 let consumer_id = dummy_consumer_id();
1155 let offered_faucet = dummy_faucet_id(0xaa);
1156 let requested_faucet = dummy_faucet_id(0xbb);
1157
1158 let offered_asset = FungibleAsset::new(offered_faucet, 100)
1159 .unwrap()
1160 .with_callbacks(AssetCallbackFlag::Enabled);
1161 let requested_asset = FungibleAsset::new(requested_faucet, 50)
1162 .unwrap()
1163 .with_callbacks(AssetCallbackFlag::Enabled);
1164 let (pswap, _) = build_pswap_note(offered_asset, requested_asset, creator_id);
1165
1166 let account_fill = FungibleAsset::new(requested_faucet, 20)
1168 .unwrap()
1169 .with_callbacks(AssetCallbackFlag::Enabled);
1170 let (payback, remainder) = pswap.execute(consumer_id, Some(account_fill), None).unwrap();
1171
1172 let Asset::Fungible(fa) = payback.assets().iter().next().unwrap() else {
1173 panic!("expected fungible payback asset");
1174 };
1175 assert_eq!(fa.callbacks(), AssetCallbackFlag::Enabled);
1176
1177 let remainder = remainder.expect("partial fill should produce remainder");
1178 assert_eq!(
1179 remainder.offered_asset().callbacks(),
1180 AssetCallbackFlag::Enabled,
1181 "remainder offered asset must inherit callbacks",
1182 );
1183 assert_eq!(
1184 remainder.storage().requested_asset().callbacks(),
1185 AssetCallbackFlag::Enabled,
1186 "remainder storage's requested asset must inherit callbacks",
1187 );
1188
1189 let payback_attachment =
1191 PswapNoteAttachment::new(AssetAmount::new(20).unwrap(), pswap.order_id(), 1);
1192 let reconstructed_payback = pswap.payback_note(consumer_id, &payback_attachment).unwrap();
1193 let Asset::Fungible(fa) = reconstructed_payback.assets().iter().next().unwrap() else {
1194 panic!("expected fungible payback asset");
1195 };
1196 assert_eq!(
1197 fa.callbacks(),
1198 AssetCallbackFlag::Enabled,
1199 "payback_note must preserve requested asset's callback flag",
1200 );
1201
1202 let remainder_attachment =
1204 PswapNoteAttachment::new(AssetAmount::new(40).unwrap(), pswap.order_id(), 1);
1205 let reconstructed_remainder = pswap
1206 .remainder_note(
1207 consumer_id,
1208 &remainder_attachment,
1209 AssetAmount::new(60).unwrap(),
1210 AssetAmount::new(30).unwrap(),
1211 )
1212 .unwrap();
1213 let Asset::Fungible(fa) = reconstructed_remainder.assets().iter().next().unwrap() else {
1214 panic!("expected fungible remainder asset");
1215 };
1216 assert_eq!(
1217 fa.callbacks(),
1218 AssetCallbackFlag::Enabled,
1219 "remainder_note must preserve offered asset's callback flag",
1220 );
1221 }
1222}