1use alloc::vec;
2
3use miden_protocol::account::AccountId;
4use miden_protocol::assembly::Path;
5use miden_protocol::asset::{Asset, AssetAmount, 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)]
63pub struct PswapNoteStorage {
64 min_requested_asset: FungibleAsset,
65
66 creator_account_id: AccountId,
67
68 #[builder(default = NoteType::Private)]
74 payback_note_type: NoteType,
75}
76
77impl PswapNoteStorage {
78 pub const NUM_STORAGE_ITEMS: usize = 6;
83
84 pub fn into_recipient(self, serial_num: Word) -> NoteRecipient {
86 NoteRecipient::new(serial_num, PswapNote::script(), NoteStorage::from(self))
87 }
88
89 pub fn min_requested_asset(&self) -> &FungibleAsset {
94 &self.min_requested_asset
95 }
96
97 pub fn payback_note_tag(&self) -> NoteTag {
99 NoteTag::with_account_target(self.creator_account_id)
100 }
101
102 pub fn creator_account_id(&self) -> AccountId {
104 self.creator_account_id
105 }
106
107 pub fn payback_note_type(&self) -> NoteType {
109 self.payback_note_type
110 }
111
112 pub fn requested_faucet_id(&self) -> AccountId {
114 self.min_requested_asset.faucet_id()
115 }
116
117 pub fn min_requested_amount(&self) -> u64 {
119 self.min_requested_asset.amount().as_u64()
120 }
121}
122
123impl From<PswapNoteStorage> for NoteStorage {
125 fn from(storage: PswapNoteStorage) -> Self {
126 let storage_items = vec![
127 storage.min_requested_asset.faucet_id().suffix(),
129 storage.min_requested_asset.faucet_id().prefix().as_felt(),
130 Felt::from(storage.min_requested_asset.amount()),
131 Felt::from(storage.payback_note_type.as_u8()),
133 storage.creator_account_id.prefix().as_felt(),
135 storage.creator_account_id.suffix(),
136 ];
137 NoteStorage::new(storage_items)
138 .expect("number of storage items should not exceed max storage items")
139 }
140}
141
142impl TryFrom<&[Felt]> for PswapNoteStorage {
144 type Error = NoteError;
145
146 fn try_from(note_storage: &[Felt]) -> Result<Self, Self::Error> {
147 if note_storage.len() != Self::NUM_STORAGE_ITEMS {
148 return Err(NoteError::InvalidNoteStorageLength {
149 expected: Self::NUM_STORAGE_ITEMS,
150 actual: note_storage.len(),
151 });
152 }
153
154 let faucet_id = AccountId::try_from_elements(note_storage[0], note_storage[1])
157 .map_err(|e| NoteError::other_with_source("failed to parse requested faucet ID", e))?;
158
159 let amount = note_storage[2].as_canonical_u64();
160 let min_requested_asset = FungibleAsset::new(faucet_id, amount)
161 .map_err(|e| NoteError::other_with_source("failed to create requested asset", e))?;
162
163 let payback_note_type = NoteType::try_from(
165 u8::try_from(note_storage[3].as_canonical_u64())
166 .map_err(|_| NoteError::other("payback_note_type exceeds u8"))?,
167 )
168 .map_err(|e| NoteError::other_with_source("failed to parse payback note type", e))?;
169
170 let creator_account_id = AccountId::try_from_elements(note_storage[5], note_storage[4])
172 .map_err(|e| NoteError::other_with_source("failed to parse creator account ID", e))?;
173
174 Ok(Self {
175 min_requested_asset,
176 creator_account_id,
177 payback_note_type,
178 })
179 }
180}
181
182#[derive(Debug, Clone, Copy, PartialEq, Eq)]
188pub struct PswapNoteAttachment {
189 amount: AssetAmount,
190 order_id: Felt,
191 depth: u32,
192}
193
194impl PswapNoteAttachment {
195 pub fn new(amount: AssetAmount, order_id: Felt, depth: u32) -> Self {
197 Self { amount, order_id, depth }
198 }
199
200 pub fn amount(&self) -> AssetAmount {
201 self.amount
202 }
203
204 pub fn order_id(&self) -> Felt {
205 self.order_id
206 }
207
208 pub fn depth(&self) -> u32 {
209 self.depth
210 }
211}
212
213impl From<PswapNoteAttachment> for NoteAttachment {
214 fn from(attachment: PswapNoteAttachment) -> Self {
215 let word = Word::from([
216 Felt::from(attachment.amount),
217 attachment.order_id,
218 Felt::from(attachment.depth),
219 ZERO,
220 ]);
221 NoteAttachment::with_word(PswapNote::PSWAP_ATTACHMENT_SCHEME, word)
222 }
223}
224
225#[derive(Debug, Clone, bon::Builder)]
241#[builder(finish_fn(vis = "", name = build_internal))]
242pub struct PswapNote {
243 sender: AccountId,
244 storage: PswapNoteStorage,
245 serial_number: Word,
246
247 #[builder(default = NoteType::Private)]
248 note_type: NoteType,
249
250 offered_asset: FungibleAsset,
251
252 attachment: Option<NoteAttachment>,
253}
254
255impl<S: pswap_note_builder::State> PswapNoteBuilder<S>
256where
257 S: pswap_note_builder::IsComplete,
258{
259 pub fn build(self) -> Result<PswapNote, NoteError> {
265 let note = self.build_internal();
266
267 if note.offered_asset.faucet_id() == note.storage.requested_faucet_id() {
268 return Err(NoteError::other(
269 "offered and requested assets must have different faucets",
270 ));
271 }
272
273 Ok(note)
274 }
275}
276
277impl PswapNote {
278 pub const NUM_STORAGE_ITEMS: usize = PswapNoteStorage::NUM_STORAGE_ITEMS;
283
284 pub const PSWAP_ATTACHMENT_SCHEME: NoteAttachmentScheme =
287 StandardNoteAttachment::PswapAttachment.attachment_scheme();
288
289 const PARENT_ATTACHMENT_DEPTH_OFFSET: usize = 2;
291
292 pub fn script() -> NoteScript {
297 PSWAP_SCRIPT.clone()
298 }
299
300 pub fn script_root() -> NoteScriptRoot {
302 PSWAP_SCRIPT.root()
303 }
304
305 pub fn create_args(account_fill: u64, note_fill: u64) -> Result<Word, NoteError> {
328 let account_fill = Felt::try_from(account_fill)
329 .map_err(|e| NoteError::other_with_source("account_fill is not a valid felt", e))?;
330 let note_fill = Felt::try_from(note_fill)
331 .map_err(|e| NoteError::other_with_source("note_fill is not a valid felt", e))?;
332 Ok(Word::from([account_fill, note_fill, ZERO, ZERO]))
333 }
334
335 pub fn sender(&self) -> AccountId {
337 self.sender
338 }
339
340 pub fn storage(&self) -> &PswapNoteStorage {
342 &self.storage
343 }
344
345 pub fn serial_number(&self) -> Word {
347 self.serial_number
348 }
349
350 pub fn note_type(&self) -> NoteType {
352 self.note_type
353 }
354
355 pub fn offered_asset(&self) -> &FungibleAsset {
357 &self.offered_asset
358 }
359
360 pub fn attachments(&self) -> Option<&NoteAttachment> {
368 self.attachment.as_ref()
369 }
370
371 pub fn order_id(&self) -> Felt {
373 self.serial_number[1]
374 }
375
376 pub fn parent_depth(&self) -> u64 {
383 match self.attachment.as_ref() {
384 Some(att) if att.attachment_scheme() == Self::PSWAP_ATTACHMENT_SCHEME => {
385 let attachment_word = att.content().as_words()[0];
386 attachment_word[Self::PARENT_ATTACHMENT_DEPTH_OFFSET].as_canonical_u64()
387 },
388 _ => 0,
389 }
390 }
391
392 pub fn execute_full_fill(&self, consumer_account_id: AccountId) -> Result<Note, NoteError> {
403 let requested_faucet_id = self.storage.requested_faucet_id();
404 let min_requested_amount = self.storage.min_requested_amount();
405
406 let fill_asset = FungibleAsset::new(requested_faucet_id, min_requested_amount)
407 .map_err(|e| NoteError::other_with_source("failed to create full fill asset", e))?;
408
409 self.create_payback_note(consumer_account_id, fill_asset, min_requested_amount)
410 }
411
412 pub fn execute(
428 &self,
429 consumer_account_id: AccountId,
430 account_fill_asset: Option<FungibleAsset>,
431 note_fill_asset: Option<FungibleAsset>,
432 ) -> Result<(Note, Option<PswapNote>), NoteError> {
433 let payback_asset = match (account_fill_asset, note_fill_asset) {
435 (Some(account_fill), Some(note_fill)) => account_fill.add(note_fill).map_err(|e| {
436 NoteError::other_with_source(
437 "failed to combine account fill and note fill assets",
438 e,
439 )
440 })?,
441 (Some(asset), None) | (None, Some(asset)) => asset,
442 (None, None) => {
443 return Err(NoteError::other(
444 "at least one of account_fill_asset or note_fill_asset must be provided",
445 ));
446 },
447 };
448 let fill_amount = payback_asset.amount().as_u64();
449
450 let total_offered_amount = self.offered_asset.amount().as_u64();
451 let requested_faucet_id = self.storage.requested_faucet_id();
452 let min_requested_amount = self.storage.min_requested_amount();
453
454 if fill_amount == 0 {
456 return Err(NoteError::other("Fill amount must be greater than 0"));
457 }
458
459 let account_fill_amount = account_fill_asset.as_ref().map_or(0, |a| a.amount().as_u64());
460 let note_fill_amount = note_fill_asset.as_ref().map_or(0, |a| a.amount().as_u64());
461
462 let fill_reference = fill_amount.max(min_requested_amount);
468
469 let payout_for_account_fill = Self::calculate_output_amount(
473 total_offered_amount,
474 fill_reference,
475 account_fill_amount,
476 )?;
477 let payout_for_note_fill =
478 Self::calculate_output_amount(total_offered_amount, fill_reference, note_fill_amount)?;
479 let offered_amount_for_fill = payout_for_account_fill + payout_for_note_fill;
480
481 let payback_note =
482 self.create_payback_note(consumer_account_id, payback_asset, fill_amount)?;
483
484 let remainder = if fill_amount < min_requested_amount {
486 let remaining_offered = total_offered_amount - offered_amount_for_fill;
487 let remaining_requested = min_requested_amount - fill_amount;
488
489 let remaining_offered_asset =
490 FungibleAsset::new(self.offered_asset.faucet_id(), remaining_offered).map_err(
491 |e| NoteError::other_with_source("failed to create remainder asset", e),
492 )?;
493
494 let remaining_min_requested_asset =
495 FungibleAsset::new(requested_faucet_id, remaining_requested).map_err(|e| {
496 NoteError::other_with_source("failed to create remaining requested asset", e)
497 })?;
498
499 Some(self.create_remainder_pswap_note(
500 consumer_account_id,
501 remaining_offered_asset,
502 remaining_min_requested_asset,
503 offered_amount_for_fill,
504 )?)
505 } else {
506 None
507 };
508
509 Ok((payback_note, remainder))
510 }
511
512 pub fn calculate_offered_for_requested(&self, fill_amount: u64) -> Result<u64, NoteError> {
523 let min_requested = self.storage.min_requested_amount();
524 let total_offered = self.offered_asset.amount().as_u64();
525
526 let fill_reference = fill_amount.max(min_requested);
527 Self::calculate_output_amount(total_offered, fill_reference, fill_amount)
528 }
529
530 pub fn payback_note(
545 &self,
546 consumer_account_id: AccountId,
547 attachment: &PswapNoteAttachment,
548 ) -> Result<Note, NoteError> {
549 let depth = attachment.depth();
550 if depth == 0 {
551 return Err(NoteError::other("depth must be >= 1"));
552 }
553 let parent_depth = Felt::from(depth - 1);
554 let p2id_serial = Word::from([
555 self.serial_number[0] + ONE,
556 self.serial_number[1],
557 self.serial_number[2],
558 self.serial_number[3] + parent_depth,
559 ]);
560
561 let recipient =
562 P2idNoteStorage::new(self.storage.creator_account_id).into_recipient(p2id_serial);
563
564 let fill_asset =
565 FungibleAsset::new(self.storage.requested_faucet_id(), u64::from(attachment.amount()))
566 .map_err(|e| NoteError::other_with_source("invalid fill amount", e))?;
567 let assets = NoteAssets::new(vec![fill_asset.into()])?;
568
569 let metadata =
570 PartialNoteMetadata::new(consumer_account_id, self.storage.payback_note_type)
571 .with_tag(self.storage.payback_note_tag());
572
573 Ok(Note::with_attachments(
574 assets,
575 metadata,
576 recipient,
577 NoteAttachments::from(NoteAttachment::from(*attachment)),
578 ))
579 }
580
581 pub fn remainder_note(
599 &self,
600 consumer_account_id: AccountId,
601 attachment: &PswapNoteAttachment,
602 remaining_offered: AssetAmount,
603 remaining_requested: AssetAmount,
604 ) -> Result<Note, NoteError> {
605 let depth = attachment.depth();
606 if depth == 0 {
607 return Err(NoteError::other("depth must be >= 1"));
608 }
609 let remainder_serial = Word::from([
610 self.serial_number[0],
611 self.serial_number[1],
612 self.serial_number[2],
613 self.serial_number[3] + Felt::from(depth),
614 ]);
615
616 let min_requested_asset =
617 FungibleAsset::new(self.storage.requested_faucet_id(), u64::from(remaining_requested))
618 .map_err(|e| {
619 NoteError::other_with_source("invalid remaining_requested amount", e)
620 })?;
621 let offered_asset =
622 FungibleAsset::new(self.offered_asset.faucet_id(), u64::from(remaining_offered))
623 .map_err(|e| NoteError::other_with_source("invalid remaining_offered amount", e))?;
624
625 let new_storage = PswapNoteStorage::builder()
626 .min_requested_asset(min_requested_asset)
627 .creator_account_id(self.storage.creator_account_id)
628 .payback_note_type(self.storage.payback_note_type)
629 .build();
630 let recipient = new_storage.into_recipient(remainder_serial);
631
632 let assets = NoteAssets::new(vec![offered_asset.into()])?;
633
634 let tag = Self::create_tag(self.note_type, &offered_asset, &min_requested_asset);
635 let metadata = PartialNoteMetadata::new(consumer_account_id, self.note_type).with_tag(tag);
636
637 Ok(Note::with_attachments(
638 assets,
639 metadata,
640 recipient,
641 NoteAttachments::from(NoteAttachment::from(*attachment)),
642 ))
643 }
644
645 pub fn create_tag(
657 note_type: NoteType,
658 offered_asset: &FungibleAsset,
659 min_requested_asset: &FungibleAsset,
660 ) -> NoteTag {
661 let pswap_root_bytes = Self::script().root().as_bytes();
662
663 let mut pswap_use_case_id = (pswap_root_bytes[0] as u16) << 6;
666 pswap_use_case_id |= (pswap_root_bytes[1] >> 2) as u16;
667
668 let offered_asset_id: u64 = offered_asset.faucet_id().prefix().into();
670 let offered_asset_tag = (offered_asset_id >> 56) as u8;
671
672 let min_requested_asset_id: u64 = min_requested_asset.faucet_id().prefix().into();
673 let min_requested_asset_tag = (min_requested_asset_id >> 56) as u8;
674
675 let asset_pair = ((offered_asset_tag as u16) << 8) | (min_requested_asset_tag as u16);
676
677 let tag = ((note_type as u8 as u32) << 30)
678 | ((pswap_use_case_id as u32) << 16)
679 | asset_pair as u32;
680
681 NoteTag::new(tag)
682 }
683
684 fn calculate_output_amount(
695 offered_total: u64,
696 fill_reference: u64,
697 fill_amount: u64,
698 ) -> Result<u64, NoteError> {
699 let product = (offered_total as u128) * (fill_amount as u128);
700 let quotient = product / (fill_reference as u128);
701 let amount = u64::try_from(quotient)
702 .map_err(|_| NoteError::other("payout quotient does not fit in u64"))?;
703 AssetAmount::new(amount).map_err(|e| {
705 NoteError::other_with_source("payout amount exceeds max fungible asset amount", e)
706 })?;
707 Ok(amount)
708 }
709
710 fn pswap_output_attachment(
716 amount: u64,
717 order_id: Felt,
718 depth: u64,
719 ) -> Result<NoteAttachment, NoteError> {
720 let amount = AssetAmount::new(amount)
721 .map_err(|e| NoteError::other_with_source("amount is not a valid asset amount", e))?;
722 let depth = u32::try_from(depth)
723 .map_err(|_| NoteError::other("PSWAP depth does not fit in u32"))?;
724 Ok(PswapNoteAttachment::new(amount, order_id, depth).into())
725 }
726
727 fn create_payback_note(
737 &self,
738 consumer_account_id: AccountId,
739 payback_asset: FungibleAsset,
740 fill_amount: u64,
741 ) -> Result<Note, NoteError> {
742 let payback_note_tag = self.storage.payback_note_tag();
743 let p2id_serial_num = Word::from([
745 self.serial_number[0] + ONE,
746 self.serial_number[1],
747 self.serial_number[2],
748 self.serial_number[3],
749 ]);
750
751 let recipient =
753 P2idNoteStorage::new(self.storage.creator_account_id).into_recipient(p2id_serial_num);
754
755 let current_depth = self.parent_depth() + 1;
756 let attachment =
757 Self::pswap_output_attachment(fill_amount, self.order_id(), current_depth)?;
758
759 let p2id_assets = NoteAssets::new(vec![payback_asset.into()])?;
760 let p2id_metadata =
761 PartialNoteMetadata::new(consumer_account_id, self.storage.payback_note_type)
762 .with_tag(payback_note_tag);
763
764 Ok(Note::with_attachments(
765 p2id_assets,
766 p2id_metadata,
767 recipient,
768 NoteAttachments::from(attachment),
769 ))
770 }
771
772 fn create_remainder_pswap_note(
782 &self,
783 consumer_account_id: AccountId,
784 remaining_offered_asset: FungibleAsset,
785 remaining_min_requested_asset: FungibleAsset,
786 offered_amount_for_fill: u64,
787 ) -> Result<PswapNote, NoteError> {
788 let new_storage = PswapNoteStorage::builder()
789 .min_requested_asset(remaining_min_requested_asset)
790 .creator_account_id(self.storage.creator_account_id)
791 .payback_note_type(self.storage.payback_note_type)
792 .build();
793
794 let remainder_serial_num = Word::from([
797 self.serial_number[0],
798 self.serial_number[1],
799 self.serial_number[2],
800 self.serial_number[3] + ONE,
801 ]);
802
803 let current_depth = self.parent_depth() + 1;
804 let attachment =
805 Self::pswap_output_attachment(offered_amount_for_fill, self.order_id(), current_depth)?;
806
807 PswapNote::builder()
808 .sender(consumer_account_id)
809 .storage(new_storage)
810 .serial_number(remainder_serial_num)
811 .note_type(self.note_type)
812 .offered_asset(remaining_offered_asset)
813 .attachment(attachment)
814 .build()
815 }
816}
817
818impl From<PswapNote> for Note {
823 fn from(pswap: PswapNote) -> Self {
824 let tag = PswapNote::create_tag(
825 pswap.note_type,
826 &pswap.offered_asset,
827 pswap.storage.min_requested_asset(),
828 );
829
830 let recipient = pswap.storage.into_recipient(pswap.serial_number);
831
832 let assets = NoteAssets::new(vec![pswap.offered_asset.into()])
833 .expect("single fungible asset should be valid");
834
835 let metadata = PartialNoteMetadata::new(pswap.sender, pswap.note_type).with_tag(tag);
836
837 let attachments = pswap.attachment.map(NoteAttachments::from).unwrap_or_default();
838
839 Note::with_attachments(assets, metadata, recipient, attachments)
840 }
841}
842
843impl TryFrom<&Note> for PswapNote {
845 type Error = NoteError;
846
847 fn try_from(note: &Note) -> Result<Self, Self::Error> {
848 if note.recipient().script().root() != PswapNote::script_root() {
849 return Err(NoteError::other("note script root does not match PSWAP script root"));
850 }
851
852 let storage = PswapNoteStorage::try_from(note.recipient().storage().items())?;
853
854 if note.assets().num_assets() != 1 {
855 return Err(NoteError::other("PSWAP note must have exactly one asset"));
856 }
857 let offered_asset = match note.assets().iter().next().unwrap() {
858 Asset::Fungible(fa) => *fa,
859 Asset::NonFungible(_) => {
860 return Err(NoteError::other("PSWAP note asset must be fungible"));
861 },
862 };
863
864 let attachment = match note.attachments().num_attachments() {
865 0 => None,
866 1 => {
867 Some(note.attachments().get(0).expect("length should have been validated").clone())
868 },
869 _ => return Err(NoteError::other("pswap note supports only one attachment")),
870 };
871
872 PswapNote::builder()
873 .sender(note.metadata().sender())
874 .storage(storage)
875 .serial_number(note.recipient().serial_num())
876 .note_type(note.metadata().note_type())
877 .offered_asset(offered_asset)
878 .maybe_attachment(attachment)
879 .build()
880 }
881}
882
883#[cfg(test)]
887mod tests {
888 use miden_protocol::account::{AccountId, AccountIdVersion, AccountType, AssetCallbackFlag};
889 use miden_protocol::asset::FungibleAsset;
890 use miden_protocol::crypto::rand::{FeltRng, RandomCoin};
891
892 use super::*;
893
894 fn dummy_faucet_id(byte: u8) -> AccountId {
898 AccountId::builder()
899 .account_type(AccountType::Public)
900 .build_with_seed([byte; 32])
901 }
902
903 fn dummy_creator_id() -> AccountId {
904 AccountId::builder().account_type(AccountType::Public).build_with_seed([1; 32])
905 }
906
907 fn dummy_consumer_id() -> AccountId {
908 AccountId::builder().account_type(AccountType::Public).build_with_seed([2; 32])
909 }
910
911 fn build_pswap_note(
912 offered_asset: FungibleAsset,
913 min_requested_asset: FungibleAsset,
914 creator_id: AccountId,
915 ) -> (PswapNote, Note) {
916 let mut rng = RandomCoin::new(Word::default());
917 let storage = PswapNoteStorage::builder()
918 .min_requested_asset(min_requested_asset)
919 .creator_account_id(creator_id)
920 .build();
921 let pswap = PswapNote::builder()
922 .sender(creator_id)
923 .storage(storage)
924 .serial_number(rng.draw_word())
925 .note_type(NoteType::Public)
926 .offered_asset(offered_asset)
927 .build()
928 .unwrap();
929 let note: Note = pswap.clone().into();
930 (pswap, note)
931 }
932
933 #[test]
937 fn pswap_note_creation_and_script() {
938 let creator_id = dummy_creator_id();
939 let offered_asset = FungibleAsset::new(dummy_faucet_id(0xaa), 1000).unwrap();
940 let min_requested_asset = FungibleAsset::new(dummy_faucet_id(0xbb), 500).unwrap();
941
942 let (pswap, note) = build_pswap_note(offered_asset, min_requested_asset, creator_id);
943
944 assert_eq!(pswap.sender(), creator_id);
945 assert_eq!(pswap.note_type(), NoteType::Public);
946
947 let script = PswapNote::script();
948 assert!(Word::from(script.root()) != Word::default(), "Script root should not be zero");
949 assert_eq!(note.metadata().sender(), creator_id);
950 assert_eq!(note.metadata().note_type(), NoteType::Public);
951 assert_eq!(note.assets().num_assets(), 1);
952 assert_eq!(note.recipient().script().root(), script.root());
953 assert_eq!(
954 note.recipient().storage().num_items(),
955 PswapNoteStorage::NUM_STORAGE_ITEMS as u16,
956 );
957 }
958
959 #[test]
960 fn pswap_note_builder() {
961 let creator_id = dummy_creator_id();
962 let offered_asset = FungibleAsset::new(dummy_faucet_id(0xaa), 1000).unwrap();
963 let min_requested_asset = FungibleAsset::new(dummy_faucet_id(0xbb), 500).unwrap();
964
965 let (pswap, note) = build_pswap_note(offered_asset, min_requested_asset, creator_id);
966
967 assert_eq!(pswap.sender(), creator_id);
968 assert_eq!(pswap.note_type(), NoteType::Public);
969 assert_eq!(note.metadata().sender(), creator_id);
970 assert_eq!(note.metadata().note_type(), NoteType::Public);
971 assert_eq!(note.assets().num_assets(), 1);
972 assert_eq!(
973 note.recipient().storage().num_items(),
974 PswapNoteStorage::NUM_STORAGE_ITEMS as u16,
975 );
976 }
977
978 #[test]
979 fn pswap_tag() {
980 let mut offered_faucet_bytes = [0; 15];
981 offered_faucet_bytes[0] = 0xcd;
982 offered_faucet_bytes[1] = 0xb1;
983
984 let mut requested_faucet_bytes = [0; 15];
985 requested_faucet_bytes[0] = 0xab;
986 requested_faucet_bytes[1] = 0xec;
987
988 let offered_asset = FungibleAsset::new(
989 AccountId::dummy(
990 offered_faucet_bytes,
991 AccountIdVersion::Version1,
992 AccountType::Public,
993 AssetCallbackFlag::Disabled,
994 ),
995 100,
996 )
997 .unwrap();
998 let min_requested_asset = FungibleAsset::new(
999 AccountId::dummy(
1000 requested_faucet_bytes,
1001 AccountIdVersion::Version1,
1002 AccountType::Public,
1003 AssetCallbackFlag::Disabled,
1004 ),
1005 200,
1006 )
1007 .unwrap();
1008
1009 let tag = PswapNote::create_tag(NoteType::Public, &offered_asset, &min_requested_asset);
1010 let tag_u32 = u32::from(tag);
1011
1012 let note_type_bits = tag_u32 >> 30;
1014 assert_eq!(note_type_bits, NoteType::Public as u32);
1015 }
1016
1017 #[test]
1018 fn calculate_output_amount() {
1019 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();
1025 assert!(result > 0, "Should produce non-zero output");
1026 }
1027
1028 #[test]
1029 fn pswap_note_storage_try_from() {
1030 let creator_id = dummy_creator_id();
1031 let min_requested_asset = FungibleAsset::new(dummy_faucet_id(0xaa), 500).unwrap();
1032
1033 let storage_items = vec![
1034 min_requested_asset.faucet_id().suffix(),
1035 min_requested_asset.faucet_id().prefix().as_felt(),
1036 Felt::from(min_requested_asset.amount()),
1037 Felt::from(NoteType::Private.as_u8()), creator_id.prefix().as_felt(),
1039 creator_id.suffix(),
1040 ];
1041
1042 let parsed = PswapNoteStorage::try_from(storage_items.as_slice()).unwrap();
1043 assert_eq!(parsed.creator_account_id(), creator_id);
1044 assert_eq!(parsed.min_requested_amount(), 500);
1045 }
1046
1047 #[test]
1048 fn pswap_note_storage_roundtrip() {
1049 let creator_id = dummy_creator_id();
1050 let min_requested_asset = FungibleAsset::new(dummy_faucet_id(0xaa), 500).unwrap();
1051
1052 let storage = PswapNoteStorage::builder()
1053 .min_requested_asset(min_requested_asset)
1054 .creator_account_id(creator_id)
1055 .build();
1056
1057 let note_storage = NoteStorage::from(storage.clone());
1058 let parsed = PswapNoteStorage::try_from(note_storage.items()).unwrap();
1059
1060 assert_eq!(parsed.creator_account_id(), creator_id);
1061 assert_eq!(parsed.min_requested_amount(), 500);
1062 }
1063
1064 #[test]
1069 fn pswap_execute_combined_account_fill_and_note_fill_partial_fill() {
1070 let creator_id = dummy_creator_id();
1071 let consumer_id = dummy_consumer_id();
1072 let offered_faucet = dummy_faucet_id(0xaa);
1073 let requested_faucet = dummy_faucet_id(0xbb);
1074
1075 let offered_asset = FungibleAsset::new(offered_faucet, 100).unwrap();
1077 let min_requested_asset = FungibleAsset::new(requested_faucet, 50).unwrap();
1078 let (pswap, _) = build_pswap_note(offered_asset, min_requested_asset, creator_id);
1079
1080 let account_fill = FungibleAsset::new(requested_faucet, 10).unwrap();
1082 let note_fill = FungibleAsset::new(requested_faucet, 20).unwrap();
1083
1084 let (payback, remainder) =
1085 pswap.execute(consumer_id, Some(account_fill), Some(note_fill)).unwrap();
1086
1087 assert_eq!(payback.assets().num_assets(), 1);
1089 let payback_asset = payback.assets().iter().next().unwrap();
1090 let Asset::Fungible(fa) = payback_asset else {
1091 panic!("expected fungible payback asset");
1092 };
1093 assert_eq!(fa.faucet_id(), requested_faucet);
1094 assert_eq!(fa.amount().as_u64(), 30);
1095
1096 let remainder = remainder.expect("partial fill should produce remainder");
1099 assert_eq!(remainder.storage().min_requested_amount(), 20);
1100 assert_eq!(remainder.offered_asset().amount().as_u64(), 40);
1101 assert_eq!(remainder.storage().creator_account_id(), creator_id);
1102 }
1103
1104 #[test]
1108 fn pswap_execute_combined_account_fill_and_note_fill_full_fill() {
1109 let creator_id = dummy_creator_id();
1110 let consumer_id = dummy_consumer_id();
1111 let offered_faucet = dummy_faucet_id(0xaa);
1112 let requested_faucet = dummy_faucet_id(0xbb);
1113
1114 let offered_asset = FungibleAsset::new(offered_faucet, 100).unwrap();
1115 let min_requested_asset = FungibleAsset::new(requested_faucet, 50).unwrap();
1116 let (pswap, _) = build_pswap_note(offered_asset, min_requested_asset, creator_id);
1117
1118 let account_fill = FungibleAsset::new(requested_faucet, 30).unwrap();
1120 let note_fill = FungibleAsset::new(requested_faucet, 20).unwrap();
1121
1122 let (payback, remainder) =
1123 pswap.execute(consumer_id, Some(account_fill), Some(note_fill)).unwrap();
1124
1125 assert_eq!(payback.assets().num_assets(), 1);
1127 let payback_asset = payback.assets().iter().next().unwrap();
1128 let Asset::Fungible(fa) = payback_asset else {
1129 panic!("expected fungible payback asset");
1130 };
1131 assert_eq!(fa.faucet_id(), requested_faucet);
1132 assert_eq!(fa.amount().as_u64(), 50);
1133
1134 assert!(remainder.is_none(), "full fill must not produce a remainder");
1136 }
1137}