1use alloc::vec;
2use alloc::vec::Vec;
3
4use miden_protocol::account::AccountId;
5use miden_protocol::assembly::Path;
6use miden_protocol::asset::{Asset, AssetAmount, FungibleAsset};
7use miden_protocol::errors::NoteError;
8use miden_protocol::note::{
9 Note,
10 NoteAssets,
11 NoteAttachment,
12 NoteAttachmentScheme,
13 NoteAttachments,
14 NoteRecipient,
15 NoteScript,
16 NoteScriptRoot,
17 NoteStorage,
18 NoteTag,
19 NoteType,
20 PartialNoteMetadata,
21};
22use miden_protocol::utils::sync::LazyLock;
23use miden_protocol::{Felt, ONE, Word, ZERO};
24
25use crate::StandardsLib;
26use crate::note::costs::{NoteConsumptionCost, PSWAP_CONSUMPTION_CYCLES};
27use crate::note::{P2idNote, P2idNoteStorage, StandardNoteAttachment};
28
29const PSWAP_SCRIPT_PATH: &str = "::miden::standards::notes::pswap::main";
34
35static PSWAP_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
37 let standards_lib = StandardsLib::default();
38 let path = Path::new(PSWAP_SCRIPT_PATH);
39 NoteScript::from_package_reference(standards_lib.as_ref(), path)
40 .expect("Standards library contains PSWAP note script procedure")
41});
42
43#[derive(Debug, Clone, PartialEq, Eq, bon::Builder)]
66pub struct PswapNoteStorage {
67 min_requested_asset: FungibleAsset,
68
69 creator_account_id: AccountId,
70
71 #[builder(default = NoteType::Private)]
77 payback_note_type: NoteType,
78
79 #[builder(default = AssetAmount::ZERO)]
92 min_fill_step: AssetAmount,
93}
94
95impl PswapNoteStorage {
96 pub const NUM_STORAGE_ITEMS: usize = 7;
101
102 pub fn into_recipient(self, serial_num: Word) -> NoteRecipient {
104 NoteRecipient::new(serial_num, PswapNote::script(), NoteStorage::from(self))
105 }
106
107 pub fn min_requested_asset(&self) -> &FungibleAsset {
112 &self.min_requested_asset
113 }
114
115 pub fn payback_note_tag(&self) -> NoteTag {
117 NoteTag::with_account_target(self.creator_account_id)
118 }
119
120 pub fn creator_account_id(&self) -> AccountId {
122 self.creator_account_id
123 }
124
125 pub fn payback_note_type(&self) -> NoteType {
127 self.payback_note_type
128 }
129
130 pub fn requested_faucet_id(&self) -> AccountId {
132 self.min_requested_asset.faucet_id()
133 }
134
135 pub fn min_requested_amount(&self) -> u64 {
137 self.min_requested_asset.amount().as_u64()
138 }
139
140 pub fn min_fill_step(&self) -> AssetAmount {
142 self.min_fill_step
143 }
144}
145
146impl From<PswapNoteStorage> for NoteStorage {
148 fn from(storage: PswapNoteStorage) -> Self {
149 let storage_items = vec![
150 storage.min_requested_asset.faucet_id().suffix(),
152 storage.min_requested_asset.faucet_id().prefix().as_felt(),
153 Felt::from(storage.min_requested_asset.amount()),
154 Felt::from(storage.min_fill_step),
156 Felt::from(storage.payback_note_type.as_u8()),
158 storage.creator_account_id.suffix(),
160 storage.creator_account_id.prefix().as_felt(),
161 ];
162 NoteStorage::new(storage_items)
163 .expect("number of storage items should not exceed max storage items")
164 }
165}
166
167impl TryFrom<&[Felt]> for PswapNoteStorage {
169 type Error = NoteError;
170
171 fn try_from(note_storage: &[Felt]) -> Result<Self, Self::Error> {
172 if note_storage.len() != Self::NUM_STORAGE_ITEMS {
173 return Err(NoteError::InvalidNoteStorageLength {
174 expected: Self::NUM_STORAGE_ITEMS,
175 actual: note_storage.len(),
176 });
177 }
178
179 let faucet_id = AccountId::try_from_elements(note_storage[0], note_storage[1])
182 .map_err(|e| NoteError::other_with_source("failed to parse requested faucet ID", e))?;
183
184 let amount = note_storage[2].as_canonical_u64();
185 let min_requested_asset = FungibleAsset::new(faucet_id, amount)
186 .map_err(|e| NoteError::other_with_source("failed to create requested asset", e))?;
187
188 let min_fill_step = AssetAmount::new(note_storage[3].as_canonical_u64())
190 .map_err(|e| NoteError::other_with_source("failed to parse min_fill_step", e))?;
191
192 let payback_note_type = NoteType::try_from(
194 u8::try_from(note_storage[4].as_canonical_u64())
195 .map_err(|_| NoteError::other("payback_note_type exceeds u8"))?,
196 )
197 .map_err(|e| NoteError::other_with_source("failed to parse payback note type", e))?;
198
199 let creator_account_id = AccountId::try_from_elements(note_storage[5], note_storage[6])
201 .map_err(|e| NoteError::other_with_source("failed to parse creator account ID", e))?;
202
203 Ok(Self {
204 min_requested_asset,
205 creator_account_id,
206 payback_note_type,
207 min_fill_step,
208 })
209 }
210}
211
212#[derive(Debug, Clone, Copy, PartialEq, Eq)]
218pub struct PswapNoteAttachment {
219 amount: AssetAmount,
220 order_id: Felt,
221 depth: u32,
222}
223
224impl PswapNoteAttachment {
225 pub fn new(amount: AssetAmount, order_id: Felt, depth: u32) -> Self {
227 Self { amount, order_id, depth }
228 }
229
230 pub fn amount(&self) -> AssetAmount {
231 self.amount
232 }
233
234 pub fn order_id(&self) -> Felt {
235 self.order_id
236 }
237
238 pub fn depth(&self) -> u32 {
239 self.depth
240 }
241}
242
243impl From<PswapNoteAttachment> for NoteAttachment {
244 fn from(attachment: PswapNoteAttachment) -> Self {
245 let word = Word::from([
246 Felt::from(attachment.amount),
247 attachment.order_id,
248 Felt::from(attachment.depth),
249 ZERO,
250 ]);
251 NoteAttachment::with_word(PswapNote::PSWAP_ATTACHMENT_SCHEME, word)
252 }
253}
254
255#[derive(Debug, Clone, bon::Builder)]
271#[builder(finish_fn(vis = "", name = build_internal))]
272pub struct PswapNote {
273 sender: AccountId,
274 storage: PswapNoteStorage,
275 serial_number: Word,
276
277 #[builder(default = NoteType::Private)]
278 note_type: NoteType,
279
280 offered_asset: FungibleAsset,
281
282 attachment: Option<NoteAttachment>,
283}
284
285impl<S: pswap_note_builder::State> PswapNoteBuilder<S>
286where
287 S: pswap_note_builder::IsComplete,
288{
289 pub fn build(self) -> Result<PswapNote, NoteError> {
295 let note = self.build_internal();
296
297 if note.offered_asset.faucet_id() == note.storage.requested_faucet_id() {
298 return Err(NoteError::other(
299 "offered and requested assets must have different faucets",
300 ));
301 }
302
303 Ok(note)
304 }
305}
306
307impl PswapNote {
308 pub const NUM_STORAGE_ITEMS: usize = PswapNoteStorage::NUM_STORAGE_ITEMS;
313
314 pub const PSWAP_ATTACHMENT_SCHEME: NoteAttachmentScheme =
317 StandardNoteAttachment::PswapAttachment.attachment_scheme();
318
319 const PARENT_ATTACHMENT_DEPTH_OFFSET: usize = 2;
321
322 pub fn script() -> NoteScript {
327 PSWAP_SCRIPT.clone()
328 }
329
330 pub fn script_root() -> NoteScriptRoot {
332 PSWAP_SCRIPT.root()
333 }
334
335 pub fn create_args(account_fill: u64, note_fill: u64) -> Result<Word, NoteError> {
358 let account_fill = Felt::try_from(account_fill)
359 .map_err(|e| NoteError::other_with_source("account_fill is not a valid felt", e))?;
360 let note_fill = Felt::try_from(note_fill)
361 .map_err(|e| NoteError::other_with_source("note_fill is not a valid felt", e))?;
362 Ok(Word::from([account_fill, note_fill, ZERO, ZERO]))
363 }
364
365 pub fn sender(&self) -> AccountId {
367 self.sender
368 }
369
370 pub fn storage(&self) -> &PswapNoteStorage {
372 &self.storage
373 }
374
375 pub fn serial_number(&self) -> Word {
377 self.serial_number
378 }
379
380 pub fn note_type(&self) -> NoteType {
382 self.note_type
383 }
384
385 pub fn offered_asset(&self) -> &FungibleAsset {
387 &self.offered_asset
388 }
389
390 pub fn attachments(&self) -> Option<&NoteAttachment> {
398 self.attachment.as_ref()
399 }
400
401 pub fn order_id(&self) -> Felt {
403 self.serial_number[1]
404 }
405
406 pub fn parent_depth(&self) -> u64 {
413 match self.attachment.as_ref() {
414 Some(att) if att.attachment_scheme() == Self::PSWAP_ATTACHMENT_SCHEME => {
415 let attachment_word = att.content().as_words()[0];
416 attachment_word[Self::PARENT_ATTACHMENT_DEPTH_OFFSET].as_canonical_u64()
417 },
418 _ => 0,
419 }
420 }
421
422 pub fn execute_full_fill(&self, consumer_account_id: AccountId) -> Result<Note, NoteError> {
433 let requested_faucet_id = self.storage.requested_faucet_id();
434 let min_requested_amount = self.storage.min_requested_amount();
435
436 let fill_asset = FungibleAsset::new(requested_faucet_id, min_requested_amount)
437 .map_err(|e| NoteError::other_with_source("failed to create full fill asset", e))?;
438
439 self.create_payback_note(consumer_account_id, fill_asset, min_requested_amount)
440 }
441
442 pub fn execute(
458 &self,
459 consumer_account_id: AccountId,
460 account_fill_asset: Option<FungibleAsset>,
461 note_fill_asset: Option<FungibleAsset>,
462 ) -> Result<(Note, Option<PswapNote>), NoteError> {
463 let payback_asset = match (account_fill_asset, note_fill_asset) {
465 (Some(account_fill), Some(note_fill)) => account_fill.add(note_fill).map_err(|e| {
466 NoteError::other_with_source(
467 "failed to combine account fill and note fill assets",
468 e,
469 )
470 })?,
471 (Some(asset), None) | (None, Some(asset)) => asset,
472 (None, None) => {
473 return Err(NoteError::other(
474 "at least one of account_fill_asset or note_fill_asset must be provided",
475 ));
476 },
477 };
478 let fill_amount = payback_asset.amount().as_u64();
479
480 let total_offered_amount = self.offered_asset.amount().as_u64();
481 let requested_faucet_id = self.storage.requested_faucet_id();
482 let min_requested_amount = self.storage.min_requested_amount();
483
484 if fill_amount == 0 {
486 return Err(NoteError::other("Fill amount must be greater than 0"));
487 }
488
489 let account_fill_amount = account_fill_asset.as_ref().map_or(0, |a| a.amount().as_u64());
490 let note_fill_amount = note_fill_asset.as_ref().map_or(0, |a| a.amount().as_u64());
491
492 let effective_floor = self.storage.min_fill_step().as_u64().min(min_requested_amount);
497 if fill_amount < effective_floor {
498 return Err(NoteError::other("PSWAP fill amount is below the minimum fill step"));
499 }
500
501 let fill_reference = fill_amount.max(min_requested_amount);
507
508 let payout_for_account_fill = Self::calculate_output_amount(
512 total_offered_amount,
513 fill_reference,
514 account_fill_amount,
515 )?;
516 let payout_for_note_fill =
517 Self::calculate_output_amount(total_offered_amount, fill_reference, note_fill_amount)?;
518 let offered_amount_for_fill = payout_for_account_fill + payout_for_note_fill;
519
520 let payback_note =
521 self.create_payback_note(consumer_account_id, payback_asset, fill_amount)?;
522
523 let remainder = if fill_amount < min_requested_amount {
525 let remaining_offered = total_offered_amount - offered_amount_for_fill;
526 let remaining_requested = min_requested_amount - fill_amount;
527
528 let remaining_offered_asset =
529 FungibleAsset::new(self.offered_asset.faucet_id(), remaining_offered).map_err(
530 |e| NoteError::other_with_source("failed to create remainder asset", e),
531 )?;
532
533 let remaining_min_requested_asset =
534 FungibleAsset::new(requested_faucet_id, remaining_requested).map_err(|e| {
535 NoteError::other_with_source("failed to create remaining requested asset", e)
536 })?;
537
538 Some(self.create_remainder_pswap_note(
539 consumer_account_id,
540 remaining_offered_asset,
541 remaining_min_requested_asset,
542 offered_amount_for_fill,
543 )?)
544 } else {
545 None
546 };
547
548 Ok((payback_note, remainder))
549 }
550
551 pub fn calculate_offered_for_requested(&self, fill_amount: u64) -> Result<u64, NoteError> {
562 let min_requested = self.storage.min_requested_amount();
563 let total_offered = self.offered_asset.amount().as_u64();
564
565 let fill_reference = fill_amount.max(min_requested);
566 Self::calculate_output_amount(total_offered, fill_reference, fill_amount)
567 }
568
569 pub fn payback_note(
584 &self,
585 consumer_account_id: AccountId,
586 attachment: &PswapNoteAttachment,
587 ) -> Result<Note, NoteError> {
588 let depth = attachment.depth();
589 if depth == 0 {
590 return Err(NoteError::other("depth must be >= 1"));
591 }
592 let parent_depth = Felt::from(depth - 1);
593 let p2id_serial = Word::from([
594 self.serial_number[0] + ONE,
595 self.serial_number[1],
596 self.serial_number[2],
597 self.serial_number[3] + parent_depth,
598 ]);
599
600 let recipient =
601 P2idNoteStorage::new(self.storage.creator_account_id).into_recipient(p2id_serial);
602
603 let fill_asset =
604 FungibleAsset::new(self.storage.requested_faucet_id(), u64::from(attachment.amount()))
605 .map_err(|e| NoteError::other_with_source("invalid fill amount", e))?;
606 let assets = NoteAssets::new(vec![fill_asset.into()])?;
607
608 let metadata =
609 PartialNoteMetadata::new(consumer_account_id, self.storage.payback_note_type)
610 .with_tag(self.storage.payback_note_tag());
611
612 Ok(Note::with_attachments(
613 assets,
614 metadata,
615 recipient,
616 NoteAttachments::from(NoteAttachment::from(*attachment)),
617 ))
618 }
619
620 pub fn remainder_note(
638 &self,
639 consumer_account_id: AccountId,
640 attachment: &PswapNoteAttachment,
641 remaining_offered: AssetAmount,
642 remaining_requested: AssetAmount,
643 ) -> Result<Note, NoteError> {
644 let depth = attachment.depth();
645 if depth == 0 {
646 return Err(NoteError::other("depth must be >= 1"));
647 }
648 let remainder_serial = Word::from([
649 self.serial_number[0],
650 self.serial_number[1],
651 self.serial_number[2],
652 self.serial_number[3] + Felt::from(depth),
653 ]);
654
655 let min_requested_asset =
656 FungibleAsset::new(self.storage.requested_faucet_id(), u64::from(remaining_requested))
657 .map_err(|e| {
658 NoteError::other_with_source("invalid remaining_requested amount", e)
659 })?;
660 let offered_asset =
661 FungibleAsset::new(self.offered_asset.faucet_id(), u64::from(remaining_offered))
662 .map_err(|e| NoteError::other_with_source("invalid remaining_offered amount", e))?;
663
664 let new_storage = PswapNoteStorage::builder()
665 .min_requested_asset(min_requested_asset)
666 .creator_account_id(self.storage.creator_account_id)
667 .payback_note_type(self.storage.payback_note_type)
668 .min_fill_step(self.storage.min_fill_step())
669 .build();
670 let recipient = new_storage.into_recipient(remainder_serial);
671
672 let assets = NoteAssets::new(vec![offered_asset.into()])?;
673
674 let tag = Self::create_tag(self.note_type, &offered_asset, &min_requested_asset);
675 let metadata = PartialNoteMetadata::new(consumer_account_id, self.note_type).with_tag(tag);
676
677 Ok(Note::with_attachments(
678 assets,
679 metadata,
680 recipient,
681 NoteAttachments::from(NoteAttachment::from(*attachment)),
682 ))
683 }
684
685 pub fn create_tag(
697 note_type: NoteType,
698 offered_asset: &FungibleAsset,
699 min_requested_asset: &FungibleAsset,
700 ) -> NoteTag {
701 let pswap_root_bytes = Self::script().root().as_bytes();
702
703 let mut pswap_use_case_id = (pswap_root_bytes[0] as u16) << 6;
706 pswap_use_case_id |= (pswap_root_bytes[1] >> 2) as u16;
707
708 let offered_asset_id: u64 = offered_asset.faucet_id().prefix().into();
710 let offered_asset_tag = (offered_asset_id >> 56) as u8;
711
712 let min_requested_asset_id: u64 = min_requested_asset.faucet_id().prefix().into();
713 let min_requested_asset_tag = (min_requested_asset_id >> 56) as u8;
714
715 let asset_pair = ((offered_asset_tag as u16) << 8) | (min_requested_asset_tag as u16);
716
717 let tag = ((note_type as u8 as u32) << 30)
718 | ((pswap_use_case_id as u32) << 16)
719 | asset_pair as u32;
720
721 NoteTag::new(tag)
722 }
723
724 fn calculate_output_amount(
735 offered_total: u64,
736 fill_reference: u64,
737 fill_amount: u64,
738 ) -> Result<u64, NoteError> {
739 let product = (offered_total as u128) * (fill_amount as u128);
740 let quotient = product / (fill_reference as u128);
741 let amount = u64::try_from(quotient)
742 .map_err(|_| NoteError::other("payout quotient does not fit in u64"))?;
743 AssetAmount::new(amount).map_err(|e| {
745 NoteError::other_with_source("payout amount exceeds max fungible asset amount", e)
746 })?;
747 Ok(amount)
748 }
749
750 fn pswap_output_attachment(
756 amount: u64,
757 order_id: Felt,
758 depth: u64,
759 ) -> Result<NoteAttachment, NoteError> {
760 let amount = AssetAmount::new(amount)
761 .map_err(|e| NoteError::other_with_source("amount is not a valid asset amount", e))?;
762 let depth = u32::try_from(depth)
763 .map_err(|_| NoteError::other("PSWAP depth does not fit in u32"))?;
764 Ok(PswapNoteAttachment::new(amount, order_id, depth).into())
765 }
766
767 fn create_payback_note(
777 &self,
778 consumer_account_id: AccountId,
779 payback_asset: FungibleAsset,
780 fill_amount: u64,
781 ) -> Result<Note, NoteError> {
782 let payback_note_tag = self.storage.payback_note_tag();
783 let p2id_serial_num = Word::from([
785 self.serial_number[0] + ONE,
786 self.serial_number[1],
787 self.serial_number[2],
788 self.serial_number[3],
789 ]);
790
791 let recipient =
793 P2idNoteStorage::new(self.storage.creator_account_id).into_recipient(p2id_serial_num);
794
795 let current_depth = self.parent_depth() + 1;
796 let attachment =
797 Self::pswap_output_attachment(fill_amount, self.order_id(), current_depth)?;
798
799 let p2id_assets = NoteAssets::new(vec![payback_asset.into()])?;
800 let p2id_metadata =
801 PartialNoteMetadata::new(consumer_account_id, self.storage.payback_note_type)
802 .with_tag(payback_note_tag);
803
804 Ok(Note::with_attachments(
805 p2id_assets,
806 p2id_metadata,
807 recipient,
808 NoteAttachments::from(attachment),
809 ))
810 }
811
812 fn create_remainder_pswap_note(
822 &self,
823 consumer_account_id: AccountId,
824 remaining_offered_asset: FungibleAsset,
825 remaining_min_requested_asset: FungibleAsset,
826 offered_amount_for_fill: u64,
827 ) -> Result<PswapNote, NoteError> {
828 let new_storage = PswapNoteStorage::builder()
829 .min_requested_asset(remaining_min_requested_asset)
830 .creator_account_id(self.storage.creator_account_id)
831 .payback_note_type(self.storage.payback_note_type)
832 .min_fill_step(self.storage.min_fill_step())
833 .build();
834
835 let remainder_serial_num = Word::from([
838 self.serial_number[0],
839 self.serial_number[1],
840 self.serial_number[2],
841 self.serial_number[3] + ONE,
842 ]);
843
844 let current_depth = self.parent_depth() + 1;
845 let attachment =
846 Self::pswap_output_attachment(offered_amount_for_fill, self.order_id(), current_depth)?;
847
848 PswapNote::builder()
849 .sender(consumer_account_id)
850 .storage(new_storage)
851 .serial_number(remainder_serial_num)
852 .note_type(self.note_type)
853 .offered_asset(remaining_offered_asset)
854 .attachment(attachment)
855 .build()
856 }
857}
858
859impl From<PswapNote> for Note {
864 fn from(pswap: PswapNote) -> Self {
865 let tag = PswapNote::create_tag(
866 pswap.note_type,
867 &pswap.offered_asset,
868 pswap.storage.min_requested_asset(),
869 );
870
871 let recipient = pswap.storage.into_recipient(pswap.serial_number);
872
873 let assets = NoteAssets::new(vec![pswap.offered_asset.into()])
874 .expect("single fungible asset should be valid");
875
876 let metadata = PartialNoteMetadata::new(pswap.sender, pswap.note_type).with_tag(tag);
877
878 let attachments = pswap.attachment.map(NoteAttachments::from).unwrap_or_default();
879
880 Note::with_attachments(assets, metadata, recipient, attachments)
881 }
882}
883
884impl TryFrom<&Note> for PswapNote {
886 type Error = NoteError;
887
888 fn try_from(note: &Note) -> Result<Self, Self::Error> {
889 if note.recipient().script().root() != PswapNote::script_root() {
890 return Err(NoteError::other("note script root does not match PSWAP script root"));
891 }
892
893 let storage = PswapNoteStorage::try_from(note.recipient().storage().items())?;
894
895 if note.assets().num_assets() != 1 {
896 return Err(NoteError::other("PSWAP note must have exactly one asset"));
897 }
898 let offered_asset = match note.assets().iter().next().unwrap() {
899 Asset::Fungible(fa) => *fa,
900 Asset::NonFungible(_) => {
901 return Err(NoteError::other("PSWAP note asset must be fungible"));
902 },
903 };
904
905 let attachment = match note.attachments().num_attachments() {
906 0 => None,
907 1 => {
908 Some(note.attachments().get(0).expect("length should have been validated").clone())
909 },
910 _ => return Err(NoteError::other("pswap note supports only one attachment")),
911 };
912
913 PswapNote::builder()
914 .sender(note.metadata().sender())
915 .storage(storage)
916 .serial_number(note.recipient().serial_num())
917 .note_type(note.metadata().note_type())
918 .offered_asset(offered_asset)
919 .maybe_attachment(attachment)
920 .build()
921 }
922}
923
924impl NoteConsumptionCost for PswapNote {
928 fn consumption_cycles() -> u32 {
929 PSWAP_CONSUMPTION_CYCLES
930 }
931
932 fn created_notes() -> Vec<NoteScriptRoot> {
935 vec![P2idNote::script_root(), PswapNote::script_root()]
936 }
937}
938
939#[cfg(test)]
943mod tests {
944 use miden_protocol::account::{AccountId, AccountIdVersion, AccountType, AssetCallbackFlag};
945 use miden_protocol::asset::FungibleAsset;
946 use miden_protocol::crypto::rand::{FeltRng, RandomCoin};
947 use rstest::rstest;
948
949 use super::*;
950
951 fn dummy_faucet_id(byte: u8) -> AccountId {
955 AccountId::builder()
956 .account_type(AccountType::Public)
957 .build_with_seed([byte; 32])
958 }
959
960 fn dummy_creator_id() -> AccountId {
961 AccountId::builder().account_type(AccountType::Public).build_with_seed([1; 32])
962 }
963
964 fn dummy_consumer_id() -> AccountId {
965 AccountId::builder().account_type(AccountType::Public).build_with_seed([2; 32])
966 }
967
968 fn build_pswap_note(
969 offered_asset: FungibleAsset,
970 min_requested_asset: FungibleAsset,
971 creator_id: AccountId,
972 ) -> (PswapNote, Note) {
973 let mut rng = RandomCoin::new(Word::default());
974 let storage = PswapNoteStorage::builder()
975 .min_requested_asset(min_requested_asset)
976 .creator_account_id(creator_id)
977 .build();
978 let pswap = PswapNote::builder()
979 .sender(creator_id)
980 .storage(storage)
981 .serial_number(rng.draw_word())
982 .note_type(NoteType::Public)
983 .offered_asset(offered_asset)
984 .build()
985 .unwrap();
986 let note: Note = pswap.clone().into();
987 (pswap, note)
988 }
989
990 #[test]
994 fn pswap_note_creation_and_script() {
995 let creator_id = dummy_creator_id();
996 let offered_asset = FungibleAsset::new(dummy_faucet_id(0xaa), 1000).unwrap();
997 let min_requested_asset = FungibleAsset::new(dummy_faucet_id(0xbb), 500).unwrap();
998
999 let (pswap, note) = build_pswap_note(offered_asset, min_requested_asset, creator_id);
1000
1001 assert_eq!(pswap.sender(), creator_id);
1002 assert_eq!(pswap.note_type(), NoteType::Public);
1003
1004 let script = PswapNote::script();
1005 assert!(Word::from(script.root()) != Word::default(), "Script root should not be zero");
1006 assert_eq!(note.metadata().sender(), creator_id);
1007 assert_eq!(note.metadata().note_type(), NoteType::Public);
1008 assert_eq!(note.assets().num_assets(), 1);
1009 assert_eq!(note.recipient().script().root(), script.root());
1010 assert_eq!(
1011 note.recipient().storage().num_items(),
1012 PswapNoteStorage::NUM_STORAGE_ITEMS as u16,
1013 );
1014 }
1015
1016 #[test]
1017 fn pswap_note_builder() {
1018 let creator_id = dummy_creator_id();
1019 let offered_asset = FungibleAsset::new(dummy_faucet_id(0xaa), 1000).unwrap();
1020 let min_requested_asset = FungibleAsset::new(dummy_faucet_id(0xbb), 500).unwrap();
1021
1022 let (pswap, note) = build_pswap_note(offered_asset, min_requested_asset, creator_id);
1023
1024 assert_eq!(pswap.sender(), creator_id);
1025 assert_eq!(pswap.note_type(), NoteType::Public);
1026 assert_eq!(note.metadata().sender(), creator_id);
1027 assert_eq!(note.metadata().note_type(), NoteType::Public);
1028 assert_eq!(note.assets().num_assets(), 1);
1029 assert_eq!(
1030 note.recipient().storage().num_items(),
1031 PswapNoteStorage::NUM_STORAGE_ITEMS as u16,
1032 );
1033 }
1034
1035 #[test]
1036 fn pswap_tag() {
1037 let mut offered_faucet_bytes = [0; 15];
1038 offered_faucet_bytes[0] = 0xcd;
1039 offered_faucet_bytes[1] = 0xb1;
1040
1041 let mut requested_faucet_bytes = [0; 15];
1042 requested_faucet_bytes[0] = 0xab;
1043 requested_faucet_bytes[1] = 0xec;
1044
1045 let offered_asset = FungibleAsset::new(
1046 AccountId::dummy(
1047 offered_faucet_bytes,
1048 AccountIdVersion::Version1,
1049 AccountType::Public,
1050 AssetCallbackFlag::Disabled,
1051 ),
1052 100,
1053 )
1054 .unwrap();
1055 let min_requested_asset = FungibleAsset::new(
1056 AccountId::dummy(
1057 requested_faucet_bytes,
1058 AccountIdVersion::Version1,
1059 AccountType::Public,
1060 AssetCallbackFlag::Disabled,
1061 ),
1062 200,
1063 )
1064 .unwrap();
1065
1066 let tag = PswapNote::create_tag(NoteType::Public, &offered_asset, &min_requested_asset);
1067 let tag_u32 = u32::from(tag);
1068
1069 let note_type_bits = tag_u32 >> 30;
1071 assert_eq!(note_type_bits, NoteType::Public as u32);
1072 }
1073
1074 #[test]
1075 fn calculate_output_amount() {
1076 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();
1082 assert!(result > 0, "Should produce non-zero output");
1083 }
1084
1085 #[test]
1086 fn pswap_note_storage_try_from() {
1087 let creator_id = dummy_creator_id();
1088 let min_requested_asset = FungibleAsset::new(dummy_faucet_id(0xaa), 500).unwrap();
1089
1090 let storage_items = vec![
1093 min_requested_asset.faucet_id().suffix(),
1094 min_requested_asset.faucet_id().prefix().as_felt(),
1095 Felt::from(min_requested_asset.amount()),
1096 Felt::try_from(100u64).unwrap(), Felt::from(NoteType::Private.as_u8()), creator_id.suffix(),
1099 creator_id.prefix().as_felt(),
1100 ];
1101
1102 let parsed = PswapNoteStorage::try_from(storage_items.as_slice()).unwrap();
1103 assert_eq!(parsed.creator_account_id(), creator_id);
1104 assert_eq!(parsed.min_requested_amount(), 500);
1105 assert_eq!(parsed.min_fill_step().as_u64(), 100);
1106 }
1107
1108 #[test]
1109 fn pswap_note_storage_roundtrip() {
1110 let creator_id = dummy_creator_id();
1111 let min_requested_asset = FungibleAsset::new(dummy_faucet_id(0xaa), 500).unwrap();
1112
1113 let storage = PswapNoteStorage::builder()
1114 .min_requested_asset(min_requested_asset)
1115 .creator_account_id(creator_id)
1116 .min_fill_step(AssetAmount::new(42).unwrap())
1117 .build();
1118
1119 let note_storage = NoteStorage::from(storage.clone());
1120 assert_eq!(note_storage.num_items(), PswapNoteStorage::NUM_STORAGE_ITEMS as u16);
1121
1122 let parsed = PswapNoteStorage::try_from(note_storage.items()).unwrap();
1123
1124 assert_eq!(parsed.creator_account_id(), creator_id);
1125 assert_eq!(parsed.min_requested_amount(), 500);
1126 assert_eq!(parsed.min_fill_step().as_u64(), 42);
1127 }
1128
1129 #[test]
1130 fn pswap_note_storage_defaults_min_fill_step_to_zero() {
1131 let creator_id = dummy_creator_id();
1132 let min_requested_asset = FungibleAsset::new(dummy_faucet_id(0xaa), 500).unwrap();
1133
1134 let storage = PswapNoteStorage::builder()
1135 .min_requested_asset(min_requested_asset)
1136 .creator_account_id(creator_id)
1137 .build();
1138
1139 assert_eq!(
1140 storage.min_fill_step(),
1141 AssetAmount::ZERO,
1142 "min_fill_step must default to zero (no floor)",
1143 );
1144 }
1145
1146 #[rstest]
1151 #[case::below_floor(100, 30, 29, 0, false)]
1153 #[case::equal_floor(100, 30, 30, 0, true)]
1154 #[case::above_floor(100, 30, 50, 0, true)]
1155 #[case::clamped_full_fill(20, 50, 20, 0, true)]
1158 #[case::clamped_below_both(20, 50, 10, 0, false)]
1159 #[case::two_legs_meet_floor(100, 30, 20, 20, true)]
1161 #[case::two_legs_below_floor(100, 30, 10, 10, false)]
1162 fn pswap_execute_enforces_min_fill_step(
1163 #[case] min_requested: u64,
1164 #[case] min_fill_step: u64,
1165 #[case] account_fill: u64,
1166 #[case] note_fill: u64,
1167 #[case] expect_ok: bool,
1168 ) {
1169 let creator_id = dummy_creator_id();
1170 let consumer_id = dummy_consumer_id();
1171 let offered_faucet = dummy_faucet_id(0xaa);
1172 let requested_faucet = dummy_faucet_id(0xbb);
1173
1174 let offered_asset = FungibleAsset::new(offered_faucet, 200).unwrap();
1175 let min_requested_asset = FungibleAsset::new(requested_faucet, min_requested).unwrap();
1176 let storage = PswapNoteStorage::builder()
1177 .min_requested_asset(min_requested_asset)
1178 .creator_account_id(creator_id)
1179 .min_fill_step(AssetAmount::new(min_fill_step).unwrap())
1180 .build();
1181 let mut rng = RandomCoin::new(Word::default());
1182 let pswap = PswapNote::builder()
1183 .sender(creator_id)
1184 .storage(storage)
1185 .serial_number(rng.draw_word())
1186 .note_type(NoteType::Public)
1187 .offered_asset(offered_asset)
1188 .build()
1189 .unwrap();
1190
1191 let leg = |amt: u64| (amt > 0).then(|| FungibleAsset::new(requested_faucet, amt).unwrap());
1192 let result = pswap.execute(consumer_id, leg(account_fill), leg(note_fill));
1193
1194 assert_eq!(result.is_ok(), expect_ok, "unexpected accept/reject for this fill");
1195
1196 if let Ok((_, remainder)) = result {
1197 if account_fill + note_fill < min_requested {
1200 let rem = remainder.expect("partial fill should produce a remainder");
1201 assert_eq!(
1202 rem.storage().min_fill_step().as_u64(),
1203 min_fill_step,
1204 "remainder must inherit min_fill_step",
1205 );
1206 } else {
1207 assert!(remainder.is_none(), "full fill must complete the swap with no remainder");
1208 }
1209 }
1210 }
1211
1212 #[test]
1217 fn pswap_execute_combined_account_fill_and_note_fill_partial_fill() {
1218 let creator_id = dummy_creator_id();
1219 let consumer_id = dummy_consumer_id();
1220 let offered_faucet = dummy_faucet_id(0xaa);
1221 let requested_faucet = dummy_faucet_id(0xbb);
1222
1223 let offered_asset = FungibleAsset::new(offered_faucet, 100).unwrap();
1225 let min_requested_asset = FungibleAsset::new(requested_faucet, 50).unwrap();
1226 let (pswap, _) = build_pswap_note(offered_asset, min_requested_asset, creator_id);
1227
1228 let account_fill = FungibleAsset::new(requested_faucet, 10).unwrap();
1230 let note_fill = FungibleAsset::new(requested_faucet, 20).unwrap();
1231
1232 let (payback, remainder) =
1233 pswap.execute(consumer_id, Some(account_fill), Some(note_fill)).unwrap();
1234
1235 assert_eq!(payback.assets().num_assets(), 1);
1237 let payback_asset = payback.assets().iter().next().unwrap();
1238 let Asset::Fungible(fa) = payback_asset else {
1239 panic!("expected fungible payback asset");
1240 };
1241 assert_eq!(fa.faucet_id(), requested_faucet);
1242 assert_eq!(fa.amount().as_u64(), 30);
1243
1244 let remainder = remainder.expect("partial fill should produce remainder");
1247 assert_eq!(remainder.storage().min_requested_amount(), 20);
1248 assert_eq!(remainder.offered_asset().amount().as_u64(), 40);
1249 assert_eq!(remainder.storage().creator_account_id(), creator_id);
1250 }
1251
1252 #[test]
1256 fn pswap_execute_combined_account_fill_and_note_fill_full_fill() {
1257 let creator_id = dummy_creator_id();
1258 let consumer_id = dummy_consumer_id();
1259 let offered_faucet = dummy_faucet_id(0xaa);
1260 let requested_faucet = dummy_faucet_id(0xbb);
1261
1262 let offered_asset = FungibleAsset::new(offered_faucet, 100).unwrap();
1263 let min_requested_asset = FungibleAsset::new(requested_faucet, 50).unwrap();
1264 let (pswap, _) = build_pswap_note(offered_asset, min_requested_asset, creator_id);
1265
1266 let account_fill = FungibleAsset::new(requested_faucet, 30).unwrap();
1268 let note_fill = FungibleAsset::new(requested_faucet, 20).unwrap();
1269
1270 let (payback, remainder) =
1271 pswap.execute(consumer_id, Some(account_fill), Some(note_fill)).unwrap();
1272
1273 assert_eq!(payback.assets().num_assets(), 1);
1275 let payback_asset = payback.assets().iter().next().unwrap();
1276 let Asset::Fungible(fa) = payback_asset else {
1277 panic!("expected fungible payback asset");
1278 };
1279 assert_eq!(fa.faucet_id(), requested_faucet);
1280 assert_eq!(fa.amount().as_u64(), 50);
1281
1282 assert!(remainder.is_none(), "full fill must not produce a remainder");
1284 }
1285}