From cf0a8165b8ccc8be2fd206c04ae9c271b2e52f6f Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Thu, 19 Mar 2026 15:07:27 +0530 Subject: [PATCH 1/6] Added the pswap masm contract and its supportive functions --- .../asm/standards/notes/pswap.masm | 653 ++++++++ crates/miden-standards/src/note/mod.rs | 17 +- crates/miden-standards/src/note/pswap.rs | 777 +++++++++ crates/miden-testing/tests/scripts/mod.rs | 1 + crates/miden-testing/tests/scripts/pswap.rs | 1481 +++++++++++++++++ 5 files changed, 2926 insertions(+), 3 deletions(-) create mode 100644 crates/miden-standards/asm/standards/notes/pswap.masm create mode 100644 crates/miden-standards/src/note/pswap.rs create mode 100644 crates/miden-testing/tests/scripts/pswap.rs diff --git a/crates/miden-standards/asm/standards/notes/pswap.masm b/crates/miden-standards/asm/standards/notes/pswap.masm new file mode 100644 index 0000000000..636b5d6412 --- /dev/null +++ b/crates/miden-standards/asm/standards/notes/pswap.masm @@ -0,0 +1,653 @@ +use miden::protocol::active_note +use miden::protocol::output_note +use miden::protocol::note +use miden::standards::wallets::basic->wallet +use miden::core::sys +use miden::protocol::active_account +use miden::core::math::u64 +use miden::protocol::asset + +# CONSTANTS +# ================================================================================================= + +const NOTE_TYPE_MASK=0x03 +const FACTOR=0x000186A0 # 1e5 +const MAX_U32=0x0000000100000000 + +# Memory Addresses +# ================================================================================================= + +# Memory Address Layout: +# - PSWAP Note Storage: addresses 0x0000 - 0x0011 (loaded from note storage) +# - Price Calculation: addresses 0x0028 - 0x0036 +# - TokenId: addresses 0x002D - 0x0030 +# - Full Word (word-aligned): addresses 0x0018+ + +# PSWAP Note Storage (18 items loaded at address 0) +# REQUESTED_ASSET_WORD_INPUT is the base address of an 8-cell block: +# addresses 0x0000-0x0003 = ASSET_KEY, 0x0004-0x0007 = ASSET_VALUE +const REQUESTED_ASSET_WORD_INPUT = 0x0000 +const REQUESTED_ASSET_VALUE_INPUT = 0x0004 +const SWAPP_TAG_INPUT = 0x0008 +const P2ID_TAG_INPUT = 0x0009 +const SWAPP_COUNT_INPUT = 0x000C +const SWAPP_CREATOR_PREFIX_INPUT = 0x0010 +const SWAPP_CREATOR_SUFFIX_INPUT = 0x0011 + +# Memory Addresses for Price Calculation Procedure +const AMT_OFFERED = 0x0028 +const AMT_REQUESTED = 0x0029 +const AMT_REQUESTED_IN = 0x002A +const AMT_OFFERED_OUT = 0x002B +const CALC_AMT_IN = 0x0031 + +# Inflight and split calculation addresses +const AMT_REQUESTED_INFLIGHT = 0x0033 +const AMT_OFFERED_OUT_INPUT = 0x0034 +const AMT_OFFERED_OUT_INFLIGHT = 0x0036 + +# TokenId Memory Addresses +const TOKEN_OFFERED_ID_PREFIX = 0x002D +const TOKEN_OFFERED_ID_SUFFIX = 0x002E +const TOKEN_REQUESTED_ID_PREFIX = 0x002F +const TOKEN_REQUESTED_ID_SUFFIX = 0x0030 + +# Full Word Memory Addresses +# Asset storage (8 cells each, word-aligned) +const OFFERED_ASSET_WORD = 0x0018 + +# Note indices and type +const P2ID_NOTE_IDX = 0x007C +const SWAPP_NOTE_IDX = 0x0080 +const NOTE_TYPE = 0x0084 + +# ERRORS +# ================================================================================================= + +# PSWAP script expects exactly 18 note storage items +const ERR_PSWAP_WRONG_NUMBER_OF_INPUTS="PSWAP wrong number of inputs" + +# PSWAP script requires exactly one note asset +const ERR_PSWAP_WRONG_NUMBER_OF_ASSETS="PSWAP wrong number of assets" + +# PRICE CALCULATION +# ================================================================================================= + +#! Computes the proportional amount of offered tokens for a given requested input. +#! +#! Uses u64 integer arithmetic with a precision factor of 1e5 to handle +#! non-integer ratios without floating point. +#! +#! Formula: +#! if input == requested: result = offered (full fill, avoids precision loss) +#! if offered >= requested: result = (offered * FACTOR / requested) * input / FACTOR +#! if requested > offered: result = (input * FACTOR) / (requested * FACTOR / offered) +#! +#! Inputs: [offered, requested, input] (offered on top) +#! Outputs: [offered_out] +#! +proc calculate_tokens_offered_for_requested + # u64 convention: lo is on TOP after u32split + # u32split(a) => [lo (top), hi] + # u64::wrapping_mul: [b_lo, b_hi, a_lo, a_hi] => [c_lo, c_hi] c = a*b + # u64::div: [b_lo, b_hi, a_lo, a_hi] => [q_lo, q_hi] q = a/b + # combine [lo, hi] => single Felt: swap push.MAX_U32 mul add + + movup.2 mem_store.CALC_AMT_IN + # => [offered, requested] + + # Early return: if input == requested (full fill), return offered directly. + # This avoids precision loss from integer division with the FACTOR. + dup.1 mem_load.CALC_AMT_IN eq + # => [requested == input, offered, requested] + + if.true + # Full fill: consumer provides all requested, gets all offered + swap drop + # => [offered] + else + + dup.1 dup.1 + # => [offered, requested, offered, requested] + + gt + # gt pops [b=offered, a=requested], pushes (a > b) i.e. (requested > offered) + # => [requested > offered, offered, requested] + + if.true + # Case: requested > offered + # ratio = (requested * FACTOR) / offered + # result = (input * FACTOR) / ratio + + swap + # => [requested, offered] + + u32split push.FACTOR u32split + # => [F_lo, F_hi, req_lo, req_hi, offered] + + exec.u64::wrapping_mul + # => [(req*F)_lo, (req*F)_hi, offered] + + movup.2 u32split + # => [off_lo, off_hi, (req*F)_lo, (req*F)_hi] + + exec.u64::div + # => [ratio_lo, ratio_hi] + + mem_load.CALC_AMT_IN u32split push.FACTOR u32split + # => [F_lo, F_hi, in_lo, in_hi, ratio_lo, ratio_hi] + + exec.u64::wrapping_mul + # => [(in*F)_lo, (in*F)_hi, ratio_lo, ratio_hi] + + movup.3 movup.3 + # => [ratio_lo, ratio_hi, (in*F)_lo, (in*F)_hi] + + exec.u64::div + # => [result_lo, result_hi] + + swap push.MAX_U32 mul add + # => [result] + + else + # Case: offered >= requested + # result = ((offered * FACTOR) / requested) * input / FACTOR + + u32split push.FACTOR u32split + # => [F_lo, F_hi, off_lo, off_hi, requested] + + exec.u64::wrapping_mul + # => [(off*F)_lo, (off*F)_hi, requested] + + movup.2 u32split + # => [req_lo, req_hi, (off*F)_lo, (off*F)_hi] + + exec.u64::div + # => [ratio_lo, ratio_hi] + + mem_load.CALC_AMT_IN u32split + # => [in_lo, in_hi, ratio_lo, ratio_hi] + + exec.u64::wrapping_mul + # => [(rat*in)_lo, (rat*in)_hi] + + push.FACTOR u32split + # => [F_lo, F_hi, (rat*in)_lo, (rat*in)_hi] + + exec.u64::div + # => [result_lo, result_hi] + + swap push.MAX_U32 mul add + # => [result] + + end + + end +end + +# METADATA PROCEDURES +# ================================================================================================= + +#! Extracts the note_type from the active note's metadata and stores it at NOTE_TYPE. +#! +#! Metadata layout: [NOTE_ATTACHMENT(4), METADATA_HEADER(4)] +#! METADATA_HEADER[0] = sender_suffix_and_note_type (note_type in bits 0-1) +#! +#! Inputs: [] +#! Outputs: [] +#! +proc extract_note_type + exec.active_note::get_metadata + # => [NOTE_ATTACHMENT(4), METADATA_HEADER(4)] + dropw + # => [METADATA_HEADER] = [hdr3, hdr2, hdr1, hdr0] + # hdr[3] = sender_id_prefix (top) + # hdr[0] = sender_suffix_and_note_type (bottom, contains note_type in bits 0-1) + # Keep hdr0 (bottom), drop hdr3/hdr2/hdr1 from top + movdn.3 drop drop drop + # => [hdr0 = sender_suffix_and_note_type] + u32split + # => [lo32, hi32] (note_type in bits 0-1 of lo32, lo32 on top) + push.NOTE_TYPE_MASK u32and + # => [note_type, hi32] + mem_store.NOTE_TYPE + drop + # => [] +end + +# HASHING PROCEDURES +# ================================================================================================= + +#! Builds the P2ID recipient hash for the swap creator. +#! +#! Loads the creator's account ID from note storage (SWAPP_CREATOR_SUFFIX/PREFIX_INPUT), +#! stores it as P2ID note storage [suffix, prefix] at a temp address, and calls +#! note::build_recipient to compute the recipient commitment. +#! +#! Inputs: [SERIAL_NUM, SCRIPT_ROOT] +#! Outputs: [P2ID_RECIPIENT] +#! +proc build_p2id_recipient_hash + # Store creator [suffix, prefix] at word-aligned temp address 4000 + mem_load.SWAPP_CREATOR_SUFFIX_INPUT mem_store.4000 + mem_load.SWAPP_CREATOR_PREFIX_INPUT mem_store.4001 + # => [SERIAL_NUM, SCRIPT_ROOT] + + # note::build_recipient: [storage_ptr, num_storage_items, SERIAL_NUM, SCRIPT_ROOT] => [RECIPIENT] + push.2.4000 + # => [storage_ptr=4000, num_storage_items=2, SERIAL_NUM, SCRIPT_ROOT] + + exec.note::build_recipient + # => [P2ID_RECIPIENT] +end + +# P2ID NOTE CREATION PROCEDURE +# ================================================================================================= + +#! Creates a P2ID output note for the swap creator. +#! +#! Derives a unique serial number from the swap count and the active note's serial, +#! computes the P2ID recipient, creates the output note, sets the attachment, +#! and adds the requested assets (from vault and/or inflight). +#! +#! Inputs: [] +#! Outputs: [] +#! +proc create_p2id_note + # 1. Load P2ID script root (miden-standards v0.14.0-beta.2, P2idNote::script_root()) + push.17577144666381623537.251255385102954082.10949974299239761467.7391338276508122752 + # => [P2ID_SCRIPT_ROOT] + + # 2. Increment swap count (ensures unique serial per P2ID note in chained fills) + mem_load.SWAPP_COUNT_INPUT add.1 mem_store.SWAPP_COUNT_INPUT + # => [P2ID_SCRIPT_ROOT] + + # 3. Load swap count word from memory + # mem_loadw_le: Word[0]=mem[addr] on top + padw mem_loadw_le.SWAPP_COUNT_INPUT + # => [SWAP_COUNT_WORD, P2ID_SCRIPT_ROOT] + + # 4. Get serial number from active note + exec.active_note::get_serial_number + # => [SERIAL_NUM, SWAP_COUNT_WORD, P2ID_SCRIPT_ROOT] + + # 5. Derive P2ID serial: hmerge(SWAP_COUNT_WORD, SERIAL_NUM) + swapw + # => [SWAP_COUNT_WORD, SERIAL_NUM, P2ID_SCRIPT_ROOT] + hmerge + # => [P2ID_SERIAL_NUM, P2ID_SCRIPT_ROOT] + + # 6. Build P2ID recipient + exec.build_p2id_recipient_hash + # => [P2ID_RECIPIENT] + + # 7. Create output note (note_type inherited from active note metadata) + mem_load.NOTE_TYPE + # => [note_type, P2ID_RECIPIENT] + + mem_load.P2ID_TAG_INPUT + # => [tag, note_type, RECIPIENT] + + exec.output_note::create + # => [note_idx] + + mem_store.P2ID_NOTE_IDX + # => [] + + # 8. Set attachment: aux = input_amount + inflight_amount (total fill) + mem_load.AMT_REQUESTED_IN mem_load.AMT_REQUESTED_INFLIGHT add + push.0.0.0 + # => [0, 0, 0, aux] + + push.0 mem_load.P2ID_NOTE_IDX + # => [note_idx, attachment_scheme, ATTACHMENT] + + exec.output_note::set_word_attachment + # => [] + + # 9. Move input_amount from consumer's vault to P2ID note (if > 0) + mem_load.AMT_REQUESTED_IN dup push.0 neq + if.true + drop + + # Build 16-element call frame: [ASSET_KEY, ASSET_VALUE, note_idx, pad(7)] + padw push.0.0.0 + # => [pad(7)] + + mem_load.P2ID_NOTE_IDX + # => [note_idx, pad(7)] + + # Create fungible asset: expects [suffix, prefix, amount] with suffix on top + mem_load.AMT_REQUESTED_IN + mem_load.TOKEN_REQUESTED_ID_PREFIX + mem_load.TOKEN_REQUESTED_ID_SUFFIX + # => [suffix, prefix, amount, note_idx, pad(7)] + + exec.asset::create_fungible_asset + # => [ASSET_KEY, ASSET_VALUE, note_idx, pad(7)] + + call.wallet::move_asset_to_note + # => [pad(16)] + + dropw dropw dropw dropw + # => [] + else + drop + end + + # 10. Add inflight_amount directly to P2ID note (no vault debit, if > 0) + mem_load.AMT_REQUESTED_INFLIGHT dup push.0 neq + if.true + drop + + mem_load.P2ID_NOTE_IDX + # => [note_idx] + + mem_load.AMT_REQUESTED_INFLIGHT + mem_load.TOKEN_REQUESTED_ID_PREFIX + mem_load.TOKEN_REQUESTED_ID_SUFFIX + # => [suffix, prefix, amount, note_idx] + + exec.asset::create_fungible_asset + # => [ASSET_KEY, ASSET_VALUE, note_idx] + + exec.output_note::add_asset + # => [] + else + drop + end +end + +# REMAINDER NOTE CREATION PROCEDURE +# ================================================================================================= + +#! Creates a SWAPp remainder output note for partial fills. +#! +#! Updates the requested amount in note storage, builds a new remainder recipient +#! (using the active note's script root and a serial derived by incrementing the +#! top element of the active note's serial number), creates the output note, +#! sets the attachment, and adds the remaining offered asset. +#! +#! Inputs: [remaining_requested] +#! Outputs: [] +#! +proc create_remainder_note + # Update note storage with new requested amount + mem_store.REQUESTED_ASSET_VALUE_INPUT + # => [] + + # Build SWAPp remainder recipient using the same script as the active note + exec.active_note::get_script_root + # => [SCRIPT_ROOT] + + # Derive remainder serial: increment top element of active note's serial number + exec.active_note::get_serial_number add.1 + # => [SERIAL_NUM', SCRIPT_ROOT] + + # Build recipient from all 18 note storage items (now with updated requested amount) + push.18.0 + # => [storage_ptr=0, num_storage_items=18, SERIAL_NUM', SCRIPT_ROOT] + + exec.note::build_recipient + # => [RECIPIENT_SWAPP] + + mem_load.NOTE_TYPE + mem_load.SWAPP_TAG_INPUT + + exec.output_note::create + # => [note_idx] + + mem_store.SWAPP_NOTE_IDX + # => [] + + # Set attachment: aux = total offered_out amount + mem_load.AMT_OFFERED_OUT push.0.0.0 + # => [0, 0, 0, aux] + + push.0 mem_load.SWAPP_NOTE_IDX + # => [note_idx, attachment_scheme, ATTACHMENT] + + exec.output_note::set_word_attachment + # => [] + + # Add remaining offered asset to remainder note + # remainder_amount = total_offered - offered_out + mem_load.SWAPP_NOTE_IDX + # => [note_idx] + + mem_load.AMT_OFFERED mem_load.AMT_OFFERED_OUT sub + # => [remainder_amount, note_idx] + + mem_load.TOKEN_OFFERED_ID_PREFIX + mem_load.TOKEN_OFFERED_ID_SUFFIX + # => [suffix, prefix, remainder_amount, note_idx] + + exec.asset::create_fungible_asset + # => [ASSET_KEY, ASSET_VALUE, note_idx] + + exec.output_note::add_asset + # => [] +end + +#! Checks if the currently consuming account is the creator of the note. +#! +#! Compares the active account's ID against the creator ID stored in note storage. +#! Note storage must already be loaded to memory by the caller. +#! +#! Inputs: [] +#! Outputs: [is_creator] +#! +proc is_consumer_creator + exec.active_account::get_id + # => [acct_id_suffix, acct_id_prefix] + + mem_load.SWAPP_CREATOR_SUFFIX_INPUT mem_load.SWAPP_CREATOR_PREFIX_INPUT + # => [creator_prefix, creator_suffix, acct_id_suffix, acct_id_prefix] + + movup.3 eq + # => [prefix_eq, creator_suffix, acct_id_suffix] + + movdn.2 + # => [creator_suffix, acct_id_suffix, prefix_eq] + + eq + # => [suffix_eq, prefix_eq] + + and + # => [is_creator] +end + +#! Reclaims all assets from the note back to the creator's vault. +#! +#! Called when the consumer IS the creator (cancel/reclaim path). +#! +#! Inputs: [] +#! Outputs: [] +#! +proc handle_reclaim + push.OFFERED_ASSET_WORD exec.active_note::get_assets + # => [num_assets, dest_ptr] + drop drop + # => [] + + # Load asset from memory (KEY+VALUE format, 8 cells) + push.OFFERED_ASSET_WORD exec.asset::load + # => [ASSET_KEY, ASSET_VALUE] + + # Build 16-element call frame: [ASSET_KEY, ASSET_VALUE, pad(8)] + padw padw swapdw + # => [ASSET_KEY, ASSET_VALUE, pad(8)] + + call.wallet::receive_asset + # => [pad(16)] + + dropw dropw dropw dropw + # => [] +end + +# PSWAP EXECUTION +# ================================================================================================= +# +# Executes the partially-fillable swap. Sends offered tokens to consumer, requested tokens +# to creator via P2ID, and creates a remainder note if partially filled. +# +# Note args (Word[0] on top after mem_loadw_le): +# Word[0] = input_amount: debited from consumer's vault +# Word[1] = inflight_amount: added directly to P2ID note (no vault debit) +# Word[2..3] = unused (0) +# + +# => [] +proc execute_pswap + # Load note assets to OFFERED_ASSET_WORD + push.OFFERED_ASSET_WORD exec.active_note::get_assets + # => [num_assets, asset_ptr] + + push.1 eq assert.err=ERR_PSWAP_WRONG_NUMBER_OF_ASSETS + # => [asset_ptr] + + drop + # => [] + + # Load offered asset from known address + push.OFFERED_ASSET_WORD exec.asset::load + # => [ASSET_KEY, ASSET_VALUE] + + # Extract offered amount + exec.asset::fungible_to_amount + # => [amount, ASSET_KEY, ASSET_VALUE] + + mem_store.AMT_OFFERED + # => [ASSET_KEY, ASSET_VALUE] + + # Extract offered faucet IDs from key + exec.asset::key_into_faucet_id + # => [faucet_id_suffix, faucet_id_prefix, ASSET_VALUE] + mem_store.TOKEN_OFFERED_ID_SUFFIX + mem_store.TOKEN_OFFERED_ID_PREFIX + # => [ASSET_VALUE] + dropw + # => [] + + # Load requested asset from note storage (ASSET_KEY + ASSET_VALUE, 8 cells) + push.REQUESTED_ASSET_WORD_INPUT exec.asset::load + # => [ASSET_KEY, ASSET_VALUE] + + # Extract requested amount + exec.asset::fungible_to_amount + # => [amount, ASSET_KEY, ASSET_VALUE] + + mem_store.AMT_REQUESTED + # => [ASSET_KEY, ASSET_VALUE] + + # Extract requested faucet IDs from key + exec.asset::key_into_faucet_id + # => [faucet_id_suffix, faucet_id_prefix, ASSET_VALUE] + mem_store.TOKEN_REQUESTED_ID_SUFFIX + mem_store.TOKEN_REQUESTED_ID_PREFIX + # => [ASSET_VALUE] + dropw + # => [] + + # Calculate offered_out for input_amount + mem_load.AMT_REQUESTED_IN + mem_load.AMT_REQUESTED + mem_load.AMT_OFFERED + # => [offered, requested, input_amount] + exec.calculate_tokens_offered_for_requested + # => [input_offered_out] + + mem_store.AMT_OFFERED_OUT_INPUT + # => [] + + # Calculate offered_out for inflight_amount + mem_load.AMT_REQUESTED_INFLIGHT + mem_load.AMT_REQUESTED + mem_load.AMT_OFFERED + # => [offered, requested, inflight_amount] + + exec.calculate_tokens_offered_for_requested + # => [inflight_offered_out] + mem_store.AMT_OFFERED_OUT_INFLIGHT + # => [] + + # total_offered_out = input_offered_out + inflight_offered_out + mem_load.AMT_OFFERED_OUT_INPUT mem_load.AMT_OFFERED_OUT_INFLIGHT add + # => [total_offered_out] + + mem_store.AMT_OFFERED_OUT + # => [] + + # Create P2ID note for creator + exec.create_p2id_note + # => [] + + # Consumer receives only input_offered_out into vault (not inflight portion) + padw padw + push.OFFERED_ASSET_WORD exec.asset::load + # => [ASSET_KEY, ASSET_VALUE, pad(8)] + # Replace amount (ASSET_VALUE[0]) with input_offered_out + movup.4 + drop + mem_load.AMT_OFFERED_OUT_INPUT + movdn.4 + # => [ASSET_KEY, ASSET_VALUE', pad(8)] + call.wallet::receive_asset + dropw dropw dropw dropw + # => [] + + # Check if partial fill: total_in < total_requested + mem_load.AMT_REQUESTED_IN mem_load.AMT_REQUESTED_INFLIGHT add + mem_load.AMT_REQUESTED + dup.1 dup.1 lt + + if.true + # remaining_requested = total_requested - total_in + swap sub + # => [remaining_requested] + + exec.create_remainder_note + else + drop drop + end + + exec.sys::truncate_stack +end + +@note_script +pub proc main + # => [NOTE_ARGS] + # Stack (top to bottom): [input_amount, inflight_amount, 0, 0] + # (Word[0] on top after mem_loadw_le in kernel prologue) + + mem_store.AMT_REQUESTED_IN + # => [inflight_amount, 0, 0] + + mem_store.AMT_REQUESTED_INFLIGHT + # => [0, 0] + drop drop + # => [] + + # Load all 18 note storage items to memory starting at address 0 + push.0 exec.active_note::get_storage + # => [num_storage_items, storage_ptr] + + eq.18 assert.err=ERR_PSWAP_WRONG_NUMBER_OF_INPUTS + # => [storage_ptr] + + drop + # => [] + + # Extract and store note_type from active note metadata + exec.extract_note_type + # => [] + + exec.is_consumer_creator + # => [is_creator] + + if.true + exec.handle_reclaim + else + exec.execute_pswap + end +end diff --git a/crates/miden-standards/src/note/mod.rs b/crates/miden-standards/src/note/mod.rs index 82b94c6a68..d61633c637 100644 --- a/crates/miden-standards/src/note/mod.rs +++ b/crates/miden-standards/src/note/mod.rs @@ -26,6 +26,9 @@ pub use p2id::{P2idNote, P2idNoteStorage}; mod p2ide; pub use p2ide::{P2ideNote, P2ideNoteStorage}; +mod pswap; +pub use pswap::{PswapNote, PswapParsedInputs}; + mod swap; pub use swap::SwapNote; @@ -46,6 +49,7 @@ pub enum StandardNote { P2ID, P2IDE, SWAP, + PSWAP, MINT, BURN, } @@ -72,6 +76,9 @@ impl StandardNote { if root == SwapNote::script_root() { return Some(Self::SWAP); } + if root == PswapNote::script_root() { + return Some(Self::PSWAP); + } if root == MintNote::script_root() { return Some(Self::MINT); } @@ -91,6 +98,7 @@ impl StandardNote { Self::P2ID => "P2ID", Self::P2IDE => "P2IDE", Self::SWAP => "SWAP", + Self::PSWAP => "PSWAP", Self::MINT => "MINT", Self::BURN => "BURN", } @@ -102,6 +110,7 @@ impl StandardNote { Self::P2ID => P2idNote::NUM_STORAGE_ITEMS, Self::P2IDE => P2ideNote::NUM_STORAGE_ITEMS, Self::SWAP => SwapNote::NUM_STORAGE_ITEMS, + Self::PSWAP => PswapNote::NUM_STORAGE_ITEMS, Self::MINT => MintNote::NUM_STORAGE_ITEMS_PRIVATE, Self::BURN => BurnNote::NUM_STORAGE_ITEMS, } @@ -113,6 +122,7 @@ impl StandardNote { Self::P2ID => P2idNote::script(), Self::P2IDE => P2ideNote::script(), Self::SWAP => SwapNote::script(), + Self::PSWAP => PswapNote::script(), Self::MINT => MintNote::script(), Self::BURN => BurnNote::script(), } @@ -124,6 +134,7 @@ impl StandardNote { Self::P2ID => P2idNote::script_root(), Self::P2IDE => P2ideNote::script_root(), Self::SWAP => SwapNote::script_root(), + Self::PSWAP => PswapNote::script_root(), Self::MINT => MintNote::script_root(), Self::BURN => BurnNote::script_root(), } @@ -143,9 +154,9 @@ impl StandardNote { // the provided account interface. interface_proc_digests.contains(&BasicWallet::receive_asset_digest()) }, - Self::SWAP => { - // To consume SWAP note, the `receive_asset` and `move_asset_to_note` procedures - // must be present in the provided account interface. + Self::SWAP | Self::PSWAP => { + // To consume SWAP/PSWAP notes, the `receive_asset` and `move_asset_to_note` + // procedures must be present in the provided account interface. interface_proc_digests.contains(&BasicWallet::receive_asset_digest()) && interface_proc_digests.contains(&BasicWallet::move_asset_to_note_digest()) }, diff --git a/crates/miden-standards/src/note/pswap.rs b/crates/miden-standards/src/note/pswap.rs new file mode 100644 index 0000000000..18ac3d1437 --- /dev/null +++ b/crates/miden-standards/src/note/pswap.rs @@ -0,0 +1,777 @@ +use alloc::vec; + +use miden_protocol::Hasher; +use miden_protocol::account::AccountId; +use miden_protocol::assembly::Path; +use miden_protocol::asset::{Asset, FungibleAsset}; +use miden_protocol::crypto::rand::FeltRng; +use miden_protocol::errors::NoteError; +use miden_protocol::note::{ + Note, NoteAssets, NoteAttachment, NoteAttachmentScheme, NoteMetadata, NoteRecipient, + NoteScript, NoteStorage, NoteTag, NoteType, +}; +use miden_protocol::utils::sync::LazyLock; +use miden_protocol::{Felt, Word, ZERO}; + +use crate::StandardsLib; +use crate::note::P2idNoteStorage; + +// NOTE SCRIPT +// ================================================================================================ + +/// Path to the PSWAP note script procedure in the standards library. +const PSWAP_SCRIPT_PATH: &str = "::miden::standards::notes::pswap::main"; + +// Initialize the PSWAP note script only once +static PSWAP_SCRIPT: LazyLock = LazyLock::new(|| { + let standards_lib = StandardsLib::default(); + let path = Path::new(PSWAP_SCRIPT_PATH); + NoteScript::from_library_reference(standards_lib.as_ref(), path) + .expect("Standards library contains PSWAP note script procedure") +}); + +// PSWAP NOTE +// ================================================================================================ + +/// Parsed PSWAP note storage fields. +pub struct PswapParsedInputs { + /// Requested asset key word [0-3] + pub requested_key: Word, + /// Requested asset value word [4-7] + pub requested_value: Word, + /// SWAPp note tag + pub swapp_tag: NoteTag, + /// P2ID routing tag + pub p2id_tag: NoteTag, + /// Current swap count + pub swap_count: u64, + /// Creator account ID + pub creator_account_id: AccountId, +} + +/// Partial swap (pswap) note for decentralized asset exchange. +/// +/// This note implements a partially-fillable swap mechanism where: +/// - Creator offers an asset and requests another asset +/// - Note can be partially or fully filled by consumers +/// - Unfilled portions create remainder notes +/// - Creator receives requested assets via P2ID notes +pub struct PswapNote; + +impl PswapNote { + // CONSTANTS + // -------------------------------------------------------------------------------------------- + + /// Expected number of storage items for the PSWAP note. + /// + /// Layout (18 Felts, matching pswap.masm memory addresses): + /// - [0-3]: ASSET_KEY (requested asset key from asset.to_key_word()) + /// - [4-7]: ASSET_VALUE (requested asset value from asset.to_value_word()) + /// - [8]: SWAPp tag + /// - [9]: P2ID routing tag + /// - [10-11]: Reserved (zero) + /// - [12]: Swap count + /// - [13-15]: Reserved (zero) + /// - [16]: Creator account ID prefix + /// - [17]: Creator account ID suffix + pub const NUM_STORAGE_ITEMS: usize = 18; + + // PUBLIC ACCESSORS + // -------------------------------------------------------------------------------------------- + + /// Returns the script of the PSWAP note. + pub fn script() -> NoteScript { + PSWAP_SCRIPT.clone() + } + + /// Returns the PSWAP note script root. + pub fn script_root() -> Word { + PSWAP_SCRIPT.root() + } + + // BUILDERS + // -------------------------------------------------------------------------------------------- + + /// Creates a PSWAP note offering one asset in exchange for another. + /// + /// # Arguments + /// + /// * `creator_account_id` - The account creating the swap offer + /// * `offered_asset` - The asset being offered (will be locked in the note) + /// * `requested_asset` - The asset being requested in exchange + /// * `note_type` - Whether the note is public or private + /// * `note_attachment` - Attachment data for the note + /// * `rng` - Random number generator for serial number + /// + /// # Returns + /// + /// Returns a `Note` that can be consumed by anyone willing to provide the requested asset. + /// + /// # Errors + /// + /// Returns an error if: + /// - Assets are invalid or have the same faucet ID + /// - Note construction fails + pub fn create( + creator_account_id: AccountId, + offered_asset: Asset, + requested_asset: Asset, + note_type: NoteType, + note_attachment: NoteAttachment, + rng: &mut R, + ) -> Result { + if offered_asset.faucet_id().prefix() == requested_asset.faucet_id().prefix() { + return Err(NoteError::other( + "offered and requested assets must have different faucets", + )); + } + + let note_script = Self::script(); + + // Build note storage (18 items) using the ASSET_KEY + ASSET_VALUE format + let tag = Self::build_tag(note_type, &offered_asset, &requested_asset); + let swapp_tag_felt = Felt::new(u32::from(tag) as u64); + let p2id_tag_felt = Self::compute_p2id_tag_felt(creator_account_id); + + let key_word = requested_asset.to_key_word(); + let value_word = requested_asset.to_value_word(); + + let inputs = vec![ + // ASSET_KEY [0-3] + key_word[0], + key_word[1], + key_word[2], + key_word[3], + // ASSET_VALUE [4-7] + value_word[0], + value_word[1], + value_word[2], + value_word[3], + // Tags [8-9] + swapp_tag_felt, + p2id_tag_felt, + // Padding [10-11] + ZERO, + ZERO, + // Swap count [12-15] + ZERO, + ZERO, + ZERO, + ZERO, + // Creator ID [16-17] + creator_account_id.prefix().as_felt(), + creator_account_id.suffix(), + ]; + + let note_inputs = NoteStorage::new(inputs)?; + + // Generate serial number + let serial_num = rng.draw_word(); + + // Build the outgoing note + let metadata = NoteMetadata::new(creator_account_id, note_type) + .with_tag(tag) + .with_attachment(note_attachment); + + let assets = NoteAssets::new(vec![offered_asset])?; + let recipient = NoteRecipient::new(serial_num, note_script, note_inputs); + let note = Note::new(assets, metadata, recipient); + + Ok(note) + } + + /// Creates output notes when consuming a swap note (P2ID + optional remainder). + /// + /// Handles both full and partial fills: + /// - **Full fill**: Returns P2ID note with full requested amount, no remainder + /// - **Partial fill**: Returns P2ID note with partial amount + remainder swap note + /// + /// # Arguments + /// + /// * `original_swap_note` - The original swap note being consumed + /// * `consumer_account_id` - The account consuming the swap note + /// * `input_amount` - Amount debited from consumer's vault + /// * `inflight_amount` - Amount added directly (no vault debit, for cross-swaps) + /// + /// # Returns + /// + /// Returns a tuple of `(p2id_note, Option)` + pub fn create_output_notes( + original_swap_note: &Note, + consumer_account_id: AccountId, + input_amount: u64, + inflight_amount: u64, + ) -> Result<(Note, Option), NoteError> { + let inputs = original_swap_note.recipient().storage(); + let parsed = Self::parse_inputs(inputs.items())?; + let note_type = original_swap_note.metadata().note_type(); + + let fill_amount = input_amount + inflight_amount; + + // Reconstruct requested faucet ID from ASSET_KEY + let requested_faucet_id = Self::faucet_id_from_key(&parsed.requested_key)?; + let total_requested_amount = Self::amount_from_value(&parsed.requested_value); + + // Ensure offered asset exists and is fungible + let offered_assets = original_swap_note.assets(); + if offered_assets.num_assets() != 1 { + return Err(NoteError::other("Swap note must have exactly 1 offered asset")); + } + let offered_asset = + offered_assets.iter().next().ok_or(NoteError::other("No offered asset found"))?; + let (offered_faucet_id, total_offered_amount) = match offered_asset { + Asset::Fungible(fa) => (fa.faucet_id(), fa.amount()), + _ => return Err(NoteError::other("Non-fungible offered asset not supported")), + }; + + // Validate fill amount + if fill_amount == 0 { + return Err(NoteError::other("Fill amount must be greater than 0")); + } + if fill_amount > total_requested_amount { + return Err(NoteError::other(alloc::format!( + "Fill amount {} exceeds requested amount {}", + fill_amount, + total_requested_amount + ))); + } + + // Calculate proportional offered amount + let offered_amount_for_fill = Self::calculate_output_amount( + total_offered_amount, + total_requested_amount, + fill_amount, + ); + + // Build the P2ID asset + let payback_asset = + Asset::Fungible(FungibleAsset::new(requested_faucet_id, fill_amount).map_err(|e| { + NoteError::other(alloc::format!("Failed to create P2ID asset: {}", e)) + })?); + + let aux_word = Word::from([Felt::new(fill_amount), ZERO, ZERO, ZERO]); + + let p2id_note = Self::create_p2id_payback_note( + original_swap_note, + consumer_account_id, + payback_asset, + note_type, + parsed.p2id_tag, + aux_word, + )?; + + // Create remainder note if partial fill + let remainder_note = if fill_amount < total_requested_amount { + let remaining_offered = total_offered_amount - offered_amount_for_fill; + let remaining_requested = total_requested_amount - fill_amount; + + let remaining_offered_asset = + Asset::Fungible(FungibleAsset::new(offered_faucet_id, remaining_offered).map_err( + |e| NoteError::other(alloc::format!("Failed to create remainder asset: {}", e)), + )?); + + Some(Self::create_remainder_note( + original_swap_note, + consumer_account_id, + remaining_offered_asset, + remaining_requested, + offered_amount_for_fill, + )?) + } else { + None + }; + + Ok((p2id_note, remainder_note)) + } + + /// Creates a P2ID (Pay-to-ID) note for the swap creator as payback. + /// + /// Derives a unique serial number matching the MASM: `hmerge(swap_count_word, serial_num)`. + pub fn create_p2id_payback_note( + original_swap_note: &Note, + consumer_account_id: AccountId, + payback_asset: Asset, + note_type: NoteType, + p2id_tag: NoteTag, + aux_word: Word, + ) -> Result { + let inputs = original_swap_note.recipient().storage(); + let parsed = Self::parse_inputs(inputs.items())?; + + // Derive P2ID serial matching PSWAP.masm: + // hmerge([SWAP_COUNT_WORD (top), SERIAL_NUM (second)]) + // = Hasher::merge(&[swap_count_word, serial_num]) + // Word[0] = count+1, matching mem_loadw_le which puts mem[addr] into Word[0] + let swap_count_word = Word::from([Felt::new(parsed.swap_count + 1), ZERO, ZERO, ZERO]); + let original_serial = original_swap_note.recipient().serial_num(); + let p2id_serial_digest = Hasher::merge(&[swap_count_word.into(), original_serial.into()]); + let p2id_serial_num: Word = Word::from(p2id_serial_digest); + + // P2ID recipient targets the creator + let recipient = + P2idNoteStorage::new(parsed.creator_account_id).into_recipient(p2id_serial_num); + + let attachment = NoteAttachment::new_word(NoteAttachmentScheme::none(), aux_word); + + let p2id_assets = NoteAssets::new(vec![payback_asset])?; + let p2id_metadata = NoteMetadata::new(consumer_account_id, note_type) + .with_tag(p2id_tag) + .with_attachment(attachment); + + Ok(Note::new(p2id_assets, p2id_metadata, recipient)) + } + + /// Creates a remainder note for partial fills. + /// + /// Builds updated note storage with the remaining requested amount and incremented + /// swap count, using the ASSET_KEY + ASSET_VALUE format (18 items). + pub fn create_remainder_note( + original_swap_note: &Note, + consumer_account_id: AccountId, + remaining_offered_asset: Asset, + remaining_requested_amount: u64, + offered_amount_for_fill: u64, + ) -> Result { + let original_inputs = original_swap_note.recipient().storage(); + let parsed = Self::parse_inputs(original_inputs.items())?; + let note_type = original_swap_note.metadata().note_type(); + + // Build new requested asset with updated amount + let requested_faucet_id = Self::faucet_id_from_key(&parsed.requested_key)?; + let remaining_requested_asset = Asset::Fungible( + FungibleAsset::new(requested_faucet_id, remaining_requested_amount).map_err(|e| { + NoteError::other(alloc::format!( + "Failed to create remaining requested asset: {}", + e + )) + })?, + ); + + // Build new storage with updated amounts (18 items) + let key_word = remaining_requested_asset.to_key_word(); + let value_word = remaining_requested_asset.to_value_word(); + + let inputs = vec![ + // ASSET_KEY [0-3] + key_word[0], + key_word[1], + key_word[2], + key_word[3], + // ASSET_VALUE [4-7] + value_word[0], + value_word[1], + value_word[2], + value_word[3], + // Tags [8-9] (preserved) + Felt::new(u32::from(parsed.swapp_tag) as u64), + Felt::new(u32::from(parsed.p2id_tag) as u64), + // Padding [10-11] + ZERO, + ZERO, + // Swap count [12-15] (incremented) + Felt::new(parsed.swap_count + 1), + ZERO, + ZERO, + ZERO, + // Creator ID [16-17] (preserved) + parsed.creator_account_id.prefix().as_felt(), + parsed.creator_account_id.suffix(), + ]; + + let note_inputs = NoteStorage::new(inputs)?; + + // Remainder serial: increment top element of serial (matching MASM add.1 on Word[0]) + let original_serial = original_swap_note.recipient().serial_num(); + let remainder_serial_num = Word::from([ + Felt::new(original_serial[0].as_canonical_u64() + 1), + original_serial[1], + original_serial[2], + original_serial[3], + ]); + + let note_script = Self::script(); + let recipient = NoteRecipient::new(remainder_serial_num, note_script, note_inputs); + + // Build tag for the remainder note + let tag = Self::build_tag( + note_type, + &remaining_offered_asset, + &Asset::from(remaining_requested_asset), + ); + + let aux_word = Word::from([Felt::new(offered_amount_for_fill), ZERO, ZERO, ZERO]); + let attachment = NoteAttachment::new_word(NoteAttachmentScheme::none(), aux_word); + + let metadata = NoteMetadata::new(consumer_account_id, note_type) + .with_tag(tag) + .with_attachment(attachment); + + let assets = NoteAssets::new(vec![remaining_offered_asset])?; + Ok(Note::new(assets, metadata, recipient)) + } + + // TAG CONSTRUCTION + // -------------------------------------------------------------------------------------------- + + /// Returns a note tag for a pswap note with the specified parameters. + /// + /// Layout: + /// ```text + /// [ note_type (2 bits) | script_root (14 bits) + /// | offered_asset_faucet_id (8 bits) | requested_asset_faucet_id (8 bits) ] + /// ``` + pub fn build_tag( + note_type: NoteType, + offered_asset: &Asset, + requested_asset: &Asset, + ) -> NoteTag { + let pswap_root_bytes = Self::script().root().as_bytes(); + + // Construct the pswap use case ID from the 14 most significant bits of the script root. + // This leaves the two most significant bits zero. + let mut pswap_use_case_id = (pswap_root_bytes[0] as u16) << 6; + pswap_use_case_id |= (pswap_root_bytes[1] >> 2) as u16; + + // Get bits 0..8 from the faucet IDs of both assets which will form the tag payload. + let offered_asset_id: u64 = offered_asset.faucet_id().prefix().into(); + let offered_asset_tag = (offered_asset_id >> 56) as u8; + + let requested_asset_id: u64 = requested_asset.faucet_id().prefix().into(); + let requested_asset_tag = (requested_asset_id >> 56) as u8; + + let asset_pair = ((offered_asset_tag as u16) << 8) | (requested_asset_tag as u16); + + let tag = ((note_type as u8 as u32) << 30) + | ((pswap_use_case_id as u32) << 16) + | asset_pair as u32; + + NoteTag::new(tag) + } + + // HELPER FUNCTIONS + // -------------------------------------------------------------------------------------------- + + /// Computes the P2ID tag for routing payback notes to the creator. + fn compute_p2id_tag_felt(account_id: AccountId) -> Felt { + let p2id_tag = NoteTag::with_account_target(account_id); + Felt::new(u32::from(p2id_tag) as u64) + } + + /// Extracts the faucet ID from an ASSET_KEY word. + fn faucet_id_from_key(key: &Word) -> Result { + // asset::key_into_faucet_id extracts [suffix, prefix] from the key. + // Key layout: [key[0], key[1], faucet_suffix, faucet_prefix] + // key[2] = suffix, key[3] = prefix (after key_into_faucet_id drops top 2) + AccountId::try_from_elements(key[2], key[3]).map_err(|e| { + NoteError::other(alloc::format!("Failed to parse faucet ID from key: {}", e)) + }) + } + + /// Extracts the amount from an ASSET_VALUE word. + fn amount_from_value(value: &Word) -> u64 { + // ASSET_VALUE[0] = amount (from asset::fungible_to_amount) + value[0].as_canonical_u64() + } + + // PARSING FUNCTIONS + // -------------------------------------------------------------------------------------------- + + /// Parses note storage items to extract swap parameters. + /// + /// # Arguments + /// + /// * `inputs` - The note storage items (must be exactly 18 Felts) + /// + /// # Errors + /// + /// Returns an error if input length is not 18 or account ID construction fails. + pub fn parse_inputs(inputs: &[Felt]) -> Result { + if inputs.len() != Self::NUM_STORAGE_ITEMS { + return Err(NoteError::other(alloc::format!( + "PSWAP note should have {} storage items, but {} were provided", + Self::NUM_STORAGE_ITEMS, + inputs.len() + ))); + } + + let requested_key = Word::from([inputs[0], inputs[1], inputs[2], inputs[3]]); + let requested_value = Word::from([inputs[4], inputs[5], inputs[6], inputs[7]]); + let swapp_tag = NoteTag::new(inputs[8].as_canonical_u64() as u32); + let p2id_tag = NoteTag::new(inputs[9].as_canonical_u64() as u32); + let swap_count = inputs[12].as_canonical_u64(); + + let creator_account_id = + AccountId::try_from_elements(inputs[17], inputs[16]).map_err(|e| { + NoteError::other(alloc::format!("Failed to parse creator account ID: {}", e)) + })?; + + Ok(PswapParsedInputs { + requested_key, + requested_value, + swapp_tag, + p2id_tag, + swap_count, + creator_account_id, + }) + } + + /// Extracts the requested asset from note storage. + pub fn get_requested_asset(inputs: &[Felt]) -> Result { + let parsed = Self::parse_inputs(inputs)?; + let faucet_id = Self::faucet_id_from_key(&parsed.requested_key)?; + let amount = Self::amount_from_value(&parsed.requested_value); + Ok(Asset::Fungible(FungibleAsset::new(faucet_id, amount).map_err(|e| { + NoteError::other(alloc::format!("Failed to create asset: {}", e)) + })?)) + } + + /// Extracts the creator account ID from note storage. + pub fn get_creator_account_id(inputs: &[Felt]) -> Result { + Ok(Self::parse_inputs(inputs)?.creator_account_id) + } + + /// Checks if the given account is the creator of this swap note. + pub fn is_creator(inputs: &[Felt], account_id: AccountId) -> Result { + let creator_id = Self::get_creator_account_id(inputs)?; + Ok(creator_id == account_id) + } + + /// Calculates the output amount for a fill using u64 integer arithmetic + /// with a precision factor of 1e5 (matching the MASM on-chain calculation). + pub fn calculate_output_amount( + offered_total: u64, + requested_total: u64, + input_amount: u64, + ) -> u64 { + const PRECISION_FACTOR: u64 = 100_000; + + if requested_total == input_amount { + return offered_total; + } + + if offered_total > requested_total { + let ratio = (offered_total * PRECISION_FACTOR) / requested_total; + (input_amount * ratio) / PRECISION_FACTOR + } else { + let ratio = (requested_total * PRECISION_FACTOR) / offered_total; + (input_amount * PRECISION_FACTOR) / ratio + } + } + + /// Calculates how many offered tokens a consumer receives for a given requested input, + /// reading the offered and requested totals directly from the swap note. + /// + /// This is the Rust equivalent of `calculate_tokens_offered_for_requested` in pswap.masm. + /// + /// # Arguments + /// + /// * `swap_note` - The PSWAP note being consumed + /// * `input_amount` - Amount of requested asset the consumer is providing + /// + /// # Returns + /// + /// The proportional amount of offered asset the consumer will receive. + /// + /// # Errors + /// + /// Returns an error if the note storage cannot be parsed or the offered asset is invalid. + pub fn calculate_offered_for_requested( + swap_note: &Note, + input_amount: u64, + ) -> Result { + let parsed = Self::parse_inputs(swap_note.recipient().storage().items())?; + let total_requested = Self::amount_from_value(&parsed.requested_value); + + let offered_asset = swap_note + .assets() + .iter() + .next() + .ok_or(NoteError::other("No offered asset found"))?; + let total_offered = match offered_asset { + Asset::Fungible(fa) => fa.amount(), + _ => return Err(NoteError::other("Non-fungible offered asset not supported")), + }; + + Ok(Self::calculate_output_amount(total_offered, total_requested, input_amount)) + } +} + +// TESTS +// ================================================================================================ + +#[cfg(test)] +mod tests { + use miden_protocol::account::{AccountId, AccountIdVersion, AccountStorageMode, AccountType}; + use miden_protocol::asset::FungibleAsset; + + use super::*; + + #[test] + fn pswap_note_creation_and_script() { + let mut offered_faucet_bytes = [0; 15]; + offered_faucet_bytes[0] = 0xaa; + + let mut requested_faucet_bytes = [0; 15]; + requested_faucet_bytes[0] = 0xbb; + + let offered_faucet_id = AccountId::dummy( + offered_faucet_bytes, + AccountIdVersion::Version0, + AccountType::FungibleFaucet, + AccountStorageMode::Public, + ); + + let requested_faucet_id = AccountId::dummy( + requested_faucet_bytes, + AccountIdVersion::Version0, + AccountType::FungibleFaucet, + AccountStorageMode::Public, + ); + + let creator_id = AccountId::dummy( + [1; 15], + AccountIdVersion::Version0, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Public, + ); + + let offered_asset = Asset::Fungible(FungibleAsset::new(offered_faucet_id, 1000).unwrap()); + let requested_asset = + Asset::Fungible(FungibleAsset::new(requested_faucet_id, 500).unwrap()); + + use miden_protocol::crypto::rand::RpoRandomCoin; + let mut rng = RpoRandomCoin::new(Word::default()); + + let script = PswapNote::script(); + assert!(script.root() != Word::default(), "Script root should not be zero"); + + let note = PswapNote::create( + creator_id, + offered_asset, + requested_asset, + NoteType::Public, + NoteAttachment::default(), + &mut rng, + ); + + assert!(note.is_ok(), "Note creation should succeed"); + let note = note.unwrap(); + + assert_eq!(note.metadata().sender(), creator_id); + assert_eq!(note.metadata().note_type(), NoteType::Public); + assert_eq!(note.assets().num_assets(), 1); + assert_eq!(note.recipient().script().root(), script.root()); + + // Verify storage has 18 items + assert_eq!(note.recipient().storage().num_items(), PswapNote::NUM_STORAGE_ITEMS as u16,); + } + + #[test] + fn pswap_tag() { + let mut offered_faucet_bytes = [0; 15]; + offered_faucet_bytes[0] = 0xcd; + offered_faucet_bytes[1] = 0xb1; + + let mut requested_faucet_bytes = [0; 15]; + requested_faucet_bytes[0] = 0xab; + requested_faucet_bytes[1] = 0xec; + + let offered_asset = Asset::Fungible( + FungibleAsset::new( + AccountId::dummy( + offered_faucet_bytes, + AccountIdVersion::Version0, + AccountType::FungibleFaucet, + AccountStorageMode::Public, + ), + 100, + ) + .unwrap(), + ); + let requested_asset = Asset::Fungible( + FungibleAsset::new( + AccountId::dummy( + requested_faucet_bytes, + AccountIdVersion::Version0, + AccountType::FungibleFaucet, + AccountStorageMode::Public, + ), + 200, + ) + .unwrap(), + ); + + let tag = PswapNote::build_tag(NoteType::Public, &offered_asset, &requested_asset); + let tag_u32 = u32::from(tag); + + // Verify note_type bits (top 2 bits should be 10 for Public) + let note_type_bits = tag_u32 >> 30; + assert_eq!(note_type_bits, NoteType::Public as u32); + } + + #[test] + fn calculate_output_amount() { + // Equal ratio + assert_eq!(PswapNote::calculate_output_amount(100, 100, 50), 50); + + // 2:1 ratio + assert_eq!(PswapNote::calculate_output_amount(200, 100, 50), 100); + + // 1:2 ratio + assert_eq!(PswapNote::calculate_output_amount(100, 200, 50), 25); + + // Non-integer ratio (100/73) + let result = PswapNote::calculate_output_amount(100, 73, 7); + assert!(result > 0, "Should produce non-zero output"); + } + + #[test] + fn parse_inputs_v014_format() { + let creator_id = AccountId::dummy( + [1; 15], + AccountIdVersion::Version0, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Public, + ); + + let faucet_id = AccountId::dummy( + [0xaa; 15], + AccountIdVersion::Version0, + AccountType::FungibleFaucet, + AccountStorageMode::Public, + ); + + let asset = Asset::Fungible(FungibleAsset::new(faucet_id, 500).unwrap()); + let key_word = asset.to_key_word(); + let value_word = asset.to_value_word(); + + let inputs = vec![ + key_word[0], + key_word[1], + key_word[2], + key_word[3], + value_word[0], + value_word[1], + value_word[2], + value_word[3], + Felt::new(0xC0000000), // swapp_tag + Felt::new(0x80000001), // p2id_tag + ZERO, + ZERO, + Felt::new(3), // swap_count + ZERO, + ZERO, + ZERO, + creator_id.prefix().as_felt(), + creator_id.suffix(), + ]; + + let parsed = PswapNote::parse_inputs(&inputs).unwrap(); + assert_eq!(parsed.swap_count, 3); + assert_eq!(parsed.creator_account_id, creator_id); + assert_eq!( + parsed.requested_key, + Word::from([key_word[0], key_word[1], key_word[2], key_word[3]]) + ); + } +} diff --git a/crates/miden-testing/tests/scripts/mod.rs b/crates/miden-testing/tests/scripts/mod.rs index 8d15402744..9b8c3e12e5 100644 --- a/crates/miden-testing/tests/scripts/mod.rs +++ b/crates/miden-testing/tests/scripts/mod.rs @@ -3,5 +3,6 @@ mod fee; mod ownable2step; mod p2id; mod p2ide; +mod pswap; mod send_note; mod swap; diff --git a/crates/miden-testing/tests/scripts/pswap.rs b/crates/miden-testing/tests/scripts/pswap.rs new file mode 100644 index 0000000000..8a8d664070 --- /dev/null +++ b/crates/miden-testing/tests/scripts/pswap.rs @@ -0,0 +1,1481 @@ +use std::collections::BTreeMap; + +use miden_protocol::account::auth::AuthScheme; +use miden_protocol::account::AccountId; +use miden_protocol::asset::{Asset, FungibleAsset}; +use miden_protocol::note::{ + Note, NoteAssets, NoteAttachment, NoteMetadata, NoteRecipient, NoteStorage, NoteTag, NoteType, +}; +use miden_protocol::transaction::RawOutputNote; +use miden_protocol::{Felt, Word, ZERO}; +use miden_standards::note::PswapNote; +use miden_testing::{Auth, MockChain}; + +use crate::prove_and_verify_transaction; + +// CONSTANTS +// ================================================================================================ + +const BASIC_AUTH: Auth = Auth::BasicAuth { + auth_scheme: AuthScheme::Falcon512Poseidon2, +}; + +// HELPER FUNCTIONS +// ================================================================================================ + +/// Compute the P2ID tag for a local account +fn compute_p2id_tag_for_local_account(account_id: AccountId) -> NoteTag { + NoteTag::with_account_target(account_id) +} + +/// Helper function to compute P2ID tag as Felt for use in note storage +fn compute_p2id_tag_felt(account_id: AccountId) -> Felt { + let p2id_tag = compute_p2id_tag_for_local_account(account_id); + Felt::new(u32::from(p2id_tag) as u64) +} + +/// Create a PSWAP note via PswapNote::create. +fn create_pswap_note( + sender_id: AccountId, + note_assets: NoteAssets, + storage_items: Vec, + _note_tag: NoteTag, +) -> Note { + create_pswap_note_with_type(sender_id, note_assets, storage_items, _note_tag, NoteType::Public) +} + +/// Create a PSWAP note with specified note type via PswapNote::create. +fn create_pswap_note_with_type( + sender_id: AccountId, + note_assets: NoteAssets, + storage_items: Vec, + _note_tag: NoteTag, + note_type: NoteType, +) -> Note { + let offered_asset = *note_assets.iter().next().expect("must have offered asset"); + let requested_asset = PswapNote::get_requested_asset(&storage_items) + .expect("Failed to parse requested asset from storage"); + + use miden_protocol::crypto::rand::RpoRandomCoin; + let mut rng = RpoRandomCoin::new(Word::default()); + + PswapNote::create( + sender_id, + offered_asset, + requested_asset, + note_type, + NoteAttachment::default(), + &mut rng, + ) + .expect("Failed to create PSWAP note") +} + +/// Delegates to PswapNote::calculate_output_amount. +fn calculate_output_amount(offered_total: u64, requested_total: u64, input_amount: u64) -> u64 { + PswapNote::calculate_output_amount(offered_total, requested_total, input_amount) +} + +/// Build 18-item storage vector for a PSWAP note (KEY+VALUE format). +/// Kept for tests that construct notes with custom serials (chained fills). +fn build_pswap_storage( + requested_faucet_id: AccountId, + requested_amount: u64, + _swapp_tag_felt: Felt, + _p2id_tag_felt: Felt, + swap_count: u64, + creator_id: AccountId, +) -> Vec { + let requested_asset = Asset::Fungible( + FungibleAsset::new(requested_faucet_id, requested_amount) + .expect("Failed to create requested fungible asset"), + ); + let offered_dummy = Asset::Fungible( + FungibleAsset::new(requested_faucet_id, 1).expect("dummy offered asset"), + ); + let key_word = requested_asset.to_key_word(); + let value_word = requested_asset.to_value_word(); + let tag = PswapNote::build_tag(NoteType::Public, &offered_dummy, &requested_asset); + let swapp_tag_felt = Felt::new(u32::from(tag) as u64); + let p2id_tag = NoteTag::with_account_target(creator_id); + let p2id_tag_felt = Felt::new(u32::from(p2id_tag) as u64); + + vec![ + key_word[0], key_word[1], key_word[2], key_word[3], + value_word[0], value_word[1], value_word[2], value_word[3], + swapp_tag_felt, p2id_tag_felt, + ZERO, ZERO, + Felt::new(swap_count), ZERO, ZERO, ZERO, + creator_id.prefix().as_felt(), creator_id.suffix(), + ] +} + +/// Create expected P2ID note via PswapNote::create_p2id_payback_note. +fn create_expected_pswap_p2id_note( + swap_note: &Note, + consumer_id: AccountId, + _creator_id: AccountId, + _swap_count: u64, + total_fill: u64, + requested_faucet_id: AccountId, + p2id_tag: NoteTag, +) -> anyhow::Result { + let note_type = swap_note.metadata().note_type(); + create_expected_pswap_p2id_note_with_type( + swap_note, + consumer_id, + _creator_id, + _swap_count, + total_fill, + requested_faucet_id, + p2id_tag, + note_type, + ) +} + +/// Create expected P2ID note with explicit note type via PswapNote::create_p2id_payback_note. +fn create_expected_pswap_p2id_note_with_type( + swap_note: &Note, + consumer_id: AccountId, + _creator_id: AccountId, + _swap_count: u64, + total_fill: u64, + requested_faucet_id: AccountId, + p2id_tag: NoteTag, + note_type: NoteType, +) -> anyhow::Result { + let payback_asset = Asset::Fungible(FungibleAsset::new(requested_faucet_id, total_fill)?); + let aux_word = Word::from([Felt::new(total_fill), ZERO, ZERO, ZERO]); + + Ok(PswapNote::create_p2id_payback_note( + swap_note, consumer_id, payback_asset, note_type, p2id_tag, aux_word, + )?) +} + +/// Create NoteAssets with a single fungible asset +fn make_note_assets(faucet_id: AccountId, amount: u64) -> anyhow::Result { + let asset = FungibleAsset::new(faucet_id, amount)?; + Ok(NoteAssets::new(vec![asset.into()])?) +} + +/// Create a dummy SWAPp tag and its Felt representation. +/// Kept for backward compatibility with test call sites. +fn make_swapp_tag() -> (NoteTag, Felt) { + let tag = NoteTag::new(0xC0000000); + let felt = Felt::new(u32::from(tag) as u64); + (tag, felt) +} + +/// Build note args Word from input and inflight amounts. +/// LE stack orientation: Word[0] = input_amount (on top), Word[1] = inflight_amount +fn make_note_args(input_amount: u64, inflight_amount: u64) -> Word { + Word::from([ + Felt::new(input_amount), + Felt::new(inflight_amount), + ZERO, + ZERO, + ]) +} + +/// Create expected remainder note via PswapNote::create_remainder_note. +fn create_expected_pswap_remainder_note( + swap_note: &Note, + consumer_id: AccountId, + _creator_id: AccountId, + remaining_offered: u64, + remaining_requested: u64, + offered_out: u64, + _swap_count: u64, + offered_faucet_id: AccountId, + _requested_faucet_id: AccountId, + _swapp_tag: NoteTag, + _swapp_tag_felt: Felt, + _p2id_tag_felt: Felt, +) -> anyhow::Result { + let remaining_offered_asset = + Asset::Fungible(FungibleAsset::new(offered_faucet_id, remaining_offered)?); + + Ok(PswapNote::create_remainder_note( + swap_note, + consumer_id, + remaining_offered_asset, + remaining_requested, + offered_out, + )?) +} + +// TESTS +// ================================================================================================ + +#[tokio::test] +async fn pswap_note_full_fill_test() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + + let usdc_faucet = + builder.add_existing_basic_faucet(BASIC_AUTH, "USDC", 1000, Some(150))?; + let eth_faucet = builder.add_existing_basic_faucet(BASIC_AUTH, "ETH", 1000, Some(50))?; + + let alice = builder.add_existing_wallet_with_assets( + BASIC_AUTH, + [FungibleAsset::new(usdc_faucet.id(), 50)?.into()], + )?; + let bob = builder.add_existing_wallet_with_assets( + BASIC_AUTH, + [FungibleAsset::new(eth_faucet.id(), 25)?.into()], + )?; + + let (swapp_tag, swapp_tag_felt) = make_swapp_tag(); + let p2id_tag_felt = compute_p2id_tag_felt(alice.id()); + + let storage_items = build_pswap_storage( + eth_faucet.id(), + 25, + swapp_tag_felt, + p2id_tag_felt, + 0, + alice.id(), + ); + let note_assets = make_note_assets(usdc_faucet.id(), 50)?; + let swap_note = create_pswap_note(alice.id(), note_assets, storage_items, swapp_tag); + builder.add_output_note(RawOutputNote::Full(swap_note.clone())); + + let mut mock_chain = builder.build()?; + + let mut note_args_map = BTreeMap::new(); + note_args_map.insert(swap_note.id(), make_note_args(25, 0)); + + let p2id_note = create_expected_pswap_p2id_note( + &swap_note, + bob.id(), + alice.id(), + 0, + 25, + eth_faucet.id(), + compute_p2id_tag_for_local_account(alice.id()), + )?; + + let tx_context = mock_chain + .build_tx_context(bob.id(), &[swap_note.id()], &[])? + .extend_note_args(note_args_map) + .extend_expected_output_notes(vec![RawOutputNote::Full(p2id_note.clone())]) + .build()?; + + let executed_transaction = tx_context.execute().await?; + + // Verify: 1 P2ID note with 25 ETH + let output_notes = executed_transaction.output_notes(); + assert_eq!(output_notes.num_notes(), 1, "Expected exactly 1 P2ID note"); + + let actual_recipient = output_notes.get_note(0).recipient_digest(); + let expected_recipient = p2id_note.recipient().digest(); + assert_eq!(actual_recipient, expected_recipient, "RECIPIENT MISMATCH!"); + + let p2id_assets = output_notes.get_note(0).assets(); + assert_eq!(p2id_assets.num_assets(), 1); + if let Asset::Fungible(f) = p2id_assets.iter().next().unwrap() { + assert_eq!(f.faucet_id(), eth_faucet.id()); + assert_eq!(f.amount(), 25); + } else { + panic!("Expected fungible asset in P2ID note"); + } + + // Verify Bob's vault delta: +50 USDC, -25 ETH + let vault_delta = executed_transaction.account_delta().vault(); + let added: Vec = vault_delta.added_assets().collect(); + let removed: Vec = vault_delta.removed_assets().collect(); + + assert_eq!(added.len(), 1); + assert_eq!(removed.len(), 1); + if let Asset::Fungible(f) = &added[0] { + assert_eq!(f.faucet_id(), usdc_faucet.id()); + assert_eq!(f.amount(), 50); + } + if let Asset::Fungible(f) = &removed[0] { + assert_eq!(f.faucet_id(), eth_faucet.id()); + assert_eq!(f.amount(), 25); + } + + mock_chain.add_pending_executed_transaction(&executed_transaction)?; + let _ = mock_chain.prove_next_block(); + + Ok(()) +} + +#[tokio::test] +async fn pswap_note_private_full_fill_test() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + + let usdc_faucet = + builder.add_existing_basic_faucet(BASIC_AUTH, "USDC", 1000, Some(150))?; + let eth_faucet = builder.add_existing_basic_faucet(BASIC_AUTH, "ETH", 1000, Some(50))?; + + let alice = builder.add_existing_wallet_with_assets( + BASIC_AUTH, + [FungibleAsset::new(usdc_faucet.id(), 50)?.into()], + )?; + let bob = builder.add_existing_wallet_with_assets( + BASIC_AUTH, + [FungibleAsset::new(eth_faucet.id(), 25)?.into()], + )?; + + let (swapp_tag, swapp_tag_felt) = make_swapp_tag(); + let p2id_tag_felt = compute_p2id_tag_felt(alice.id()); + + let storage_items = build_pswap_storage( + eth_faucet.id(), + 25, + swapp_tag_felt, + p2id_tag_felt, + 0, + alice.id(), + ); + let note_assets = make_note_assets(usdc_faucet.id(), 50)?; + + // Create a PRIVATE swap note (output notes should also be Private) + let swap_note = create_pswap_note_with_type( + alice.id(), + note_assets, + storage_items, + swapp_tag, + NoteType::Private, + ); + builder.add_output_note(RawOutputNote::Full(swap_note.clone())); + + let mut mock_chain = builder.build()?; + + let mut note_args_map = BTreeMap::new(); + note_args_map.insert(swap_note.id(), make_note_args(25, 0)); + + // Expected P2ID note should inherit Private type from swap note + let p2id_note = create_expected_pswap_p2id_note_with_type( + &swap_note, + bob.id(), + alice.id(), + 0, + 25, + eth_faucet.id(), + compute_p2id_tag_for_local_account(alice.id()), + NoteType::Private, + )?; + + let tx_context = mock_chain + .build_tx_context(bob.id(), &[swap_note.id()], &[])? + .extend_note_args(note_args_map) + .extend_expected_output_notes(vec![RawOutputNote::Full(p2id_note)]) + .build()?; + + let executed_transaction = tx_context.execute().await?; + + // Verify: 1 P2ID note with 25 ETH + let output_notes = executed_transaction.output_notes(); + assert_eq!(output_notes.num_notes(), 1, "Expected exactly 1 P2ID note"); + + let p2id_assets = output_notes.get_note(0).assets(); + assert_eq!(p2id_assets.num_assets(), 1); + if let Asset::Fungible(f) = p2id_assets.iter().next().unwrap() { + assert_eq!(f.faucet_id(), eth_faucet.id()); + assert_eq!(f.amount(), 25); + } else { + panic!("Expected fungible asset in P2ID note"); + } + + // Verify Bob's vault delta: +50 USDC, -25 ETH + let vault_delta = executed_transaction.account_delta().vault(); + let added: Vec = vault_delta.added_assets().collect(); + let removed: Vec = vault_delta.removed_assets().collect(); + + assert_eq!(added.len(), 1); + assert_eq!(removed.len(), 1); + if let Asset::Fungible(f) = &added[0] { + assert_eq!(f.faucet_id(), usdc_faucet.id()); + assert_eq!(f.amount(), 50); + } + if let Asset::Fungible(f) = &removed[0] { + assert_eq!(f.faucet_id(), eth_faucet.id()); + assert_eq!(f.amount(), 25); + } + + mock_chain.add_pending_executed_transaction(&executed_transaction)?; + let _ = mock_chain.prove_next_block(); + + Ok(()) +} + +#[tokio::test] +async fn pswap_note_partial_fill_test() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + + let usdc_faucet = + builder.add_existing_basic_faucet(BASIC_AUTH, "USDC", 1000, Some(150))?; + let eth_faucet = builder.add_existing_basic_faucet(BASIC_AUTH, "ETH", 1000, Some(50))?; + + let alice = builder.add_existing_wallet_with_assets( + BASIC_AUTH, + [FungibleAsset::new(usdc_faucet.id(), 50)?.into()], + )?; + let bob = builder.add_existing_wallet_with_assets( + BASIC_AUTH, + [FungibleAsset::new(eth_faucet.id(), 20)?.into()], + )?; + + let (swapp_tag, swapp_tag_felt) = make_swapp_tag(); + let p2id_tag_felt = compute_p2id_tag_felt(alice.id()); + + let storage_items = build_pswap_storage( + eth_faucet.id(), + 25, + swapp_tag_felt, + p2id_tag_felt, + 0, + alice.id(), + ); + let note_assets = make_note_assets(usdc_faucet.id(), 50)?; + let swap_note = create_pswap_note(alice.id(), note_assets, storage_items, swapp_tag); + builder.add_output_note(RawOutputNote::Full(swap_note.clone())); + + let mut mock_chain = builder.build()?; + + let mut note_args_map = BTreeMap::new(); + note_args_map.insert(swap_note.id(), make_note_args(20, 0)); + + // Expected P2ID note: 20 ETH for Alice + let p2id_note = create_expected_pswap_p2id_note( + &swap_note, + bob.id(), + alice.id(), + 0, + 20, + eth_faucet.id(), + compute_p2id_tag_for_local_account(alice.id()), + )?; + + // Expected SWAPp remainder: 10 USDC for 5 ETH (offered_out=40, remaining=50-40=10) + let remainder_note = create_expected_pswap_remainder_note( + &swap_note, + bob.id(), + alice.id(), + 10, + 5, + 40, + 0, + usdc_faucet.id(), + eth_faucet.id(), + swapp_tag, + swapp_tag_felt, + p2id_tag_felt, + )?; + + let tx_context = mock_chain + .build_tx_context(bob.id(), &[swap_note.id()], &[])? + .extend_note_args(note_args_map) + .extend_expected_output_notes(vec![ + RawOutputNote::Full(p2id_note), + RawOutputNote::Full(remainder_note), + ]) + .build()?; + + let executed_transaction = tx_context.execute().await?; + + // Verify: 2 output notes (P2ID + remainder) + let output_notes = executed_transaction.output_notes(); + assert_eq!(output_notes.num_notes(), 2); + + // P2ID note: 20 ETH + if let Asset::Fungible(f) = output_notes + .get_note(0) + .assets() + .iter() + .next() + .unwrap() + { + assert_eq!(f.faucet_id(), eth_faucet.id()); + assert_eq!(f.amount(), 20); + } + + // SWAPp remainder: 10 USDC + if let Asset::Fungible(f) = output_notes + .get_note(1) + .assets() + .iter() + .next() + .unwrap() + { + assert_eq!(f.faucet_id(), usdc_faucet.id()); + assert_eq!(f.amount(), 10); + } + + // Bob's vault: +40 USDC, -20 ETH + let vault_delta = executed_transaction.account_delta().vault(); + let added: Vec = vault_delta.added_assets().collect(); + let removed: Vec = vault_delta.removed_assets().collect(); + assert_eq!(added.len(), 1); + assert_eq!(removed.len(), 1); + if let Asset::Fungible(f) = &added[0] { + assert_eq!(f.faucet_id(), usdc_faucet.id()); + assert_eq!(f.amount(), 40); + } + if let Asset::Fungible(f) = &removed[0] { + assert_eq!(f.faucet_id(), eth_faucet.id()); + assert_eq!(f.amount(), 20); + } + + mock_chain.add_pending_executed_transaction(&executed_transaction)?; + let _ = mock_chain.prove_next_block(); + + Ok(()) +} + +#[tokio::test] +async fn pswap_note_inflight_cross_swap_test() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + + let usdc_faucet = + builder.add_existing_basic_faucet(BASIC_AUTH, "USDC", 1000, Some(150))?; + let eth_faucet = builder.add_existing_basic_faucet(BASIC_AUTH, "ETH", 1000, Some(50))?; + + let alice = builder.add_existing_wallet_with_assets( + BASIC_AUTH, + [FungibleAsset::new(usdc_faucet.id(), 50)?.into()], + )?; + let bob = builder.add_existing_wallet_with_assets( + BASIC_AUTH, + [FungibleAsset::new(eth_faucet.id(), 25)?.into()], + )?; + let charlie = builder.add_existing_wallet_with_assets(BASIC_AUTH, [])?; + + let (swapp_tag, swapp_tag_felt) = make_swapp_tag(); + + // Alice's note: offers 50 USDC, requests 25 ETH + let alice_storage = build_pswap_storage( + eth_faucet.id(), + 25, + swapp_tag_felt, + compute_p2id_tag_felt(alice.id()), + 0, + alice.id(), + ); + let alice_swap_note = create_pswap_note( + alice.id(), + make_note_assets(usdc_faucet.id(), 50)?, + alice_storage, + swapp_tag, + ); + builder.add_output_note(RawOutputNote::Full(alice_swap_note.clone())); + + // Bob's note: offers 25 ETH, requests 50 USDC + let bob_storage = build_pswap_storage( + usdc_faucet.id(), + 50, + swapp_tag_felt, + compute_p2id_tag_felt(bob.id()), + 0, + bob.id(), + ); + let bob_swap_note = create_pswap_note( + bob.id(), + make_note_assets(eth_faucet.id(), 25)?, + bob_storage, + swapp_tag, + ); + builder.add_output_note(RawOutputNote::Full(bob_swap_note.clone())); + + let mock_chain = builder.build()?; + + // Note args: pure inflight (input=0, inflight=full amount) + let mut note_args_map = BTreeMap::new(); + note_args_map.insert(alice_swap_note.id(), make_note_args(0, 25)); + note_args_map.insert(bob_swap_note.id(), make_note_args(0, 50)); + + // Expected P2ID notes + let alice_p2id_note = create_expected_pswap_p2id_note( + &alice_swap_note, + charlie.id(), + alice.id(), + 0, + 25, + eth_faucet.id(), + compute_p2id_tag_for_local_account(alice.id()), + )?; + let bob_p2id_note = create_expected_pswap_p2id_note( + &bob_swap_note, + charlie.id(), + bob.id(), + 0, + 50, + usdc_faucet.id(), + compute_p2id_tag_for_local_account(bob.id()), + )?; + + let tx_context = mock_chain + .build_tx_context( + charlie.id(), + &[alice_swap_note.id(), bob_swap_note.id()], + &[], + )? + .extend_note_args(note_args_map) + .extend_expected_output_notes(vec![ + RawOutputNote::Full(alice_p2id_note), + RawOutputNote::Full(bob_p2id_note), + ]) + .build()?; + + let executed_transaction = tx_context.execute().await?; + + // Verify: 2 P2ID notes + let output_notes = executed_transaction.output_notes(); + assert_eq!(output_notes.num_notes(), 2); + + let mut alice_found = false; + let mut bob_found = false; + for idx in 0..output_notes.num_notes() { + if let Asset::Fungible(f) = output_notes + .get_note(idx) + .assets() + .iter() + .next() + .unwrap() + { + if f.faucet_id() == eth_faucet.id() && f.amount() == 25 { + alice_found = true; + } + if f.faucet_id() == usdc_faucet.id() && f.amount() == 50 { + bob_found = true; + } + } + } + assert!(alice_found, "Alice's P2ID note (25 ETH) not found"); + assert!(bob_found, "Bob's P2ID note (50 USDC) not found"); + + // Charlie's vault should be unchanged + let vault_delta = executed_transaction.account_delta().vault(); + assert_eq!(vault_delta.added_assets().count(), 0); + assert_eq!(vault_delta.removed_assets().count(), 0); + + Ok(()) +} + +#[tokio::test] +async fn pswap_note_creator_reclaim_test() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + + let usdc_faucet = builder.add_existing_basic_faucet(BASIC_AUTH, "USDC", 1000, Some(50))?; + let _eth_faucet = builder.add_existing_basic_faucet(BASIC_AUTH, "ETH", 1000, Some(25))?; + + let alice = builder.add_existing_wallet_with_assets( + BASIC_AUTH, + [FungibleAsset::new(usdc_faucet.id(), 50)?.into()], + )?; + + let (swapp_tag, swapp_tag_felt) = make_swapp_tag(); + let p2id_tag_felt = compute_p2id_tag_felt(alice.id()); + + let storage_items = build_pswap_storage( + _eth_faucet.id(), + 25, + swapp_tag_felt, + p2id_tag_felt, + 0, + alice.id(), + ); + let swap_note = create_pswap_note( + alice.id(), + make_note_assets(usdc_faucet.id(), 50)?, + storage_items, + swapp_tag, + ); + builder.add_output_note(RawOutputNote::Full(swap_note.clone())); + + let mock_chain = builder.build()?; + + let tx_context = mock_chain + .build_tx_context(alice.id(), &[swap_note.id()], &[])? + .build()?; + + let executed_transaction = tx_context.execute().await?; + + // Verify: 0 output notes, Alice gets 50 USDC back + let output_notes = executed_transaction.output_notes(); + assert_eq!(output_notes.num_notes(), 0, "Expected 0 output notes for reclaim"); + + let account_delta = executed_transaction.account_delta(); + let vault_delta = account_delta.vault(); + let added_assets: Vec = vault_delta.added_assets().collect(); + + assert_eq!(added_assets.len(), 1, "Alice should receive 1 asset back"); + let usdc_reclaimed = match added_assets[0] { + Asset::Fungible(f) => f, + _ => panic!("Expected fungible USDC asset"), + }; + assert_eq!(usdc_reclaimed.faucet_id(), usdc_faucet.id()); + assert_eq!(usdc_reclaimed.amount(), 50); + + Ok(()) +} + +#[tokio::test] +async fn pswap_note_invalid_input_test() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + + let usdc_faucet = builder.add_existing_basic_faucet(BASIC_AUTH, "USDC", 1000, Some(50))?; + let eth_faucet = builder.add_existing_basic_faucet(BASIC_AUTH, "ETH", 1000, Some(30))?; + + let alice = builder.add_existing_wallet_with_assets( + BASIC_AUTH, + [FungibleAsset::new(usdc_faucet.id(), 50)?.into()], + )?; + let bob = builder.add_existing_wallet_with_assets( + BASIC_AUTH, + [FungibleAsset::new(eth_faucet.id(), 30)?.into()], + )?; + + let (swapp_tag, swapp_tag_felt) = make_swapp_tag(); + let p2id_tag_felt = compute_p2id_tag_felt(alice.id()); + + let storage_items = build_pswap_storage( + eth_faucet.id(), + 25, + swapp_tag_felt, + p2id_tag_felt, + 0, + alice.id(), + ); + let swap_note = create_pswap_note( + alice.id(), + make_note_assets(usdc_faucet.id(), 50)?, + storage_items, + swapp_tag, + ); + builder.add_output_note(RawOutputNote::Full(swap_note.clone())); + let mock_chain = builder.build()?; + + // Try to fill with 30 ETH when only 25 is requested - should fail + let mut note_args_map = BTreeMap::new(); + note_args_map.insert(swap_note.id(), make_note_args(30, 0)); + + let tx_context = mock_chain + .build_tx_context(bob.id(), &[swap_note.id()], &[])? + .extend_note_args(note_args_map) + .build()?; + + let result = tx_context.execute().await; + assert!( + result.is_err(), + "Transaction should fail when input_amount > requested_asset_total" + ); + + Ok(()) +} + +#[tokio::test] +async fn pswap_note_multiple_partial_fills_test() -> anyhow::Result<()> { + let test_scenarios = vec![ + (5u64, "5 ETH - 20% fill"), + (7, "7 ETH - 28% fill"), + (10, "10 ETH - 40% fill"), + (13, "13 ETH - 52% fill"), + (15, "15 ETH - 60% fill"), + (19, "19 ETH - 76% fill"), + (20, "20 ETH - 80% fill"), + (23, "23 ETH - 92% fill"), + (25, "25 ETH - 100% fill (full)"), + ]; + + for (input_amount, _description) in test_scenarios { + let mut builder = MockChain::builder(); + let usdc_faucet = + builder.add_existing_basic_faucet(BASIC_AUTH, "USDC", 1000, Some(150))?; + let eth_faucet = + builder.add_existing_basic_faucet(BASIC_AUTH, "ETH", 1000, Some(50))?; + + let alice = builder.add_existing_wallet_with_assets( + BASIC_AUTH, + [FungibleAsset::new(usdc_faucet.id(), 50)?.into()], + )?; + + let bob = builder.add_existing_wallet_with_assets( + BASIC_AUTH, + [FungibleAsset::new(eth_faucet.id(), input_amount)?.into()], + )?; + + let (swapp_tag, swapp_tag_felt) = make_swapp_tag(); + let p2id_tag_felt = compute_p2id_tag_felt(alice.id()); + + let storage_items = build_pswap_storage( + eth_faucet.id(), + 25, + swapp_tag_felt, + p2id_tag_felt, + 0, + alice.id(), + ); + let swap_note = create_pswap_note( + alice.id(), + make_note_assets(usdc_faucet.id(), 50)?, + storage_items, + swapp_tag, + ); + builder.add_output_note(RawOutputNote::Full(swap_note.clone())); + + let mock_chain = builder.build()?; + + let offered_out = calculate_output_amount(50, 25, input_amount); + let remaining_usdc = 50 - offered_out; + let remaining_eth = 25 - input_amount; + + let mut note_args_map = BTreeMap::new(); + note_args_map.insert(swap_note.id(), make_note_args(input_amount, 0)); + + let p2id_note = create_expected_pswap_p2id_note( + &swap_note, + bob.id(), + alice.id(), + 0, + input_amount, + eth_faucet.id(), + compute_p2id_tag_for_local_account(alice.id()), + )?; + + let mut expected_notes = vec![RawOutputNote::Full(p2id_note)]; + + if input_amount < 25 { + let remainder_note = create_expected_pswap_remainder_note( + &swap_note, + bob.id(), + alice.id(), + remaining_usdc, + remaining_eth, + offered_out, + 0, + usdc_faucet.id(), + eth_faucet.id(), + swapp_tag, + swapp_tag_felt, + p2id_tag_felt, + )?; + expected_notes.push(RawOutputNote::Full(remainder_note)); + } + + let tx_context = mock_chain + .build_tx_context(bob.id(), &[swap_note.id()], &[])? + .extend_expected_output_notes(expected_notes) + .extend_note_args(note_args_map) + .build()?; + + let executed_transaction = tx_context.execute().await?; + + let output_notes = executed_transaction.output_notes(); + let expected_count = if input_amount < 25 { 2 } else { 1 }; + assert_eq!(output_notes.num_notes(), expected_count); + + // Verify Bob's vault + let vault_delta = executed_transaction.account_delta().vault(); + let added: Vec = vault_delta.added_assets().collect(); + assert_eq!(added.len(), 1); + if let Asset::Fungible(f) = added[0] { + assert_eq!(f.amount(), offered_out); + } + } + + Ok(()) +} + +#[tokio::test] +async fn pswap_note_non_exact_ratio_partial_fill_test() -> anyhow::Result<()> { + let offered_total = 100u64; + let requested_total = 30u64; + let input_amount = 7u64; + let expected_output = calculate_output_amount(offered_total, requested_total, input_amount); + + let mut builder = MockChain::builder(); + let usdc_faucet = + builder.add_existing_basic_faucet(BASIC_AUTH, "USDC", 10000, Some(1000))?; + let eth_faucet = builder.add_existing_basic_faucet(BASIC_AUTH, "ETH", 10000, Some(100))?; + + let alice = builder.add_existing_wallet_with_assets( + BASIC_AUTH, + [FungibleAsset::new(usdc_faucet.id(), offered_total)?.into()], + )?; + let bob = builder.add_existing_wallet_with_assets( + BASIC_AUTH, + [FungibleAsset::new(eth_faucet.id(), input_amount)?.into()], + )?; + + let swapp_tag = NoteTag::new(0xC0000000); + let swapp_tag_felt = Felt::new(u32::from(swapp_tag) as u64); + let p2id_tag_felt = compute_p2id_tag_felt(alice.id()); + + let storage_items = build_pswap_storage( + eth_faucet.id(), + requested_total, + swapp_tag_felt, + p2id_tag_felt, + 0, + alice.id(), + ); + let note_assets = make_note_assets(usdc_faucet.id(), offered_total)?; + let swap_note = create_pswap_note(alice.id(), note_assets, storage_items, swapp_tag); + builder.add_output_note(RawOutputNote::Full(swap_note.clone())); + + let mock_chain = builder.build()?; + + let mut note_args_map = BTreeMap::new(); + note_args_map.insert(swap_note.id(), make_note_args(input_amount, 0)); + + let remaining_offered = offered_total - expected_output; + let remaining_requested = requested_total - input_amount; + + let p2id_note = create_expected_pswap_p2id_note( + &swap_note, + bob.id(), + alice.id(), + 0, + input_amount, + eth_faucet.id(), + compute_p2id_tag_for_local_account(alice.id()), + )?; + let remainder = create_expected_pswap_remainder_note( + &swap_note, + bob.id(), + alice.id(), + remaining_offered, + remaining_requested, + expected_output, + 0, + usdc_faucet.id(), + eth_faucet.id(), + swapp_tag, + swapp_tag_felt, + p2id_tag_felt, + )?; + + let tx_context = mock_chain + .build_tx_context(bob.id(), &[swap_note.id()], &[])? + .extend_expected_output_notes(vec![ + RawOutputNote::Full(p2id_note), + RawOutputNote::Full(remainder), + ]) + .extend_note_args(note_args_map) + .build()?; + + let executed_tx = tx_context.execute().await?; + + let output_notes = executed_tx.output_notes(); + assert_eq!(output_notes.num_notes(), 2); + + let vault_delta = executed_tx.account_delta().vault(); + let added: Vec = vault_delta.added_assets().collect(); + assert_eq!(added.len(), 1); + if let Asset::Fungible(f) = &added[0] { + assert_eq!(f.amount(), expected_output); + } + + Ok(()) +} + +#[tokio::test] +async fn pswap_note_partial_fill_non_integer_ratio_fuzz_test() -> anyhow::Result<()> { + // (offered_usdc, requested_eth, fill_eth) + let test_cases: Vec<(u64, u64, u64)> = vec![ + (23, 20, 7), + (23, 20, 13), + (23, 20, 19), + (17, 13, 5), + (97, 89, 37), + (53, 47, 23), + (7, 5, 3), + (7, 5, 1), + (7, 5, 4), + (89, 55, 21), + (233, 144, 55), + (34, 21, 8), + (50, 97, 30), + (13, 47, 20), + (3, 7, 5), + (101, 100, 50), + (100, 99, 50), + (997, 991, 500), + (1000, 3, 1), + (1000, 3, 2), + (3, 1000, 500), + (9999, 7777, 3333), + (5000, 3333, 1111), + (127, 63, 31), + (255, 127, 63), + (511, 255, 100), + ]; + + for (i, (offered_usdc, requested_eth, fill_eth)) in test_cases.iter().enumerate() { + let offered_out = calculate_output_amount(*offered_usdc, *requested_eth, *fill_eth); + let remaining_offered = offered_usdc - offered_out; + let remaining_requested = requested_eth - fill_eth; + + assert!(offered_out > 0, "Case {}: offered_out must be > 0", i + 1); + assert!( + offered_out <= *offered_usdc, + "Case {}: offered_out > offered", + i + 1 + ); + + let mut builder = MockChain::builder(); + let max_supply = 100_000u64; + + let usdc_faucet = builder.add_existing_basic_faucet( + BASIC_AUTH, + "USDC", + max_supply, + Some(*offered_usdc), + )?; + let eth_faucet = builder.add_existing_basic_faucet( + BASIC_AUTH, + "ETH", + max_supply, + Some(*fill_eth), + )?; + + let alice = builder.add_existing_wallet_with_assets( + BASIC_AUTH, + [FungibleAsset::new(usdc_faucet.id(), *offered_usdc)?.into()], + )?; + let bob = builder.add_existing_wallet_with_assets( + BASIC_AUTH, + [FungibleAsset::new(eth_faucet.id(), *fill_eth)?.into()], + )?; + + let swapp_tag = NoteTag::new(0xC0000000); + let swapp_tag_felt = Felt::new(u32::from(swapp_tag) as u64); + let p2id_tag_felt = compute_p2id_tag_felt(alice.id()); + + let storage_items = build_pswap_storage( + eth_faucet.id(), + *requested_eth, + swapp_tag_felt, + p2id_tag_felt, + 0, + alice.id(), + ); + let note_assets = make_note_assets(usdc_faucet.id(), *offered_usdc)?; + let swap_note = create_pswap_note(alice.id(), note_assets, storage_items, swapp_tag); + builder.add_output_note(RawOutputNote::Full(swap_note.clone())); + + let mock_chain = builder.build()?; + + let mut note_args_map = BTreeMap::new(); + note_args_map.insert(swap_note.id(), make_note_args(*fill_eth, 0)); + + let p2id_note = create_expected_pswap_p2id_note( + &swap_note, + bob.id(), + alice.id(), + 0, + *fill_eth, + eth_faucet.id(), + compute_p2id_tag_for_local_account(alice.id()), + )?; + + let mut expected_notes = vec![RawOutputNote::Full(p2id_note)]; + if remaining_requested > 0 { + let remainder = create_expected_pswap_remainder_note( + &swap_note, + bob.id(), + alice.id(), + remaining_offered, + remaining_requested, + offered_out, + 0, + usdc_faucet.id(), + eth_faucet.id(), + swapp_tag, + swapp_tag_felt, + p2id_tag_felt, + )?; + expected_notes.push(RawOutputNote::Full(remainder)); + } + + let tx_context = mock_chain + .build_tx_context(bob.id(), &[swap_note.id()], &[])? + .extend_expected_output_notes(expected_notes) + .extend_note_args(note_args_map) + .build()?; + + let executed_tx = tx_context.execute().await.map_err(|e| { + anyhow::anyhow!( + "Case {} failed: {} (offered={}, requested={}, fill={})", + i + 1, + e, + offered_usdc, + requested_eth, + fill_eth + ) + })?; + + let output_notes = executed_tx.output_notes(); + let expected_count = if remaining_requested > 0 { 2 } else { 1 }; + assert_eq!(output_notes.num_notes(), expected_count, "Case {}", i + 1); + + let vault_delta = executed_tx.account_delta().vault(); + let added: Vec = vault_delta.added_assets().collect(); + let removed: Vec = vault_delta.removed_assets().collect(); + assert_eq!(added.len(), 1, "Case {}", i + 1); + if let Asset::Fungible(f) = &added[0] { + assert_eq!(f.amount(), offered_out, "Case {}", i + 1); + } + assert_eq!(removed.len(), 1, "Case {}", i + 1); + if let Asset::Fungible(f) = &removed[0] { + assert_eq!(f.amount(), *fill_eth, "Case {}", i + 1); + } + + assert_eq!( + offered_out + remaining_offered, + *offered_usdc, + "Case {}: conservation", + i + 1 + ); + } + + Ok(()) +} + +#[tokio::test] +async fn pswap_note_chained_partial_fills_non_integer_ratio_test() -> anyhow::Result<()> { + let test_chains: Vec<(u64, u64, Vec)> = vec![ + (100, 73, vec![17, 23, 19]), + (53, 47, vec![7, 11, 13, 5]), + (200, 137, vec![41, 37, 29]), + (7, 5, vec![2, 1]), + (1000, 777, vec![100, 200, 150, 100]), + (50, 97, vec![20, 30, 15]), + (89, 55, vec![13, 8, 21]), + (23, 20, vec![3, 5, 4, 3]), + (997, 991, vec![300, 300, 200]), + (3, 2, vec![1]), + ]; + + for (chain_idx, (initial_offered, initial_requested, fills)) in test_chains.iter().enumerate() { + let mut current_offered = *initial_offered; + let mut current_requested = *initial_requested; + let mut total_usdc_to_bob = 0u64; + let mut total_eth_from_bob = 0u64; + let mut current_swap_count = 0u64; + + // Track serial for remainder chain + use miden_protocol::crypto::rand::{FeltRng, RpoRandomCoin}; + let mut rng = RpoRandomCoin::new(Word::default()); + let mut current_serial = rng.draw_word(); + + for (fill_idx, fill_amount) in fills.iter().enumerate() { + let offered_out = + calculate_output_amount(current_offered, current_requested, *fill_amount); + let remaining_offered = current_offered - offered_out; + let remaining_requested = current_requested - fill_amount; + + let mut builder = MockChain::builder(); + let max_supply = 100_000u64; + + let usdc_faucet = builder.add_existing_basic_faucet( + BASIC_AUTH, + "USDC", + max_supply, + Some(current_offered), + )?; + let eth_faucet = builder.add_existing_basic_faucet( + BASIC_AUTH, + "ETH", + max_supply, + Some(*fill_amount), + )?; + + let alice = builder.add_existing_wallet_with_assets( + BASIC_AUTH, + [FungibleAsset::new(usdc_faucet.id(), current_offered)?.into()], + )?; + let bob = builder.add_existing_wallet_with_assets( + BASIC_AUTH, + [FungibleAsset::new(eth_faucet.id(), *fill_amount)?.into()], + )?; + + let swapp_tag = NoteTag::new(0xC0000000); + let swapp_tag_felt = Felt::new(u32::from(swapp_tag) as u64); + let p2id_tag_felt = compute_p2id_tag_felt(alice.id()); + + let storage_items = build_pswap_storage( + eth_faucet.id(), + current_requested, + swapp_tag_felt, + p2id_tag_felt, + current_swap_count, + alice.id(), + ); + let note_assets = make_note_assets(usdc_faucet.id(), current_offered)?; + + // Create note with the correct serial for this chain position + let note_storage = NoteStorage::new(storage_items)?; + let recipient = + NoteRecipient::new(current_serial, PswapNote::script(), note_storage); + let metadata = + NoteMetadata::new(alice.id(), NoteType::Public).with_tag(swapp_tag); + let swap_note = Note::new(note_assets, metadata, recipient); + + builder.add_output_note(RawOutputNote::Full(swap_note.clone())); + let mock_chain = builder.build()?; + + let mut note_args_map = BTreeMap::new(); + note_args_map.insert(swap_note.id(), make_note_args(*fill_amount, 0)); + + let p2id_note = create_expected_pswap_p2id_note( + &swap_note, + bob.id(), + alice.id(), + current_swap_count, + *fill_amount, + eth_faucet.id(), + compute_p2id_tag_for_local_account(alice.id()), + )?; + + let mut expected_notes = vec![RawOutputNote::Full(p2id_note)]; + if remaining_requested > 0 { + let remainder = create_expected_pswap_remainder_note( + &swap_note, + bob.id(), + alice.id(), + remaining_offered, + remaining_requested, + offered_out, + current_swap_count, + usdc_faucet.id(), + eth_faucet.id(), + swapp_tag, + swapp_tag_felt, + p2id_tag_felt, + )?; + expected_notes.push(RawOutputNote::Full(remainder)); + } + + let tx_context = mock_chain + .build_tx_context(bob.id(), &[swap_note.id()], &[])? + .extend_expected_output_notes(expected_notes) + .extend_note_args(note_args_map) + .build()?; + + let executed_tx = tx_context.execute().await.map_err(|e| { + anyhow::anyhow!( + "Chain {} fill {} failed: {} (offered={}, requested={}, fill={})", + chain_idx + 1, + fill_idx + 1, + e, + current_offered, + current_requested, + fill_amount + ) + })?; + + let output_notes = executed_tx.output_notes(); + let expected_count = if remaining_requested > 0 { 2 } else { 1 }; + assert_eq!( + output_notes.num_notes(), + expected_count, + "Chain {} fill {}", + chain_idx + 1, + fill_idx + 1 + ); + + let vault_delta = executed_tx.account_delta().vault(); + let added: Vec = vault_delta.added_assets().collect(); + assert_eq!( + added.len(), + 1, + "Chain {} fill {}", + chain_idx + 1, + fill_idx + 1 + ); + if let Asset::Fungible(f) = &added[0] { + assert_eq!( + f.amount(), + offered_out, + "Chain {} fill {}: Bob should get {} USDC", + chain_idx + 1, + fill_idx + 1, + offered_out + ); + } + + // Update state for next fill + total_usdc_to_bob += offered_out; + total_eth_from_bob += fill_amount; + current_offered = remaining_offered; + current_requested = remaining_requested; + current_swap_count += 1; + // Remainder serial: [0] + 1 (matching MASM LE orientation) + current_serial = Word::from([ + Felt::new(current_serial[0].as_canonical_u64() + 1), + current_serial[1], + current_serial[2], + current_serial[3], + ]); + } + + // Verify conservation + let total_fills: u64 = fills.iter().sum(); + assert_eq!( + total_eth_from_bob, total_fills, + "Chain {}: ETH conservation", + chain_idx + 1 + ); + assert_eq!( + total_usdc_to_bob + current_offered, + *initial_offered, + "Chain {}: USDC conservation", + chain_idx + 1 + ); + } + + Ok(()) +} + +/// Test that PswapNote::create and PswapNote::create_output_notes produce correct results +#[test] +fn compare_pswap_create_output_notes_vs_test_helper() { + use miden_protocol::crypto::rand::{FeltRng, RpoRandomCoin}; + + let mut builder = MockChain::builder(); + let usdc_faucet = builder + .add_existing_basic_faucet(BASIC_AUTH, "USDC", 1000, Some(150)) + .unwrap(); + let eth_faucet = builder + .add_existing_basic_faucet(BASIC_AUTH, "ETH", 1000, Some(50)) + .unwrap(); + let alice = builder + .add_existing_wallet_with_assets( + BASIC_AUTH, + [FungibleAsset::new(usdc_faucet.id(), 50).unwrap().into()], + ) + .unwrap(); + let bob = builder + .add_existing_wallet_with_assets( + BASIC_AUTH, + [FungibleAsset::new(eth_faucet.id(), 25).unwrap().into()], + ) + .unwrap(); + + // Create swap note using PswapNote::create + let mut rng = RpoRandomCoin::new(Word::default()); + let swap_note_lib = PswapNote::create( + alice.id(), + Asset::Fungible(FungibleAsset::new(usdc_faucet.id(), 50).unwrap()), + Asset::Fungible(FungibleAsset::new(eth_faucet.id(), 25).unwrap()), + NoteType::Public, + NoteAttachment::default(), + &mut rng, + ) + .unwrap(); + + // Create output notes using library + let (lib_p2id, _) = PswapNote::create_output_notes(&swap_note_lib, bob.id(), 25, 0).unwrap(); + + // Create same swap note using test helper (same serial) + let (_swapp_tag, swapp_tag_felt) = make_swapp_tag(); + let p2id_tag_felt = compute_p2id_tag_felt(alice.id()); + let storage_items = build_pswap_storage( + eth_faucet.id(), + 25, + swapp_tag_felt, + p2id_tag_felt, + 0, + alice.id(), + ); + let note_assets = make_note_assets(usdc_faucet.id(), 50).unwrap(); + + // Use the SAME serial as the library note + let test_serial = swap_note_lib.recipient().serial_num(); + let test_storage = NoteStorage::new(storage_items).unwrap(); + let test_recipient = NoteRecipient::new(test_serial, PswapNote::script(), test_storage); + let test_metadata = + NoteMetadata::new(alice.id(), NoteType::Public).with_tag(NoteTag::new(0xC0000000)); + let swap_note_test = Note::new(note_assets, test_metadata, test_recipient); + + // Create expected P2ID using test helper + let test_p2id = create_expected_pswap_p2id_note( + &swap_note_test, + bob.id(), + alice.id(), + 0, + 25, + eth_faucet.id(), + compute_p2id_tag_for_local_account(alice.id()), + ) + .unwrap(); + + // Compare components + assert_eq!( + lib_p2id.recipient().serial_num(), + test_p2id.recipient().serial_num(), + "Serial mismatch!" + ); + assert_eq!( + lib_p2id.recipient().script().root(), + test_p2id.recipient().script().root(), + "Script root mismatch!" + ); + assert_eq!( + lib_p2id.recipient().digest(), + test_p2id.recipient().digest(), + "Recipient digest mismatch!" + ); + assert_eq!( + lib_p2id.metadata().tag(), + test_p2id.metadata().tag(), + "Tag mismatch!" + ); + assert_eq!( + lib_p2id.metadata().sender(), + test_p2id.metadata().sender(), + "Sender mismatch!" + ); + assert_eq!( + lib_p2id.metadata().note_type(), + test_p2id.metadata().note_type(), + "Note type mismatch!" + ); + assert_eq!(lib_p2id.id(), test_p2id.id(), "NOTE ID MISMATCH!"); +} + +/// Test that PswapNote::parse_inputs roundtrips correctly +#[test] +fn pswap_parse_inputs_roundtrip() { + use miden_protocol::crypto::rand::{FeltRng, RpoRandomCoin}; + + let mut builder = MockChain::builder(); + let usdc_faucet = builder + .add_existing_basic_faucet(BASIC_AUTH, "USDC", 1000, Some(150)) + .unwrap(); + let eth_faucet = builder + .add_existing_basic_faucet(BASIC_AUTH, "ETH", 1000, Some(50)) + .unwrap(); + let alice = builder + .add_existing_wallet_with_assets( + BASIC_AUTH, + [FungibleAsset::new(usdc_faucet.id(), 50).unwrap().into()], + ) + .unwrap(); + + let mut rng = RpoRandomCoin::new(Word::default()); + let swap_note = PswapNote::create( + alice.id(), + Asset::Fungible(FungibleAsset::new(usdc_faucet.id(), 50).unwrap()), + Asset::Fungible(FungibleAsset::new(eth_faucet.id(), 25).unwrap()), + NoteType::Public, + NoteAttachment::default(), + &mut rng, + ) + .unwrap(); + + let storage = swap_note.recipient().storage(); + let items = storage.items(); + + let parsed = PswapNote::parse_inputs(items).unwrap(); + + assert_eq!(parsed.creator_account_id, alice.id(), "Creator ID roundtrip failed!"); + assert_eq!(parsed.swap_count, 0, "Swap count should be 0"); + + // Verify requested amount from value word + let requested_amount = parsed.requested_value[0].as_canonical_u64(); + assert_eq!(requested_amount, 25, "Requested amount should be 25"); +} From 16031ae834f2b2b778b365ea4ab10c8cde17590a Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Fri, 20 Mar 2026 09:06:29 +0530 Subject: [PATCH 2/6] Added the pswap masm contract and its supportive functions --- Cargo.lock | 72 ++ Cargo.toml | 1 + crates/miden-standards/Cargo.toml | 1 + .../asm/standards/notes/pswap.masm | 4 +- crates/miden-standards/src/note/mod.rs | 4 +- crates/miden-standards/src/note/pswap.rs | 911 +++++++++++------- crates/miden-testing/tests/scripts/pswap.rs | 145 +-- 7 files changed, 690 insertions(+), 448 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 01f569efe3..127501d4af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -281,6 +281,31 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bon" +version = "3.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f47dbe92550676ee653353c310dfb9cf6ba17ee70396e1f7cf0a2020ad49b2fe" +dependencies = [ + "bon-macros", + "rustversion", +] + +[[package]] +name = "bon-macros" +version = "3.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "519bd3116aeeb42d5372c29d982d16d0170d3d4a5ed85fc7dd91642ffff3c67c" +dependencies = [ + "darling", + "ident_case", + "prettyplease", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.117", +] + [[package]] name = "bumpalo" version = "3.20.2" @@ -605,6 +630,40 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.117", +] + [[package]] name = "debugid" version = "0.8.0" @@ -1137,6 +1196,12 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "indenter" version = "0.3.4" @@ -1777,6 +1842,7 @@ version = "0.14.0-alpha.1" dependencies = [ "anyhow", "assert_matches", + "bon", "fs-err", "miden-assembly", "miden-core", @@ -3156,6 +3222,12 @@ dependencies = [ "vte", ] +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "subtle" version = "2.6.1" diff --git a/Cargo.toml b/Cargo.toml index c5b384107a..a474dd6939 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,6 +60,7 @@ miden-verifier = { default-features = false, version = "0.21" } # External dependencies anyhow = { default-features = false, features = ["backtrace", "std"], version = "1.0" } +bon = { default-features = false, version = "3" } assert_matches = { default-features = false, version = "1.5" } fs-err = { default-features = false, version = "3" } primitive-types = { default-features = false, version = "0.14" } diff --git a/crates/miden-standards/Cargo.toml b/crates/miden-standards/Cargo.toml index d4876b5cd9..2a9c42d7e0 100644 --- a/crates/miden-standards/Cargo.toml +++ b/crates/miden-standards/Cargo.toml @@ -25,6 +25,7 @@ miden-processor = { workspace = true } miden-protocol = { workspace = true } # External dependencies +bon = { workspace = true } rand = { optional = true, workspace = true } thiserror = { workspace = true } diff --git a/crates/miden-standards/asm/standards/notes/pswap.masm b/crates/miden-standards/asm/standards/notes/pswap.masm index 636b5d6412..18b1c1d384 100644 --- a/crates/miden-standards/asm/standards/notes/pswap.masm +++ b/crates/miden-standards/asm/standards/notes/pswap.masm @@ -361,7 +361,7 @@ end # REMAINDER NOTE CREATION PROCEDURE # ================================================================================================= -#! Creates a SWAPp remainder output note for partial fills. +#! Creates a PSWAP remainder output note for partial fills. #! #! Updates the requested amount in note storage, builds a new remainder recipient #! (using the active note's script root and a serial derived by incrementing the @@ -376,7 +376,7 @@ proc create_remainder_note mem_store.REQUESTED_ASSET_VALUE_INPUT # => [] - # Build SWAPp remainder recipient using the same script as the active note + # Build PSWAP remainder recipient using the same script as the active note exec.active_note::get_script_root # => [SCRIPT_ROOT] diff --git a/crates/miden-standards/src/note/mod.rs b/crates/miden-standards/src/note/mod.rs index d61633c637..c90e805921 100644 --- a/crates/miden-standards/src/note/mod.rs +++ b/crates/miden-standards/src/note/mod.rs @@ -27,7 +27,7 @@ mod p2ide; pub use p2ide::{P2ideNote, P2ideNoteStorage}; mod pswap; -pub use pswap::{PswapNote, PswapParsedInputs}; +pub use pswap::{PswapNote, PswapNoteStorage}; mod swap; pub use swap::SwapNote; @@ -110,7 +110,7 @@ impl StandardNote { Self::P2ID => P2idNote::NUM_STORAGE_ITEMS, Self::P2IDE => P2ideNote::NUM_STORAGE_ITEMS, Self::SWAP => SwapNote::NUM_STORAGE_ITEMS, - Self::PSWAP => PswapNote::NUM_STORAGE_ITEMS, + Self::PSWAP => PswapNoteStorage::NUM_ITEMS, Self::MINT => MintNote::NUM_STORAGE_ITEMS_PRIVATE, Self::BURN => BurnNote::NUM_STORAGE_ITEMS, } diff --git a/crates/miden-standards/src/note/pswap.rs b/crates/miden-standards/src/note/pswap.rs index 18ac3d1437..f3a8ae1668 100644 --- a/crates/miden-standards/src/note/pswap.rs +++ b/crates/miden-standards/src/note/pswap.rs @@ -30,25 +30,215 @@ static PSWAP_SCRIPT: LazyLock = LazyLock::new(|| { .expect("Standards library contains PSWAP note script procedure") }); -// PSWAP NOTE +// PSWAP NOTE STORAGE // ================================================================================================ -/// Parsed PSWAP note storage fields. -pub struct PswapParsedInputs { - /// Requested asset key word [0-3] - pub requested_key: Word, - /// Requested asset value word [4-7] - pub requested_value: Word, - /// SWAPp note tag - pub swapp_tag: NoteTag, - /// P2ID routing tag - pub p2id_tag: NoteTag, - /// Current swap count - pub swap_count: u64, - /// Creator account ID - pub creator_account_id: AccountId, +/// Typed storage representation for a PSWAP note. +/// +/// Encapsulates the 18-item storage layout used by the PSWAP MASM contract: +/// - [0-3]: ASSET_KEY (requested asset key from asset.to_key_word()) +/// - [4-7]: ASSET_VALUE (requested asset value from asset.to_value_word()) +/// - [8]: PSWAP tag +/// - [9]: P2ID routing tag +/// - [10-11]: Reserved (zero) +/// - [12]: Swap count +/// - [13-15]: Reserved (zero) +/// - [16]: Creator account ID prefix +/// - [17]: Creator account ID suffix +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PswapNoteStorage { + requested_key: Word, + requested_value: Word, + pswap_tag: NoteTag, + p2id_tag: NoteTag, + swap_count: u64, + creator_account_id: AccountId, } +impl PswapNoteStorage { + // CONSTANTS + // -------------------------------------------------------------------------------------------- + + /// Expected number of storage items for the PSWAP note. + pub const NUM_STORAGE_ITEMS: usize = 18; + + // CONSTRUCTORS + // -------------------------------------------------------------------------------------------- + + /// Creates storage for a new PSWAP note from the requested asset and creator. + /// + /// The `pswap_tag` is defaulted and will be computed when converting to a [`Note`]. + /// The `swap_count` starts at 0. + pub fn new(requested_asset: Asset, creator_account_id: AccountId) -> Self { + let p2id_tag = NoteTag::with_account_target(creator_account_id); + Self { + requested_key: requested_asset.to_key_word(), + requested_value: requested_asset.to_value_word(), + pswap_tag: NoteTag::new(0), + p2id_tag, + swap_count: 0, + creator_account_id, + } + } + + /// Creates storage with all fields specified explicitly. + /// + /// Used for remainder notes where all fields (including swap count and tags) are known. + pub fn from_parts( + requested_key: Word, + requested_value: Word, + pswap_tag: NoteTag, + p2id_tag: NoteTag, + swap_count: u64, + creator_account_id: AccountId, + ) -> Self { + Self { + requested_key, + requested_value, + pswap_tag, + p2id_tag, + swap_count, + creator_account_id, + } + } + + /// Consumes the storage and returns a PSWAP [`NoteRecipient`] with the provided serial number. + pub fn into_recipient(self, serial_num: Word) -> NoteRecipient { + NoteRecipient::new(serial_num, PswapNote::script(), NoteStorage::from(self)) + } + + /// Sets the pswap_tag on this storage, returning the modified storage. + pub(crate) fn with_pswap_tag(mut self, tag: NoteTag) -> Self { + self.pswap_tag = tag; + self + } + + // PUBLIC ACCESSORS + // -------------------------------------------------------------------------------------------- + + /// Returns the requested asset key word. + pub fn requested_key(&self) -> Word { + self.requested_key + } + + /// Returns the requested asset value word. + pub fn requested_value(&self) -> Word { + self.requested_value + } + + /// Returns the PSWAP note tag. + pub fn pswap_tag(&self) -> NoteTag { + self.pswap_tag + } + + /// Returns the P2ID routing tag. + pub fn p2id_tag(&self) -> NoteTag { + self.p2id_tag + } + + /// Returns the current swap count. + pub fn swap_count(&self) -> u64 { + self.swap_count + } + + /// Returns the creator account ID. + pub fn creator_account_id(&self) -> AccountId { + self.creator_account_id + } + + /// Reconstructs the requested asset from the key and value words. + pub fn requested_asset(&self) -> Result { + let faucet_id = self.requested_faucet_id()?; + let amount = self.requested_amount(); + Ok(Asset::Fungible(FungibleAsset::new(faucet_id, amount).map_err(|e| { + NoteError::other_with_source("failed to create requested asset", e) + })?)) + } + + /// Extracts the faucet ID from the requested key word. + pub fn requested_faucet_id(&self) -> Result { + // Key layout: [key[0], key[1], faucet_suffix, faucet_prefix] + AccountId::try_from_elements(self.requested_key[2], self.requested_key[3]).map_err(|e| { + NoteError::other_with_source("failed to parse faucet ID from key", e) + }) + } + + /// Extracts the requested amount from the value word. + pub fn requested_amount(&self) -> u64 { + // ASSET_VALUE[0] = amount (from asset::fungible_to_amount) + self.requested_value[0].as_canonical_u64() + } +} + +impl From for NoteStorage { + fn from(storage: PswapNoteStorage) -> Self { + let inputs = vec![ + // ASSET_KEY [0-3] + storage.requested_key[0], + storage.requested_key[1], + storage.requested_key[2], + storage.requested_key[3], + // ASSET_VALUE [4-7] + storage.requested_value[0], + storage.requested_value[1], + storage.requested_value[2], + storage.requested_value[3], + // Tags [8-9] + Felt::new(u32::from(storage.pswap_tag) as u64), + Felt::new(u32::from(storage.p2id_tag) as u64), + // Padding [10-11] + ZERO, + ZERO, + // Swap count [12-15] + Felt::new(storage.swap_count), + ZERO, + ZERO, + ZERO, + // Creator ID [16-17] + storage.creator_account_id.prefix().as_felt(), + storage.creator_account_id.suffix(), + ]; + NoteStorage::new(inputs) + .expect("number of storage items should not exceed max storage items") + } +} + +impl TryFrom<&[Felt]> for PswapNoteStorage { + type Error = NoteError; + + fn try_from(inputs: &[Felt]) -> Result { + if inputs.len() != Self::NUM_STORAGE_ITEMS { + return Err(NoteError::InvalidNoteStorageLength { + expected: Self::NUM_STORAGE_ITEMS, + actual: inputs.len(), + }); + } + + let requested_key = Word::from([inputs[0], inputs[1], inputs[2], inputs[3]]); + let requested_value = Word::from([inputs[4], inputs[5], inputs[6], inputs[7]]); + let pswap_tag = NoteTag::new(inputs[8].as_canonical_u64() as u32); + let p2id_tag = NoteTag::new(inputs[9].as_canonical_u64() as u32); + let swap_count = inputs[12].as_canonical_u64(); + + let creator_account_id = + AccountId::try_from_elements(inputs[17], inputs[16]).map_err(|e| { + NoteError::other_with_source("failed to parse creator account ID", e) + })?; + + Ok(Self { + requested_key, + requested_value, + pswap_tag, + p2id_tag, + swap_count, + creator_account_id, + }) + } +} + +// PSWAP NOTE +// ================================================================================================ + /// Partial swap (pswap) note for decentralized asset exchange. /// /// This note implements a partially-fillable swap mechanism where: @@ -56,25 +246,28 @@ pub struct PswapParsedInputs { /// - Note can be partially or fully filled by consumers /// - Unfilled portions create remainder notes /// - Creator receives requested assets via P2ID notes -pub struct PswapNote; +#[derive(Debug, Clone, bon::Builder)] +pub struct PswapNote { + sender: AccountId, + storage: PswapNoteStorage, + serial_number: Word, + + #[builder(default = NoteType::Private)] + note_type: NoteType, + + #[builder(default)] + assets: NoteAssets, + + #[builder(default)] + attachment: NoteAttachment, +} impl PswapNote { // CONSTANTS // -------------------------------------------------------------------------------------------- /// Expected number of storage items for the PSWAP note. - /// - /// Layout (18 Felts, matching pswap.masm memory addresses): - /// - [0-3]: ASSET_KEY (requested asset key from asset.to_key_word()) - /// - [4-7]: ASSET_VALUE (requested asset value from asset.to_value_word()) - /// - [8]: SWAPp tag - /// - [9]: P2ID routing tag - /// - [10-11]: Reserved (zero) - /// - [12]: Swap count - /// - [13-15]: Reserved (zero) - /// - [16]: Creator account ID prefix - /// - [17]: Creator account ID suffix - pub const NUM_STORAGE_ITEMS: usize = 18; + pub const NUM_STORAGE_ITEMS: usize = PswapNoteStorage::NUM_STORAGE_ITEMS; // PUBLIC ACCESSORS // -------------------------------------------------------------------------------------------- @@ -89,29 +282,47 @@ impl PswapNote { PSWAP_SCRIPT.root() } + /// Returns the sender account ID. + pub fn sender(&self) -> AccountId { + self.sender + } + + /// Returns a reference to the note storage. + pub fn storage(&self) -> &PswapNoteStorage { + &self.storage + } + + /// Returns the serial number. + pub fn serial_number(&self) -> Word { + self.serial_number + } + + /// Returns the note type. + pub fn note_type(&self) -> NoteType { + self.note_type + } + + /// Returns a reference to the note assets. + pub fn assets(&self) -> &NoteAssets { + &self.assets + } + + /// Returns a reference to the note attachment. + pub fn attachment(&self) -> &NoteAttachment { + &self.attachment + } + // BUILDERS // -------------------------------------------------------------------------------------------- /// Creates a PSWAP note offering one asset in exchange for another. /// - /// # Arguments - /// - /// * `creator_account_id` - The account creating the swap offer - /// * `offered_asset` - The asset being offered (will be locked in the note) - /// * `requested_asset` - The asset being requested in exchange - /// * `note_type` - Whether the note is public or private - /// * `note_attachment` - Attachment data for the note - /// * `rng` - Random number generator for serial number - /// - /// # Returns - /// - /// Returns a `Note` that can be consumed by anyone willing to provide the requested asset. + /// This is a convenience method that constructs a [`PswapNote`] and converts it to a + /// protocol [`Note`]. /// /// # Errors /// - /// Returns an error if: - /// - Assets are invalid or have the same faucet ID - /// - Note construction fails + /// Returns an error if assets are invalid or have the same faucet ID. pub fn create( creator_account_id: AccountId, offered_asset: Asset, @@ -126,99 +337,54 @@ impl PswapNote { )); } - let note_script = Self::script(); - - // Build note storage (18 items) using the ASSET_KEY + ASSET_VALUE format - let tag = Self::build_tag(note_type, &offered_asset, &requested_asset); - let swapp_tag_felt = Felt::new(u32::from(tag) as u64); - let p2id_tag_felt = Self::compute_p2id_tag_felt(creator_account_id); - - let key_word = requested_asset.to_key_word(); - let value_word = requested_asset.to_value_word(); - - let inputs = vec![ - // ASSET_KEY [0-3] - key_word[0], - key_word[1], - key_word[2], - key_word[3], - // ASSET_VALUE [4-7] - value_word[0], - value_word[1], - value_word[2], - value_word[3], - // Tags [8-9] - swapp_tag_felt, - p2id_tag_felt, - // Padding [10-11] - ZERO, - ZERO, - // Swap count [12-15] - ZERO, - ZERO, - ZERO, - ZERO, - // Creator ID [16-17] - creator_account_id.prefix().as_felt(), - creator_account_id.suffix(), - ]; - - let note_inputs = NoteStorage::new(inputs)?; - - // Generate serial number - let serial_num = rng.draw_word(); - - // Build the outgoing note - let metadata = NoteMetadata::new(creator_account_id, note_type) - .with_tag(tag) - .with_attachment(note_attachment); - - let assets = NoteAssets::new(vec![offered_asset])?; - let recipient = NoteRecipient::new(serial_num, note_script, note_inputs); - let note = Note::new(assets, metadata, recipient); - - Ok(note) + let storage = PswapNoteStorage::new(requested_asset, creator_account_id); + let pswap = PswapNote::builder() + .sender(creator_account_id) + .storage(storage) + .serial_number(rng.draw_word()) + .note_type(note_type) + .assets(NoteAssets::new(vec![offered_asset])?) + .attachment(note_attachment) + .build(); + + Ok(Note::from(pswap)) } - /// Creates output notes when consuming a swap note (P2ID + optional remainder). + // INSTANCE METHODS + // -------------------------------------------------------------------------------------------- + + /// Executes the swap by creating output notes for a fill. /// /// Handles both full and partial fills: /// - **Full fill**: Returns P2ID note with full requested amount, no remainder - /// - **Partial fill**: Returns P2ID note with partial amount + remainder swap note + /// - **Partial fill**: Returns P2ID note with partial amount + remainder PswapNote /// /// # Arguments /// - /// * `original_swap_note` - The original swap note being consumed /// * `consumer_account_id` - The account consuming the swap note /// * `input_amount` - Amount debited from consumer's vault /// * `inflight_amount` - Amount added directly (no vault debit, for cross-swaps) /// /// # Returns /// - /// Returns a tuple of `(p2id_note, Option)` - pub fn create_output_notes( - original_swap_note: &Note, + /// Returns a tuple of `(p2id_note, Option)` + pub fn execute( + &self, consumer_account_id: AccountId, input_amount: u64, inflight_amount: u64, - ) -> Result<(Note, Option), NoteError> { - let inputs = original_swap_note.recipient().storage(); - let parsed = Self::parse_inputs(inputs.items())?; - let note_type = original_swap_note.metadata().note_type(); - + ) -> Result<(Note, Option), NoteError> { let fill_amount = input_amount + inflight_amount; - // Reconstruct requested faucet ID from ASSET_KEY - let requested_faucet_id = Self::faucet_id_from_key(&parsed.requested_key)?; - let total_requested_amount = Self::amount_from_value(&parsed.requested_value); + let requested_faucet_id = self.storage.requested_faucet_id()?; + let total_requested_amount = self.storage.requested_amount(); // Ensure offered asset exists and is fungible - let offered_assets = original_swap_note.assets(); - if offered_assets.num_assets() != 1 { + if self.assets.num_assets() != 1 { return Err(NoteError::other("Swap note must have exactly 1 offered asset")); } let offered_asset = - offered_assets.iter().next().ok_or(NoteError::other("No offered asset found"))?; + self.assets.iter().next().ok_or(NoteError::other("No offered asset found"))?; let (offered_faucet_id, total_offered_amount) = match offered_asset { Asset::Fungible(fa) => (fa.faucet_id(), fa.amount()), _ => return Err(NoteError::other("Non-fungible offered asset not supported")), @@ -236,42 +402,45 @@ impl PswapNote { ))); } - // Calculate proportional offered amount - let offered_amount_for_fill = Self::calculate_output_amount( + // Calculate offered amounts separately for input and inflight, matching the MASM + // which calls calculate_tokens_offered_for_requested twice. + let offered_for_input = Self::calculate_output_amount( + total_offered_amount, + total_requested_amount, + input_amount, + ); + let offered_for_inflight = Self::calculate_output_amount( total_offered_amount, total_requested_amount, - fill_amount, + inflight_amount, ); + let offered_amount_for_fill = offered_for_input + offered_for_inflight; - // Build the P2ID asset + // Build the P2ID payback note let payback_asset = Asset::Fungible(FungibleAsset::new(requested_faucet_id, fill_amount).map_err(|e| { - NoteError::other(alloc::format!("Failed to create P2ID asset: {}", e)) + NoteError::other_with_source("failed to create P2ID asset", e) })?); let aux_word = Word::from([Felt::new(fill_amount), ZERO, ZERO, ZERO]); - let p2id_note = Self::create_p2id_payback_note( - original_swap_note, + let p2id_note = self.build_p2id_payback_note( consumer_account_id, payback_asset, - note_type, - parsed.p2id_tag, aux_word, )?; // Create remainder note if partial fill - let remainder_note = if fill_amount < total_requested_amount { + let remainder = if fill_amount < total_requested_amount { let remaining_offered = total_offered_amount - offered_amount_for_fill; let remaining_requested = total_requested_amount - fill_amount; let remaining_offered_asset = Asset::Fungible(FungibleAsset::new(offered_faucet_id, remaining_offered).map_err( - |e| NoteError::other(alloc::format!("Failed to create remainder asset: {}", e)), + |e| NoteError::other_with_source("failed to create remainder asset", e), )?); - Some(Self::create_remainder_note( - original_swap_note, + Some(self.build_remainder_pswap_note( consumer_account_id, remaining_offered_asset, remaining_requested, @@ -281,136 +450,32 @@ impl PswapNote { None }; - Ok((p2id_note, remainder_note)) + Ok((p2id_note, remainder)) } - /// Creates a P2ID (Pay-to-ID) note for the swap creator as payback. + /// Calculates how many offered tokens a consumer receives for a given requested input. /// - /// Derives a unique serial number matching the MASM: `hmerge(swap_count_word, serial_num)`. - pub fn create_p2id_payback_note( - original_swap_note: &Note, - consumer_account_id: AccountId, - payback_asset: Asset, - note_type: NoteType, - p2id_tag: NoteTag, - aux_word: Word, - ) -> Result { - let inputs = original_swap_note.recipient().storage(); - let parsed = Self::parse_inputs(inputs.items())?; - - // Derive P2ID serial matching PSWAP.masm: - // hmerge([SWAP_COUNT_WORD (top), SERIAL_NUM (second)]) - // = Hasher::merge(&[swap_count_word, serial_num]) - // Word[0] = count+1, matching mem_loadw_le which puts mem[addr] into Word[0] - let swap_count_word = Word::from([Felt::new(parsed.swap_count + 1), ZERO, ZERO, ZERO]); - let original_serial = original_swap_note.recipient().serial_num(); - let p2id_serial_digest = Hasher::merge(&[swap_count_word.into(), original_serial.into()]); - let p2id_serial_num: Word = Word::from(p2id_serial_digest); - - // P2ID recipient targets the creator - let recipient = - P2idNoteStorage::new(parsed.creator_account_id).into_recipient(p2id_serial_num); - - let attachment = NoteAttachment::new_word(NoteAttachmentScheme::none(), aux_word); - - let p2id_assets = NoteAssets::new(vec![payback_asset])?; - let p2id_metadata = NoteMetadata::new(consumer_account_id, note_type) - .with_tag(p2id_tag) - .with_attachment(attachment); - - Ok(Note::new(p2id_assets, p2id_metadata, recipient)) - } - - /// Creates a remainder note for partial fills. - /// - /// Builds updated note storage with the remaining requested amount and incremented - /// swap count, using the ASSET_KEY + ASSET_VALUE format (18 items). - pub fn create_remainder_note( - original_swap_note: &Note, - consumer_account_id: AccountId, - remaining_offered_asset: Asset, - remaining_requested_amount: u64, - offered_amount_for_fill: u64, - ) -> Result { - let original_inputs = original_swap_note.recipient().storage(); - let parsed = Self::parse_inputs(original_inputs.items())?; - let note_type = original_swap_note.metadata().note_type(); - - // Build new requested asset with updated amount - let requested_faucet_id = Self::faucet_id_from_key(&parsed.requested_key)?; - let remaining_requested_asset = Asset::Fungible( - FungibleAsset::new(requested_faucet_id, remaining_requested_amount).map_err(|e| { - NoteError::other(alloc::format!( - "Failed to create remaining requested asset: {}", - e - )) - })?, - ); - - // Build new storage with updated amounts (18 items) - let key_word = remaining_requested_asset.to_key_word(); - let value_word = remaining_requested_asset.to_value_word(); - - let inputs = vec![ - // ASSET_KEY [0-3] - key_word[0], - key_word[1], - key_word[2], - key_word[3], - // ASSET_VALUE [4-7] - value_word[0], - value_word[1], - value_word[2], - value_word[3], - // Tags [8-9] (preserved) - Felt::new(u32::from(parsed.swapp_tag) as u64), - Felt::new(u32::from(parsed.p2id_tag) as u64), - // Padding [10-11] - ZERO, - ZERO, - // Swap count [12-15] (incremented) - Felt::new(parsed.swap_count + 1), - ZERO, - ZERO, - ZERO, - // Creator ID [16-17] (preserved) - parsed.creator_account_id.prefix().as_felt(), - parsed.creator_account_id.suffix(), - ]; - - let note_inputs = NoteStorage::new(inputs)?; - - // Remainder serial: increment top element of serial (matching MASM add.1 on Word[0]) - let original_serial = original_swap_note.recipient().serial_num(); - let remainder_serial_num = Word::from([ - Felt::new(original_serial[0].as_canonical_u64() + 1), - original_serial[1], - original_serial[2], - original_serial[3], - ]); - - let note_script = Self::script(); - let recipient = NoteRecipient::new(remainder_serial_num, note_script, note_inputs); - - // Build tag for the remainder note - let tag = Self::build_tag( - note_type, - &remaining_offered_asset, - &Asset::from(remaining_requested_asset), - ); - - let aux_word = Word::from([Felt::new(offered_amount_for_fill), ZERO, ZERO, ZERO]); - let attachment = NoteAttachment::new_word(NoteAttachmentScheme::none(), aux_word); + /// This is the Rust equivalent of `calculate_tokens_offered_for_requested` in pswap.masm. + pub fn calculate_offered_for_requested( + &self, + input_amount: u64, + ) -> Result { + let total_requested = self.storage.requested_amount(); - let metadata = NoteMetadata::new(consumer_account_id, note_type) - .with_tag(tag) - .with_attachment(attachment); + let offered_asset = self + .assets + .iter() + .next() + .ok_or(NoteError::other("No offered asset found"))?; + let total_offered = match offered_asset { + Asset::Fungible(fa) => fa.amount(), + _ => return Err(NoteError::other("Non-fungible offered asset not supported")), + }; - let assets = NoteAssets::new(vec![remaining_offered_asset])?; - Ok(Note::new(assets, metadata, recipient)) + Ok(Self::calculate_output_amount(total_offered, total_requested, input_amount)) } - // TAG CONSTRUCTION + // ASSOCIATED FUNCTIONS // -------------------------------------------------------------------------------------------- /// Returns a note tag for a pswap note with the specified parameters. @@ -448,94 +513,6 @@ impl PswapNote { NoteTag::new(tag) } - // HELPER FUNCTIONS - // -------------------------------------------------------------------------------------------- - - /// Computes the P2ID tag for routing payback notes to the creator. - fn compute_p2id_tag_felt(account_id: AccountId) -> Felt { - let p2id_tag = NoteTag::with_account_target(account_id); - Felt::new(u32::from(p2id_tag) as u64) - } - - /// Extracts the faucet ID from an ASSET_KEY word. - fn faucet_id_from_key(key: &Word) -> Result { - // asset::key_into_faucet_id extracts [suffix, prefix] from the key. - // Key layout: [key[0], key[1], faucet_suffix, faucet_prefix] - // key[2] = suffix, key[3] = prefix (after key_into_faucet_id drops top 2) - AccountId::try_from_elements(key[2], key[3]).map_err(|e| { - NoteError::other(alloc::format!("Failed to parse faucet ID from key: {}", e)) - }) - } - - /// Extracts the amount from an ASSET_VALUE word. - fn amount_from_value(value: &Word) -> u64 { - // ASSET_VALUE[0] = amount (from asset::fungible_to_amount) - value[0].as_canonical_u64() - } - - // PARSING FUNCTIONS - // -------------------------------------------------------------------------------------------- - - /// Parses note storage items to extract swap parameters. - /// - /// # Arguments - /// - /// * `inputs` - The note storage items (must be exactly 18 Felts) - /// - /// # Errors - /// - /// Returns an error if input length is not 18 or account ID construction fails. - pub fn parse_inputs(inputs: &[Felt]) -> Result { - if inputs.len() != Self::NUM_STORAGE_ITEMS { - return Err(NoteError::other(alloc::format!( - "PSWAP note should have {} storage items, but {} were provided", - Self::NUM_STORAGE_ITEMS, - inputs.len() - ))); - } - - let requested_key = Word::from([inputs[0], inputs[1], inputs[2], inputs[3]]); - let requested_value = Word::from([inputs[4], inputs[5], inputs[6], inputs[7]]); - let swapp_tag = NoteTag::new(inputs[8].as_canonical_u64() as u32); - let p2id_tag = NoteTag::new(inputs[9].as_canonical_u64() as u32); - let swap_count = inputs[12].as_canonical_u64(); - - let creator_account_id = - AccountId::try_from_elements(inputs[17], inputs[16]).map_err(|e| { - NoteError::other(alloc::format!("Failed to parse creator account ID: {}", e)) - })?; - - Ok(PswapParsedInputs { - requested_key, - requested_value, - swapp_tag, - p2id_tag, - swap_count, - creator_account_id, - }) - } - - /// Extracts the requested asset from note storage. - pub fn get_requested_asset(inputs: &[Felt]) -> Result { - let parsed = Self::parse_inputs(inputs)?; - let faucet_id = Self::faucet_id_from_key(&parsed.requested_key)?; - let amount = Self::amount_from_value(&parsed.requested_value); - Ok(Asset::Fungible(FungibleAsset::new(faucet_id, amount).map_err(|e| { - NoteError::other(alloc::format!("Failed to create asset: {}", e)) - })?)) - } - - /// Extracts the creator account ID from note storage. - pub fn get_creator_account_id(inputs: &[Felt]) -> Result { - Ok(Self::parse_inputs(inputs)?.creator_account_id) - } - - /// Checks if the given account is the creator of this swap note. - pub fn is_creator(inputs: &[Felt], account_id: AccountId) -> Result { - let creator_id = Self::get_creator_account_id(inputs)?; - Ok(creator_id == account_id) - } - /// Calculates the output amount for a fill using u64 integer arithmetic /// with a precision factor of 1e5 (matching the MASM on-chain calculation). pub fn calculate_output_amount( @@ -558,41 +535,139 @@ impl PswapNote { } } - /// Calculates how many offered tokens a consumer receives for a given requested input, - /// reading the offered and requested totals directly from the swap note. - /// - /// This is the Rust equivalent of `calculate_tokens_offered_for_requested` in pswap.masm. - /// - /// # Arguments - /// - /// * `swap_note` - The PSWAP note being consumed - /// * `input_amount` - Amount of requested asset the consumer is providing - /// - /// # Returns + /// Builds a P2ID (Pay-to-ID) payback note for the swap creator. /// - /// The proportional amount of offered asset the consumer will receive. - /// - /// # Errors + /// The P2ID note inherits the note type from this PSWAP note. + /// Derives a unique serial number matching the MASM: `hmerge(swap_count_word, serial_num)`. + pub fn build_p2id_payback_note( + &self, + consumer_account_id: AccountId, + payback_asset: Asset, + aux_word: Word, + ) -> Result { + let p2id_tag = self.storage.p2id_tag(); + // Derive P2ID serial matching PSWAP.masm + let swap_count_word = + Word::from([Felt::new(self.storage.swap_count + 1), ZERO, ZERO, ZERO]); + let p2id_serial_digest = + Hasher::merge(&[swap_count_word.into(), self.serial_number.into()]); + let p2id_serial_num: Word = Word::from(p2id_serial_digest); + + // P2ID recipient targets the creator + let recipient = + P2idNoteStorage::new(self.storage.creator_account_id).into_recipient(p2id_serial_num); + + let attachment = NoteAttachment::new_word(NoteAttachmentScheme::none(), aux_word); + + let p2id_assets = NoteAssets::new(vec![payback_asset])?; + let p2id_metadata = NoteMetadata::new(consumer_account_id, self.note_type) + .with_tag(p2id_tag) + .with_attachment(attachment); + + Ok(Note::new(p2id_assets, p2id_metadata, recipient)) + } + + /// Builds a remainder note for partial fills. /// - /// Returns an error if the note storage cannot be parsed or the offered asset is invalid. - pub fn calculate_offered_for_requested( - swap_note: &Note, - input_amount: u64, - ) -> Result { - let parsed = Self::parse_inputs(swap_note.recipient().storage().items())?; - let total_requested = Self::amount_from_value(&parsed.requested_value); + /// Builds updated note storage with the remaining requested amount and incremented + /// swap count, returning a [`PswapNote`] that can be converted to a protocol [`Note`]. + pub fn build_remainder_pswap_note( + &self, + consumer_account_id: AccountId, + remaining_offered_asset: Asset, + remaining_requested_amount: u64, + offered_amount_for_fill: u64, + ) -> Result { + let requested_faucet_id = self.storage.requested_faucet_id()?; + let remaining_requested_asset = Asset::Fungible( + FungibleAsset::new(requested_faucet_id, remaining_requested_amount).map_err(|e| { + NoteError::other_with_source("failed to create remaining requested asset", e) + })?, + ); - let offered_asset = swap_note - .assets() + let key_word = remaining_requested_asset.to_key_word(); + let value_word = remaining_requested_asset.to_value_word(); + + let new_storage = PswapNoteStorage::from_parts( + key_word, + value_word, + self.storage.pswap_tag, + self.storage.p2id_tag, + self.storage.swap_count + 1, + self.storage.creator_account_id, + ); + + // Remainder serial: increment top element (matching MASM add.1 on Word[0]) + let remainder_serial_num = Word::from([ + Felt::new(self.serial_number[0].as_canonical_u64() + 1), + self.serial_number[1], + self.serial_number[2], + self.serial_number[3], + ]); + + let aux_word = Word::from([Felt::new(offered_amount_for_fill), ZERO, ZERO, ZERO]); + let attachment = NoteAttachment::new_word(NoteAttachmentScheme::none(), aux_word); + + let assets = NoteAssets::new(vec![remaining_offered_asset])?; + + Ok(PswapNote { + sender: consumer_account_id, + storage: new_storage, + serial_number: remainder_serial_num, + note_type: self.note_type, + assets, + attachment, + }) + } +} + +// CONVERSIONS +// ================================================================================================ + +impl From for Note { + fn from(pswap: PswapNote) -> Self { + let offered_asset = pswap + .assets .iter() .next() - .ok_or(NoteError::other("No offered asset found"))?; - let total_offered = match offered_asset { - Asset::Fungible(fa) => fa.amount(), - _ => return Err(NoteError::other("Non-fungible offered asset not supported")), - }; + .expect("PswapNote must have an offered asset"); + let requested_asset = pswap + .storage + .requested_asset() + .expect("PswapNote must have a valid requested asset"); + let tag = PswapNote::build_tag(pswap.note_type, &offered_asset, &requested_asset); - Ok(Self::calculate_output_amount(total_offered, total_requested, input_amount)) + let storage = pswap.storage.with_pswap_tag(tag); + let recipient = storage.into_recipient(pswap.serial_number); + + let metadata = NoteMetadata::new(pswap.sender, pswap.note_type) + .with_tag(tag) + .with_attachment(pswap.attachment); + + Note::new(pswap.assets, metadata, recipient) + } +} + +impl From<&PswapNote> for Note { + fn from(pswap: &PswapNote) -> Self { + Note::from(pswap.clone()) + } +} + +impl TryFrom<&Note> for PswapNote { + type Error = NoteError; + + fn try_from(note: &Note) -> Result { + let storage = PswapNoteStorage::try_from(note.recipient().storage().items())?; + + Ok(Self { + sender: note.metadata().sender(), + storage, + serial_number: note.recipient().serial_num(), + note_type: note.metadata().note_type(), + assets: note.assets().clone(), + attachment: note.metadata().attachment().clone(), + }) } } @@ -663,7 +738,70 @@ mod tests { assert_eq!(note.recipient().script().root(), script.root()); // Verify storage has 18 items - assert_eq!(note.recipient().storage().num_items(), PswapNote::NUM_STORAGE_ITEMS as u16,); + assert_eq!( + note.recipient().storage().num_items(), + PswapNote::NUM_STORAGE_ITEMS as u16, + ); + } + + #[test] + fn pswap_note_builder() { + let mut offered_faucet_bytes = [0; 15]; + offered_faucet_bytes[0] = 0xaa; + + let mut requested_faucet_bytes = [0; 15]; + requested_faucet_bytes[0] = 0xbb; + + let offered_faucet_id = AccountId::dummy( + offered_faucet_bytes, + AccountIdVersion::Version0, + AccountType::FungibleFaucet, + AccountStorageMode::Public, + ); + + let requested_faucet_id = AccountId::dummy( + requested_faucet_bytes, + AccountIdVersion::Version0, + AccountType::FungibleFaucet, + AccountStorageMode::Public, + ); + + let creator_id = AccountId::dummy( + [1; 15], + AccountIdVersion::Version0, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Public, + ); + + let offered_asset = Asset::Fungible(FungibleAsset::new(offered_faucet_id, 1000).unwrap()); + let requested_asset = + Asset::Fungible(FungibleAsset::new(requested_faucet_id, 500).unwrap()); + + use miden_protocol::crypto::rand::{FeltRng, RpoRandomCoin}; + let mut rng = RpoRandomCoin::new(Word::default()); + + let storage = PswapNoteStorage::new(requested_asset, creator_id); + let pswap = PswapNote::builder() + .sender(creator_id) + .storage(storage) + .serial_number(rng.draw_word()) + .note_type(NoteType::Public) + .assets(NoteAssets::new(vec![offered_asset]).unwrap()) + .build(); + + assert_eq!(pswap.sender(), creator_id); + assert_eq!(pswap.note_type(), NoteType::Public); + assert_eq!(pswap.assets().num_assets(), 1); + + // Convert to Note + let note: Note = pswap.into(); + assert_eq!(note.metadata().sender(), creator_id); + assert_eq!(note.metadata().note_type(), NoteType::Public); + assert_eq!(note.assets().num_assets(), 1); + assert_eq!( + note.recipient().storage().num_items(), + PswapNote::NUM_STORAGE_ITEMS as u16, + ); } #[test] @@ -726,7 +864,7 @@ mod tests { } #[test] - fn parse_inputs_v014_format() { + fn pswap_note_storage_try_from() { let creator_id = AccountId::dummy( [1; 15], AccountIdVersion::Version0, @@ -754,7 +892,7 @@ mod tests { value_word[1], value_word[2], value_word[3], - Felt::new(0xC0000000), // swapp_tag + Felt::new(0xC0000000), // pswap_tag Felt::new(0x80000001), // p2id_tag ZERO, ZERO, @@ -766,12 +904,41 @@ mod tests { creator_id.suffix(), ]; - let parsed = PswapNote::parse_inputs(&inputs).unwrap(); - assert_eq!(parsed.swap_count, 3); - assert_eq!(parsed.creator_account_id, creator_id); + let parsed = PswapNoteStorage::try_from(inputs.as_slice()).unwrap(); + assert_eq!(parsed.swap_count(), 3); + assert_eq!(parsed.creator_account_id(), creator_id); assert_eq!( - parsed.requested_key, + parsed.requested_key(), Word::from([key_word[0], key_word[1], key_word[2], key_word[3]]) ); + assert_eq!(parsed.requested_amount(), 500); + } + + #[test] + fn pswap_note_storage_roundtrip() { + let creator_id = AccountId::dummy( + [1; 15], + AccountIdVersion::Version0, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Public, + ); + + let faucet_id = AccountId::dummy( + [0xaa; 15], + AccountIdVersion::Version0, + AccountType::FungibleFaucet, + AccountStorageMode::Public, + ); + + let requested_asset = Asset::Fungible(FungibleAsset::new(faucet_id, 500).unwrap()); + let storage = PswapNoteStorage::new(requested_asset, creator_id); + + // Convert to NoteStorage and back + let note_storage = NoteStorage::from(storage.clone()); + let parsed = PswapNoteStorage::try_from(note_storage.items()).unwrap(); + + assert_eq!(parsed.creator_account_id(), creator_id); + assert_eq!(parsed.swap_count(), 0); + assert_eq!(parsed.requested_amount(), 500); } } diff --git a/crates/miden-testing/tests/scripts/pswap.rs b/crates/miden-testing/tests/scripts/pswap.rs index 8a8d664070..b577d7b0db 100644 --- a/crates/miden-testing/tests/scripts/pswap.rs +++ b/crates/miden-testing/tests/scripts/pswap.rs @@ -8,7 +8,7 @@ use miden_protocol::note::{ }; use miden_protocol::transaction::RawOutputNote; use miden_protocol::{Felt, Word, ZERO}; -use miden_standards::note::PswapNote; +use miden_standards::note::{PswapNote, PswapNoteStorage}; use miden_testing::{Auth, MockChain}; use crate::prove_and_verify_transaction; @@ -53,7 +53,9 @@ fn create_pswap_note_with_type( note_type: NoteType, ) -> Note { let offered_asset = *note_assets.iter().next().expect("must have offered asset"); - let requested_asset = PswapNote::get_requested_asset(&storage_items) + let requested_asset = PswapNoteStorage::try_from(storage_items.as_slice()) + .expect("Failed to parse storage") + .requested_asset() .expect("Failed to parse requested asset from storage"); use miden_protocol::crypto::rand::RpoRandomCoin; @@ -80,7 +82,7 @@ fn calculate_output_amount(offered_total: u64, requested_total: u64, input_amoun fn build_pswap_storage( requested_faucet_id: AccountId, requested_amount: u64, - _swapp_tag_felt: Felt, + _pswap_tag_felt: Felt, _p2id_tag_felt: Felt, swap_count: u64, creator_id: AccountId, @@ -95,14 +97,14 @@ fn build_pswap_storage( let key_word = requested_asset.to_key_word(); let value_word = requested_asset.to_value_word(); let tag = PswapNote::build_tag(NoteType::Public, &offered_dummy, &requested_asset); - let swapp_tag_felt = Felt::new(u32::from(tag) as u64); + let pswap_tag_felt = Felt::new(u32::from(tag) as u64); let p2id_tag = NoteTag::with_account_target(creator_id); let p2id_tag_felt = Felt::new(u32::from(p2id_tag) as u64); vec![ key_word[0], key_word[1], key_word[2], key_word[3], value_word[0], value_word[1], value_word[2], value_word[3], - swapp_tag_felt, p2id_tag_felt, + pswap_tag_felt, p2id_tag_felt, ZERO, ZERO, Felt::new(swap_count), ZERO, ZERO, ZERO, creator_id.prefix().as_felt(), creator_id.suffix(), @@ -117,7 +119,7 @@ fn create_expected_pswap_p2id_note( _swap_count: u64, total_fill: u64, requested_faucet_id: AccountId, - p2id_tag: NoteTag, + _p2id_tag: NoteTag, ) -> anyhow::Result { let note_type = swap_note.metadata().note_type(); create_expected_pswap_p2id_note_with_type( @@ -127,12 +129,13 @@ fn create_expected_pswap_p2id_note( _swap_count, total_fill, requested_faucet_id, - p2id_tag, note_type, ) } -/// Create expected P2ID note with explicit note type via PswapNote::create_p2id_payback_note. +/// Create expected P2ID note via PswapNote::build_p2id_payback_note. +/// +/// The P2ID note inherits its note type from the swap note. fn create_expected_pswap_p2id_note_with_type( swap_note: &Note, consumer_id: AccountId, @@ -140,15 +143,13 @@ fn create_expected_pswap_p2id_note_with_type( _swap_count: u64, total_fill: u64, requested_faucet_id: AccountId, - p2id_tag: NoteTag, - note_type: NoteType, + _note_type: NoteType, ) -> anyhow::Result { + let pswap = PswapNote::try_from(swap_note)?; let payback_asset = Asset::Fungible(FungibleAsset::new(requested_faucet_id, total_fill)?); let aux_word = Word::from([Felt::new(total_fill), ZERO, ZERO, ZERO]); - Ok(PswapNote::create_p2id_payback_note( - swap_note, consumer_id, payback_asset, note_type, p2id_tag, aux_word, - )?) + Ok(pswap.build_p2id_payback_note(consumer_id, payback_asset, aux_word)?) } /// Create NoteAssets with a single fungible asset @@ -159,7 +160,7 @@ fn make_note_assets(faucet_id: AccountId, amount: u64) -> anyhow::Result (NoteTag, Felt) { +fn make_pswap_tag() -> (NoteTag, Felt) { let tag = NoteTag::new(0xC0000000); let felt = Felt::new(u32::from(tag) as u64); (tag, felt) @@ -187,20 +188,20 @@ fn create_expected_pswap_remainder_note( _swap_count: u64, offered_faucet_id: AccountId, _requested_faucet_id: AccountId, - _swapp_tag: NoteTag, - _swapp_tag_felt: Felt, + _pswap_tag: NoteTag, + _pswap_tag_felt: Felt, _p2id_tag_felt: Felt, ) -> anyhow::Result { + let pswap = PswapNote::try_from(swap_note)?; let remaining_offered_asset = Asset::Fungible(FungibleAsset::new(offered_faucet_id, remaining_offered)?); - Ok(PswapNote::create_remainder_note( - swap_note, + Ok(Note::from(pswap.build_remainder_pswap_note( consumer_id, remaining_offered_asset, remaining_requested, offered_out, - )?) + )?)) } // TESTS @@ -223,19 +224,19 @@ async fn pswap_note_full_fill_test() -> anyhow::Result<()> { [FungibleAsset::new(eth_faucet.id(), 25)?.into()], )?; - let (swapp_tag, swapp_tag_felt) = make_swapp_tag(); + let (pswap_tag, pswap_tag_felt) = make_pswap_tag(); let p2id_tag_felt = compute_p2id_tag_felt(alice.id()); let storage_items = build_pswap_storage( eth_faucet.id(), 25, - swapp_tag_felt, + pswap_tag_felt, p2id_tag_felt, 0, alice.id(), ); let note_assets = make_note_assets(usdc_faucet.id(), 50)?; - let swap_note = create_pswap_note(alice.id(), note_assets, storage_items, swapp_tag); + let swap_note = create_pswap_note(alice.id(), note_assets, storage_items, pswap_tag); builder.add_output_note(RawOutputNote::Full(swap_note.clone())); let mut mock_chain = builder.build()?; @@ -317,13 +318,13 @@ async fn pswap_note_private_full_fill_test() -> anyhow::Result<()> { [FungibleAsset::new(eth_faucet.id(), 25)?.into()], )?; - let (swapp_tag, swapp_tag_felt) = make_swapp_tag(); + let (pswap_tag, pswap_tag_felt) = make_pswap_tag(); let p2id_tag_felt = compute_p2id_tag_felt(alice.id()); let storage_items = build_pswap_storage( eth_faucet.id(), 25, - swapp_tag_felt, + pswap_tag_felt, p2id_tag_felt, 0, alice.id(), @@ -335,7 +336,7 @@ async fn pswap_note_private_full_fill_test() -> anyhow::Result<()> { alice.id(), note_assets, storage_items, - swapp_tag, + pswap_tag, NoteType::Private, ); builder.add_output_note(RawOutputNote::Full(swap_note.clone())); @@ -417,19 +418,19 @@ async fn pswap_note_partial_fill_test() -> anyhow::Result<()> { [FungibleAsset::new(eth_faucet.id(), 20)?.into()], )?; - let (swapp_tag, swapp_tag_felt) = make_swapp_tag(); + let (pswap_tag, pswap_tag_felt) = make_pswap_tag(); let p2id_tag_felt = compute_p2id_tag_felt(alice.id()); let storage_items = build_pswap_storage( eth_faucet.id(), 25, - swapp_tag_felt, + pswap_tag_felt, p2id_tag_felt, 0, alice.id(), ); let note_assets = make_note_assets(usdc_faucet.id(), 50)?; - let swap_note = create_pswap_note(alice.id(), note_assets, storage_items, swapp_tag); + let swap_note = create_pswap_note(alice.id(), note_assets, storage_items, pswap_tag); builder.add_output_note(RawOutputNote::Full(swap_note.clone())); let mut mock_chain = builder.build()?; @@ -459,8 +460,8 @@ async fn pswap_note_partial_fill_test() -> anyhow::Result<()> { 0, usdc_faucet.id(), eth_faucet.id(), - swapp_tag, - swapp_tag_felt, + pswap_tag, + pswap_tag_felt, p2id_tag_felt, )?; @@ -542,13 +543,13 @@ async fn pswap_note_inflight_cross_swap_test() -> anyhow::Result<()> { )?; let charlie = builder.add_existing_wallet_with_assets(BASIC_AUTH, [])?; - let (swapp_tag, swapp_tag_felt) = make_swapp_tag(); + let (pswap_tag, pswap_tag_felt) = make_pswap_tag(); // Alice's note: offers 50 USDC, requests 25 ETH let alice_storage = build_pswap_storage( eth_faucet.id(), 25, - swapp_tag_felt, + pswap_tag_felt, compute_p2id_tag_felt(alice.id()), 0, alice.id(), @@ -557,7 +558,7 @@ async fn pswap_note_inflight_cross_swap_test() -> anyhow::Result<()> { alice.id(), make_note_assets(usdc_faucet.id(), 50)?, alice_storage, - swapp_tag, + pswap_tag, ); builder.add_output_note(RawOutputNote::Full(alice_swap_note.clone())); @@ -565,7 +566,7 @@ async fn pswap_note_inflight_cross_swap_test() -> anyhow::Result<()> { let bob_storage = build_pswap_storage( usdc_faucet.id(), 50, - swapp_tag_felt, + pswap_tag_felt, compute_p2id_tag_felt(bob.id()), 0, bob.id(), @@ -574,7 +575,7 @@ async fn pswap_note_inflight_cross_swap_test() -> anyhow::Result<()> { bob.id(), make_note_assets(eth_faucet.id(), 25)?, bob_storage, - swapp_tag, + pswap_tag, ); builder.add_output_note(RawOutputNote::Full(bob_swap_note.clone())); @@ -665,13 +666,13 @@ async fn pswap_note_creator_reclaim_test() -> anyhow::Result<()> { [FungibleAsset::new(usdc_faucet.id(), 50)?.into()], )?; - let (swapp_tag, swapp_tag_felt) = make_swapp_tag(); + let (pswap_tag, pswap_tag_felt) = make_pswap_tag(); let p2id_tag_felt = compute_p2id_tag_felt(alice.id()); let storage_items = build_pswap_storage( _eth_faucet.id(), 25, - swapp_tag_felt, + pswap_tag_felt, p2id_tag_felt, 0, alice.id(), @@ -680,7 +681,7 @@ async fn pswap_note_creator_reclaim_test() -> anyhow::Result<()> { alice.id(), make_note_assets(usdc_faucet.id(), 50)?, storage_items, - swapp_tag, + pswap_tag, ); builder.add_output_note(RawOutputNote::Full(swap_note.clone())); @@ -727,13 +728,13 @@ async fn pswap_note_invalid_input_test() -> anyhow::Result<()> { [FungibleAsset::new(eth_faucet.id(), 30)?.into()], )?; - let (swapp_tag, swapp_tag_felt) = make_swapp_tag(); + let (pswap_tag, pswap_tag_felt) = make_pswap_tag(); let p2id_tag_felt = compute_p2id_tag_felt(alice.id()); let storage_items = build_pswap_storage( eth_faucet.id(), 25, - swapp_tag_felt, + pswap_tag_felt, p2id_tag_felt, 0, alice.id(), @@ -742,7 +743,7 @@ async fn pswap_note_invalid_input_test() -> anyhow::Result<()> { alice.id(), make_note_assets(usdc_faucet.id(), 50)?, storage_items, - swapp_tag, + pswap_tag, ); builder.add_output_note(RawOutputNote::Full(swap_note.clone())); let mock_chain = builder.build()?; @@ -796,13 +797,13 @@ async fn pswap_note_multiple_partial_fills_test() -> anyhow::Result<()> { [FungibleAsset::new(eth_faucet.id(), input_amount)?.into()], )?; - let (swapp_tag, swapp_tag_felt) = make_swapp_tag(); + let (pswap_tag, pswap_tag_felt) = make_pswap_tag(); let p2id_tag_felt = compute_p2id_tag_felt(alice.id()); let storage_items = build_pswap_storage( eth_faucet.id(), 25, - swapp_tag_felt, + pswap_tag_felt, p2id_tag_felt, 0, alice.id(), @@ -811,7 +812,7 @@ async fn pswap_note_multiple_partial_fills_test() -> anyhow::Result<()> { alice.id(), make_note_assets(usdc_faucet.id(), 50)?, storage_items, - swapp_tag, + pswap_tag, ); builder.add_output_note(RawOutputNote::Full(swap_note.clone())); @@ -847,8 +848,8 @@ async fn pswap_note_multiple_partial_fills_test() -> anyhow::Result<()> { 0, usdc_faucet.id(), eth_faucet.id(), - swapp_tag, - swapp_tag_felt, + pswap_tag, + pswap_tag_felt, p2id_tag_felt, )?; expected_notes.push(RawOutputNote::Full(remainder_note)); @@ -899,20 +900,20 @@ async fn pswap_note_non_exact_ratio_partial_fill_test() -> anyhow::Result<()> { [FungibleAsset::new(eth_faucet.id(), input_amount)?.into()], )?; - let swapp_tag = NoteTag::new(0xC0000000); - let swapp_tag_felt = Felt::new(u32::from(swapp_tag) as u64); + let pswap_tag = NoteTag::new(0xC0000000); + let pswap_tag_felt = Felt::new(u32::from(pswap_tag) as u64); let p2id_tag_felt = compute_p2id_tag_felt(alice.id()); let storage_items = build_pswap_storage( eth_faucet.id(), requested_total, - swapp_tag_felt, + pswap_tag_felt, p2id_tag_felt, 0, alice.id(), ); let note_assets = make_note_assets(usdc_faucet.id(), offered_total)?; - let swap_note = create_pswap_note(alice.id(), note_assets, storage_items, swapp_tag); + let swap_note = create_pswap_note(alice.id(), note_assets, storage_items, pswap_tag); builder.add_output_note(RawOutputNote::Full(swap_note.clone())); let mock_chain = builder.build()?; @@ -942,8 +943,8 @@ async fn pswap_note_non_exact_ratio_partial_fill_test() -> anyhow::Result<()> { 0, usdc_faucet.id(), eth_faucet.id(), - swapp_tag, - swapp_tag_felt, + pswap_tag, + pswap_tag_felt, p2id_tag_felt, )?; @@ -1040,20 +1041,20 @@ async fn pswap_note_partial_fill_non_integer_ratio_fuzz_test() -> anyhow::Result [FungibleAsset::new(eth_faucet.id(), *fill_eth)?.into()], )?; - let swapp_tag = NoteTag::new(0xC0000000); - let swapp_tag_felt = Felt::new(u32::from(swapp_tag) as u64); + let pswap_tag = NoteTag::new(0xC0000000); + let pswap_tag_felt = Felt::new(u32::from(pswap_tag) as u64); let p2id_tag_felt = compute_p2id_tag_felt(alice.id()); let storage_items = build_pswap_storage( eth_faucet.id(), *requested_eth, - swapp_tag_felt, + pswap_tag_felt, p2id_tag_felt, 0, alice.id(), ); let note_assets = make_note_assets(usdc_faucet.id(), *offered_usdc)?; - let swap_note = create_pswap_note(alice.id(), note_assets, storage_items, swapp_tag); + let swap_note = create_pswap_note(alice.id(), note_assets, storage_items, pswap_tag); builder.add_output_note(RawOutputNote::Full(swap_note.clone())); let mock_chain = builder.build()?; @@ -1083,8 +1084,8 @@ async fn pswap_note_partial_fill_non_integer_ratio_fuzz_test() -> anyhow::Result 0, usdc_faucet.id(), eth_faucet.id(), - swapp_tag, - swapp_tag_felt, + pswap_tag, + pswap_tag_felt, p2id_tag_felt, )?; expected_notes.push(RawOutputNote::Full(remainder)); @@ -1192,14 +1193,14 @@ async fn pswap_note_chained_partial_fills_non_integer_ratio_test() -> anyhow::Re [FungibleAsset::new(eth_faucet.id(), *fill_amount)?.into()], )?; - let swapp_tag = NoteTag::new(0xC0000000); - let swapp_tag_felt = Felt::new(u32::from(swapp_tag) as u64); + let pswap_tag = NoteTag::new(0xC0000000); + let pswap_tag_felt = Felt::new(u32::from(pswap_tag) as u64); let p2id_tag_felt = compute_p2id_tag_felt(alice.id()); let storage_items = build_pswap_storage( eth_faucet.id(), current_requested, - swapp_tag_felt, + pswap_tag_felt, p2id_tag_felt, current_swap_count, alice.id(), @@ -1211,7 +1212,7 @@ async fn pswap_note_chained_partial_fills_non_integer_ratio_test() -> anyhow::Re let recipient = NoteRecipient::new(current_serial, PswapNote::script(), note_storage); let metadata = - NoteMetadata::new(alice.id(), NoteType::Public).with_tag(swapp_tag); + NoteMetadata::new(alice.id(), NoteType::Public).with_tag(pswap_tag); let swap_note = Note::new(note_assets, metadata, recipient); builder.add_output_note(RawOutputNote::Full(swap_note.clone())); @@ -1242,8 +1243,8 @@ async fn pswap_note_chained_partial_fills_non_integer_ratio_test() -> anyhow::Re current_swap_count, usdc_faucet.id(), eth_faucet.id(), - swapp_tag, - swapp_tag_felt, + pswap_tag, + pswap_tag_felt, p2id_tag_felt, )?; expected_notes.push(RawOutputNote::Full(remainder)); @@ -1368,15 +1369,16 @@ fn compare_pswap_create_output_notes_vs_test_helper() { .unwrap(); // Create output notes using library - let (lib_p2id, _) = PswapNote::create_output_notes(&swap_note_lib, bob.id(), 25, 0).unwrap(); + let pswap = PswapNote::try_from(&swap_note_lib).unwrap(); + let (lib_p2id, _) = pswap.execute(bob.id(), 25, 0).unwrap(); // Create same swap note using test helper (same serial) - let (_swapp_tag, swapp_tag_felt) = make_swapp_tag(); + let (_pswap_tag, pswap_tag_felt) = make_pswap_tag(); let p2id_tag_felt = compute_p2id_tag_felt(alice.id()); let storage_items = build_pswap_storage( eth_faucet.id(), 25, - swapp_tag_felt, + pswap_tag_felt, p2id_tag_felt, 0, alice.id(), @@ -1470,12 +1472,11 @@ fn pswap_parse_inputs_roundtrip() { let storage = swap_note.recipient().storage(); let items = storage.items(); - let parsed = PswapNote::parse_inputs(items).unwrap(); + let parsed = PswapNoteStorage::try_from(items).unwrap(); - assert_eq!(parsed.creator_account_id, alice.id(), "Creator ID roundtrip failed!"); - assert_eq!(parsed.swap_count, 0, "Swap count should be 0"); + assert_eq!(parsed.creator_account_id(), alice.id(), "Creator ID roundtrip failed!"); + assert_eq!(parsed.swap_count(), 0, "Swap count should be 0"); // Verify requested amount from value word - let requested_amount = parsed.requested_value[0].as_canonical_u64(); - assert_eq!(requested_amount, 25, "Requested amount should be 25"); + assert_eq!(parsed.requested_amount(), 25, "Requested amount should be 25"); } From 02270752e9cc42cb6865b021311209a394eec6ae Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Fri, 20 Mar 2026 09:06:43 +0530 Subject: [PATCH 3/6] refactor: rename swapp_tag to pswap_tag, NUM_ITEMS to NUM_STORAGE_ITEMS, remove create_ wrappers, add builder finish_fn - Rename swapp_tag -> pswap_tag and SWAPp -> PSWAP throughout - Rename NUM_ITEMS -> NUM_STORAGE_ITEMS for clarity - Remove create_p2id_payback_note and create_remainder_note wrappers, make build_ functions public instead - Compute p2id_tag inside build_p2id_payback_note from self.storage - Add #[builder(finish_fn(vis = "", name = build_internal))] to PswapNote --- crates/miden-standards/src/note/pswap.rs | 5 +- crates/miden-testing/tests/scripts/pswap.rs | 242 ++++++-------------- 2 files changed, 69 insertions(+), 178 deletions(-) diff --git a/crates/miden-standards/src/note/pswap.rs b/crates/miden-standards/src/note/pswap.rs index f3a8ae1668..ae68ca2cea 100644 --- a/crates/miden-standards/src/note/pswap.rs +++ b/crates/miden-standards/src/note/pswap.rs @@ -247,6 +247,7 @@ impl TryFrom<&[Felt]> for PswapNoteStorage { /// - Unfilled portions create remainder notes /// - Creator receives requested assets via P2ID notes #[derive(Debug, Clone, bon::Builder)] +#[builder(finish_fn(vis = "", name = build_internal))] pub struct PswapNote { sender: AccountId, storage: PswapNoteStorage, @@ -345,7 +346,7 @@ impl PswapNote { .note_type(note_type) .assets(NoteAssets::new(vec![offered_asset])?) .attachment(note_attachment) - .build(); + .build_internal(); Ok(Note::from(pswap)) } @@ -787,7 +788,7 @@ mod tests { .serial_number(rng.draw_word()) .note_type(NoteType::Public) .assets(NoteAssets::new(vec![offered_asset]).unwrap()) - .build(); + .build_internal(); assert_eq!(pswap.sender(), creator_id); assert_eq!(pswap.note_type(), NoteType::Public); diff --git a/crates/miden-testing/tests/scripts/pswap.rs b/crates/miden-testing/tests/scripts/pswap.rs index b577d7b0db..b4a544128a 100644 --- a/crates/miden-testing/tests/scripts/pswap.rs +++ b/crates/miden-testing/tests/scripts/pswap.rs @@ -1,7 +1,7 @@ use std::collections::BTreeMap; -use miden_protocol::account::auth::AuthScheme; use miden_protocol::account::AccountId; +use miden_protocol::account::auth::AuthScheme; use miden_protocol::asset::{Asset, FungibleAsset}; use miden_protocol::note::{ Note, NoteAssets, NoteAttachment, NoteMetadata, NoteRecipient, NoteStorage, NoteTag, NoteType, @@ -91,9 +91,8 @@ fn build_pswap_storage( FungibleAsset::new(requested_faucet_id, requested_amount) .expect("Failed to create requested fungible asset"), ); - let offered_dummy = Asset::Fungible( - FungibleAsset::new(requested_faucet_id, 1).expect("dummy offered asset"), - ); + let offered_dummy = + Asset::Fungible(FungibleAsset::new(requested_faucet_id, 1).expect("dummy offered asset")); let key_word = requested_asset.to_key_word(); let value_word = requested_asset.to_value_word(); let tag = PswapNote::build_tag(NoteType::Public, &offered_dummy, &requested_asset); @@ -102,12 +101,24 @@ fn build_pswap_storage( let p2id_tag_felt = Felt::new(u32::from(p2id_tag) as u64); vec![ - key_word[0], key_word[1], key_word[2], key_word[3], - value_word[0], value_word[1], value_word[2], value_word[3], - pswap_tag_felt, p2id_tag_felt, - ZERO, ZERO, - Felt::new(swap_count), ZERO, ZERO, ZERO, - creator_id.prefix().as_felt(), creator_id.suffix(), + key_word[0], + key_word[1], + key_word[2], + key_word[3], + value_word[0], + value_word[1], + value_word[2], + value_word[3], + pswap_tag_felt, + p2id_tag_felt, + ZERO, + ZERO, + Felt::new(swap_count), + ZERO, + ZERO, + ZERO, + creator_id.prefix().as_felt(), + creator_id.suffix(), ] } @@ -169,12 +180,7 @@ fn make_pswap_tag() -> (NoteTag, Felt) { /// Build note args Word from input and inflight amounts. /// LE stack orientation: Word[0] = input_amount (on top), Word[1] = inflight_amount fn make_note_args(input_amount: u64, inflight_amount: u64) -> Word { - Word::from([ - Felt::new(input_amount), - Felt::new(inflight_amount), - ZERO, - ZERO, - ]) + Word::from([Felt::new(input_amount), Felt::new(inflight_amount), ZERO, ZERO]) } /// Create expected remainder note via PswapNote::create_remainder_note. @@ -211,8 +217,7 @@ fn create_expected_pswap_remainder_note( async fn pswap_note_full_fill_test() -> anyhow::Result<()> { let mut builder = MockChain::builder(); - let usdc_faucet = - builder.add_existing_basic_faucet(BASIC_AUTH, "USDC", 1000, Some(150))?; + let usdc_faucet = builder.add_existing_basic_faucet(BASIC_AUTH, "USDC", 1000, Some(150))?; let eth_faucet = builder.add_existing_basic_faucet(BASIC_AUTH, "ETH", 1000, Some(50))?; let alice = builder.add_existing_wallet_with_assets( @@ -227,14 +232,8 @@ async fn pswap_note_full_fill_test() -> anyhow::Result<()> { let (pswap_tag, pswap_tag_felt) = make_pswap_tag(); let p2id_tag_felt = compute_p2id_tag_felt(alice.id()); - let storage_items = build_pswap_storage( - eth_faucet.id(), - 25, - pswap_tag_felt, - p2id_tag_felt, - 0, - alice.id(), - ); + let storage_items = + build_pswap_storage(eth_faucet.id(), 25, pswap_tag_felt, p2id_tag_felt, 0, alice.id()); let note_assets = make_note_assets(usdc_faucet.id(), 50)?; let swap_note = create_pswap_note(alice.id(), note_assets, storage_items, pswap_tag); builder.add_output_note(RawOutputNote::Full(swap_note.clone())); @@ -305,8 +304,7 @@ async fn pswap_note_full_fill_test() -> anyhow::Result<()> { async fn pswap_note_private_full_fill_test() -> anyhow::Result<()> { let mut builder = MockChain::builder(); - let usdc_faucet = - builder.add_existing_basic_faucet(BASIC_AUTH, "USDC", 1000, Some(150))?; + let usdc_faucet = builder.add_existing_basic_faucet(BASIC_AUTH, "USDC", 1000, Some(150))?; let eth_faucet = builder.add_existing_basic_faucet(BASIC_AUTH, "ETH", 1000, Some(50))?; let alice = builder.add_existing_wallet_with_assets( @@ -321,14 +319,8 @@ async fn pswap_note_private_full_fill_test() -> anyhow::Result<()> { let (pswap_tag, pswap_tag_felt) = make_pswap_tag(); let p2id_tag_felt = compute_p2id_tag_felt(alice.id()); - let storage_items = build_pswap_storage( - eth_faucet.id(), - 25, - pswap_tag_felt, - p2id_tag_felt, - 0, - alice.id(), - ); + let storage_items = + build_pswap_storage(eth_faucet.id(), 25, pswap_tag_felt, p2id_tag_felt, 0, alice.id()); let note_assets = make_note_assets(usdc_faucet.id(), 50)?; // Create a PRIVATE swap note (output notes should also be Private) @@ -405,8 +397,7 @@ async fn pswap_note_private_full_fill_test() -> anyhow::Result<()> { async fn pswap_note_partial_fill_test() -> anyhow::Result<()> { let mut builder = MockChain::builder(); - let usdc_faucet = - builder.add_existing_basic_faucet(BASIC_AUTH, "USDC", 1000, Some(150))?; + let usdc_faucet = builder.add_existing_basic_faucet(BASIC_AUTH, "USDC", 1000, Some(150))?; let eth_faucet = builder.add_existing_basic_faucet(BASIC_AUTH, "ETH", 1000, Some(50))?; let alice = builder.add_existing_wallet_with_assets( @@ -421,14 +412,8 @@ async fn pswap_note_partial_fill_test() -> anyhow::Result<()> { let (pswap_tag, pswap_tag_felt) = make_pswap_tag(); let p2id_tag_felt = compute_p2id_tag_felt(alice.id()); - let storage_items = build_pswap_storage( - eth_faucet.id(), - 25, - pswap_tag_felt, - p2id_tag_felt, - 0, - alice.id(), - ); + let storage_items = + build_pswap_storage(eth_faucet.id(), 25, pswap_tag_felt, p2id_tag_felt, 0, alice.id()); let note_assets = make_note_assets(usdc_faucet.id(), 50)?; let swap_note = create_pswap_note(alice.id(), note_assets, storage_items, pswap_tag); builder.add_output_note(RawOutputNote::Full(swap_note.clone())); @@ -481,25 +466,13 @@ async fn pswap_note_partial_fill_test() -> anyhow::Result<()> { assert_eq!(output_notes.num_notes(), 2); // P2ID note: 20 ETH - if let Asset::Fungible(f) = output_notes - .get_note(0) - .assets() - .iter() - .next() - .unwrap() - { + if let Asset::Fungible(f) = output_notes.get_note(0).assets().iter().next().unwrap() { assert_eq!(f.faucet_id(), eth_faucet.id()); assert_eq!(f.amount(), 20); } // SWAPp remainder: 10 USDC - if let Asset::Fungible(f) = output_notes - .get_note(1) - .assets() - .iter() - .next() - .unwrap() - { + if let Asset::Fungible(f) = output_notes.get_note(1).assets().iter().next().unwrap() { assert_eq!(f.faucet_id(), usdc_faucet.id()); assert_eq!(f.amount(), 10); } @@ -529,8 +502,7 @@ async fn pswap_note_partial_fill_test() -> anyhow::Result<()> { async fn pswap_note_inflight_cross_swap_test() -> anyhow::Result<()> { let mut builder = MockChain::builder(); - let usdc_faucet = - builder.add_existing_basic_faucet(BASIC_AUTH, "USDC", 1000, Some(150))?; + let usdc_faucet = builder.add_existing_basic_faucet(BASIC_AUTH, "USDC", 1000, Some(150))?; let eth_faucet = builder.add_existing_basic_faucet(BASIC_AUTH, "ETH", 1000, Some(50))?; let alice = builder.add_existing_wallet_with_assets( @@ -571,12 +543,8 @@ async fn pswap_note_inflight_cross_swap_test() -> anyhow::Result<()> { 0, bob.id(), ); - let bob_swap_note = create_pswap_note( - bob.id(), - make_note_assets(eth_faucet.id(), 25)?, - bob_storage, - pswap_tag, - ); + let bob_swap_note = + create_pswap_note(bob.id(), make_note_assets(eth_faucet.id(), 25)?, bob_storage, pswap_tag); builder.add_output_note(RawOutputNote::Full(bob_swap_note.clone())); let mock_chain = builder.build()?; @@ -607,11 +575,7 @@ async fn pswap_note_inflight_cross_swap_test() -> anyhow::Result<()> { )?; let tx_context = mock_chain - .build_tx_context( - charlie.id(), - &[alice_swap_note.id(), bob_swap_note.id()], - &[], - )? + .build_tx_context(charlie.id(), &[alice_swap_note.id(), bob_swap_note.id()], &[])? .extend_note_args(note_args_map) .extend_expected_output_notes(vec![ RawOutputNote::Full(alice_p2id_note), @@ -628,13 +592,7 @@ async fn pswap_note_inflight_cross_swap_test() -> anyhow::Result<()> { let mut alice_found = false; let mut bob_found = false; for idx in 0..output_notes.num_notes() { - if let Asset::Fungible(f) = output_notes - .get_note(idx) - .assets() - .iter() - .next() - .unwrap() - { + if let Asset::Fungible(f) = output_notes.get_note(idx).assets().iter().next().unwrap() { if f.faucet_id() == eth_faucet.id() && f.amount() == 25 { alice_found = true; } @@ -669,14 +627,8 @@ async fn pswap_note_creator_reclaim_test() -> anyhow::Result<()> { let (pswap_tag, pswap_tag_felt) = make_pswap_tag(); let p2id_tag_felt = compute_p2id_tag_felt(alice.id()); - let storage_items = build_pswap_storage( - _eth_faucet.id(), - 25, - pswap_tag_felt, - p2id_tag_felt, - 0, - alice.id(), - ); + let storage_items = + build_pswap_storage(_eth_faucet.id(), 25, pswap_tag_felt, p2id_tag_felt, 0, alice.id()); let swap_note = create_pswap_note( alice.id(), make_note_assets(usdc_faucet.id(), 50)?, @@ -687,9 +639,7 @@ async fn pswap_note_creator_reclaim_test() -> anyhow::Result<()> { let mock_chain = builder.build()?; - let tx_context = mock_chain - .build_tx_context(alice.id(), &[swap_note.id()], &[])? - .build()?; + let tx_context = mock_chain.build_tx_context(alice.id(), &[swap_note.id()], &[])?.build()?; let executed_transaction = tx_context.execute().await?; @@ -731,14 +681,8 @@ async fn pswap_note_invalid_input_test() -> anyhow::Result<()> { let (pswap_tag, pswap_tag_felt) = make_pswap_tag(); let p2id_tag_felt = compute_p2id_tag_felt(alice.id()); - let storage_items = build_pswap_storage( - eth_faucet.id(), - 25, - pswap_tag_felt, - p2id_tag_felt, - 0, - alice.id(), - ); + let storage_items = + build_pswap_storage(eth_faucet.id(), 25, pswap_tag_felt, p2id_tag_felt, 0, alice.id()); let swap_note = create_pswap_note( alice.id(), make_note_assets(usdc_faucet.id(), 50)?, @@ -782,10 +726,8 @@ async fn pswap_note_multiple_partial_fills_test() -> anyhow::Result<()> { for (input_amount, _description) in test_scenarios { let mut builder = MockChain::builder(); - let usdc_faucet = - builder.add_existing_basic_faucet(BASIC_AUTH, "USDC", 1000, Some(150))?; - let eth_faucet = - builder.add_existing_basic_faucet(BASIC_AUTH, "ETH", 1000, Some(50))?; + let usdc_faucet = builder.add_existing_basic_faucet(BASIC_AUTH, "USDC", 1000, Some(150))?; + let eth_faucet = builder.add_existing_basic_faucet(BASIC_AUTH, "ETH", 1000, Some(50))?; let alice = builder.add_existing_wallet_with_assets( BASIC_AUTH, @@ -800,14 +742,8 @@ async fn pswap_note_multiple_partial_fills_test() -> anyhow::Result<()> { let (pswap_tag, pswap_tag_felt) = make_pswap_tag(); let p2id_tag_felt = compute_p2id_tag_felt(alice.id()); - let storage_items = build_pswap_storage( - eth_faucet.id(), - 25, - pswap_tag_felt, - p2id_tag_felt, - 0, - alice.id(), - ); + let storage_items = + build_pswap_storage(eth_faucet.id(), 25, pswap_tag_felt, p2id_tag_felt, 0, alice.id()); let swap_note = create_pswap_note( alice.id(), make_note_assets(usdc_faucet.id(), 50)?, @@ -887,8 +823,7 @@ async fn pswap_note_non_exact_ratio_partial_fill_test() -> anyhow::Result<()> { let expected_output = calculate_output_amount(offered_total, requested_total, input_amount); let mut builder = MockChain::builder(); - let usdc_faucet = - builder.add_existing_basic_faucet(BASIC_AUTH, "USDC", 10000, Some(1000))?; + let usdc_faucet = builder.add_existing_basic_faucet(BASIC_AUTH, "USDC", 10000, Some(1000))?; let eth_faucet = builder.add_existing_basic_faucet(BASIC_AUTH, "ETH", 10000, Some(100))?; let alice = builder.add_existing_wallet_with_assets( @@ -1010,11 +945,7 @@ async fn pswap_note_partial_fill_non_integer_ratio_fuzz_test() -> anyhow::Result let remaining_requested = requested_eth - fill_eth; assert!(offered_out > 0, "Case {}: offered_out must be > 0", i + 1); - assert!( - offered_out <= *offered_usdc, - "Case {}: offered_out > offered", - i + 1 - ); + assert!(offered_out <= *offered_usdc, "Case {}: offered_out > offered", i + 1); let mut builder = MockChain::builder(); let max_supply = 100_000u64; @@ -1025,12 +956,8 @@ async fn pswap_note_partial_fill_non_integer_ratio_fuzz_test() -> anyhow::Result max_supply, Some(*offered_usdc), )?; - let eth_faucet = builder.add_existing_basic_faucet( - BASIC_AUTH, - "ETH", - max_supply, - Some(*fill_eth), - )?; + let eth_faucet = + builder.add_existing_basic_faucet(BASIC_AUTH, "ETH", max_supply, Some(*fill_eth))?; let alice = builder.add_existing_wallet_with_assets( BASIC_AUTH, @@ -1124,12 +1051,7 @@ async fn pswap_note_partial_fill_non_integer_ratio_fuzz_test() -> anyhow::Result assert_eq!(f.amount(), *fill_eth, "Case {}", i + 1); } - assert_eq!( - offered_out + remaining_offered, - *offered_usdc, - "Case {}: conservation", - i + 1 - ); + assert_eq!(offered_out + remaining_offered, *offered_usdc, "Case {}: conservation", i + 1); } Ok(()) @@ -1209,10 +1131,8 @@ async fn pswap_note_chained_partial_fills_non_integer_ratio_test() -> anyhow::Re // Create note with the correct serial for this chain position let note_storage = NoteStorage::new(storage_items)?; - let recipient = - NoteRecipient::new(current_serial, PswapNote::script(), note_storage); - let metadata = - NoteMetadata::new(alice.id(), NoteType::Public).with_tag(pswap_tag); + let recipient = NoteRecipient::new(current_serial, PswapNote::script(), note_storage); + let metadata = NoteMetadata::new(alice.id(), NoteType::Public).with_tag(pswap_tag); let swap_note = Note::new(note_assets, metadata, recipient); builder.add_output_note(RawOutputNote::Full(swap_note.clone())); @@ -1280,13 +1200,7 @@ async fn pswap_note_chained_partial_fills_non_integer_ratio_test() -> anyhow::Re let vault_delta = executed_tx.account_delta().vault(); let added: Vec = vault_delta.added_assets().collect(); - assert_eq!( - added.len(), - 1, - "Chain {} fill {}", - chain_idx + 1, - fill_idx + 1 - ); + assert_eq!(added.len(), 1, "Chain {} fill {}", chain_idx + 1, fill_idx + 1); if let Asset::Fungible(f) = &added[0] { assert_eq!( f.amount(), @@ -1315,11 +1229,7 @@ async fn pswap_note_chained_partial_fills_non_integer_ratio_test() -> anyhow::Re // Verify conservation let total_fills: u64 = fills.iter().sum(); - assert_eq!( - total_eth_from_bob, total_fills, - "Chain {}: ETH conservation", - chain_idx + 1 - ); + assert_eq!(total_eth_from_bob, total_fills, "Chain {}: ETH conservation", chain_idx + 1); assert_eq!( total_usdc_to_bob + current_offered, *initial_offered, @@ -1337,12 +1247,9 @@ fn compare_pswap_create_output_notes_vs_test_helper() { use miden_protocol::crypto::rand::{FeltRng, RpoRandomCoin}; let mut builder = MockChain::builder(); - let usdc_faucet = builder - .add_existing_basic_faucet(BASIC_AUTH, "USDC", 1000, Some(150)) - .unwrap(); - let eth_faucet = builder - .add_existing_basic_faucet(BASIC_AUTH, "ETH", 1000, Some(50)) - .unwrap(); + let usdc_faucet = + builder.add_existing_basic_faucet(BASIC_AUTH, "USDC", 1000, Some(150)).unwrap(); + let eth_faucet = builder.add_existing_basic_faucet(BASIC_AUTH, "ETH", 1000, Some(50)).unwrap(); let alice = builder .add_existing_wallet_with_assets( BASIC_AUTH, @@ -1375,14 +1282,8 @@ fn compare_pswap_create_output_notes_vs_test_helper() { // Create same swap note using test helper (same serial) let (_pswap_tag, pswap_tag_felt) = make_pswap_tag(); let p2id_tag_felt = compute_p2id_tag_felt(alice.id()); - let storage_items = build_pswap_storage( - eth_faucet.id(), - 25, - pswap_tag_felt, - p2id_tag_felt, - 0, - alice.id(), - ); + let storage_items = + build_pswap_storage(eth_faucet.id(), 25, pswap_tag_felt, p2id_tag_felt, 0, alice.id()); let note_assets = make_note_assets(usdc_faucet.id(), 50).unwrap(); // Use the SAME serial as the library note @@ -1421,16 +1322,8 @@ fn compare_pswap_create_output_notes_vs_test_helper() { test_p2id.recipient().digest(), "Recipient digest mismatch!" ); - assert_eq!( - lib_p2id.metadata().tag(), - test_p2id.metadata().tag(), - "Tag mismatch!" - ); - assert_eq!( - lib_p2id.metadata().sender(), - test_p2id.metadata().sender(), - "Sender mismatch!" - ); + assert_eq!(lib_p2id.metadata().tag(), test_p2id.metadata().tag(), "Tag mismatch!"); + assert_eq!(lib_p2id.metadata().sender(), test_p2id.metadata().sender(), "Sender mismatch!"); assert_eq!( lib_p2id.metadata().note_type(), test_p2id.metadata().note_type(), @@ -1445,12 +1338,9 @@ fn pswap_parse_inputs_roundtrip() { use miden_protocol::crypto::rand::{FeltRng, RpoRandomCoin}; let mut builder = MockChain::builder(); - let usdc_faucet = builder - .add_existing_basic_faucet(BASIC_AUTH, "USDC", 1000, Some(150)) - .unwrap(); - let eth_faucet = builder - .add_existing_basic_faucet(BASIC_AUTH, "ETH", 1000, Some(50)) - .unwrap(); + let usdc_faucet = + builder.add_existing_basic_faucet(BASIC_AUTH, "USDC", 1000, Some(150)).unwrap(); + let eth_faucet = builder.add_existing_basic_faucet(BASIC_AUTH, "ETH", 1000, Some(50)).unwrap(); let alice = builder .add_existing_wallet_with_assets( BASIC_AUTH, From 3bd6da1e4539ae8a9b09e28e5813c40301b3011a Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Fri, 20 Mar 2026 09:15:32 +0530 Subject: [PATCH 4/6] refactor: remove redundant test helpers, use pswap lib functions directly Replace all test helper wrappers with direct calls to library functions: - create_pswap_note -> PswapNote::create() - create_expected_pswap_p2id_note + create_expected_pswap_remainder_note -> pswap.execute() - build_pswap_storage -> PswapNoteStorage::from_parts() - Remove make_pswap_tag, make_note_assets, make_note_args, compute_p2id_tag_* - Inline calculate_output_amount as PswapNote::calculate_output_amount() --- crates/miden-standards/src/note/mod.rs | 2 +- crates/miden-testing/tests/scripts/pswap.rs | 767 ++++++-------------- 2 files changed, 225 insertions(+), 544 deletions(-) diff --git a/crates/miden-standards/src/note/mod.rs b/crates/miden-standards/src/note/mod.rs index c90e805921..87c6795368 100644 --- a/crates/miden-standards/src/note/mod.rs +++ b/crates/miden-standards/src/note/mod.rs @@ -110,7 +110,7 @@ impl StandardNote { Self::P2ID => P2idNote::NUM_STORAGE_ITEMS, Self::P2IDE => P2ideNote::NUM_STORAGE_ITEMS, Self::SWAP => SwapNote::NUM_STORAGE_ITEMS, - Self::PSWAP => PswapNoteStorage::NUM_ITEMS, + Self::PSWAP => PswapNoteStorage::NUM_STORAGE_ITEMS, Self::MINT => MintNote::NUM_STORAGE_ITEMS_PRIVATE, Self::BURN => BurnNote::NUM_STORAGE_ITEMS, } diff --git a/crates/miden-testing/tests/scripts/pswap.rs b/crates/miden-testing/tests/scripts/pswap.rs index b4a544128a..87cc103fee 100644 --- a/crates/miden-testing/tests/scripts/pswap.rs +++ b/crates/miden-testing/tests/scripts/pswap.rs @@ -1,8 +1,8 @@ use std::collections::BTreeMap; -use miden_protocol::account::AccountId; use miden_protocol::account::auth::AuthScheme; use miden_protocol::asset::{Asset, FungibleAsset}; +use miden_protocol::crypto::rand::{FeltRng, RpoRandomCoin}; use miden_protocol::note::{ Note, NoteAssets, NoteAttachment, NoteMetadata, NoteRecipient, NoteStorage, NoteTag, NoteType, }; @@ -11,8 +11,6 @@ use miden_protocol::{Felt, Word, ZERO}; use miden_standards::note::{PswapNote, PswapNoteStorage}; use miden_testing::{Auth, MockChain}; -use crate::prove_and_verify_transaction; - // CONSTANTS // ================================================================================================ @@ -20,196 +18,6 @@ const BASIC_AUTH: Auth = Auth::BasicAuth { auth_scheme: AuthScheme::Falcon512Poseidon2, }; -// HELPER FUNCTIONS -// ================================================================================================ - -/// Compute the P2ID tag for a local account -fn compute_p2id_tag_for_local_account(account_id: AccountId) -> NoteTag { - NoteTag::with_account_target(account_id) -} - -/// Helper function to compute P2ID tag as Felt for use in note storage -fn compute_p2id_tag_felt(account_id: AccountId) -> Felt { - let p2id_tag = compute_p2id_tag_for_local_account(account_id); - Felt::new(u32::from(p2id_tag) as u64) -} - -/// Create a PSWAP note via PswapNote::create. -fn create_pswap_note( - sender_id: AccountId, - note_assets: NoteAssets, - storage_items: Vec, - _note_tag: NoteTag, -) -> Note { - create_pswap_note_with_type(sender_id, note_assets, storage_items, _note_tag, NoteType::Public) -} - -/// Create a PSWAP note with specified note type via PswapNote::create. -fn create_pswap_note_with_type( - sender_id: AccountId, - note_assets: NoteAssets, - storage_items: Vec, - _note_tag: NoteTag, - note_type: NoteType, -) -> Note { - let offered_asset = *note_assets.iter().next().expect("must have offered asset"); - let requested_asset = PswapNoteStorage::try_from(storage_items.as_slice()) - .expect("Failed to parse storage") - .requested_asset() - .expect("Failed to parse requested asset from storage"); - - use miden_protocol::crypto::rand::RpoRandomCoin; - let mut rng = RpoRandomCoin::new(Word::default()); - - PswapNote::create( - sender_id, - offered_asset, - requested_asset, - note_type, - NoteAttachment::default(), - &mut rng, - ) - .expect("Failed to create PSWAP note") -} - -/// Delegates to PswapNote::calculate_output_amount. -fn calculate_output_amount(offered_total: u64, requested_total: u64, input_amount: u64) -> u64 { - PswapNote::calculate_output_amount(offered_total, requested_total, input_amount) -} - -/// Build 18-item storage vector for a PSWAP note (KEY+VALUE format). -/// Kept for tests that construct notes with custom serials (chained fills). -fn build_pswap_storage( - requested_faucet_id: AccountId, - requested_amount: u64, - _pswap_tag_felt: Felt, - _p2id_tag_felt: Felt, - swap_count: u64, - creator_id: AccountId, -) -> Vec { - let requested_asset = Asset::Fungible( - FungibleAsset::new(requested_faucet_id, requested_amount) - .expect("Failed to create requested fungible asset"), - ); - let offered_dummy = - Asset::Fungible(FungibleAsset::new(requested_faucet_id, 1).expect("dummy offered asset")); - let key_word = requested_asset.to_key_word(); - let value_word = requested_asset.to_value_word(); - let tag = PswapNote::build_tag(NoteType::Public, &offered_dummy, &requested_asset); - let pswap_tag_felt = Felt::new(u32::from(tag) as u64); - let p2id_tag = NoteTag::with_account_target(creator_id); - let p2id_tag_felt = Felt::new(u32::from(p2id_tag) as u64); - - vec![ - key_word[0], - key_word[1], - key_word[2], - key_word[3], - value_word[0], - value_word[1], - value_word[2], - value_word[3], - pswap_tag_felt, - p2id_tag_felt, - ZERO, - ZERO, - Felt::new(swap_count), - ZERO, - ZERO, - ZERO, - creator_id.prefix().as_felt(), - creator_id.suffix(), - ] -} - -/// Create expected P2ID note via PswapNote::create_p2id_payback_note. -fn create_expected_pswap_p2id_note( - swap_note: &Note, - consumer_id: AccountId, - _creator_id: AccountId, - _swap_count: u64, - total_fill: u64, - requested_faucet_id: AccountId, - _p2id_tag: NoteTag, -) -> anyhow::Result { - let note_type = swap_note.metadata().note_type(); - create_expected_pswap_p2id_note_with_type( - swap_note, - consumer_id, - _creator_id, - _swap_count, - total_fill, - requested_faucet_id, - note_type, - ) -} - -/// Create expected P2ID note via PswapNote::build_p2id_payback_note. -/// -/// The P2ID note inherits its note type from the swap note. -fn create_expected_pswap_p2id_note_with_type( - swap_note: &Note, - consumer_id: AccountId, - _creator_id: AccountId, - _swap_count: u64, - total_fill: u64, - requested_faucet_id: AccountId, - _note_type: NoteType, -) -> anyhow::Result { - let pswap = PswapNote::try_from(swap_note)?; - let payback_asset = Asset::Fungible(FungibleAsset::new(requested_faucet_id, total_fill)?); - let aux_word = Word::from([Felt::new(total_fill), ZERO, ZERO, ZERO]); - - Ok(pswap.build_p2id_payback_note(consumer_id, payback_asset, aux_word)?) -} - -/// Create NoteAssets with a single fungible asset -fn make_note_assets(faucet_id: AccountId, amount: u64) -> anyhow::Result { - let asset = FungibleAsset::new(faucet_id, amount)?; - Ok(NoteAssets::new(vec![asset.into()])?) -} - -/// Create a dummy SWAPp tag and its Felt representation. -/// Kept for backward compatibility with test call sites. -fn make_pswap_tag() -> (NoteTag, Felt) { - let tag = NoteTag::new(0xC0000000); - let felt = Felt::new(u32::from(tag) as u64); - (tag, felt) -} - -/// Build note args Word from input and inflight amounts. -/// LE stack orientation: Word[0] = input_amount (on top), Word[1] = inflight_amount -fn make_note_args(input_amount: u64, inflight_amount: u64) -> Word { - Word::from([Felt::new(input_amount), Felt::new(inflight_amount), ZERO, ZERO]) -} - -/// Create expected remainder note via PswapNote::create_remainder_note. -fn create_expected_pswap_remainder_note( - swap_note: &Note, - consumer_id: AccountId, - _creator_id: AccountId, - remaining_offered: u64, - remaining_requested: u64, - offered_out: u64, - _swap_count: u64, - offered_faucet_id: AccountId, - _requested_faucet_id: AccountId, - _pswap_tag: NoteTag, - _pswap_tag_felt: Felt, - _p2id_tag_felt: Felt, -) -> anyhow::Result { - let pswap = PswapNote::try_from(swap_note)?; - let remaining_offered_asset = - Asset::Fungible(FungibleAsset::new(offered_faucet_id, remaining_offered)?); - - Ok(Note::from(pswap.build_remainder_pswap_note( - consumer_id, - remaining_offered_asset, - remaining_requested, - offered_out, - )?)) -} - // TESTS // ================================================================================================ @@ -229,29 +37,30 @@ async fn pswap_note_full_fill_test() -> anyhow::Result<()> { [FungibleAsset::new(eth_faucet.id(), 25)?.into()], )?; - let (pswap_tag, pswap_tag_felt) = make_pswap_tag(); - let p2id_tag_felt = compute_p2id_tag_felt(alice.id()); + let offered_asset = Asset::Fungible(FungibleAsset::new(usdc_faucet.id(), 50)?); + let requested_asset = Asset::Fungible(FungibleAsset::new(eth_faucet.id(), 25)?); - let storage_items = - build_pswap_storage(eth_faucet.id(), 25, pswap_tag_felt, p2id_tag_felt, 0, alice.id()); - let note_assets = make_note_assets(usdc_faucet.id(), 50)?; - let swap_note = create_pswap_note(alice.id(), note_assets, storage_items, pswap_tag); + let mut rng = RpoRandomCoin::new(Word::default()); + let swap_note = PswapNote::create( + alice.id(), + offered_asset, + requested_asset, + NoteType::Public, + NoteAttachment::default(), + &mut rng, + )?; builder.add_output_note(RawOutputNote::Full(swap_note.clone())); let mut mock_chain = builder.build()?; let mut note_args_map = BTreeMap::new(); - note_args_map.insert(swap_note.id(), make_note_args(25, 0)); + note_args_map.insert( + swap_note.id(), + Word::from([Felt::new(25), Felt::new(0), ZERO, ZERO]), + ); - let p2id_note = create_expected_pswap_p2id_note( - &swap_note, - bob.id(), - alice.id(), - 0, - 25, - eth_faucet.id(), - compute_p2id_tag_for_local_account(alice.id()), - )?; + let pswap = PswapNote::try_from(&swap_note)?; + let (p2id_note, _remainder) = pswap.execute(bob.id(), 25, 0)?; let tx_context = mock_chain .build_tx_context(bob.id(), &[swap_note.id()], &[])? @@ -316,39 +125,32 @@ async fn pswap_note_private_full_fill_test() -> anyhow::Result<()> { [FungibleAsset::new(eth_faucet.id(), 25)?.into()], )?; - let (pswap_tag, pswap_tag_felt) = make_pswap_tag(); - let p2id_tag_felt = compute_p2id_tag_felt(alice.id()); - - let storage_items = - build_pswap_storage(eth_faucet.id(), 25, pswap_tag_felt, p2id_tag_felt, 0, alice.id()); - let note_assets = make_note_assets(usdc_faucet.id(), 50)?; + let offered_asset = Asset::Fungible(FungibleAsset::new(usdc_faucet.id(), 50)?); + let requested_asset = Asset::Fungible(FungibleAsset::new(eth_faucet.id(), 25)?); + let mut rng = RpoRandomCoin::new(Word::default()); // Create a PRIVATE swap note (output notes should also be Private) - let swap_note = create_pswap_note_with_type( + let swap_note = PswapNote::create( alice.id(), - note_assets, - storage_items, - pswap_tag, + offered_asset, + requested_asset, NoteType::Private, - ); + NoteAttachment::default(), + &mut rng, + )?; builder.add_output_note(RawOutputNote::Full(swap_note.clone())); let mut mock_chain = builder.build()?; let mut note_args_map = BTreeMap::new(); - note_args_map.insert(swap_note.id(), make_note_args(25, 0)); + note_args_map.insert( + swap_note.id(), + Word::from([Felt::new(25), Felt::new(0), ZERO, ZERO]), + ); // Expected P2ID note should inherit Private type from swap note - let p2id_note = create_expected_pswap_p2id_note_with_type( - &swap_note, - bob.id(), - alice.id(), - 0, - 25, - eth_faucet.id(), - compute_p2id_tag_for_local_account(alice.id()), - NoteType::Private, - )?; + let pswap = PswapNote::try_from(&swap_note)?; + let (p2id_note, _remainder) = pswap.execute(bob.id(), 25, 0)?; let tx_context = mock_chain .build_tx_context(bob.id(), &[swap_note.id()], &[])? @@ -409,46 +211,31 @@ async fn pswap_note_partial_fill_test() -> anyhow::Result<()> { [FungibleAsset::new(eth_faucet.id(), 20)?.into()], )?; - let (pswap_tag, pswap_tag_felt) = make_pswap_tag(); - let p2id_tag_felt = compute_p2id_tag_felt(alice.id()); + let offered_asset = Asset::Fungible(FungibleAsset::new(usdc_faucet.id(), 50)?); + let requested_asset = Asset::Fungible(FungibleAsset::new(eth_faucet.id(), 25)?); - let storage_items = - build_pswap_storage(eth_faucet.id(), 25, pswap_tag_felt, p2id_tag_felt, 0, alice.id()); - let note_assets = make_note_assets(usdc_faucet.id(), 50)?; - let swap_note = create_pswap_note(alice.id(), note_assets, storage_items, pswap_tag); + let mut rng = RpoRandomCoin::new(Word::default()); + let swap_note = PswapNote::create( + alice.id(), + offered_asset, + requested_asset, + NoteType::Public, + NoteAttachment::default(), + &mut rng, + )?; builder.add_output_note(RawOutputNote::Full(swap_note.clone())); let mut mock_chain = builder.build()?; let mut note_args_map = BTreeMap::new(); - note_args_map.insert(swap_note.id(), make_note_args(20, 0)); - - // Expected P2ID note: 20 ETH for Alice - let p2id_note = create_expected_pswap_p2id_note( - &swap_note, - bob.id(), - alice.id(), - 0, - 20, - eth_faucet.id(), - compute_p2id_tag_for_local_account(alice.id()), - )?; + note_args_map.insert( + swap_note.id(), + Word::from([Felt::new(20), Felt::new(0), ZERO, ZERO]), + ); - // Expected SWAPp remainder: 10 USDC for 5 ETH (offered_out=40, remaining=50-40=10) - let remainder_note = create_expected_pswap_remainder_note( - &swap_note, - bob.id(), - alice.id(), - 10, - 5, - 40, - 0, - usdc_faucet.id(), - eth_faucet.id(), - pswap_tag, - pswap_tag_felt, - p2id_tag_felt, - )?; + let pswap = PswapNote::try_from(&swap_note)?; + let (p2id_note, remainder_pswap) = pswap.execute(bob.id(), 20, 0)?; + let remainder_note = Note::from(remainder_pswap.expect("partial fill should produce remainder")); let tx_context = mock_chain .build_tx_context(bob.id(), &[swap_note.id()], &[])? @@ -515,64 +302,49 @@ async fn pswap_note_inflight_cross_swap_test() -> anyhow::Result<()> { )?; let charlie = builder.add_existing_wallet_with_assets(BASIC_AUTH, [])?; - let (pswap_tag, pswap_tag_felt) = make_pswap_tag(); + let mut rng = RpoRandomCoin::new(Word::default()); // Alice's note: offers 50 USDC, requests 25 ETH - let alice_storage = build_pswap_storage( - eth_faucet.id(), - 25, - pswap_tag_felt, - compute_p2id_tag_felt(alice.id()), - 0, - alice.id(), - ); - let alice_swap_note = create_pswap_note( + let alice_swap_note = PswapNote::create( alice.id(), - make_note_assets(usdc_faucet.id(), 50)?, - alice_storage, - pswap_tag, - ); + Asset::Fungible(FungibleAsset::new(usdc_faucet.id(), 50)?), + Asset::Fungible(FungibleAsset::new(eth_faucet.id(), 25)?), + NoteType::Public, + NoteAttachment::default(), + &mut rng, + )?; builder.add_output_note(RawOutputNote::Full(alice_swap_note.clone())); // Bob's note: offers 25 ETH, requests 50 USDC - let bob_storage = build_pswap_storage( - usdc_faucet.id(), - 50, - pswap_tag_felt, - compute_p2id_tag_felt(bob.id()), - 0, + let bob_swap_note = PswapNote::create( bob.id(), - ); - let bob_swap_note = - create_pswap_note(bob.id(), make_note_assets(eth_faucet.id(), 25)?, bob_storage, pswap_tag); + Asset::Fungible(FungibleAsset::new(eth_faucet.id(), 25)?), + Asset::Fungible(FungibleAsset::new(usdc_faucet.id(), 50)?), + NoteType::Public, + NoteAttachment::default(), + &mut rng, + )?; builder.add_output_note(RawOutputNote::Full(bob_swap_note.clone())); let mock_chain = builder.build()?; // Note args: pure inflight (input=0, inflight=full amount) let mut note_args_map = BTreeMap::new(); - note_args_map.insert(alice_swap_note.id(), make_note_args(0, 25)); - note_args_map.insert(bob_swap_note.id(), make_note_args(0, 50)); + note_args_map.insert( + alice_swap_note.id(), + Word::from([Felt::new(0), Felt::new(25), ZERO, ZERO]), + ); + note_args_map.insert( + bob_swap_note.id(), + Word::from([Felt::new(0), Felt::new(50), ZERO, ZERO]), + ); // Expected P2ID notes - let alice_p2id_note = create_expected_pswap_p2id_note( - &alice_swap_note, - charlie.id(), - alice.id(), - 0, - 25, - eth_faucet.id(), - compute_p2id_tag_for_local_account(alice.id()), - )?; - let bob_p2id_note = create_expected_pswap_p2id_note( - &bob_swap_note, - charlie.id(), - bob.id(), - 0, - 50, - usdc_faucet.id(), - compute_p2id_tag_for_local_account(bob.id()), - )?; + let alice_pswap = PswapNote::try_from(&alice_swap_note)?; + let (alice_p2id_note, _) = alice_pswap.execute(charlie.id(), 0, 25)?; + + let bob_pswap = PswapNote::try_from(&bob_swap_note)?; + let (bob_p2id_note, _) = bob_pswap.execute(charlie.id(), 0, 50)?; let tx_context = mock_chain .build_tx_context(charlie.id(), &[alice_swap_note.id(), bob_swap_note.id()], &[])? @@ -617,24 +389,22 @@ async fn pswap_note_creator_reclaim_test() -> anyhow::Result<()> { let mut builder = MockChain::builder(); let usdc_faucet = builder.add_existing_basic_faucet(BASIC_AUTH, "USDC", 1000, Some(50))?; - let _eth_faucet = builder.add_existing_basic_faucet(BASIC_AUTH, "ETH", 1000, Some(25))?; + let eth_faucet = builder.add_existing_basic_faucet(BASIC_AUTH, "ETH", 1000, Some(25))?; let alice = builder.add_existing_wallet_with_assets( BASIC_AUTH, [FungibleAsset::new(usdc_faucet.id(), 50)?.into()], )?; - let (pswap_tag, pswap_tag_felt) = make_pswap_tag(); - let p2id_tag_felt = compute_p2id_tag_felt(alice.id()); - - let storage_items = - build_pswap_storage(_eth_faucet.id(), 25, pswap_tag_felt, p2id_tag_felt, 0, alice.id()); - let swap_note = create_pswap_note( + let mut rng = RpoRandomCoin::new(Word::default()); + let swap_note = PswapNote::create( alice.id(), - make_note_assets(usdc_faucet.id(), 50)?, - storage_items, - pswap_tag, - ); + Asset::Fungible(FungibleAsset::new(usdc_faucet.id(), 50)?), + Asset::Fungible(FungibleAsset::new(eth_faucet.id(), 25)?), + NoteType::Public, + NoteAttachment::default(), + &mut rng, + )?; builder.add_output_note(RawOutputNote::Full(swap_note.clone())); let mock_chain = builder.build()?; @@ -678,23 +448,24 @@ async fn pswap_note_invalid_input_test() -> anyhow::Result<()> { [FungibleAsset::new(eth_faucet.id(), 30)?.into()], )?; - let (pswap_tag, pswap_tag_felt) = make_pswap_tag(); - let p2id_tag_felt = compute_p2id_tag_felt(alice.id()); - - let storage_items = - build_pswap_storage(eth_faucet.id(), 25, pswap_tag_felt, p2id_tag_felt, 0, alice.id()); - let swap_note = create_pswap_note( + let mut rng = RpoRandomCoin::new(Word::default()); + let swap_note = PswapNote::create( alice.id(), - make_note_assets(usdc_faucet.id(), 50)?, - storage_items, - pswap_tag, - ); + Asset::Fungible(FungibleAsset::new(usdc_faucet.id(), 50)?), + Asset::Fungible(FungibleAsset::new(eth_faucet.id(), 25)?), + NoteType::Public, + NoteAttachment::default(), + &mut rng, + )?; builder.add_output_note(RawOutputNote::Full(swap_note.clone())); let mock_chain = builder.build()?; // Try to fill with 30 ETH when only 25 is requested - should fail let mut note_args_map = BTreeMap::new(); - note_args_map.insert(swap_note.id(), make_note_args(30, 0)); + note_args_map.insert( + swap_note.id(), + Word::from([Felt::new(30), Felt::new(0), ZERO, ZERO]), + ); let tx_context = mock_chain .build_tx_context(bob.id(), &[swap_note.id()], &[])? @@ -739,56 +510,34 @@ async fn pswap_note_multiple_partial_fills_test() -> anyhow::Result<()> { [FungibleAsset::new(eth_faucet.id(), input_amount)?.into()], )?; - let (pswap_tag, pswap_tag_felt) = make_pswap_tag(); - let p2id_tag_felt = compute_p2id_tag_felt(alice.id()); - - let storage_items = - build_pswap_storage(eth_faucet.id(), 25, pswap_tag_felt, p2id_tag_felt, 0, alice.id()); - let swap_note = create_pswap_note( + let mut rng = RpoRandomCoin::new(Word::default()); + let swap_note = PswapNote::create( alice.id(), - make_note_assets(usdc_faucet.id(), 50)?, - storage_items, - pswap_tag, - ); + Asset::Fungible(FungibleAsset::new(usdc_faucet.id(), 50)?), + Asset::Fungible(FungibleAsset::new(eth_faucet.id(), 25)?), + NoteType::Public, + NoteAttachment::default(), + &mut rng, + )?; builder.add_output_note(RawOutputNote::Full(swap_note.clone())); let mock_chain = builder.build()?; - let offered_out = calculate_output_amount(50, 25, input_amount); - let remaining_usdc = 50 - offered_out; - let remaining_eth = 25 - input_amount; + let offered_out = PswapNote::calculate_output_amount(50, 25, input_amount); let mut note_args_map = BTreeMap::new(); - note_args_map.insert(swap_note.id(), make_note_args(input_amount, 0)); + note_args_map.insert( + swap_note.id(), + Word::from([Felt::new(input_amount), Felt::new(0), ZERO, ZERO]), + ); - let p2id_note = create_expected_pswap_p2id_note( - &swap_note, - bob.id(), - alice.id(), - 0, - input_amount, - eth_faucet.id(), - compute_p2id_tag_for_local_account(alice.id()), - )?; + let pswap = PswapNote::try_from(&swap_note)?; + let (p2id_note, remainder_pswap) = pswap.execute(bob.id(), input_amount, 0)?; let mut expected_notes = vec![RawOutputNote::Full(p2id_note)]; - if input_amount < 25 { - let remainder_note = create_expected_pswap_remainder_note( - &swap_note, - bob.id(), - alice.id(), - remaining_usdc, - remaining_eth, - offered_out, - 0, - usdc_faucet.id(), - eth_faucet.id(), - pswap_tag, - pswap_tag_felt, - p2id_tag_felt, - )?; - expected_notes.push(RawOutputNote::Full(remainder_note)); + if let Some(remainder) = remainder_pswap { + expected_notes.push(RawOutputNote::Full(Note::from(remainder))); } let tx_context = mock_chain @@ -820,7 +569,8 @@ async fn pswap_note_non_exact_ratio_partial_fill_test() -> anyhow::Result<()> { let offered_total = 100u64; let requested_total = 30u64; let input_amount = 7u64; - let expected_output = calculate_output_amount(offered_total, requested_total, input_amount); + let expected_output = + PswapNote::calculate_output_amount(offered_total, requested_total, input_amount); let mut builder = MockChain::builder(); let usdc_faucet = builder.add_existing_basic_faucet(BASIC_AUTH, "USDC", 10000, Some(1000))?; @@ -835,53 +585,28 @@ async fn pswap_note_non_exact_ratio_partial_fill_test() -> anyhow::Result<()> { [FungibleAsset::new(eth_faucet.id(), input_amount)?.into()], )?; - let pswap_tag = NoteTag::new(0xC0000000); - let pswap_tag_felt = Felt::new(u32::from(pswap_tag) as u64); - let p2id_tag_felt = compute_p2id_tag_felt(alice.id()); - - let storage_items = build_pswap_storage( - eth_faucet.id(), - requested_total, - pswap_tag_felt, - p2id_tag_felt, - 0, + let mut rng = RpoRandomCoin::new(Word::default()); + let swap_note = PswapNote::create( alice.id(), - ); - let note_assets = make_note_assets(usdc_faucet.id(), offered_total)?; - let swap_note = create_pswap_note(alice.id(), note_assets, storage_items, pswap_tag); + Asset::Fungible(FungibleAsset::new(usdc_faucet.id(), offered_total)?), + Asset::Fungible(FungibleAsset::new(eth_faucet.id(), requested_total)?), + NoteType::Public, + NoteAttachment::default(), + &mut rng, + )?; builder.add_output_note(RawOutputNote::Full(swap_note.clone())); let mock_chain = builder.build()?; let mut note_args_map = BTreeMap::new(); - note_args_map.insert(swap_note.id(), make_note_args(input_amount, 0)); + note_args_map.insert( + swap_note.id(), + Word::from([Felt::new(input_amount), Felt::new(0), ZERO, ZERO]), + ); - let remaining_offered = offered_total - expected_output; - let remaining_requested = requested_total - input_amount; - - let p2id_note = create_expected_pswap_p2id_note( - &swap_note, - bob.id(), - alice.id(), - 0, - input_amount, - eth_faucet.id(), - compute_p2id_tag_for_local_account(alice.id()), - )?; - let remainder = create_expected_pswap_remainder_note( - &swap_note, - bob.id(), - alice.id(), - remaining_offered, - remaining_requested, - expected_output, - 0, - usdc_faucet.id(), - eth_faucet.id(), - pswap_tag, - pswap_tag_felt, - p2id_tag_felt, - )?; + let pswap = PswapNote::try_from(&swap_note)?; + let (p2id_note, remainder_pswap) = pswap.execute(bob.id(), input_amount, 0)?; + let remainder = Note::from(remainder_pswap.expect("partial fill should produce remainder")); let tx_context = mock_chain .build_tx_context(bob.id(), &[swap_note.id()], &[])? @@ -940,7 +665,8 @@ async fn pswap_note_partial_fill_non_integer_ratio_fuzz_test() -> anyhow::Result ]; for (i, (offered_usdc, requested_eth, fill_eth)) in test_cases.iter().enumerate() { - let offered_out = calculate_output_amount(*offered_usdc, *requested_eth, *fill_eth); + let offered_out = + PswapNote::calculate_output_amount(*offered_usdc, *requested_eth, *fill_eth); let remaining_offered = offered_usdc - offered_out; let remaining_requested = requested_eth - fill_eth; @@ -968,53 +694,32 @@ async fn pswap_note_partial_fill_non_integer_ratio_fuzz_test() -> anyhow::Result [FungibleAsset::new(eth_faucet.id(), *fill_eth)?.into()], )?; - let pswap_tag = NoteTag::new(0xC0000000); - let pswap_tag_felt = Felt::new(u32::from(pswap_tag) as u64); - let p2id_tag_felt = compute_p2id_tag_felt(alice.id()); - - let storage_items = build_pswap_storage( - eth_faucet.id(), - *requested_eth, - pswap_tag_felt, - p2id_tag_felt, - 0, + let mut rng = RpoRandomCoin::new(Word::default()); + let swap_note = PswapNote::create( alice.id(), - ); - let note_assets = make_note_assets(usdc_faucet.id(), *offered_usdc)?; - let swap_note = create_pswap_note(alice.id(), note_assets, storage_items, pswap_tag); + Asset::Fungible(FungibleAsset::new(usdc_faucet.id(), *offered_usdc)?), + Asset::Fungible(FungibleAsset::new(eth_faucet.id(), *requested_eth)?), + NoteType::Public, + NoteAttachment::default(), + &mut rng, + )?; builder.add_output_note(RawOutputNote::Full(swap_note.clone())); let mock_chain = builder.build()?; let mut note_args_map = BTreeMap::new(); - note_args_map.insert(swap_note.id(), make_note_args(*fill_eth, 0)); + note_args_map.insert( + swap_note.id(), + Word::from([Felt::new(*fill_eth), Felt::new(0), ZERO, ZERO]), + ); - let p2id_note = create_expected_pswap_p2id_note( - &swap_note, - bob.id(), - alice.id(), - 0, - *fill_eth, - eth_faucet.id(), - compute_p2id_tag_for_local_account(alice.id()), - )?; + let pswap = PswapNote::try_from(&swap_note)?; + let (p2id_note, remainder_pswap) = pswap.execute(bob.id(), *fill_eth, 0)?; let mut expected_notes = vec![RawOutputNote::Full(p2id_note)]; if remaining_requested > 0 { - let remainder = create_expected_pswap_remainder_note( - &swap_note, - bob.id(), - alice.id(), - remaining_offered, - remaining_requested, - offered_out, - 0, - usdc_faucet.id(), - eth_faucet.id(), - pswap_tag, - pswap_tag_felt, - p2id_tag_felt, - )?; + let remainder = + Note::from(remainder_pswap.expect("partial fill should produce remainder")); expected_notes.push(RawOutputNote::Full(remainder)); } @@ -1080,13 +785,12 @@ async fn pswap_note_chained_partial_fills_non_integer_ratio_test() -> anyhow::Re let mut current_swap_count = 0u64; // Track serial for remainder chain - use miden_protocol::crypto::rand::{FeltRng, RpoRandomCoin}; let mut rng = RpoRandomCoin::new(Word::default()); let mut current_serial = rng.draw_word(); for (fill_idx, fill_amount) in fills.iter().enumerate() { let offered_out = - calculate_output_amount(current_offered, current_requested, *fill_amount); + PswapNote::calculate_output_amount(current_offered, current_requested, *fill_amount); let remaining_offered = current_offered - offered_out; let remaining_requested = current_requested - fill_amount; @@ -1115,58 +819,49 @@ async fn pswap_note_chained_partial_fills_non_integer_ratio_test() -> anyhow::Re [FungibleAsset::new(eth_faucet.id(), *fill_amount)?.into()], )?; - let pswap_tag = NoteTag::new(0xC0000000); - let pswap_tag_felt = Felt::new(u32::from(pswap_tag) as u64); - let p2id_tag_felt = compute_p2id_tag_felt(alice.id()); + // Build storage and note manually to use the correct serial for chain position + let offered_asset = + Asset::Fungible(FungibleAsset::new(usdc_faucet.id(), current_offered)?); + let requested_asset = + Asset::Fungible(FungibleAsset::new(eth_faucet.id(), current_requested)?); + + let pswap_tag = PswapNote::build_tag(NoteType::Public, &offered_asset, &requested_asset); + let p2id_tag = NoteTag::with_account_target(alice.id()); - let storage_items = build_pswap_storage( - eth_faucet.id(), - current_requested, - pswap_tag_felt, - p2id_tag_felt, + let storage = PswapNoteStorage::from_parts( + requested_asset.to_key_word(), + requested_asset.to_value_word(), + pswap_tag, + p2id_tag, current_swap_count, alice.id(), ); - let note_assets = make_note_assets(usdc_faucet.id(), current_offered)?; + let note_assets = NoteAssets::new(vec![offered_asset])?; // Create note with the correct serial for this chain position - let note_storage = NoteStorage::new(storage_items)?; - let recipient = NoteRecipient::new(current_serial, PswapNote::script(), note_storage); - let metadata = NoteMetadata::new(alice.id(), NoteType::Public).with_tag(pswap_tag); + let note_storage = NoteStorage::from(storage); + let recipient = + NoteRecipient::new(current_serial, PswapNote::script(), note_storage); + let metadata = + NoteMetadata::new(alice.id(), NoteType::Public).with_tag(pswap_tag); let swap_note = Note::new(note_assets, metadata, recipient); builder.add_output_note(RawOutputNote::Full(swap_note.clone())); let mock_chain = builder.build()?; let mut note_args_map = BTreeMap::new(); - note_args_map.insert(swap_note.id(), make_note_args(*fill_amount, 0)); + note_args_map.insert( + swap_note.id(), + Word::from([Felt::new(*fill_amount), Felt::new(0), ZERO, ZERO]), + ); - let p2id_note = create_expected_pswap_p2id_note( - &swap_note, - bob.id(), - alice.id(), - current_swap_count, - *fill_amount, - eth_faucet.id(), - compute_p2id_tag_for_local_account(alice.id()), - )?; + let pswap = PswapNote::try_from(&swap_note)?; + let (p2id_note, remainder_pswap) = pswap.execute(bob.id(), *fill_amount, 0)?; let mut expected_notes = vec![RawOutputNote::Full(p2id_note)]; if remaining_requested > 0 { - let remainder = create_expected_pswap_remainder_note( - &swap_note, - bob.id(), - alice.id(), - remaining_offered, - remaining_requested, - offered_out, - current_swap_count, - usdc_faucet.id(), - eth_faucet.id(), - pswap_tag, - pswap_tag_felt, - p2id_tag_felt, - )?; + let remainder = + Note::from(remainder_pswap.expect("partial fill should produce remainder")); expected_notes.push(RawOutputNote::Full(remainder)); } @@ -1241,11 +936,9 @@ async fn pswap_note_chained_partial_fills_non_integer_ratio_test() -> anyhow::Re Ok(()) } -/// Test that PswapNote::create and PswapNote::create_output_notes produce correct results +/// Test that PswapNote::create + try_from + execute roundtrips correctly #[test] fn compare_pswap_create_output_notes_vs_test_helper() { - use miden_protocol::crypto::rand::{FeltRng, RpoRandomCoin}; - let mut builder = MockChain::builder(); let usdc_faucet = builder.add_existing_basic_faucet(BASIC_AUTH, "USDC", 1000, Some(150)).unwrap(); @@ -1265,7 +958,7 @@ fn compare_pswap_create_output_notes_vs_test_helper() { // Create swap note using PswapNote::create let mut rng = RpoRandomCoin::new(Word::default()); - let swap_note_lib = PswapNote::create( + let swap_note = PswapNote::create( alice.id(), Asset::Fungible(FungibleAsset::new(usdc_faucet.id(), 50).unwrap()), Asset::Fungible(FungibleAsset::new(eth_faucet.id(), 25).unwrap()), @@ -1275,68 +968,56 @@ fn compare_pswap_create_output_notes_vs_test_helper() { ) .unwrap(); - // Create output notes using library - let pswap = PswapNote::try_from(&swap_note_lib).unwrap(); - let (lib_p2id, _) = pswap.execute(bob.id(), 25, 0).unwrap(); - - // Create same swap note using test helper (same serial) - let (_pswap_tag, pswap_tag_felt) = make_pswap_tag(); - let p2id_tag_felt = compute_p2id_tag_felt(alice.id()); - let storage_items = - build_pswap_storage(eth_faucet.id(), 25, pswap_tag_felt, p2id_tag_felt, 0, alice.id()); - let note_assets = make_note_assets(usdc_faucet.id(), 50).unwrap(); - - // Use the SAME serial as the library note - let test_serial = swap_note_lib.recipient().serial_num(); - let test_storage = NoteStorage::new(storage_items).unwrap(); - let test_recipient = NoteRecipient::new(test_serial, PswapNote::script(), test_storage); - let test_metadata = - NoteMetadata::new(alice.id(), NoteType::Public).with_tag(NoteTag::new(0xC0000000)); - let swap_note_test = Note::new(note_assets, test_metadata, test_recipient); - - // Create expected P2ID using test helper - let test_p2id = create_expected_pswap_p2id_note( - &swap_note_test, - bob.id(), - alice.id(), - 0, - 25, - eth_faucet.id(), - compute_p2id_tag_for_local_account(alice.id()), - ) - .unwrap(); + // Roundtrip: try_from -> execute -> verify outputs + let pswap = PswapNote::try_from(&swap_note).unwrap(); + + // Verify roundtripped PswapNote preserves key fields + assert_eq!(pswap.sender(), alice.id(), "Sender mismatch after roundtrip"); + assert_eq!(pswap.note_type(), NoteType::Public, "Note type mismatch after roundtrip"); + assert_eq!(pswap.assets().num_assets(), 1, "Assets count mismatch after roundtrip"); + assert_eq!(pswap.storage().requested_amount(), 25, "Requested amount mismatch"); + assert_eq!(pswap.storage().swap_count(), 0, "Swap count should be 0"); + assert_eq!(pswap.storage().creator_account_id(), alice.id(), "Creator ID mismatch"); + + // Full fill: should produce P2ID note, no remainder + let (p2id_note, remainder) = pswap.execute(bob.id(), 25, 0).unwrap(); + assert!(remainder.is_none(), "Full fill should not produce remainder"); + + // Verify P2ID note properties + assert_eq!(p2id_note.metadata().sender(), bob.id(), "P2ID sender should be consumer"); + assert_eq!(p2id_note.metadata().note_type(), NoteType::Public, "P2ID note type mismatch"); + assert_eq!(p2id_note.assets().num_assets(), 1, "P2ID should have 1 asset"); + if let Asset::Fungible(f) = p2id_note.assets().iter().next().unwrap() { + assert_eq!(f.faucet_id(), eth_faucet.id(), "P2ID asset faucet mismatch"); + assert_eq!(f.amount(), 25, "P2ID asset amount mismatch"); + } else { + panic!("Expected fungible asset in P2ID note"); + } - // Compare components - assert_eq!( - lib_p2id.recipient().serial_num(), - test_p2id.recipient().serial_num(), - "Serial mismatch!" - ); - assert_eq!( - lib_p2id.recipient().script().root(), - test_p2id.recipient().script().root(), - "Script root mismatch!" - ); - assert_eq!( - lib_p2id.recipient().digest(), - test_p2id.recipient().digest(), - "Recipient digest mismatch!" - ); - assert_eq!(lib_p2id.metadata().tag(), test_p2id.metadata().tag(), "Tag mismatch!"); - assert_eq!(lib_p2id.metadata().sender(), test_p2id.metadata().sender(), "Sender mismatch!"); + // Partial fill: should produce P2ID note + remainder + let (p2id_partial, remainder_partial) = pswap.execute(bob.id(), 10, 0).unwrap(); + let remainder_pswap = remainder_partial.expect("Partial fill should produce remainder"); + + assert_eq!(p2id_partial.assets().num_assets(), 1); + if let Asset::Fungible(f) = p2id_partial.assets().iter().next().unwrap() { + assert_eq!(f.faucet_id(), eth_faucet.id()); + assert_eq!(f.amount(), 10); + } + + // Verify remainder properties + assert_eq!(remainder_pswap.storage().swap_count(), 1, "Remainder swap count should be 1"); assert_eq!( - lib_p2id.metadata().note_type(), - test_p2id.metadata().note_type(), - "Note type mismatch!" + remainder_pswap.storage().creator_account_id(), + alice.id(), + "Remainder creator should be Alice" ); - assert_eq!(lib_p2id.id(), test_p2id.id(), "NOTE ID MISMATCH!"); + let remaining_requested = remainder_pswap.storage().requested_amount(); + assert_eq!(remaining_requested, 15, "Remaining requested should be 15"); } /// Test that PswapNote::parse_inputs roundtrips correctly #[test] fn pswap_parse_inputs_roundtrip() { - use miden_protocol::crypto::rand::{FeltRng, RpoRandomCoin}; - let mut builder = MockChain::builder(); let usdc_faucet = builder.add_existing_basic_faucet(BASIC_AUTH, "USDC", 1000, Some(150)).unwrap(); From 29f8ec34e2b1825a593d58e45a16e94ad065001d Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Fri, 20 Mar 2026 09:27:37 +0530 Subject: [PATCH 5/6] docs: polish pswap doc comments to match sibling note style - Replace storage layout list with markdown table - Remove trivial "Returns the X" docs on simple getters - Add # Errors sections where relevant - Rewrite method docs to describe intent, not implementation - Add one-line docs on From/TryFrom conversion impls - Tighten PswapNote struct doc --- crates/miden-standards/src/note/pswap.rs | 135 ++++++++++------------- 1 file changed, 60 insertions(+), 75 deletions(-) diff --git a/crates/miden-standards/src/note/pswap.rs b/crates/miden-standards/src/note/pswap.rs index ae68ca2cea..d769f2f156 100644 --- a/crates/miden-standards/src/note/pswap.rs +++ b/crates/miden-standards/src/note/pswap.rs @@ -33,18 +33,20 @@ static PSWAP_SCRIPT: LazyLock = LazyLock::new(|| { // PSWAP NOTE STORAGE // ================================================================================================ -/// Typed storage representation for a PSWAP note. +/// Canonical storage representation for a PSWAP note. /// -/// Encapsulates the 18-item storage layout used by the PSWAP MASM contract: -/// - [0-3]: ASSET_KEY (requested asset key from asset.to_key_word()) -/// - [4-7]: ASSET_VALUE (requested asset value from asset.to_value_word()) -/// - [8]: PSWAP tag -/// - [9]: P2ID routing tag -/// - [10-11]: Reserved (zero) -/// - [12]: Swap count -/// - [13-15]: Reserved (zero) -/// - [16]: Creator account ID prefix -/// - [17]: Creator account ID suffix +/// Maps to the 18-element [`NoteStorage`] layout consumed by the on-chain MASM script: +/// +/// | Slot | Field | +/// |---------|-------| +/// | `[0-3]` | Requested asset key (`asset.to_key_word()`) | +/// | `[4-7]` | Requested asset value (`asset.to_value_word()`) | +/// | `[8]` | PSWAP note tag | +/// | `[9]` | P2ID routing tag (targets the creator) | +/// | `[10-11]` | Reserved (zero) | +/// | `[12]` | Swap count (incremented on each partial fill) | +/// | `[13-15]` | Reserved (zero) | +/// | `[16-17]` | Creator account ID (prefix, suffix) | #[derive(Debug, Clone, PartialEq, Eq)] pub struct PswapNoteStorage { requested_key: Word, @@ -65,10 +67,10 @@ impl PswapNoteStorage { // CONSTRUCTORS // -------------------------------------------------------------------------------------------- - /// Creates storage for a new PSWAP note from the requested asset and creator. + /// Creates storage for a new PSWAP note. /// - /// The `pswap_tag` is defaulted and will be computed when converting to a [`Note`]. - /// The `swap_count` starts at 0. + /// The PSWAP tag is set to a placeholder and will be computed when the note is + /// converted into a [`Note`] via [`From`]. Swap count starts at 0. pub fn new(requested_asset: Asset, creator_account_id: AccountId) -> Self { let p2id_tag = NoteTag::with_account_target(creator_account_id); Self { @@ -82,8 +84,6 @@ impl PswapNoteStorage { } /// Creates storage with all fields specified explicitly. - /// - /// Used for remainder notes where all fields (including swap count and tags) are known. pub fn from_parts( requested_key: Word, requested_value: Word, @@ -107,7 +107,8 @@ impl PswapNoteStorage { NoteRecipient::new(serial_num, PswapNote::script(), NoteStorage::from(self)) } - /// Sets the pswap_tag on this storage, returning the modified storage. + /// Overwrites the PSWAP tag. Called during [`Note`] conversion once the tag can be derived + /// from the offered/requested asset pair. pub(crate) fn with_pswap_tag(mut self, tag: NoteTag) -> Self { self.pswap_tag = tag; self @@ -116,37 +117,36 @@ impl PswapNoteStorage { // PUBLIC ACCESSORS // -------------------------------------------------------------------------------------------- - /// Returns the requested asset key word. pub fn requested_key(&self) -> Word { self.requested_key } - /// Returns the requested asset value word. pub fn requested_value(&self) -> Word { self.requested_value } - /// Returns the PSWAP note tag. pub fn pswap_tag(&self) -> NoteTag { self.pswap_tag } - /// Returns the P2ID routing tag. pub fn p2id_tag(&self) -> NoteTag { self.p2id_tag } - /// Returns the current swap count. + /// Number of times this note has been partially filled and re-created. pub fn swap_count(&self) -> u64 { self.swap_count } - /// Returns the creator account ID. pub fn creator_account_id(&self) -> AccountId { self.creator_account_id } - /// Reconstructs the requested asset from the key and value words. + /// Reconstructs the requested [`Asset`] from the stored key and value words. + /// + /// # Errors + /// + /// Returns an error if the faucet ID or amount stored in the key/value words is invalid. pub fn requested_asset(&self) -> Result { let faucet_id = self.requested_faucet_id()?; let amount = self.requested_amount(); @@ -155,21 +155,20 @@ impl PswapNoteStorage { })?)) } - /// Extracts the faucet ID from the requested key word. + /// Extracts the faucet ID of the requested asset from the key word. pub fn requested_faucet_id(&self) -> Result { - // Key layout: [key[0], key[1], faucet_suffix, faucet_prefix] AccountId::try_from_elements(self.requested_key[2], self.requested_key[3]).map_err(|e| { NoteError::other_with_source("failed to parse faucet ID from key", e) }) } - /// Extracts the requested amount from the value word. + /// Extracts the requested token amount from the value word. pub fn requested_amount(&self) -> u64 { - // ASSET_VALUE[0] = amount (from asset::fungible_to_amount) self.requested_value[0].as_canonical_u64() } } +/// Serializes [`PswapNoteStorage`] into an 18-element [`NoteStorage`]. impl From for NoteStorage { fn from(storage: PswapNoteStorage) -> Self { let inputs = vec![ @@ -203,6 +202,7 @@ impl From for NoteStorage { } } +/// Deserializes [`PswapNoteStorage`] from a slice of exactly 18 [`Felt`]s. impl TryFrom<&[Felt]> for PswapNoteStorage { type Error = NoteError; @@ -239,13 +239,12 @@ impl TryFrom<&[Felt]> for PswapNoteStorage { // PSWAP NOTE // ================================================================================================ -/// Partial swap (pswap) note for decentralized asset exchange. +/// A partially-fillable swap note for decentralized asset exchange. /// -/// This note implements a partially-fillable swap mechanism where: -/// - Creator offers an asset and requests another asset -/// - Note can be partially or fully filled by consumers -/// - Unfilled portions create remainder notes -/// - Creator receives requested assets via P2ID notes +/// A PSWAP note allows a creator to offer one fungible asset in exchange for another. +/// Unlike a regular SWAP note, consumers may fill it partially — the unfilled portion +/// is re-created as a remainder note with an incremented swap count, while the creator +/// receives the filled portion via a P2ID payback note. #[derive(Debug, Clone, bon::Builder)] #[builder(finish_fn(vis = "", name = build_internal))] pub struct PswapNote { @@ -273,42 +272,36 @@ impl PswapNote { // PUBLIC ACCESSORS // -------------------------------------------------------------------------------------------- - /// Returns the script of the PSWAP note. + /// Returns the compiled PSWAP note script. pub fn script() -> NoteScript { PSWAP_SCRIPT.clone() } - /// Returns the PSWAP note script root. + /// Returns the root hash of the PSWAP note script. pub fn script_root() -> Word { PSWAP_SCRIPT.root() } - /// Returns the sender account ID. pub fn sender(&self) -> AccountId { self.sender } - /// Returns a reference to the note storage. pub fn storage(&self) -> &PswapNoteStorage { &self.storage } - /// Returns the serial number. pub fn serial_number(&self) -> Word { self.serial_number } - /// Returns the note type. pub fn note_type(&self) -> NoteType { self.note_type } - /// Returns a reference to the note assets. pub fn assets(&self) -> &NoteAssets { &self.assets } - /// Returns a reference to the note attachment. pub fn attachment(&self) -> &NoteAttachment { &self.attachment } @@ -316,14 +309,11 @@ impl PswapNote { // BUILDERS // -------------------------------------------------------------------------------------------- - /// Creates a PSWAP note offering one asset in exchange for another. - /// - /// This is a convenience method that constructs a [`PswapNote`] and converts it to a - /// protocol [`Note`]. + /// Creates a PSWAP note offering `offered_asset` in exchange for `requested_asset`. /// /// # Errors /// - /// Returns an error if assets are invalid or have the same faucet ID. + /// Returns an error if the two assets share the same faucet or if asset construction fails. pub fn create( creator_account_id: AccountId, offered_asset: Asset, @@ -354,21 +344,13 @@ impl PswapNote { // INSTANCE METHODS // -------------------------------------------------------------------------------------------- - /// Executes the swap by creating output notes for a fill. - /// - /// Handles both full and partial fills: - /// - **Full fill**: Returns P2ID note with full requested amount, no remainder - /// - **Partial fill**: Returns P2ID note with partial amount + remainder PswapNote - /// - /// # Arguments + /// Executes the swap, producing the output notes for a given fill. /// - /// * `consumer_account_id` - The account consuming the swap note - /// * `input_amount` - Amount debited from consumer's vault - /// * `inflight_amount` - Amount added directly (no vault debit, for cross-swaps) + /// `input_amount` is debited from the consumer's vault; `inflight_amount` arrives + /// from another note in the same transaction (cross-swap). Their sum is the total fill. /// - /// # Returns - /// - /// Returns a tuple of `(p2id_note, Option)` + /// Returns `(p2id_payback_note, Option)`. The remainder is + /// `None` when the fill equals the total requested amount (full fill). pub fn execute( &self, consumer_account_id: AccountId, @@ -454,9 +436,8 @@ impl PswapNote { Ok((p2id_note, remainder)) } - /// Calculates how many offered tokens a consumer receives for a given requested input. - /// - /// This is the Rust equivalent of `calculate_tokens_offered_for_requested` in pswap.masm. + /// Returns how many offered tokens a consumer receives for `input_amount` of the + /// requested asset, based on this note's current offered/requested ratio. pub fn calculate_offered_for_requested( &self, input_amount: u64, @@ -479,12 +460,13 @@ impl PswapNote { // ASSOCIATED FUNCTIONS // -------------------------------------------------------------------------------------------- - /// Returns a note tag for a pswap note with the specified parameters. + /// Builds the 32-bit [`NoteTag`] for a PSWAP note. /// - /// Layout: /// ```text - /// [ note_type (2 bits) | script_root (14 bits) - /// | offered_asset_faucet_id (8 bits) | requested_asset_faucet_id (8 bits) ] + /// [31..30] note_type (2 bits) + /// [29..16] script_root MSBs (14 bits) + /// [15..8] offered faucet ID (8 bits, top byte of prefix) + /// [7..0] requested faucet ID (8 bits, top byte of prefix) /// ``` pub fn build_tag( note_type: NoteType, @@ -514,8 +496,9 @@ impl PswapNote { NoteTag::new(tag) } - /// Calculates the output amount for a fill using u64 integer arithmetic - /// with a precision factor of 1e5 (matching the MASM on-chain calculation). + /// Computes `offered_total * input_amount / requested_total` using fixed-point + /// u64 arithmetic with a precision factor of 10^5, matching the on-chain MASM + /// calculation. Returns the full `offered_total` when `input_amount == requested_total`. pub fn calculate_output_amount( offered_total: u64, requested_total: u64, @@ -536,10 +519,10 @@ impl PswapNote { } } - /// Builds a P2ID (Pay-to-ID) payback note for the swap creator. + /// Builds a P2ID payback note that delivers the filled assets to the swap creator. /// - /// The P2ID note inherits the note type from this PSWAP note. - /// Derives a unique serial number matching the MASM: `hmerge(swap_count_word, serial_num)`. + /// The note inherits its type (public/private) from this PSWAP note and derives a + /// deterministic serial number via `hmerge(swap_count + 1, serial_num)`. pub fn build_p2id_payback_note( &self, consumer_account_id: AccountId, @@ -568,10 +551,10 @@ impl PswapNote { Ok(Note::new(p2id_assets, p2id_metadata, recipient)) } - /// Builds a remainder note for partial fills. + /// Builds a remainder PSWAP note carrying the unfilled portion of the swap. /// - /// Builds updated note storage with the remaining requested amount and incremented - /// swap count, returning a [`PswapNote`] that can be converted to a protocol [`Note`]. + /// The remainder inherits the original creator, tags, and note type, but has an + /// incremented swap count and an updated serial number (`serial[0] + 1`). pub fn build_remainder_pswap_note( &self, consumer_account_id: AccountId, @@ -625,6 +608,7 @@ impl PswapNote { // CONVERSIONS // ================================================================================================ +/// Converts a [`PswapNote`] into a protocol [`Note`], computing the final PSWAP tag. impl From for Note { fn from(pswap: PswapNote) -> Self { let offered_asset = pswap @@ -655,6 +639,7 @@ impl From<&PswapNote> for Note { } } +/// Parses a protocol [`Note`] back into a [`PswapNote`] by deserializing its storage. impl TryFrom<&Note> for PswapNote { type Error = NoteError; From 0f1b15b6ecfed63fe7ef4fc167c9570526b3d90b Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Fri, 20 Mar 2026 09:30:49 +0530 Subject: [PATCH 6/6] refactor: make assets a required field on PswapNote builder --- crates/miden-standards/src/note/pswap.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/miden-standards/src/note/pswap.rs b/crates/miden-standards/src/note/pswap.rs index d769f2f156..66f248e74e 100644 --- a/crates/miden-standards/src/note/pswap.rs +++ b/crates/miden-standards/src/note/pswap.rs @@ -255,7 +255,6 @@ pub struct PswapNote { #[builder(default = NoteType::Private)] note_type: NoteType, - #[builder(default)] assets: NoteAssets, #[builder(default)]