From d6c38dc41545198188d5ea5e9eaaba692fe4d88c Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Thu, 19 Mar 2026 15:07:27 +0530 Subject: [PATCH 01/66] 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 7da32ea234..8ef5080344 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, SwapNoteStorage}; @@ -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 14c6f5d074509a2d3aa5dbc5cbe7c1676242d707 Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Fri, 20 Mar 2026 09:06:29 +0530 Subject: [PATCH 02/66] 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 10b8e860d3..bafdee7cf8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -350,6 +350,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" @@ -701,6 +726,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" @@ -1227,6 +1286,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" @@ -1877,6 +1942,7 @@ version = "0.15.0" dependencies = [ "anyhow", "assert_matches", + "bon", "fs-err", "miden-assembly", "miden-core", @@ -3330,6 +3396,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 932913e36b..003edd7b41 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,6 +61,7 @@ miden-verifier = { default-features = false, version = "0.22" } # External dependencies alloy-sol-types = { default-features = false, version = "1.5" } 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 8ef5080344..fed74f57b1 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, SwapNoteStorage}; @@ -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 6be38dfc3e9f3ac8789d5aa3649bb6236a108461 Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Fri, 20 Mar 2026 09:06:43 +0530 Subject: [PATCH 03/66] 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 163d81329c935d8bb37fbf3d6e67c96213a0391e Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Fri, 20 Mar 2026 09:15:32 +0530 Subject: [PATCH 04/66] 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 fed74f57b1..eaef854e68 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 5345f60fbc4b9ec427ddec4905539267f9714f7a Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Fri, 20 Mar 2026 09:27:37 +0530 Subject: [PATCH 05/66] 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 fd3bf6354649dbf4e81f74ea08e21f961e073bf6 Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Fri, 20 Mar 2026 09:30:49 +0530 Subject: [PATCH 06/66] 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)] From f0b11014bbc6361ad6f58bec33f741ecbf9b5195 Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Sat, 21 Mar 2026 04:41:00 +0530 Subject: [PATCH 07/66] fix: adapt pswap to upstream API changes after rebase - Rename RpoRandomCoin to RandomCoin (miden-crypto 0.23 rename) - Store full ASSET_KEY instead of prefix/suffix to preserve callback metadata in faucet_id_suffix_and_metadata - Replace create_fungible_asset calls with direct ASSET_KEY + manual ASSET_VALUE construction, avoiding the new enable_callbacks parameter - Update hardcoded P2ID script root to match current P2idNote::script_root() --- .../asm/standards/notes/pswap.masm | 59 ++++++------- crates/miden-standards/src/note/pswap.rs | 8 +- crates/miden-testing/tests/scripts/pswap.rs | 88 ++++++++----------- 3 files changed, 64 insertions(+), 91 deletions(-) diff --git a/crates/miden-standards/asm/standards/notes/pswap.masm b/crates/miden-standards/asm/standards/notes/pswap.masm index 18b1c1d384..9e85f00e39 100644 --- a/crates/miden-standards/asm/standards/notes/pswap.masm +++ b/crates/miden-standards/asm/standards/notes/pswap.masm @@ -20,7 +20,7 @@ const MAX_U32=0x0000000100000000 # Memory Address Layout: # - PSWAP Note Storage: addresses 0x0000 - 0x0011 (loaded from note storage) # - Price Calculation: addresses 0x0028 - 0x0036 -# - TokenId: addresses 0x002D - 0x0030 +# - Asset Keys: addresses 0x0038 - 0x003F (word-aligned) # - Full Word (word-aligned): addresses 0x0018+ # PSWAP Note Storage (18 items loaded at address 0) @@ -46,11 +46,10 @@ 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 +# Asset Key Memory Addresses (word-aligned, 4 cells each) +# ASSET_KEY = [asset_id_suffix, asset_id_prefix, faucet_id_suffix_and_metadata, faucet_id_prefix] +const OFFERED_ASSET_KEY = 0x0038 +const REQUESTED_ASSET_KEY = 0x003C # Full Word Memory Addresses # Asset storage (8 cells each, word-aligned) @@ -255,7 +254,7 @@ end #! proc create_p2id_note # 1. Load P2ID script root (miden-standards v0.14.0-beta.2, P2idNote::script_root()) - push.17577144666381623537.251255385102954082.10949974299239761467.7391338276508122752 + push.3672480923625915098.9159872301314598941.4201800931930002347.17479343943140556328 # => [P2ID_SCRIPT_ROOT] # 2. Increment swap count (ensures unique serial per P2ID note in chained fills) @@ -317,13 +316,12 @@ proc create_p2id_note 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)] + # Build ASSET_VALUE = [amount, 0, 0, 0] (amount on top) + push.0.0.0 mem_load.AMT_REQUESTED_IN + # => [amount, 0, 0, 0, note_idx, pad(7)] - exec.asset::create_fungible_asset + # Load stored ASSET_KEY + padw mem_loadw_le.REQUESTED_ASSET_KEY # => [ASSET_KEY, ASSET_VALUE, note_idx, pad(7)] call.wallet::move_asset_to_note @@ -343,12 +341,12 @@ proc create_p2id_note 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] + # Build ASSET_VALUE = [amount, 0, 0, 0] (amount on top) + push.0.0.0 mem_load.AMT_REQUESTED_INFLIGHT + # => [amount, 0, 0, 0, note_idx] - exec.asset::create_fungible_asset + # Load stored ASSET_KEY + padw mem_loadw_le.REQUESTED_ASSET_KEY # => [ASSET_KEY, ASSET_VALUE, note_idx] exec.output_note::add_asset @@ -415,14 +413,13 @@ proc create_remainder_note mem_load.SWAPP_NOTE_IDX # => [note_idx] + # Build ASSET_VALUE = [amount, 0, 0, 0] (amount on top) + push.0.0.0 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] + # => [remainder_amount, 0, 0, 0, note_idx] - exec.asset::create_fungible_asset + # Load stored ASSET_KEY + padw mem_loadw_le.OFFERED_ASSET_KEY # => [ASSET_KEY, ASSET_VALUE, note_idx] exec.output_note::add_asset @@ -520,11 +517,8 @@ proc execute_pswap 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 + # Store offered ASSET_KEY (preserves faucet ID + callback metadata) + mem_storew_le.OFFERED_ASSET_KEY dropw # => [ASSET_VALUE] dropw # => [] @@ -540,11 +534,8 @@ proc execute_pswap 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 + # Store requested ASSET_KEY (preserves faucet ID + callback metadata) + mem_storew_le.REQUESTED_ASSET_KEY dropw # => [ASSET_VALUE] dropw # => [] diff --git a/crates/miden-standards/src/note/pswap.rs b/crates/miden-standards/src/note/pswap.rs index 66f248e74e..97aa775053 100644 --- a/crates/miden-standards/src/note/pswap.rs +++ b/crates/miden-standards/src/note/pswap.rs @@ -699,8 +699,8 @@ mod tests { 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()); + use miden_protocol::crypto::rand::RandomCoin; + let mut rng = RandomCoin::new(Word::default()); let script = PswapNote::script(); assert!(script.root() != Word::default(), "Script root should not be zero"); @@ -762,8 +762,8 @@ mod tests { 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()); + use miden_protocol::crypto::rand::{FeltRng, RandomCoin}; + let mut rng = RandomCoin::new(Word::default()); let storage = PswapNoteStorage::new(requested_asset, creator_id); let pswap = PswapNote::builder() diff --git a/crates/miden-testing/tests/scripts/pswap.rs b/crates/miden-testing/tests/scripts/pswap.rs index 87cc103fee..cf37c84585 100644 --- a/crates/miden-testing/tests/scripts/pswap.rs +++ b/crates/miden-testing/tests/scripts/pswap.rs @@ -2,7 +2,7 @@ use std::collections::BTreeMap; use miden_protocol::account::auth::AuthScheme; use miden_protocol::asset::{Asset, FungibleAsset}; -use miden_protocol::crypto::rand::{FeltRng, RpoRandomCoin}; +use miden_protocol::crypto::rand::{FeltRng, RandomCoin}; use miden_protocol::note::{ Note, NoteAssets, NoteAttachment, NoteMetadata, NoteRecipient, NoteStorage, NoteTag, NoteType, }; @@ -40,7 +40,7 @@ async fn pswap_note_full_fill_test() -> anyhow::Result<()> { 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()); + let mut rng = RandomCoin::new(Word::default()); let swap_note = PswapNote::create( alice.id(), offered_asset, @@ -54,10 +54,7 @@ async fn pswap_note_full_fill_test() -> anyhow::Result<()> { let mut mock_chain = builder.build()?; let mut note_args_map = BTreeMap::new(); - note_args_map.insert( - swap_note.id(), - Word::from([Felt::new(25), Felt::new(0), ZERO, ZERO]), - ); + note_args_map.insert(swap_note.id(), Word::from([Felt::new(25), Felt::new(0), ZERO, ZERO])); let pswap = PswapNote::try_from(&swap_note)?; let (p2id_note, _remainder) = pswap.execute(bob.id(), 25, 0)?; @@ -128,7 +125,7 @@ async fn pswap_note_private_full_fill_test() -> anyhow::Result<()> { 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()); + let mut rng = RandomCoin::new(Word::default()); // Create a PRIVATE swap note (output notes should also be Private) let swap_note = PswapNote::create( alice.id(), @@ -143,10 +140,7 @@ async fn pswap_note_private_full_fill_test() -> anyhow::Result<()> { let mut mock_chain = builder.build()?; let mut note_args_map = BTreeMap::new(); - note_args_map.insert( - swap_note.id(), - Word::from([Felt::new(25), Felt::new(0), ZERO, ZERO]), - ); + 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 pswap = PswapNote::try_from(&swap_note)?; @@ -214,7 +208,7 @@ async fn pswap_note_partial_fill_test() -> anyhow::Result<()> { 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()); + let mut rng = RandomCoin::new(Word::default()); let swap_note = PswapNote::create( alice.id(), offered_asset, @@ -228,14 +222,12 @@ async fn pswap_note_partial_fill_test() -> anyhow::Result<()> { let mut mock_chain = builder.build()?; let mut note_args_map = BTreeMap::new(); - note_args_map.insert( - swap_note.id(), - Word::from([Felt::new(20), Felt::new(0), ZERO, ZERO]), - ); + note_args_map.insert(swap_note.id(), Word::from([Felt::new(20), Felt::new(0), ZERO, ZERO])); 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 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()], &[])? @@ -302,7 +294,7 @@ async fn pswap_note_inflight_cross_swap_test() -> anyhow::Result<()> { )?; let charlie = builder.add_existing_wallet_with_assets(BASIC_AUTH, [])?; - let mut rng = RpoRandomCoin::new(Word::default()); + let mut rng = RandomCoin::new(Word::default()); // Alice's note: offers 50 USDC, requests 25 ETH let alice_swap_note = PswapNote::create( @@ -330,14 +322,9 @@ async fn pswap_note_inflight_cross_swap_test() -> anyhow::Result<()> { // Note args: pure inflight (input=0, inflight=full amount) let mut note_args_map = BTreeMap::new(); - 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]), - ); + 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_pswap = PswapNote::try_from(&alice_swap_note)?; @@ -396,7 +383,7 @@ async fn pswap_note_creator_reclaim_test() -> anyhow::Result<()> { [FungibleAsset::new(usdc_faucet.id(), 50)?.into()], )?; - let mut rng = RpoRandomCoin::new(Word::default()); + let mut rng = RandomCoin::new(Word::default()); let swap_note = PswapNote::create( alice.id(), Asset::Fungible(FungibleAsset::new(usdc_faucet.id(), 50)?), @@ -448,7 +435,7 @@ async fn pswap_note_invalid_input_test() -> anyhow::Result<()> { [FungibleAsset::new(eth_faucet.id(), 30)?.into()], )?; - let mut rng = RpoRandomCoin::new(Word::default()); + let mut rng = RandomCoin::new(Word::default()); let swap_note = PswapNote::create( alice.id(), Asset::Fungible(FungibleAsset::new(usdc_faucet.id(), 50)?), @@ -462,10 +449,7 @@ async fn pswap_note_invalid_input_test() -> anyhow::Result<()> { // 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(), - Word::from([Felt::new(30), Felt::new(0), ZERO, ZERO]), - ); + 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()], &[])? @@ -510,7 +494,7 @@ async fn pswap_note_multiple_partial_fills_test() -> anyhow::Result<()> { [FungibleAsset::new(eth_faucet.id(), input_amount)?.into()], )?; - let mut rng = RpoRandomCoin::new(Word::default()); + let mut rng = RandomCoin::new(Word::default()); let swap_note = PswapNote::create( alice.id(), Asset::Fungible(FungibleAsset::new(usdc_faucet.id(), 50)?), @@ -585,7 +569,7 @@ async fn pswap_note_non_exact_ratio_partial_fill_test() -> anyhow::Result<()> { [FungibleAsset::new(eth_faucet.id(), input_amount)?.into()], )?; - let mut rng = RpoRandomCoin::new(Word::default()); + let mut rng = RandomCoin::new(Word::default()); let swap_note = PswapNote::create( alice.id(), Asset::Fungible(FungibleAsset::new(usdc_faucet.id(), offered_total)?), @@ -599,10 +583,8 @@ async fn pswap_note_non_exact_ratio_partial_fill_test() -> anyhow::Result<()> { let mock_chain = builder.build()?; let mut note_args_map = BTreeMap::new(); - note_args_map.insert( - swap_note.id(), - Word::from([Felt::new(input_amount), Felt::new(0), ZERO, ZERO]), - ); + note_args_map + .insert(swap_note.id(), Word::from([Felt::new(input_amount), Felt::new(0), ZERO, ZERO])); let pswap = PswapNote::try_from(&swap_note)?; let (p2id_note, remainder_pswap) = pswap.execute(bob.id(), input_amount, 0)?; @@ -694,7 +676,7 @@ async fn pswap_note_partial_fill_non_integer_ratio_fuzz_test() -> anyhow::Result [FungibleAsset::new(eth_faucet.id(), *fill_eth)?.into()], )?; - let mut rng = RpoRandomCoin::new(Word::default()); + let mut rng = RandomCoin::new(Word::default()); let swap_note = PswapNote::create( alice.id(), Asset::Fungible(FungibleAsset::new(usdc_faucet.id(), *offered_usdc)?), @@ -708,10 +690,8 @@ async fn pswap_note_partial_fill_non_integer_ratio_fuzz_test() -> anyhow::Result let mock_chain = builder.build()?; let mut note_args_map = BTreeMap::new(); - note_args_map.insert( - swap_note.id(), - Word::from([Felt::new(*fill_eth), Felt::new(0), ZERO, ZERO]), - ); + note_args_map + .insert(swap_note.id(), Word::from([Felt::new(*fill_eth), Felt::new(0), ZERO, ZERO])); let pswap = PswapNote::try_from(&swap_note)?; let (p2id_note, remainder_pswap) = pswap.execute(bob.id(), *fill_eth, 0)?; @@ -785,12 +765,15 @@ async fn pswap_note_chained_partial_fills_non_integer_ratio_test() -> anyhow::Re let mut current_swap_count = 0u64; // Track serial for remainder chain - let mut rng = RpoRandomCoin::new(Word::default()); + let mut rng = RandomCoin::new(Word::default()); let mut current_serial = rng.draw_word(); for (fill_idx, fill_amount) in fills.iter().enumerate() { - let offered_out = - PswapNote::calculate_output_amount(current_offered, current_requested, *fill_amount); + let offered_out = PswapNote::calculate_output_amount( + current_offered, + current_requested, + *fill_amount, + ); let remaining_offered = current_offered - offered_out; let remaining_requested = current_requested - fill_amount; @@ -825,7 +808,8 @@ async fn pswap_note_chained_partial_fills_non_integer_ratio_test() -> anyhow::Re 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 pswap_tag = + PswapNote::build_tag(NoteType::Public, &offered_asset, &requested_asset); let p2id_tag = NoteTag::with_account_target(alice.id()); let storage = PswapNoteStorage::from_parts( @@ -840,10 +824,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::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 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())); @@ -957,7 +939,7 @@ fn compare_pswap_create_output_notes_vs_test_helper() { .unwrap(); // Create swap note using PswapNote::create - let mut rng = RpoRandomCoin::new(Word::default()); + let mut rng = RandomCoin::new(Word::default()); let swap_note = PswapNote::create( alice.id(), Asset::Fungible(FungibleAsset::new(usdc_faucet.id(), 50).unwrap()), @@ -1029,7 +1011,7 @@ fn pswap_parse_inputs_roundtrip() { ) .unwrap(); - let mut rng = RpoRandomCoin::new(Word::default()); + let mut rng = RandomCoin::new(Word::default()); let swap_note = PswapNote::create( alice.id(), Asset::Fungible(FungibleAsset::new(usdc_faucet.id(), 50).unwrap()), From df523d66292770db10d98e137096e9823c8554a2 Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Mon, 23 Mar 2026 12:43:41 +0530 Subject: [PATCH 08/66] refactor: use procref for P2ID script root, clean up pswap MASM - Replace hardcoded P2ID script root with procref.p2id::main for compile-time resolution - Default to full fill when both input and inflight amounts are zero - Replace magic address 4000 with named P2ID_RECIPIENT_STORAGE constant - Remove step numbering from comments, fix memory layout docs --- .../asm/standards/notes/pswap.masm | 49 +++++++++++-------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/crates/miden-standards/asm/standards/notes/pswap.masm b/crates/miden-standards/asm/standards/notes/pswap.masm index 9e85f00e39..612c13d25c 100644 --- a/crates/miden-standards/asm/standards/notes/pswap.masm +++ b/crates/miden-standards/asm/standards/notes/pswap.masm @@ -6,6 +6,7 @@ use miden::core::sys use miden::protocol::active_account use miden::core::math::u64 use miden::protocol::asset +use miden::standards::notes::p2id # CONSTANTS # ================================================================================================= @@ -20,8 +21,8 @@ const MAX_U32=0x0000000100000000 # Memory Address Layout: # - PSWAP Note Storage: addresses 0x0000 - 0x0011 (loaded from note storage) # - Price Calculation: addresses 0x0028 - 0x0036 -# - Asset Keys: addresses 0x0038 - 0x003F (word-aligned) -# - Full Word (word-aligned): addresses 0x0018+ +# - Asset Keys: addresses 0x0038 - 0x003B, 0x003C - 0x003F (word-aligned) +# - Full Word (word-aligned): addresses 0x0018 - 0x001F # PSWAP Note Storage (18 items loaded at address 0) # REQUESTED_ASSET_WORD_INPUT is the base address of an 8-cell block: @@ -60,6 +61,10 @@ const P2ID_NOTE_IDX = 0x007C const SWAPP_NOTE_IDX = 0x0080 const NOTE_TYPE = 0x0084 +# P2ID recipient storage (creator account ID written here for build_recipient) +const P2ID_RECIPIENT_STORAGE = 0x0FA0 +const P2ID_RECIPIENT_STORAGE_1 = 0x0FA1 + # ERRORS # ================================================================================================= @@ -227,14 +232,14 @@ end #! 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 + # Store creator [suffix, prefix] for P2ID recipient hashing + mem_load.SWAPP_CREATOR_SUFFIX_INPUT mem_store.P2ID_RECIPIENT_STORAGE + mem_load.SWAPP_CREATOR_PREFIX_INPUT mem_store.P2ID_RECIPIENT_STORAGE_1 # => [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] + push.2.P2ID_RECIPIENT_STORAGE + # => [storage_ptr, num_storage_items=2, SERIAL_NUM, SCRIPT_ROOT] exec.note::build_recipient # => [P2ID_RECIPIENT] @@ -253,34 +258,33 @@ end #! Outputs: [] #! proc create_p2id_note - # 1. Load P2ID script root (miden-standards v0.14.0-beta.2, P2idNote::script_root()) - push.3672480923625915098.9159872301314598941.4201800931930002347.17479343943140556328 + # Get P2ID script root at compile time via procref + procref.p2id::main # => [P2ID_SCRIPT_ROOT] - # 2. Increment swap count (ensures unique serial per P2ID note in chained fills) + # 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 + # Load swap count word from memory padw mem_loadw_le.SWAPP_COUNT_INPUT # => [SWAP_COUNT_WORD, P2ID_SCRIPT_ROOT] - # 4. Get serial number from active note + # 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) + # 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 + # Build P2ID recipient exec.build_p2id_recipient_hash # => [P2ID_RECIPIENT] - # 7. Create output note (note_type inherited from active note metadata) + # Create output note (note_type inherited from active note metadata) mem_load.NOTE_TYPE # => [note_type, P2ID_RECIPIENT] @@ -293,7 +297,7 @@ proc create_p2id_note mem_store.P2ID_NOTE_IDX # => [] - # 8. Set attachment: aux = input_amount + inflight_amount (total fill) + # 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] @@ -304,7 +308,7 @@ proc create_p2id_note exec.output_note::set_word_attachment # => [] - # 9. Move input_amount from consumer's vault to P2ID note (if > 0) + # Move input_amount from consumer's vault to P2ID note (if > 0) mem_load.AMT_REQUESTED_IN dup push.0 neq if.true drop @@ -333,7 +337,7 @@ proc create_p2id_note drop end - # 10. Add inflight_amount directly to P2ID note (no vault debit, if > 0) + # Add inflight_amount directly to P2ID note (no vault debit, if > 0) mem_load.AMT_REQUESTED_INFLIGHT dup push.0 neq if.true drop @@ -494,7 +498,6 @@ end # Word[2..3] = unused (0) # -# => [] proc execute_pswap # Load note assets to OFFERED_ASSET_WORD push.OFFERED_ASSET_WORD exec.active_note::get_assets @@ -540,6 +543,12 @@ proc execute_pswap dropw # => [] + # If both input and inflight are 0, default to full fill (input = requested) + mem_load.AMT_REQUESTED_IN mem_load.AMT_REQUESTED_INFLIGHT add push.0 eq + if.true + mem_load.AMT_REQUESTED mem_store.AMT_REQUESTED_IN + end + # Calculate offered_out for input_amount mem_load.AMT_REQUESTED_IN mem_load.AMT_REQUESTED From b3d94607d958769f5775f34ddfed85f3a2f66fb2 Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Mon, 23 Mar 2026 23:01:40 +0530 Subject: [PATCH 09/66] refactor: address PR review comments on pswap MASM - Rename P2ID_RECIPIENT_STORAGE to P2ID_RECIPIENT_SUFFIX/PREFIX - Add METADATA_HEADER word layout docs with source reference - Document attachment_scheme parameter in set_word_attachment call - Add stack views after condition checks --- .../asm/standards/notes/pswap.masm | 37 ++++++++++++------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/crates/miden-standards/asm/standards/notes/pswap.masm b/crates/miden-standards/asm/standards/notes/pswap.masm index 612c13d25c..c795be2c4a 100644 --- a/crates/miden-standards/asm/standards/notes/pswap.masm +++ b/crates/miden-standards/asm/standards/notes/pswap.masm @@ -12,7 +12,7 @@ use miden::standards::notes::p2id # ================================================================================================= const NOTE_TYPE_MASK=0x03 -const FACTOR=0x000186A0 # 1e5 +const FACTOR=0x000186A0 # 1e5 const MAX_U32=0x0000000100000000 # Memory Addresses @@ -62,8 +62,9 @@ const SWAPP_NOTE_IDX = 0x0080 const NOTE_TYPE = 0x0084 # P2ID recipient storage (creator account ID written here for build_recipient) -const P2ID_RECIPIENT_STORAGE = 0x0FA0 -const P2ID_RECIPIENT_STORAGE_1 = 0x0FA1 +# Layout matches P2ID note storage: [suffix, prefix] +const P2ID_RECIPIENT_SUFFIX = 0x0FA0 +const P2ID_RECIPIENT_PREFIX = 0x0FA1 # ERRORS # ================================================================================================= @@ -194,8 +195,14 @@ end #! 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) +#! get_metadata returns [NOTE_ATTACHMENT(4), METADATA_HEADER(4)]. +#! METADATA_HEADER word layout (see NoteMetadataHeader in miden-protocol/src/note/metadata.rs): +#! word[0] = sender_suffix_and_note_type (note_type in bits 0-1) +#! word[1] = sender_id_prefix +#! word[2] = tag +#! word[3] = attachment_kind_scheme +#! +#! After dropw and mem_loadw_le ordering, word[0] is on top of the stack. #! #! Inputs: [] #! Outputs: [] @@ -204,12 +211,10 @@ 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 + # => [word[0]=sender_suffix_and_note_type, word[1]=prefix, word[2]=tag, word[3]=attachment] + # Keep word[0] (top), move it to bottom and drop the rest movdn.3 drop drop drop - # => [hdr0 = sender_suffix_and_note_type] + # => [sender_suffix_and_note_type] u32split # => [lo32, hi32] (note_type in bits 0-1 of lo32, lo32 on top) push.NOTE_TYPE_MASK u32and @@ -233,12 +238,12 @@ end #! proc build_p2id_recipient_hash # Store creator [suffix, prefix] for P2ID recipient hashing - mem_load.SWAPP_CREATOR_SUFFIX_INPUT mem_store.P2ID_RECIPIENT_STORAGE - mem_load.SWAPP_CREATOR_PREFIX_INPUT mem_store.P2ID_RECIPIENT_STORAGE_1 + mem_load.SWAPP_CREATOR_SUFFIX_INPUT mem_store.P2ID_RECIPIENT_SUFFIX + mem_load.SWAPP_CREATOR_PREFIX_INPUT mem_store.P2ID_RECIPIENT_PREFIX # => [SERIAL_NUM, SCRIPT_ROOT] # note::build_recipient: [storage_ptr, num_storage_items, SERIAL_NUM, SCRIPT_ROOT] => [RECIPIENT] - push.2.P2ID_RECIPIENT_STORAGE + push.2.P2ID_RECIPIENT_SUFFIX # => [storage_ptr, num_storage_items=2, SERIAL_NUM, SCRIPT_ROOT] exec.note::build_recipient @@ -298,18 +303,21 @@ proc create_p2id_note # => [] # Set attachment: aux = input_amount + inflight_amount (total fill) + # attachment_scheme = 0 (NoteAttachmentScheme::none) + # See: output_note::set_word_attachment in miden-protocol/asm/protocol/output_note.masm 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] + # => [note_idx, attachment_scheme=0, ATTACHMENT] exec.output_note::set_word_attachment # => [] # Move input_amount from consumer's vault to P2ID note (if > 0) mem_load.AMT_REQUESTED_IN dup push.0 neq + # => [amt != 0, amt] if.true drop @@ -339,6 +347,7 @@ proc create_p2id_note # Add inflight_amount directly to P2ID note (no vault debit, if > 0) mem_load.AMT_REQUESTED_INFLIGHT dup push.0 neq + # => [amt != 0, amt] if.true drop From 971a6b875c09df83b2eea08b227b258fcf98faba Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Tue, 24 Mar 2026 07:38:21 +0530 Subject: [PATCH 10/66] refactor: rename pswap storage accessors, add offered_asset_amount helper, simplify build_p2id_payback_note Rename requested_key/requested_value/requested_amount to requested_asset_key/requested_asset_value/requested_asset_amount for clarity. Extract offered_asset_amount() helper on PswapNote to deduplicate offered asset extraction. Simplify build_p2id_payback_note to take fill_amount: u64 instead of aux_word: Word, constructing the aux word internally. --- crates/miden-standards/src/note/pswap.rs | 110 +++++++++++--------- crates/miden-testing/tests/scripts/pswap.rs | 6 +- 2 files changed, 61 insertions(+), 55 deletions(-) diff --git a/crates/miden-standards/src/note/pswap.rs b/crates/miden-standards/src/note/pswap.rs index 97aa775053..62e826541a 100644 --- a/crates/miden-standards/src/note/pswap.rs +++ b/crates/miden-standards/src/note/pswap.rs @@ -49,8 +49,8 @@ static PSWAP_SCRIPT: LazyLock = LazyLock::new(|| { /// | `[16-17]` | Creator account ID (prefix, suffix) | #[derive(Debug, Clone, PartialEq, Eq)] pub struct PswapNoteStorage { - requested_key: Word, - requested_value: Word, + requested_asset_key: Word, + requested_asset_value: Word, pswap_tag: NoteTag, p2id_tag: NoteTag, swap_count: u64, @@ -74,8 +74,8 @@ impl PswapNoteStorage { 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(), + requested_asset_key: requested_asset.to_key_word(), + requested_asset_value: requested_asset.to_value_word(), pswap_tag: NoteTag::new(0), p2id_tag, swap_count: 0, @@ -85,16 +85,16 @@ impl PswapNoteStorage { /// Creates storage with all fields specified explicitly. pub fn from_parts( - requested_key: Word, - requested_value: Word, + requested_asset_key: Word, + requested_asset_value: Word, pswap_tag: NoteTag, p2id_tag: NoteTag, swap_count: u64, creator_account_id: AccountId, ) -> Self { Self { - requested_key, - requested_value, + requested_asset_key, + requested_asset_value, pswap_tag, p2id_tag, swap_count, @@ -117,12 +117,12 @@ impl PswapNoteStorage { // PUBLIC ACCESSORS // -------------------------------------------------------------------------------------------- - pub fn requested_key(&self) -> Word { - self.requested_key + pub fn requested_asset_key(&self) -> Word { + self.requested_asset_key } - pub fn requested_value(&self) -> Word { - self.requested_value + pub fn requested_asset_value(&self) -> Word { + self.requested_asset_value } pub fn pswap_tag(&self) -> NoteTag { @@ -149,7 +149,7 @@ impl PswapNoteStorage { /// 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(); + let amount = self.requested_asset_amount(); Ok(Asset::Fungible(FungibleAsset::new(faucet_id, amount).map_err(|e| { NoteError::other_with_source("failed to create requested asset", e) })?)) @@ -157,14 +157,14 @@ impl PswapNoteStorage { /// Extracts the faucet ID of the requested asset from the key word. pub fn requested_faucet_id(&self) -> Result { - AccountId::try_from_elements(self.requested_key[2], self.requested_key[3]).map_err(|e| { + AccountId::try_from_elements(self.requested_asset_key[2], self.requested_asset_key[3]).map_err(|e| { NoteError::other_with_source("failed to parse faucet ID from key", e) }) } /// Extracts the requested token amount from the value word. - pub fn requested_amount(&self) -> u64 { - self.requested_value[0].as_canonical_u64() + pub fn requested_asset_amount(&self) -> u64 { + self.requested_asset_value[0].as_canonical_u64() } } @@ -173,15 +173,15 @@ 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], + storage.requested_asset_key[0], + storage.requested_asset_key[1], + storage.requested_asset_key[2], + storage.requested_asset_key[3], // ASSET_VALUE [4-7] - storage.requested_value[0], - storage.requested_value[1], - storage.requested_value[2], - storage.requested_value[3], + storage.requested_asset_value[0], + storage.requested_asset_value[1], + storage.requested_asset_value[2], + storage.requested_asset_value[3], // Tags [8-9] Felt::new(u32::from(storage.pswap_tag) as u64), Felt::new(u32::from(storage.p2id_tag) as u64), @@ -214,8 +214,8 @@ impl TryFrom<&[Felt]> for PswapNoteStorage { }); } - 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 requested_asset_key = Word::from([inputs[0], inputs[1], inputs[2], inputs[3]]); + let requested_asset_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(); @@ -226,8 +226,8 @@ impl TryFrom<&[Felt]> for PswapNoteStorage { })?; Ok(Self { - requested_key, - requested_value, + requested_asset_key, + requested_asset_value, pswap_tag, p2id_tag, swap_count, @@ -359,17 +359,16 @@ impl PswapNote { let fill_amount = input_amount + inflight_amount; let requested_faucet_id = self.storage.requested_faucet_id()?; - let total_requested_amount = self.storage.requested_amount(); + let total_requested_amount = self.storage.requested_asset_amount(); // Ensure offered asset exists and is fungible if self.assets.num_assets() != 1 { return Err(NoteError::other("Swap note must have exactly 1 offered asset")); } - let offered_asset = - 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")), + let total_offered_amount = self.offered_asset_amount()?; + let offered_faucet_id = match self.assets.iter().next().unwrap() { + Asset::Fungible(fa) => fa.faucet_id(), + _ => unreachable!(), }; // Validate fill amount @@ -404,12 +403,10 @@ impl PswapNote { 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.build_p2id_payback_note( consumer_account_id, payback_asset, - aux_word, + fill_amount, )?; // Create remainder note if partial fill @@ -435,23 +432,31 @@ impl PswapNote { Ok((p2id_note, remainder)) } + /// Returns the amount of the offered fungible asset in this note. + /// + /// # Errors + /// + /// Returns an error if the note has no assets or the asset is non-fungible. + pub fn offered_asset_amount(&self) -> Result { + let asset = self + .assets + .iter() + .next() + .ok_or(NoteError::other("No offered asset found"))?; + match asset { + Asset::Fungible(fa) => Ok(fa.amount()), + _ => Err(NoteError::other("Non-fungible offered asset not supported")), + } + } + /// 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, ) -> Result { - let total_requested = self.storage.requested_amount(); - - 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 total_requested = self.storage.requested_asset_amount(); + let total_offered = self.offered_asset_amount()?; Ok(Self::calculate_output_amount(total_offered, total_requested, input_amount)) } @@ -526,7 +531,7 @@ impl PswapNote { &self, consumer_account_id: AccountId, payback_asset: Asset, - aux_word: Word, + fill_amount: u64, ) -> Result { let p2id_tag = self.storage.p2id_tag(); // Derive P2ID serial matching PSWAP.masm @@ -540,6 +545,7 @@ impl PswapNote { let recipient = P2idNoteStorage::new(self.storage.creator_account_id).into_recipient(p2id_serial_num); + let aux_word = Word::from([Felt::new(fill_amount), ZERO, ZERO, ZERO]); let attachment = NoteAttachment::new_word(NoteAttachmentScheme::none(), aux_word); let p2id_assets = NoteAssets::new(vec![payback_asset])?; @@ -893,10 +899,10 @@ mod tests { assert_eq!(parsed.swap_count(), 3); assert_eq!(parsed.creator_account_id(), creator_id); assert_eq!( - parsed.requested_key(), + parsed.requested_asset_key(), Word::from([key_word[0], key_word[1], key_word[2], key_word[3]]) ); - assert_eq!(parsed.requested_amount(), 500); + assert_eq!(parsed.requested_asset_amount(), 500); } #[test] @@ -924,6 +930,6 @@ mod tests { assert_eq!(parsed.creator_account_id(), creator_id); assert_eq!(parsed.swap_count(), 0); - assert_eq!(parsed.requested_amount(), 500); + assert_eq!(parsed.requested_asset_amount(), 500); } } diff --git a/crates/miden-testing/tests/scripts/pswap.rs b/crates/miden-testing/tests/scripts/pswap.rs index cf37c84585..f82d2a71be 100644 --- a/crates/miden-testing/tests/scripts/pswap.rs +++ b/crates/miden-testing/tests/scripts/pswap.rs @@ -957,7 +957,7 @@ fn compare_pswap_create_output_notes_vs_test_helper() { 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().requested_asset_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"); @@ -993,7 +993,7 @@ fn compare_pswap_create_output_notes_vs_test_helper() { alice.id(), "Remainder creator should be Alice" ); - let remaining_requested = remainder_pswap.storage().requested_amount(); + let remaining_requested = remainder_pswap.storage().requested_asset_amount(); assert_eq!(remaining_requested, 15, "Remaining requested should be 15"); } @@ -1031,5 +1031,5 @@ fn pswap_parse_inputs_roundtrip() { assert_eq!(parsed.swap_count(), 0, "Swap count should be 0"); // Verify requested amount from value word - assert_eq!(parsed.requested_amount(), 25, "Requested amount should be 25"); + assert_eq!(parsed.requested_asset_amount(), 25, "Requested amount should be 25"); } From eb60fba3f5dd0e70bb21070c18884b4ee684d464 Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Wed, 25 Mar 2026 04:31:53 +0530 Subject: [PATCH 11/66] refactor: address PR review comments on pswap Rust module - Use NoteTag::default() instead of NoteTag::new(0) - Change swap_count from u64 to u16 with safe try_into conversion - Rename p2id_tag to payback_note_tag, p2id_payback_note to payback_note - Rename build_ prefix to create_ for consistency - Rename aux_word to attachment_word - Replace Felt::new with Felt::from where possible - Rename inputs to note_storage in TryFrom impl - Make create_payback_note and create_remainder_pswap_note private - Add offered_faucet_id helper to replace unreachable!() --- crates/miden-standards/src/note/pswap.rs | 103 ++++++++++---------- crates/miden-testing/tests/scripts/pswap.rs | 8 +- 2 files changed, 56 insertions(+), 55 deletions(-) diff --git a/crates/miden-standards/src/note/pswap.rs b/crates/miden-standards/src/note/pswap.rs index 62e826541a..37ac0268fd 100644 --- a/crates/miden-standards/src/note/pswap.rs +++ b/crates/miden-standards/src/note/pswap.rs @@ -42,7 +42,7 @@ static PSWAP_SCRIPT: LazyLock = LazyLock::new(|| { /// | `[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) | +/// | `[9]` | Payback note routing tag (targets the creator) | /// | `[10-11]` | Reserved (zero) | /// | `[12]` | Swap count (incremented on each partial fill) | /// | `[13-15]` | Reserved (zero) | @@ -52,8 +52,8 @@ pub struct PswapNoteStorage { requested_asset_key: Word, requested_asset_value: Word, pswap_tag: NoteTag, - p2id_tag: NoteTag, - swap_count: u64, + payback_note_tag: NoteTag, + swap_count: u16, creator_account_id: AccountId, } @@ -72,12 +72,12 @@ impl PswapNoteStorage { /// 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); + let payback_note_tag = NoteTag::with_account_target(creator_account_id); Self { requested_asset_key: requested_asset.to_key_word(), requested_asset_value: requested_asset.to_value_word(), - pswap_tag: NoteTag::new(0), - p2id_tag, + pswap_tag: NoteTag::default(), + payback_note_tag, swap_count: 0, creator_account_id, } @@ -88,15 +88,15 @@ impl PswapNoteStorage { requested_asset_key: Word, requested_asset_value: Word, pswap_tag: NoteTag, - p2id_tag: NoteTag, - swap_count: u64, + payback_note_tag: NoteTag, + swap_count: u16, creator_account_id: AccountId, ) -> Self { Self { requested_asset_key, requested_asset_value, pswap_tag, - p2id_tag, + payback_note_tag, swap_count, creator_account_id, } @@ -129,12 +129,12 @@ impl PswapNoteStorage { self.pswap_tag } - pub fn p2id_tag(&self) -> NoteTag { - self.p2id_tag + pub fn payback_note_tag(&self) -> NoteTag { + self.payback_note_tag } /// Number of times this note has been partially filled and re-created. - pub fn swap_count(&self) -> u64 { + pub fn swap_count(&self) -> u16 { self.swap_count } @@ -171,7 +171,7 @@ impl PswapNoteStorage { /// Serializes [`PswapNoteStorage`] into an 18-element [`NoteStorage`]. impl From for NoteStorage { fn from(storage: PswapNoteStorage) -> Self { - let inputs = vec![ + let storage_items = vec![ // ASSET_KEY [0-3] storage.requested_asset_key[0], storage.requested_asset_key[1], @@ -183,13 +183,13 @@ impl From for NoteStorage { storage.requested_asset_value[2], storage.requested_asset_value[3], // Tags [8-9] - Felt::new(u32::from(storage.pswap_tag) as u64), - Felt::new(u32::from(storage.p2id_tag) as u64), + Felt::from(storage.pswap_tag), + Felt::from(storage.payback_note_tag), // Padding [10-11] ZERO, ZERO, // Swap count [12-15] - Felt::new(storage.swap_count), + Felt::from(storage.swap_count), ZERO, ZERO, ZERO, @@ -197,7 +197,7 @@ impl From for NoteStorage { storage.creator_account_id.prefix().as_felt(), storage.creator_account_id.suffix(), ]; - NoteStorage::new(inputs) + NoteStorage::new(storage_items) .expect("number of storage items should not exceed max storage items") } } @@ -206,22 +206,23 @@ impl From for NoteStorage { impl TryFrom<&[Felt]> for PswapNoteStorage { type Error = NoteError; - fn try_from(inputs: &[Felt]) -> Result { - if inputs.len() != Self::NUM_STORAGE_ITEMS { + fn try_from(note_storage: &[Felt]) -> Result { + if note_storage.len() != Self::NUM_STORAGE_ITEMS { return Err(NoteError::InvalidNoteStorageLength { expected: Self::NUM_STORAGE_ITEMS, - actual: inputs.len(), + actual: note_storage.len(), }); } - let requested_asset_key = Word::from([inputs[0], inputs[1], inputs[2], inputs[3]]); - let requested_asset_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 requested_asset_key = Word::from([note_storage[0], note_storage[1], note_storage[2], note_storage[3]]); + let requested_asset_value = Word::from([note_storage[4], note_storage[5], note_storage[6], note_storage[7]]); + let pswap_tag = NoteTag::new(note_storage[8].as_canonical_u64() as u32); + let payback_note_tag = NoteTag::new(note_storage[9].as_canonical_u64() as u32); + let swap_count: u16 = note_storage[12].as_canonical_u64().try_into() + .map_err(|_| NoteError::other("swap_count exceeds u16"))?; let creator_account_id = - AccountId::try_from_elements(inputs[17], inputs[16]).map_err(|e| { + AccountId::try_from_elements(note_storage[17], note_storage[16]).map_err(|e| { NoteError::other_with_source("failed to parse creator account ID", e) })?; @@ -229,7 +230,7 @@ impl TryFrom<&[Felt]> for PswapNoteStorage { requested_asset_key, requested_asset_value, pswap_tag, - p2id_tag, + payback_note_tag, swap_count, creator_account_id, }) @@ -244,7 +245,7 @@ impl TryFrom<&[Felt]> for PswapNoteStorage { /// 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. +/// receives the filled portion via a payback note. #[derive(Debug, Clone, bon::Builder)] #[builder(finish_fn(vis = "", name = build_internal))] pub struct PswapNote { @@ -348,7 +349,7 @@ impl PswapNote { /// `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 `(p2id_payback_note, Option)`. The remainder is + /// Returns `(payback_note, Option)`. The remainder is /// `None` when the fill equals the total requested amount (full fill). pub fn execute( &self, @@ -397,13 +398,13 @@ impl PswapNote { ); let offered_amount_for_fill = offered_for_input + offered_for_inflight; - // Build the P2ID payback note + // Build the payback note let payback_asset = Asset::Fungible(FungibleAsset::new(requested_faucet_id, fill_amount).map_err(|e| { - NoteError::other_with_source("failed to create P2ID asset", e) + NoteError::other_with_source("failed to create payback asset", e) })?); - let p2id_note = self.build_p2id_payback_note( + let payback_note = self.create_payback_note( consumer_account_id, payback_asset, fill_amount, @@ -419,7 +420,7 @@ impl PswapNote { |e| NoteError::other_with_source("failed to create remainder asset", e), )?); - Some(self.build_remainder_pswap_note( + Some(self.create_remainder_pswap_note( consumer_account_id, remaining_offered_asset, remaining_requested, @@ -429,7 +430,7 @@ impl PswapNote { None }; - Ok((p2id_note, remainder)) + Ok((payback_note, remainder)) } /// Returns the amount of the offered fungible asset in this note. @@ -472,7 +473,7 @@ impl PswapNote { /// [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( + pub fn create_tag( note_type: NoteType, offered_asset: &Asset, requested_asset: &Asset, @@ -523,20 +524,20 @@ impl PswapNote { } } - /// Builds a P2ID payback note that delivers the filled assets to the swap creator. + /// Builds a payback note that delivers the filled assets to the swap creator. /// /// 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( + fn create_payback_note( &self, consumer_account_id: AccountId, payback_asset: Asset, fill_amount: u64, ) -> Result { - let p2id_tag = self.storage.p2id_tag(); + let payback_note_tag = self.storage.payback_note_tag(); // Derive P2ID serial matching PSWAP.masm let swap_count_word = - Word::from([Felt::new(self.storage.swap_count + 1), ZERO, ZERO, ZERO]); + Word::from([Felt::from(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); @@ -545,12 +546,12 @@ impl PswapNote { let recipient = P2idNoteStorage::new(self.storage.creator_account_id).into_recipient(p2id_serial_num); - let aux_word = Word::from([Felt::new(fill_amount), ZERO, ZERO, ZERO]); - let attachment = NoteAttachment::new_word(NoteAttachmentScheme::none(), aux_word); + let attachment_word = Word::from([Felt::new(fill_amount), ZERO, ZERO, ZERO]); + let attachment = NoteAttachment::new_word(NoteAttachmentScheme::none(), attachment_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_tag(payback_note_tag) .with_attachment(attachment); Ok(Note::new(p2id_assets, p2id_metadata, recipient)) @@ -560,7 +561,7 @@ impl PswapNote { /// /// 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( + fn create_remainder_pswap_note( &self, consumer_account_id: AccountId, remaining_offered_asset: Asset, @@ -581,7 +582,7 @@ impl PswapNote { key_word, value_word, self.storage.pswap_tag, - self.storage.p2id_tag, + self.storage.payback_note_tag, self.storage.swap_count + 1, self.storage.creator_account_id, ); @@ -594,8 +595,8 @@ impl PswapNote { 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 attachment_word = Word::from([Felt::new(offered_amount_for_fill), ZERO, ZERO, ZERO]); + let attachment = NoteAttachment::new_word(NoteAttachmentScheme::none(), attachment_word); let assets = NoteAssets::new(vec![remaining_offered_asset])?; @@ -625,7 +626,7 @@ impl From for Note { .storage .requested_asset() .expect("PswapNote must have a valid requested asset"); - let tag = PswapNote::build_tag(pswap.note_type, &offered_asset, &requested_asset); + let tag = PswapNote::create_tag(pswap.note_type, &offered_asset, &requested_asset); let storage = pswap.storage.with_pswap_tag(tag); let recipient = storage.into_recipient(pswap.serial_number); @@ -830,7 +831,7 @@ mod tests { .unwrap(), ); - let tag = PswapNote::build_tag(NoteType::Public, &offered_asset, &requested_asset); + let tag = PswapNote::create_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) @@ -883,11 +884,11 @@ mod tests { value_word[1], value_word[2], value_word[3], - Felt::new(0xC0000000), // pswap_tag - Felt::new(0x80000001), // p2id_tag + Felt::from(0xC0000000u32), // pswap_tag + Felt::from(0x80000001u32), // payback_note_tag ZERO, ZERO, - Felt::new(3), // swap_count + Felt::from(3u16), // swap_count ZERO, ZERO, ZERO, diff --git a/crates/miden-testing/tests/scripts/pswap.rs b/crates/miden-testing/tests/scripts/pswap.rs index f82d2a71be..fc4a958def 100644 --- a/crates/miden-testing/tests/scripts/pswap.rs +++ b/crates/miden-testing/tests/scripts/pswap.rs @@ -809,15 +809,15 @@ async fn pswap_note_chained_partial_fills_non_integer_ratio_test() -> anyhow::Re 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()); + PswapNote::create_tag(NoteType::Public, &offered_asset, &requested_asset); + let payback_note_tag = NoteTag::with_account_target(alice.id()); let storage = PswapNoteStorage::from_parts( requested_asset.to_key_word(), requested_asset.to_value_word(), pswap_tag, - p2id_tag, - current_swap_count, + payback_note_tag, + current_swap_count as u16, alice.id(), ); let note_assets = NoteAssets::new(vec![offered_asset])?; From a4ade8748c90d81559f65e6fa8cbe40be7f42a8b Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Wed, 25 Mar 2026 06:46:43 +0530 Subject: [PATCH 12/66] refactor: use builder pattern for PswapNote and PswapNoteStorage - Remove PswapNote::create, add public build() with validation on builder - Add bon::Builder to PswapNoteStorage, remove new() and from_parts() - Remove payback_note_tag field, compute from creator_account_id on the fly - Fix clippy warnings (useless conversions, needless borrows) - Update all unit and integration tests to use builder pattern --- crates/miden-standards/src/note/pswap.rs | 239 ++++++-------- crates/miden-testing/tests/scripts/pswap.rs | 336 +++++++++++++------- 2 files changed, 316 insertions(+), 259 deletions(-) diff --git a/crates/miden-standards/src/note/pswap.rs b/crates/miden-standards/src/note/pswap.rs index 37ac0268fd..bd05ea734a 100644 --- a/crates/miden-standards/src/note/pswap.rs +++ b/crates/miden-standards/src/note/pswap.rs @@ -4,7 +4,6 @@ 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, @@ -47,13 +46,17 @@ static PSWAP_SCRIPT: LazyLock = LazyLock::new(|| { /// | `[12]` | Swap count (incremented on each partial fill) | /// | `[13-15]` | Reserved (zero) | /// | `[16-17]` | Creator account ID (prefix, suffix) | -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, bon::Builder)] pub struct PswapNoteStorage { requested_asset_key: Word, requested_asset_value: Word, + + #[builder(default)] pswap_tag: NoteTag, - payback_note_tag: NoteTag, + + #[builder(default)] swap_count: u16, + creator_account_id: AccountId, } @@ -64,44 +67,6 @@ impl PswapNoteStorage { /// Expected number of storage items for the PSWAP note. pub const NUM_STORAGE_ITEMS: usize = 18; - // CONSTRUCTORS - // -------------------------------------------------------------------------------------------- - - /// Creates storage for a new PSWAP note. - /// - /// 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 payback_note_tag = NoteTag::with_account_target(creator_account_id); - Self { - requested_asset_key: requested_asset.to_key_word(), - requested_asset_value: requested_asset.to_value_word(), - pswap_tag: NoteTag::default(), - payback_note_tag, - swap_count: 0, - creator_account_id, - } - } - - /// Creates storage with all fields specified explicitly. - pub fn from_parts( - requested_asset_key: Word, - requested_asset_value: Word, - pswap_tag: NoteTag, - payback_note_tag: NoteTag, - swap_count: u16, - creator_account_id: AccountId, - ) -> Self { - Self { - requested_asset_key, - requested_asset_value, - pswap_tag, - payback_note_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)) @@ -130,7 +95,7 @@ impl PswapNoteStorage { } pub fn payback_note_tag(&self) -> NoteTag { - self.payback_note_tag + NoteTag::with_account_target(self.creator_account_id) } /// Number of times this note has been partially filled and re-created. @@ -157,9 +122,8 @@ impl PswapNoteStorage { /// Extracts the faucet ID of the requested asset from the key word. pub fn requested_faucet_id(&self) -> Result { - AccountId::try_from_elements(self.requested_asset_key[2], self.requested_asset_key[3]).map_err(|e| { - NoteError::other_with_source("failed to parse faucet ID from key", e) - }) + AccountId::try_from_elements(self.requested_asset_key[2], self.requested_asset_key[3]) + .map_err(|e| NoteError::other_with_source("failed to parse faucet ID from key", e)) } /// Extracts the requested token amount from the value word. @@ -184,7 +148,7 @@ impl From for NoteStorage { storage.requested_asset_value[3], // Tags [8-9] Felt::from(storage.pswap_tag), - Felt::from(storage.payback_note_tag), + Felt::from(storage.payback_note_tag()), // Padding [10-11] ZERO, ZERO, @@ -214,23 +178,25 @@ impl TryFrom<&[Felt]> for PswapNoteStorage { }); } - let requested_asset_key = Word::from([note_storage[0], note_storage[1], note_storage[2], note_storage[3]]); - let requested_asset_value = Word::from([note_storage[4], note_storage[5], note_storage[6], note_storage[7]]); + let requested_asset_key = + Word::from([note_storage[0], note_storage[1], note_storage[2], note_storage[3]]); + let requested_asset_value = + Word::from([note_storage[4], note_storage[5], note_storage[6], note_storage[7]]); let pswap_tag = NoteTag::new(note_storage[8].as_canonical_u64() as u32); - let payback_note_tag = NoteTag::new(note_storage[9].as_canonical_u64() as u32); - let swap_count: u16 = note_storage[12].as_canonical_u64().try_into() + let swap_count: u16 = note_storage[12] + .as_canonical_u64() + .try_into() .map_err(|_| NoteError::other("swap_count exceeds u16"))?; - let creator_account_id = - AccountId::try_from_elements(note_storage[17], note_storage[16]).map_err(|e| { - NoteError::other_with_source("failed to parse creator account ID", e) - })?; + let creator_account_id = AccountId::try_from_elements(note_storage[17], note_storage[16]) + .map_err(|e| { + NoteError::other_with_source("failed to parse creator account ID", e) + })?; Ok(Self { requested_asset_key, requested_asset_value, pswap_tag, - payback_note_tag, swap_count, creator_account_id, }) @@ -262,6 +228,29 @@ pub struct PswapNote { attachment: NoteAttachment, } +impl PswapNoteBuilder +where + S: pswap_note_builder::IsComplete, +{ + pub fn build(self) -> Result { + let note = self.build_internal(); + + if note.assets.num_assets() != 1 { + return Err(NoteError::other("Swap note must have exactly 1 offered asset")); + } + + let offered_asset = note.assets.iter().next().unwrap(); + let requested_asset = note.storage.requested_asset()?; + if offered_asset.faucet_id().prefix() == requested_asset.faucet_id().prefix() { + return Err(NoteError::other( + "offered and requested assets must have different faucets", + )); + } + + Ok(note) + } +} + impl PswapNote { // CONSTANTS // -------------------------------------------------------------------------------------------- @@ -306,41 +295,6 @@ impl PswapNote { &self.attachment } - // BUILDERS - // -------------------------------------------------------------------------------------------- - - /// Creates a PSWAP note offering `offered_asset` in exchange for `requested_asset`. - /// - /// # Errors - /// - /// 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, - 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 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_internal(); - - Ok(Note::from(pswap)) - } - // INSTANCE METHODS // -------------------------------------------------------------------------------------------- @@ -399,16 +353,13 @@ impl PswapNote { let offered_amount_for_fill = offered_for_input + offered_for_inflight; // Build the payback note - let payback_asset = - Asset::Fungible(FungibleAsset::new(requested_faucet_id, fill_amount).map_err(|e| { - NoteError::other_with_source("failed to create payback asset", e) - })?); + let payback_asset = Asset::Fungible( + FungibleAsset::new(requested_faucet_id, fill_amount) + .map_err(|e| NoteError::other_with_source("failed to create payback asset", e))?, + ); - let payback_note = self.create_payback_note( - consumer_account_id, - payback_asset, - fill_amount, - )?; + let payback_note = + self.create_payback_note(consumer_account_id, payback_asset, fill_amount)?; // Create remainder note if partial fill let remainder = if fill_amount < total_requested_amount { @@ -439,11 +390,7 @@ impl PswapNote { /// /// Returns an error if the note has no assets or the asset is non-fungible. pub fn offered_asset_amount(&self) -> Result { - let asset = self - .assets - .iter() - .next() - .ok_or(NoteError::other("No offered asset found"))?; + let asset = self.assets.iter().next().ok_or(NoteError::other("No offered asset found"))?; match asset { Asset::Fungible(fa) => Ok(fa.amount()), _ => Err(NoteError::other("Non-fungible offered asset not supported")), @@ -452,10 +399,7 @@ impl PswapNote { /// 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, - ) -> Result { + pub fn calculate_offered_for_requested(&self, input_amount: u64) -> Result { let total_requested = self.storage.requested_asset_amount(); let total_offered = self.offered_asset_amount()?; @@ -539,8 +483,8 @@ impl PswapNote { let swap_count_word = Word::from([Felt::from(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); + Hasher::merge(&[swap_count_word, self.serial_number]); + let p2id_serial_num: Word = p2id_serial_digest; // P2ID recipient targets the creator let recipient = @@ -578,14 +522,13 @@ impl PswapNote { 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.payback_note_tag, - self.storage.swap_count + 1, - self.storage.creator_account_id, - ); + let new_storage = PswapNoteStorage::builder() + .requested_asset_key(key_word) + .requested_asset_value(value_word) + .pswap_tag(self.storage.pswap_tag) + .swap_count(self.storage.swap_count + 1) + .creator_account_id(self.storage.creator_account_id) + .build(); // Remainder serial: increment top element (matching MASM add.1 on Word[0]) let remainder_serial_num = Word::from([ @@ -617,16 +560,13 @@ impl PswapNote { /// 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 - .assets - .iter() - .next() - .expect("PswapNote must have an offered asset"); + let offered_asset = + pswap.assets.iter().next().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::create_tag(pswap.note_type, &offered_asset, &requested_asset); + let tag = PswapNote::create_tag(pswap.note_type, offered_asset, &requested_asset); let storage = pswap.storage.with_pswap_tag(tag); let recipient = storage.into_recipient(pswap.serial_number); @@ -706,23 +646,27 @@ mod tests { let requested_asset = Asset::Fungible(FungibleAsset::new(requested_faucet_id, 500).unwrap()); - use miden_protocol::crypto::rand::RandomCoin; + use miden_protocol::crypto::rand::{FeltRng, RandomCoin}; let mut rng = RandomCoin::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, - ); + let storage = PswapNoteStorage::builder() + .requested_asset_key(requested_asset.to_key_word()) + .requested_asset_value(requested_asset.to_value_word()) + .creator_account_id(creator_id) + .build(); + 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() + .unwrap(); - assert!(note.is_ok(), "Note creation should succeed"); - let note = note.unwrap(); + let note: Note = pswap.into(); assert_eq!(note.metadata().sender(), creator_id); assert_eq!(note.metadata().note_type(), NoteType::Public); @@ -730,10 +674,7 @@ 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] @@ -772,14 +713,19 @@ mod tests { use miden_protocol::crypto::rand::{FeltRng, RandomCoin}; let mut rng = RandomCoin::new(Word::default()); - let storage = PswapNoteStorage::new(requested_asset, creator_id); + let storage = PswapNoteStorage::builder() + .requested_asset_key(requested_asset.to_key_word()) + .requested_asset_value(requested_asset.to_value_word()) + .creator_account_id(creator_id) + .build(); 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_internal(); + .build() + .unwrap(); assert_eq!(pswap.sender(), creator_id); assert_eq!(pswap.note_type(), NoteType::Public); @@ -790,10 +736,7 @@ mod tests { 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, - ); + assert_eq!(note.recipient().storage().num_items(), PswapNote::NUM_STORAGE_ITEMS as u16,); } #[test] @@ -923,7 +866,11 @@ mod tests { ); let requested_asset = Asset::Fungible(FungibleAsset::new(faucet_id, 500).unwrap()); - let storage = PswapNoteStorage::new(requested_asset, creator_id); + let storage = PswapNoteStorage::builder() + .requested_asset_key(requested_asset.to_key_word()) + .requested_asset_value(requested_asset.to_value_word()) + .creator_account_id(creator_id) + .build(); // Convert to NoteStorage and back let note_storage = NoteStorage::from(storage.clone()); diff --git a/crates/miden-testing/tests/scripts/pswap.rs b/crates/miden-testing/tests/scripts/pswap.rs index fc4a958def..88e388bba3 100644 --- a/crates/miden-testing/tests/scripts/pswap.rs +++ b/crates/miden-testing/tests/scripts/pswap.rs @@ -3,9 +3,7 @@ use std::collections::BTreeMap; use miden_protocol::account::auth::AuthScheme; use miden_protocol::asset::{Asset, FungibleAsset}; use miden_protocol::crypto::rand::{FeltRng, RandomCoin}; -use miden_protocol::note::{ - Note, NoteAssets, NoteAttachment, NoteMetadata, NoteRecipient, NoteStorage, NoteTag, NoteType, -}; +use miden_protocol::note::{Note, NoteAssets, NoteMetadata, NoteRecipient, NoteStorage, NoteType}; use miden_protocol::transaction::RawOutputNote; use miden_protocol::{Felt, Word, ZERO}; use miden_standards::note::{PswapNote, PswapNoteStorage}; @@ -41,14 +39,20 @@ async fn pswap_note_full_fill_test() -> anyhow::Result<()> { let requested_asset = Asset::Fungible(FungibleAsset::new(eth_faucet.id(), 25)?); let mut rng = RandomCoin::new(Word::default()); - let swap_note = PswapNote::create( - alice.id(), - offered_asset, - requested_asset, - NoteType::Public, - NoteAttachment::default(), - &mut rng, - )?; + let swap_note: Note = PswapNote::builder() + .sender(alice.id()) + .storage( + PswapNoteStorage::builder() + .requested_asset_key(requested_asset.to_key_word()) + .requested_asset_value(requested_asset.to_value_word()) + .creator_account_id(alice.id()) + .build(), + ) + .serial_number(rng.draw_word()) + .note_type(NoteType::Public) + .assets(NoteAssets::new(vec![offered_asset])?) + .build()? + .into(); builder.add_output_note(RawOutputNote::Full(swap_note.clone())); let mut mock_chain = builder.build()?; @@ -127,14 +131,20 @@ async fn pswap_note_private_full_fill_test() -> anyhow::Result<()> { let mut rng = RandomCoin::new(Word::default()); // Create a PRIVATE swap note (output notes should also be Private) - let swap_note = PswapNote::create( - alice.id(), - offered_asset, - requested_asset, - NoteType::Private, - NoteAttachment::default(), - &mut rng, - )?; + let swap_note: Note = PswapNote::builder() + .sender(alice.id()) + .storage( + PswapNoteStorage::builder() + .requested_asset_key(requested_asset.to_key_word()) + .requested_asset_value(requested_asset.to_value_word()) + .creator_account_id(alice.id()) + .build(), + ) + .serial_number(rng.draw_word()) + .note_type(NoteType::Private) + .assets(NoteAssets::new(vec![offered_asset])?) + .build()? + .into(); builder.add_output_note(RawOutputNote::Full(swap_note.clone())); let mut mock_chain = builder.build()?; @@ -209,14 +219,20 @@ async fn pswap_note_partial_fill_test() -> anyhow::Result<()> { let requested_asset = Asset::Fungible(FungibleAsset::new(eth_faucet.id(), 25)?); let mut rng = RandomCoin::new(Word::default()); - let swap_note = PswapNote::create( - alice.id(), - offered_asset, - requested_asset, - NoteType::Public, - NoteAttachment::default(), - &mut rng, - )?; + let swap_note: Note = PswapNote::builder() + .sender(alice.id()) + .storage( + PswapNoteStorage::builder() + .requested_asset_key(requested_asset.to_key_word()) + .requested_asset_value(requested_asset.to_value_word()) + .creator_account_id(alice.id()) + .build(), + ) + .serial_number(rng.draw_word()) + .note_type(NoteType::Public) + .assets(NoteAssets::new(vec![offered_asset])?) + .build()? + .into(); builder.add_output_note(RawOutputNote::Full(swap_note.clone())); let mut mock_chain = builder.build()?; @@ -297,25 +313,45 @@ async fn pswap_note_inflight_cross_swap_test() -> anyhow::Result<()> { let mut rng = RandomCoin::new(Word::default()); // Alice's note: offers 50 USDC, requests 25 ETH - let alice_swap_note = PswapNote::create( - alice.id(), - Asset::Fungible(FungibleAsset::new(usdc_faucet.id(), 50)?), - Asset::Fungible(FungibleAsset::new(eth_faucet.id(), 25)?), - NoteType::Public, - NoteAttachment::default(), - &mut rng, - )?; + let alice_requested_asset = Asset::Fungible(FungibleAsset::new(eth_faucet.id(), 25)?); + let alice_swap_note: Note = PswapNote::builder() + .sender(alice.id()) + .storage( + PswapNoteStorage::builder() + .requested_asset_key(alice_requested_asset.to_key_word()) + .requested_asset_value(alice_requested_asset.to_value_word()) + .creator_account_id(alice.id()) + .build(), + ) + .serial_number(rng.draw_word()) + .note_type(NoteType::Public) + .assets(NoteAssets::new(vec![Asset::Fungible(FungibleAsset::new( + usdc_faucet.id(), + 50, + )?)])?) + .build()? + .into(); builder.add_output_note(RawOutputNote::Full(alice_swap_note.clone())); // Bob's note: offers 25 ETH, requests 50 USDC - let bob_swap_note = PswapNote::create( - bob.id(), - Asset::Fungible(FungibleAsset::new(eth_faucet.id(), 25)?), - Asset::Fungible(FungibleAsset::new(usdc_faucet.id(), 50)?), - NoteType::Public, - NoteAttachment::default(), - &mut rng, - )?; + let bob_requested_asset = Asset::Fungible(FungibleAsset::new(usdc_faucet.id(), 50)?); + let bob_swap_note: Note = PswapNote::builder() + .sender(bob.id()) + .storage( + PswapNoteStorage::builder() + .requested_asset_key(bob_requested_asset.to_key_word()) + .requested_asset_value(bob_requested_asset.to_value_word()) + .creator_account_id(bob.id()) + .build(), + ) + .serial_number(rng.draw_word()) + .note_type(NoteType::Public) + .assets(NoteAssets::new(vec![Asset::Fungible(FungibleAsset::new( + eth_faucet.id(), + 25, + )?)])?) + .build()? + .into(); builder.add_output_note(RawOutputNote::Full(bob_swap_note.clone())); let mock_chain = builder.build()?; @@ -384,14 +420,24 @@ async fn pswap_note_creator_reclaim_test() -> anyhow::Result<()> { )?; let mut rng = RandomCoin::new(Word::default()); - let swap_note = PswapNote::create( - alice.id(), - Asset::Fungible(FungibleAsset::new(usdc_faucet.id(), 50)?), - Asset::Fungible(FungibleAsset::new(eth_faucet.id(), 25)?), - NoteType::Public, - NoteAttachment::default(), - &mut rng, - )?; + let requested_asset = Asset::Fungible(FungibleAsset::new(eth_faucet.id(), 25)?); + let swap_note: Note = PswapNote::builder() + .sender(alice.id()) + .storage( + PswapNoteStorage::builder() + .requested_asset_key(requested_asset.to_key_word()) + .requested_asset_value(requested_asset.to_value_word()) + .creator_account_id(alice.id()) + .build(), + ) + .serial_number(rng.draw_word()) + .note_type(NoteType::Public) + .assets(NoteAssets::new(vec![Asset::Fungible(FungibleAsset::new( + usdc_faucet.id(), + 50, + )?)])?) + .build()? + .into(); builder.add_output_note(RawOutputNote::Full(swap_note.clone())); let mock_chain = builder.build()?; @@ -436,14 +482,24 @@ async fn pswap_note_invalid_input_test() -> anyhow::Result<()> { )?; let mut rng = RandomCoin::new(Word::default()); - let swap_note = PswapNote::create( - alice.id(), - Asset::Fungible(FungibleAsset::new(usdc_faucet.id(), 50)?), - Asset::Fungible(FungibleAsset::new(eth_faucet.id(), 25)?), - NoteType::Public, - NoteAttachment::default(), - &mut rng, - )?; + let requested_asset = Asset::Fungible(FungibleAsset::new(eth_faucet.id(), 25)?); + let swap_note: Note = PswapNote::builder() + .sender(alice.id()) + .storage( + PswapNoteStorage::builder() + .requested_asset_key(requested_asset.to_key_word()) + .requested_asset_value(requested_asset.to_value_word()) + .creator_account_id(alice.id()) + .build(), + ) + .serial_number(rng.draw_word()) + .note_type(NoteType::Public) + .assets(NoteAssets::new(vec![Asset::Fungible(FungibleAsset::new( + usdc_faucet.id(), + 50, + )?)])?) + .build()? + .into(); builder.add_output_note(RawOutputNote::Full(swap_note.clone())); let mock_chain = builder.build()?; @@ -495,14 +551,24 @@ async fn pswap_note_multiple_partial_fills_test() -> anyhow::Result<()> { )?; let mut rng = RandomCoin::new(Word::default()); - let swap_note = PswapNote::create( - alice.id(), - Asset::Fungible(FungibleAsset::new(usdc_faucet.id(), 50)?), - Asset::Fungible(FungibleAsset::new(eth_faucet.id(), 25)?), - NoteType::Public, - NoteAttachment::default(), - &mut rng, - )?; + let requested_asset = Asset::Fungible(FungibleAsset::new(eth_faucet.id(), 25)?); + let swap_note: Note = PswapNote::builder() + .sender(alice.id()) + .storage( + PswapNoteStorage::builder() + .requested_asset_key(requested_asset.to_key_word()) + .requested_asset_value(requested_asset.to_value_word()) + .creator_account_id(alice.id()) + .build(), + ) + .serial_number(rng.draw_word()) + .note_type(NoteType::Public) + .assets(NoteAssets::new(vec![Asset::Fungible(FungibleAsset::new( + usdc_faucet.id(), + 50, + )?)])?) + .build()? + .into(); builder.add_output_note(RawOutputNote::Full(swap_note.clone())); let mock_chain = builder.build()?; @@ -570,14 +636,25 @@ async fn pswap_note_non_exact_ratio_partial_fill_test() -> anyhow::Result<()> { )?; let mut rng = RandomCoin::new(Word::default()); - let swap_note = PswapNote::create( - alice.id(), - Asset::Fungible(FungibleAsset::new(usdc_faucet.id(), offered_total)?), - Asset::Fungible(FungibleAsset::new(eth_faucet.id(), requested_total)?), - NoteType::Public, - NoteAttachment::default(), - &mut rng, - )?; + let swap_note: Note = PswapNote::builder() + .sender(alice.id()) + .storage({ + let requested_asset = + Asset::Fungible(FungibleAsset::new(eth_faucet.id(), requested_total)?); + PswapNoteStorage::builder() + .requested_asset_key(requested_asset.to_key_word()) + .requested_asset_value(requested_asset.to_value_word()) + .creator_account_id(alice.id()) + .build() + }) + .serial_number(rng.draw_word()) + .note_type(NoteType::Public) + .assets(NoteAssets::new(vec![Asset::Fungible(FungibleAsset::new( + usdc_faucet.id(), + offered_total, + )?)])?) + .build()? + .into(); builder.add_output_note(RawOutputNote::Full(swap_note.clone())); let mock_chain = builder.build()?; @@ -677,14 +754,25 @@ async fn pswap_note_partial_fill_non_integer_ratio_fuzz_test() -> anyhow::Result )?; let mut rng = RandomCoin::new(Word::default()); - let swap_note = PswapNote::create( - alice.id(), - Asset::Fungible(FungibleAsset::new(usdc_faucet.id(), *offered_usdc)?), - Asset::Fungible(FungibleAsset::new(eth_faucet.id(), *requested_eth)?), - NoteType::Public, - NoteAttachment::default(), - &mut rng, - )?; + let swap_note: Note = PswapNote::builder() + .sender(alice.id()) + .storage({ + let requested_asset = + Asset::Fungible(FungibleAsset::new(eth_faucet.id(), *requested_eth)?); + PswapNoteStorage::builder() + .requested_asset_key(requested_asset.to_key_word()) + .requested_asset_value(requested_asset.to_value_word()) + .creator_account_id(alice.id()) + .build() + }) + .serial_number(rng.draw_word()) + .note_type(NoteType::Public) + .assets(NoteAssets::new(vec![Asset::Fungible(FungibleAsset::new( + usdc_faucet.id(), + *offered_usdc, + )?)])?) + .build()? + .into(); builder.add_output_note(RawOutputNote::Full(swap_note.clone())); let mock_chain = builder.build()?; @@ -810,16 +898,14 @@ async fn pswap_note_chained_partial_fills_non_integer_ratio_test() -> anyhow::Re let pswap_tag = PswapNote::create_tag(NoteType::Public, &offered_asset, &requested_asset); - let payback_note_tag = NoteTag::with_account_target(alice.id()); - - let storage = PswapNoteStorage::from_parts( - requested_asset.to_key_word(), - requested_asset.to_value_word(), - pswap_tag, - payback_note_tag, - current_swap_count as u16, - alice.id(), - ); + + let storage = PswapNoteStorage::builder() + .requested_asset_key(requested_asset.to_key_word()) + .requested_asset_value(requested_asset.to_value_word()) + .pswap_tag(pswap_tag) + .swap_count(current_swap_count as u16) + .creator_account_id(alice.id()) + .build(); let note_assets = NoteAssets::new(vec![offered_asset])?; // Create note with the correct serial for this chain position @@ -918,7 +1004,7 @@ async fn pswap_note_chained_partial_fills_non_integer_ratio_test() -> anyhow::Re Ok(()) } -/// Test that PswapNote::create + try_from + execute roundtrips correctly +/// Test that PswapNote builder + try_from + execute roundtrips correctly #[test] fn compare_pswap_create_output_notes_vs_test_helper() { let mut builder = MockChain::builder(); @@ -938,17 +1024,29 @@ fn compare_pswap_create_output_notes_vs_test_helper() { ) .unwrap(); - // Create swap note using PswapNote::create + // Create swap note using PswapNote builder let mut rng = RandomCoin::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 requested_asset = Asset::Fungible(FungibleAsset::new(eth_faucet.id(), 25).unwrap()); + let swap_note: Note = PswapNote::builder() + .sender(alice.id()) + .storage( + PswapNoteStorage::builder() + .requested_asset_key(requested_asset.to_key_word()) + .requested_asset_value(requested_asset.to_value_word()) + .creator_account_id(alice.id()) + .build(), + ) + .serial_number(rng.draw_word()) + .note_type(NoteType::Public) + .assets( + NoteAssets::new(vec![Asset::Fungible( + FungibleAsset::new(usdc_faucet.id(), 50).unwrap(), + )]) + .unwrap(), + ) + .build() + .unwrap() + .into(); // Roundtrip: try_from -> execute -> verify outputs let pswap = PswapNote::try_from(&swap_note).unwrap(); @@ -1012,15 +1110,27 @@ fn pswap_parse_inputs_roundtrip() { .unwrap(); let mut rng = RandomCoin::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 requested_asset = Asset::Fungible(FungibleAsset::new(eth_faucet.id(), 25).unwrap()); + let swap_note: Note = PswapNote::builder() + .sender(alice.id()) + .storage( + PswapNoteStorage::builder() + .requested_asset_key(requested_asset.to_key_word()) + .requested_asset_value(requested_asset.to_value_word()) + .creator_account_id(alice.id()) + .build(), + ) + .serial_number(rng.draw_word()) + .note_type(NoteType::Public) + .assets( + NoteAssets::new(vec![Asset::Fungible( + FungibleAsset::new(usdc_faucet.id(), 50).unwrap(), + )]) + .unwrap(), + ) + .build() + .unwrap() + .into(); let storage = swap_note.recipient().storage(); let items = storage.items(); From dae2031081c379bd609bf31350bb295d701a3400 Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Wed, 25 Mar 2026 08:14:58 +0530 Subject: [PATCH 13/66] refactor: clean up pswap tests and fix clippy warnings - Compare full faucet_id instead of just prefix in build validation - Rename swap_note to pswap_note in integration tests for consistency - Extract PswapNoteStorage builder into separate let bindings - Replace manual current_swap_count counter with enumerate index - Fix clippy useless_conversion and needless_borrow warnings --- crates/miden-standards/src/note/pswap.rs | 22 +- crates/miden-testing/tests/scripts/pswap.rs | 286 +++++++++----------- 2 files changed, 149 insertions(+), 159 deletions(-) diff --git a/crates/miden-standards/src/note/pswap.rs b/crates/miden-standards/src/note/pswap.rs index bd05ea734a..ba458838bf 100644 --- a/crates/miden-standards/src/note/pswap.rs +++ b/crates/miden-standards/src/note/pswap.rs @@ -1,16 +1,23 @@ 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::errors::NoteError; use miden_protocol::note::{ - Note, NoteAssets, NoteAttachment, NoteAttachmentScheme, NoteMetadata, NoteRecipient, - NoteScript, NoteStorage, NoteTag, NoteType, + Note, + NoteAssets, + NoteAttachment, + NoteAttachmentScheme, + NoteMetadata, + NoteRecipient, + NoteScript, + NoteStorage, + NoteTag, + NoteType, }; use miden_protocol::utils::sync::LazyLock; -use miden_protocol::{Felt, Word, ZERO}; +use miden_protocol::{Felt, Hasher, Word, ZERO}; use crate::StandardsLib; use crate::note::P2idNoteStorage; @@ -241,7 +248,7 @@ where let offered_asset = note.assets.iter().next().unwrap(); let requested_asset = note.storage.requested_asset()?; - if offered_asset.faucet_id().prefix() == requested_asset.faucet_id().prefix() { + if offered_asset.faucet_id() == requested_asset.faucet_id() { return Err(NoteError::other( "offered and requested assets must have different faucets", )); @@ -482,8 +489,7 @@ impl PswapNote { // Derive P2ID serial matching PSWAP.masm let swap_count_word = Word::from([Felt::from(self.storage.swap_count + 1), ZERO, ZERO, ZERO]); - let p2id_serial_digest = - Hasher::merge(&[swap_count_word, self.serial_number]); + let p2id_serial_digest = Hasher::merge(&[swap_count_word, self.serial_number]); let p2id_serial_num: Word = p2id_serial_digest; // P2ID recipient targets the creator @@ -827,7 +833,7 @@ mod tests { value_word[1], value_word[2], value_word[3], - Felt::from(0xC0000000u32), // pswap_tag + Felt::from(0xc0000000u32), // pswap_tag Felt::from(0x80000001u32), // payback_note_tag ZERO, ZERO, diff --git a/crates/miden-testing/tests/scripts/pswap.rs b/crates/miden-testing/tests/scripts/pswap.rs index 88e388bba3..735a6dcbf9 100644 --- a/crates/miden-testing/tests/scripts/pswap.rs +++ b/crates/miden-testing/tests/scripts/pswap.rs @@ -39,32 +39,31 @@ async fn pswap_note_full_fill_test() -> anyhow::Result<()> { let requested_asset = Asset::Fungible(FungibleAsset::new(eth_faucet.id(), 25)?); let mut rng = RandomCoin::new(Word::default()); - let swap_note: Note = PswapNote::builder() + let storage = PswapNoteStorage::builder() + .requested_asset_key(requested_asset.to_key_word()) + .requested_asset_value(requested_asset.to_value_word()) + .creator_account_id(alice.id()) + .build(); + let pswap_note: Note = PswapNote::builder() .sender(alice.id()) - .storage( - PswapNoteStorage::builder() - .requested_asset_key(requested_asset.to_key_word()) - .requested_asset_value(requested_asset.to_value_word()) - .creator_account_id(alice.id()) - .build(), - ) + .storage(storage) .serial_number(rng.draw_word()) .note_type(NoteType::Public) .assets(NoteAssets::new(vec![offered_asset])?) .build()? .into(); - builder.add_output_note(RawOutputNote::Full(swap_note.clone())); + builder.add_output_note(RawOutputNote::Full(pswap_note.clone())); let mut mock_chain = builder.build()?; let mut note_args_map = BTreeMap::new(); - note_args_map.insert(swap_note.id(), Word::from([Felt::new(25), Felt::new(0), ZERO, ZERO])); + note_args_map.insert(pswap_note.id(), Word::from([Felt::new(25), Felt::new(0), ZERO, ZERO])); - let pswap = PswapNote::try_from(&swap_note)?; + let pswap = PswapNote::try_from(&pswap_note)?; let (p2id_note, _remainder) = pswap.execute(bob.id(), 25, 0)?; let tx_context = mock_chain - .build_tx_context(bob.id(), &[swap_note.id()], &[])? + .build_tx_context(bob.id(), &[pswap_note.id()], &[])? .extend_note_args(note_args_map) .extend_expected_output_notes(vec![RawOutputNote::Full(p2id_note.clone())]) .build()?; @@ -131,33 +130,32 @@ async fn pswap_note_private_full_fill_test() -> anyhow::Result<()> { let mut rng = RandomCoin::new(Word::default()); // Create a PRIVATE swap note (output notes should also be Private) - let swap_note: Note = PswapNote::builder() + let storage = PswapNoteStorage::builder() + .requested_asset_key(requested_asset.to_key_word()) + .requested_asset_value(requested_asset.to_value_word()) + .creator_account_id(alice.id()) + .build(); + let pswap_note: Note = PswapNote::builder() .sender(alice.id()) - .storage( - PswapNoteStorage::builder() - .requested_asset_key(requested_asset.to_key_word()) - .requested_asset_value(requested_asset.to_value_word()) - .creator_account_id(alice.id()) - .build(), - ) + .storage(storage) .serial_number(rng.draw_word()) .note_type(NoteType::Private) .assets(NoteAssets::new(vec![offered_asset])?) .build()? .into(); - builder.add_output_note(RawOutputNote::Full(swap_note.clone())); + builder.add_output_note(RawOutputNote::Full(pswap_note.clone())); let mut mock_chain = builder.build()?; let mut note_args_map = BTreeMap::new(); - note_args_map.insert(swap_note.id(), Word::from([Felt::new(25), Felt::new(0), ZERO, ZERO])); + note_args_map.insert(pswap_note.id(), Word::from([Felt::new(25), Felt::new(0), ZERO, ZERO])); // Expected P2ID note should inherit Private type from swap note - let pswap = PswapNote::try_from(&swap_note)?; + let pswap = PswapNote::try_from(&pswap_note)?; let (p2id_note, _remainder) = pswap.execute(bob.id(), 25, 0)?; let tx_context = mock_chain - .build_tx_context(bob.id(), &[swap_note.id()], &[])? + .build_tx_context(bob.id(), &[pswap_note.id()], &[])? .extend_note_args(note_args_map) .extend_expected_output_notes(vec![RawOutputNote::Full(p2id_note)]) .build()?; @@ -219,34 +217,33 @@ async fn pswap_note_partial_fill_test() -> anyhow::Result<()> { let requested_asset = Asset::Fungible(FungibleAsset::new(eth_faucet.id(), 25)?); let mut rng = RandomCoin::new(Word::default()); - let swap_note: Note = PswapNote::builder() + let storage = PswapNoteStorage::builder() + .requested_asset_key(requested_asset.to_key_word()) + .requested_asset_value(requested_asset.to_value_word()) + .creator_account_id(alice.id()) + .build(); + let pswap_note: Note = PswapNote::builder() .sender(alice.id()) - .storage( - PswapNoteStorage::builder() - .requested_asset_key(requested_asset.to_key_word()) - .requested_asset_value(requested_asset.to_value_word()) - .creator_account_id(alice.id()) - .build(), - ) + .storage(storage) .serial_number(rng.draw_word()) .note_type(NoteType::Public) .assets(NoteAssets::new(vec![offered_asset])?) .build()? .into(); - builder.add_output_note(RawOutputNote::Full(swap_note.clone())); + builder.add_output_note(RawOutputNote::Full(pswap_note.clone())); let mut mock_chain = builder.build()?; let mut note_args_map = BTreeMap::new(); - note_args_map.insert(swap_note.id(), Word::from([Felt::new(20), Felt::new(0), ZERO, ZERO])); + note_args_map.insert(pswap_note.id(), Word::from([Felt::new(20), Felt::new(0), ZERO, ZERO])); - let pswap = PswapNote::try_from(&swap_note)?; + let pswap = PswapNote::try_from(&pswap_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()], &[])? + .build_tx_context(bob.id(), &[pswap_note.id()], &[])? .extend_note_args(note_args_map) .extend_expected_output_notes(vec![ RawOutputNote::Full(p2id_note), @@ -314,15 +311,14 @@ async fn pswap_note_inflight_cross_swap_test() -> anyhow::Result<()> { // Alice's note: offers 50 USDC, requests 25 ETH let alice_requested_asset = Asset::Fungible(FungibleAsset::new(eth_faucet.id(), 25)?); - let alice_swap_note: Note = PswapNote::builder() + let storage = PswapNoteStorage::builder() + .requested_asset_key(alice_requested_asset.to_key_word()) + .requested_asset_value(alice_requested_asset.to_value_word()) + .creator_account_id(alice.id()) + .build(); + let alice_pswap_note: Note = PswapNote::builder() .sender(alice.id()) - .storage( - PswapNoteStorage::builder() - .requested_asset_key(alice_requested_asset.to_key_word()) - .requested_asset_value(alice_requested_asset.to_value_word()) - .creator_account_id(alice.id()) - .build(), - ) + .storage(storage) .serial_number(rng.draw_word()) .note_type(NoteType::Public) .assets(NoteAssets::new(vec![Asset::Fungible(FungibleAsset::new( @@ -331,19 +327,18 @@ async fn pswap_note_inflight_cross_swap_test() -> anyhow::Result<()> { )?)])?) .build()? .into(); - builder.add_output_note(RawOutputNote::Full(alice_swap_note.clone())); + builder.add_output_note(RawOutputNote::Full(alice_pswap_note.clone())); // Bob's note: offers 25 ETH, requests 50 USDC let bob_requested_asset = Asset::Fungible(FungibleAsset::new(usdc_faucet.id(), 50)?); - let bob_swap_note: Note = PswapNote::builder() + let storage = PswapNoteStorage::builder() + .requested_asset_key(bob_requested_asset.to_key_word()) + .requested_asset_value(bob_requested_asset.to_value_word()) + .creator_account_id(bob.id()) + .build(); + let bob_pswap_note: Note = PswapNote::builder() .sender(bob.id()) - .storage( - PswapNoteStorage::builder() - .requested_asset_key(bob_requested_asset.to_key_word()) - .requested_asset_value(bob_requested_asset.to_value_word()) - .creator_account_id(bob.id()) - .build(), - ) + .storage(storage) .serial_number(rng.draw_word()) .note_type(NoteType::Public) .assets(NoteAssets::new(vec![Asset::Fungible(FungibleAsset::new( @@ -352,25 +347,26 @@ async fn pswap_note_inflight_cross_swap_test() -> anyhow::Result<()> { )?)])?) .build()? .into(); - builder.add_output_note(RawOutputNote::Full(bob_swap_note.clone())); + builder.add_output_note(RawOutputNote::Full(bob_pswap_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(), 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])); + .insert(alice_pswap_note.id(), Word::from([Felt::new(0), Felt::new(25), ZERO, ZERO])); + note_args_map + .insert(bob_pswap_note.id(), Word::from([Felt::new(0), Felt::new(50), ZERO, ZERO])); // Expected P2ID notes - let alice_pswap = PswapNote::try_from(&alice_swap_note)?; + let alice_pswap = PswapNote::try_from(&alice_pswap_note)?; let (alice_p2id_note, _) = alice_pswap.execute(charlie.id(), 0, 25)?; - let bob_pswap = PswapNote::try_from(&bob_swap_note)?; + let bob_pswap = PswapNote::try_from(&bob_pswap_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()], &[])? + .build_tx_context(charlie.id(), &[alice_pswap_note.id(), bob_pswap_note.id()], &[])? .extend_note_args(note_args_map) .extend_expected_output_notes(vec![ RawOutputNote::Full(alice_p2id_note), @@ -421,15 +417,14 @@ async fn pswap_note_creator_reclaim_test() -> anyhow::Result<()> { let mut rng = RandomCoin::new(Word::default()); let requested_asset = Asset::Fungible(FungibleAsset::new(eth_faucet.id(), 25)?); - let swap_note: Note = PswapNote::builder() + let storage = PswapNoteStorage::builder() + .requested_asset_key(requested_asset.to_key_word()) + .requested_asset_value(requested_asset.to_value_word()) + .creator_account_id(alice.id()) + .build(); + let pswap_note: Note = PswapNote::builder() .sender(alice.id()) - .storage( - PswapNoteStorage::builder() - .requested_asset_key(requested_asset.to_key_word()) - .requested_asset_value(requested_asset.to_value_word()) - .creator_account_id(alice.id()) - .build(), - ) + .storage(storage) .serial_number(rng.draw_word()) .note_type(NoteType::Public) .assets(NoteAssets::new(vec![Asset::Fungible(FungibleAsset::new( @@ -438,11 +433,11 @@ async fn pswap_note_creator_reclaim_test() -> anyhow::Result<()> { )?)])?) .build()? .into(); - builder.add_output_note(RawOutputNote::Full(swap_note.clone())); + builder.add_output_note(RawOutputNote::Full(pswap_note.clone())); 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(), &[pswap_note.id()], &[])?.build()?; let executed_transaction = tx_context.execute().await?; @@ -483,15 +478,14 @@ async fn pswap_note_invalid_input_test() -> anyhow::Result<()> { let mut rng = RandomCoin::new(Word::default()); let requested_asset = Asset::Fungible(FungibleAsset::new(eth_faucet.id(), 25)?); - let swap_note: Note = PswapNote::builder() + let storage = PswapNoteStorage::builder() + .requested_asset_key(requested_asset.to_key_word()) + .requested_asset_value(requested_asset.to_value_word()) + .creator_account_id(alice.id()) + .build(); + let pswap_note: Note = PswapNote::builder() .sender(alice.id()) - .storage( - PswapNoteStorage::builder() - .requested_asset_key(requested_asset.to_key_word()) - .requested_asset_value(requested_asset.to_value_word()) - .creator_account_id(alice.id()) - .build(), - ) + .storage(storage) .serial_number(rng.draw_word()) .note_type(NoteType::Public) .assets(NoteAssets::new(vec![Asset::Fungible(FungibleAsset::new( @@ -500,15 +494,15 @@ async fn pswap_note_invalid_input_test() -> anyhow::Result<()> { )?)])?) .build()? .into(); - builder.add_output_note(RawOutputNote::Full(swap_note.clone())); + builder.add_output_note(RawOutputNote::Full(pswap_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(), Word::from([Felt::new(30), Felt::new(0), ZERO, ZERO])); + note_args_map.insert(pswap_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()], &[])? + .build_tx_context(bob.id(), &[pswap_note.id()], &[])? .extend_note_args(note_args_map) .build()?; @@ -552,15 +546,14 @@ async fn pswap_note_multiple_partial_fills_test() -> anyhow::Result<()> { let mut rng = RandomCoin::new(Word::default()); let requested_asset = Asset::Fungible(FungibleAsset::new(eth_faucet.id(), 25)?); - let swap_note: Note = PswapNote::builder() + let storage = PswapNoteStorage::builder() + .requested_asset_key(requested_asset.to_key_word()) + .requested_asset_value(requested_asset.to_value_word()) + .creator_account_id(alice.id()) + .build(); + let pswap_note: Note = PswapNote::builder() .sender(alice.id()) - .storage( - PswapNoteStorage::builder() - .requested_asset_key(requested_asset.to_key_word()) - .requested_asset_value(requested_asset.to_value_word()) - .creator_account_id(alice.id()) - .build(), - ) + .storage(storage) .serial_number(rng.draw_word()) .note_type(NoteType::Public) .assets(NoteAssets::new(vec![Asset::Fungible(FungibleAsset::new( @@ -569,7 +562,7 @@ async fn pswap_note_multiple_partial_fills_test() -> anyhow::Result<()> { )?)])?) .build()? .into(); - builder.add_output_note(RawOutputNote::Full(swap_note.clone())); + builder.add_output_note(RawOutputNote::Full(pswap_note.clone())); let mock_chain = builder.build()?; @@ -577,11 +570,11 @@ async fn pswap_note_multiple_partial_fills_test() -> anyhow::Result<()> { let mut note_args_map = BTreeMap::new(); note_args_map.insert( - swap_note.id(), + pswap_note.id(), Word::from([Felt::new(input_amount), Felt::new(0), ZERO, ZERO]), ); - let pswap = PswapNote::try_from(&swap_note)?; + let pswap = PswapNote::try_from(&pswap_note)?; let (p2id_note, remainder_pswap) = pswap.execute(bob.id(), input_amount, 0)?; let mut expected_notes = vec![RawOutputNote::Full(p2id_note)]; @@ -591,7 +584,7 @@ async fn pswap_note_multiple_partial_fills_test() -> anyhow::Result<()> { } let tx_context = mock_chain - .build_tx_context(bob.id(), &[swap_note.id()], &[])? + .build_tx_context(bob.id(), &[pswap_note.id()], &[])? .extend_expected_output_notes(expected_notes) .extend_note_args(note_args_map) .build()?; @@ -636,17 +629,15 @@ async fn pswap_note_non_exact_ratio_partial_fill_test() -> anyhow::Result<()> { )?; let mut rng = RandomCoin::new(Word::default()); - let swap_note: Note = PswapNote::builder() + let requested_asset = Asset::Fungible(FungibleAsset::new(eth_faucet.id(), requested_total)?); + let storage = PswapNoteStorage::builder() + .requested_asset_key(requested_asset.to_key_word()) + .requested_asset_value(requested_asset.to_value_word()) + .creator_account_id(alice.id()) + .build(); + let pswap_note: Note = PswapNote::builder() .sender(alice.id()) - .storage({ - let requested_asset = - Asset::Fungible(FungibleAsset::new(eth_faucet.id(), requested_total)?); - PswapNoteStorage::builder() - .requested_asset_key(requested_asset.to_key_word()) - .requested_asset_value(requested_asset.to_value_word()) - .creator_account_id(alice.id()) - .build() - }) + .storage(storage) .serial_number(rng.draw_word()) .note_type(NoteType::Public) .assets(NoteAssets::new(vec![Asset::Fungible(FungibleAsset::new( @@ -655,20 +646,20 @@ async fn pswap_note_non_exact_ratio_partial_fill_test() -> anyhow::Result<()> { )?)])?) .build()? .into(); - builder.add_output_note(RawOutputNote::Full(swap_note.clone())); + builder.add_output_note(RawOutputNote::Full(pswap_note.clone())); let mock_chain = builder.build()?; let mut note_args_map = BTreeMap::new(); note_args_map - .insert(swap_note.id(), Word::from([Felt::new(input_amount), Felt::new(0), ZERO, ZERO])); + .insert(pswap_note.id(), Word::from([Felt::new(input_amount), Felt::new(0), ZERO, ZERO])); - let pswap = PswapNote::try_from(&swap_note)?; + let pswap = PswapNote::try_from(&pswap_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()], &[])? + .build_tx_context(bob.id(), &[pswap_note.id()], &[])? .extend_expected_output_notes(vec![ RawOutputNote::Full(p2id_note), RawOutputNote::Full(remainder), @@ -754,17 +745,15 @@ async fn pswap_note_partial_fill_non_integer_ratio_fuzz_test() -> anyhow::Result )?; let mut rng = RandomCoin::new(Word::default()); - let swap_note: Note = PswapNote::builder() + let requested_asset = Asset::Fungible(FungibleAsset::new(eth_faucet.id(), *requested_eth)?); + let storage = PswapNoteStorage::builder() + .requested_asset_key(requested_asset.to_key_word()) + .requested_asset_value(requested_asset.to_value_word()) + .creator_account_id(alice.id()) + .build(); + let pswap_note: Note = PswapNote::builder() .sender(alice.id()) - .storage({ - let requested_asset = - Asset::Fungible(FungibleAsset::new(eth_faucet.id(), *requested_eth)?); - PswapNoteStorage::builder() - .requested_asset_key(requested_asset.to_key_word()) - .requested_asset_value(requested_asset.to_value_word()) - .creator_account_id(alice.id()) - .build() - }) + .storage(storage) .serial_number(rng.draw_word()) .note_type(NoteType::Public) .assets(NoteAssets::new(vec![Asset::Fungible(FungibleAsset::new( @@ -773,15 +762,15 @@ async fn pswap_note_partial_fill_non_integer_ratio_fuzz_test() -> anyhow::Result )?)])?) .build()? .into(); - builder.add_output_note(RawOutputNote::Full(swap_note.clone())); + builder.add_output_note(RawOutputNote::Full(pswap_note.clone())); let mock_chain = builder.build()?; let mut note_args_map = BTreeMap::new(); note_args_map - .insert(swap_note.id(), Word::from([Felt::new(*fill_eth), Felt::new(0), ZERO, ZERO])); + .insert(pswap_note.id(), Word::from([Felt::new(*fill_eth), Felt::new(0), ZERO, ZERO])); - let pswap = PswapNote::try_from(&swap_note)?; + let pswap = PswapNote::try_from(&pswap_note)?; let (p2id_note, remainder_pswap) = pswap.execute(bob.id(), *fill_eth, 0)?; let mut expected_notes = vec![RawOutputNote::Full(p2id_note)]; @@ -792,7 +781,7 @@ async fn pswap_note_partial_fill_non_integer_ratio_fuzz_test() -> anyhow::Result } let tx_context = mock_chain - .build_tx_context(bob.id(), &[swap_note.id()], &[])? + .build_tx_context(bob.id(), &[pswap_note.id()], &[])? .extend_expected_output_notes(expected_notes) .extend_note_args(note_args_map) .build()?; @@ -850,13 +839,11 @@ async fn pswap_note_chained_partial_fills_non_integer_ratio_test() -> anyhow::Re 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 let mut rng = RandomCoin::new(Word::default()); let mut current_serial = rng.draw_word(); - for (fill_idx, fill_amount) in fills.iter().enumerate() { + for (current_swap_count, fill_amount) in fills.iter().enumerate() { let offered_out = PswapNote::calculate_output_amount( current_offered, current_requested, @@ -912,18 +899,18 @@ async fn pswap_note_chained_partial_fills_non_integer_ratio_test() -> anyhow::Re 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); + let pswap_note = Note::new(note_assets, metadata, recipient); - builder.add_output_note(RawOutputNote::Full(swap_note.clone())); + builder.add_output_note(RawOutputNote::Full(pswap_note.clone())); let mock_chain = builder.build()?; let mut note_args_map = BTreeMap::new(); note_args_map.insert( - swap_note.id(), + pswap_note.id(), Word::from([Felt::new(*fill_amount), Felt::new(0), ZERO, ZERO]), ); - let pswap = PswapNote::try_from(&swap_note)?; + let pswap = PswapNote::try_from(&pswap_note)?; let (p2id_note, remainder_pswap) = pswap.execute(bob.id(), *fill_amount, 0)?; let mut expected_notes = vec![RawOutputNote::Full(p2id_note)]; @@ -934,7 +921,7 @@ async fn pswap_note_chained_partial_fills_non_integer_ratio_test() -> anyhow::Re } let tx_context = mock_chain - .build_tx_context(bob.id(), &[swap_note.id()], &[])? + .build_tx_context(bob.id(), &[pswap_note.id()], &[])? .extend_expected_output_notes(expected_notes) .extend_note_args(note_args_map) .build()?; @@ -943,7 +930,7 @@ async fn pswap_note_chained_partial_fills_non_integer_ratio_test() -> anyhow::Re anyhow::anyhow!( "Chain {} fill {} failed: {} (offered={}, requested={}, fill={})", chain_idx + 1, - fill_idx + 1, + current_swap_count + 1, e, current_offered, current_requested, @@ -958,19 +945,19 @@ async fn pswap_note_chained_partial_fills_non_integer_ratio_test() -> anyhow::Re expected_count, "Chain {} fill {}", chain_idx + 1, - fill_idx + 1 + current_swap_count + 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); + assert_eq!(added.len(), 1, "Chain {} fill {}", chain_idx + 1, current_swap_count + 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, + current_swap_count + 1, offered_out ); } @@ -980,7 +967,6 @@ async fn pswap_note_chained_partial_fills_non_integer_ratio_test() -> anyhow::Re 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), @@ -1027,15 +1013,14 @@ fn compare_pswap_create_output_notes_vs_test_helper() { // Create swap note using PswapNote builder let mut rng = RandomCoin::new(Word::default()); let requested_asset = Asset::Fungible(FungibleAsset::new(eth_faucet.id(), 25).unwrap()); - let swap_note: Note = PswapNote::builder() + let storage = PswapNoteStorage::builder() + .requested_asset_key(requested_asset.to_key_word()) + .requested_asset_value(requested_asset.to_value_word()) + .creator_account_id(alice.id()) + .build(); + let pswap_note: Note = PswapNote::builder() .sender(alice.id()) - .storage( - PswapNoteStorage::builder() - .requested_asset_key(requested_asset.to_key_word()) - .requested_asset_value(requested_asset.to_value_word()) - .creator_account_id(alice.id()) - .build(), - ) + .storage(storage) .serial_number(rng.draw_word()) .note_type(NoteType::Public) .assets( @@ -1049,7 +1034,7 @@ fn compare_pswap_create_output_notes_vs_test_helper() { .into(); // Roundtrip: try_from -> execute -> verify outputs - let pswap = PswapNote::try_from(&swap_note).unwrap(); + let pswap = PswapNote::try_from(&pswap_note).unwrap(); // Verify roundtripped PswapNote preserves key fields assert_eq!(pswap.sender(), alice.id(), "Sender mismatch after roundtrip"); @@ -1111,15 +1096,14 @@ fn pswap_parse_inputs_roundtrip() { let mut rng = RandomCoin::new(Word::default()); let requested_asset = Asset::Fungible(FungibleAsset::new(eth_faucet.id(), 25).unwrap()); - let swap_note: Note = PswapNote::builder() + let storage = PswapNoteStorage::builder() + .requested_asset_key(requested_asset.to_key_word()) + .requested_asset_value(requested_asset.to_value_word()) + .creator_account_id(alice.id()) + .build(); + let pswap_note: Note = PswapNote::builder() .sender(alice.id()) - .storage( - PswapNoteStorage::builder() - .requested_asset_key(requested_asset.to_key_word()) - .requested_asset_value(requested_asset.to_value_word()) - .creator_account_id(alice.id()) - .build(), - ) + .storage(storage) .serial_number(rng.draw_word()) .note_type(NoteType::Public) .assets( @@ -1132,7 +1116,7 @@ fn pswap_parse_inputs_roundtrip() { .unwrap() .into(); - let storage = swap_note.recipient().storage(); + let storage = pswap_note.recipient().storage(); let items = storage.items(); let parsed = PswapNoteStorage::try_from(items).unwrap(); From 1780388fd9132f77c41c01e5f11b8471cb50439a Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Wed, 25 Mar 2026 09:04:11 +0530 Subject: [PATCH 14/66] refactor: rename inputs to storage_items in pswap_note_storage_try_from test --- crates/miden-standards/src/note/pswap.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/miden-standards/src/note/pswap.rs b/crates/miden-standards/src/note/pswap.rs index ba458838bf..615efaf05e 100644 --- a/crates/miden-standards/src/note/pswap.rs +++ b/crates/miden-standards/src/note/pswap.rs @@ -824,7 +824,7 @@ mod tests { let key_word = asset.to_key_word(); let value_word = asset.to_value_word(); - let inputs = vec![ + let storage_items = vec![ key_word[0], key_word[1], key_word[2], @@ -845,7 +845,7 @@ mod tests { creator_id.suffix(), ]; - let parsed = PswapNoteStorage::try_from(inputs.as_slice()).unwrap(); + let parsed = PswapNoteStorage::try_from(storage_items.as_slice()).unwrap(); assert_eq!(parsed.swap_count(), 3); assert_eq!(parsed.creator_account_id(), creator_id); assert_eq!( From b54686a140cfb24ea2a2aa0d1da5d34537486872 Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Wed, 25 Mar 2026 21:54:51 +0530 Subject: [PATCH 15/66] chore: fix Cargo.toml dependency ordering and add CHANGELOG entry for PSWAP --- CHANGELOG.md | 4 ++++ Cargo.toml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 53c971fab4..b34b2f5083 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## v0.15.0 (TBD) +### Features + +- Added PSWAP (partial swap) note for decentralized partial-fill asset exchange with remainder note re-creation ([#2636](https://github.com/0xMiden/protocol/pull/2636)). + ## 0.14.0 (2026-03-23) ### Features diff --git a/Cargo.toml b/Cargo.toml index 003edd7b41..a601f5238b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,8 +61,8 @@ miden-verifier = { default-features = false, version = "0.22" } # External dependencies alloy-sol-types = { default-features = false, version = "1.5" } anyhow = { default-features = false, features = ["backtrace", "std"], version = "1.0" } -bon = { default-features = false, version = "3" } assert_matches = { default-features = false, version = "1.5" } +bon = { default-features = false, version = "3" } fs-err = { default-features = false, version = "3" } primitive-types = { default-features = false, version = "0.14" } rand = { default-features = false, version = "0.9" } From 570ac5a777f3668f9a04f1516977a5d7384cde04 Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Tue, 31 Mar 2026 15:18:34 +0530 Subject: [PATCH 16/66] =?UTF-8?q?refactor:=20address=20PR=20review=20comme?= =?UTF-8?q?nts=20=E2=80=94=20use=20FungibleAsset=20types,=20improve=20safe?= =?UTF-8?q?ty=20and=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PswapNoteStorage: store FungibleAsset instead of raw key/value Words - PswapNote: store offered_asset as FungibleAsset instead of NoteAssets - execute(): take Option for input/inflight assets - Split execute into execute() and execute_full_fill_network() for clarity - create_tag(): accept &FungibleAsset instead of &Asset - Eliminate all Felt::new usage (use Felt::from, Felt::try_from, + ONE) - Add swap_count overflow protection via checked_add - Add script root verification in TryFrom<&Note> - Add MASM fill validation (fill_amount <= requested) - Rename MASM constants: _INPUT -> _ITEM, SWAPP_ -> PSWAP_ - Make calculate_output_amount private, add precision doc comment - Add doc comments on all public accessors and methods - Add network account full fill test - Document network transaction compatibility throughout --- .../asm/standards/notes/pswap.masm | 71 ++- crates/miden-standards/src/note/pswap.rs | 451 ++++++++++-------- crates/miden-testing/tests/scripts/pswap.rs | 329 ++++++++----- 3 files changed, 492 insertions(+), 359 deletions(-) diff --git a/crates/miden-standards/asm/standards/notes/pswap.masm b/crates/miden-standards/asm/standards/notes/pswap.masm index c795be2c4a..6cde177530 100644 --- a/crates/miden-standards/asm/standards/notes/pswap.masm +++ b/crates/miden-standards/asm/standards/notes/pswap.masm @@ -25,15 +25,15 @@ const MAX_U32=0x0000000100000000 # - Full Word (word-aligned): addresses 0x0018 - 0x001F # PSWAP Note Storage (18 items loaded at address 0) -# REQUESTED_ASSET_WORD_INPUT is the base address of an 8-cell block: +# REQUESTED_ASSET_WORD_ITEM 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 +const REQUESTED_ASSET_WORD_ITEM = 0x0000 +const REQUESTED_ASSET_VALUE_ITEM = 0x0004 +const PSWAP_TAG_ITEM = 0x0008 +const P2ID_TAG_ITEM = 0x0009 +const PSWAP_COUNT_ITEM = 0x000C +const PSWAP_CREATOR_PREFIX_ITEM = 0x0010 +const PSWAP_CREATOR_SUFFIX_ITEM = 0x0011 # Memory Addresses for Price Calculation Procedure const AMT_OFFERED = 0x0028 @@ -58,7 +58,7 @@ const OFFERED_ASSET_WORD = 0x0018 # Note indices and type const P2ID_NOTE_IDX = 0x007C -const SWAPP_NOTE_IDX = 0x0080 +const PSWAP_NOTE_IDX = 0x0080 const NOTE_TYPE = 0x0084 # P2ID recipient storage (creator account ID written here for build_recipient) @@ -70,10 +70,13 @@ const P2ID_RECIPIENT_PREFIX = 0x0FA1 # ================================================================================================= # PSWAP script expects exactly 18 note storage items -const ERR_PSWAP_WRONG_NUMBER_OF_INPUTS="PSWAP wrong number of inputs" +const ERR_PSWAP_WRONG_NUMBER_OF_INPUTS="PSWAP script expects exactly 18 note storage items" # PSWAP script requires exactly one note asset -const ERR_PSWAP_WRONG_NUMBER_OF_ASSETS="PSWAP wrong number of assets" +const ERR_PSWAP_WRONG_NUMBER_OF_ASSETS="PSWAP script requires exactly one note asset" + +# PSWAP fill amount (input + inflight) exceeds the total requested amount +const ERR_PSWAP_FILL_EXCEEDS_REQUESTED="PSWAP fill amount exceeds requested amount" # PRICE CALCULATION # ================================================================================================= @@ -229,7 +232,7 @@ end #! Builds the P2ID recipient hash for the swap creator. #! -#! Loads the creator's account ID from note storage (SWAPP_CREATOR_SUFFIX/PREFIX_INPUT), +#! Loads the creator's account ID from note storage (PSWAP_CREATOR_SUFFIX/PREFIX_ITEM), #! stores it as P2ID note storage [suffix, prefix] at a temp address, and calls #! note::build_recipient to compute the recipient commitment. #! @@ -238,8 +241,8 @@ end #! proc build_p2id_recipient_hash # Store creator [suffix, prefix] for P2ID recipient hashing - mem_load.SWAPP_CREATOR_SUFFIX_INPUT mem_store.P2ID_RECIPIENT_SUFFIX - mem_load.SWAPP_CREATOR_PREFIX_INPUT mem_store.P2ID_RECIPIENT_PREFIX + mem_load.PSWAP_CREATOR_SUFFIX_ITEM mem_store.P2ID_RECIPIENT_SUFFIX + mem_load.PSWAP_CREATOR_PREFIX_ITEM mem_store.P2ID_RECIPIENT_PREFIX # => [SERIAL_NUM, SCRIPT_ROOT] # note::build_recipient: [storage_ptr, num_storage_items, SERIAL_NUM, SCRIPT_ROOT] => [RECIPIENT] @@ -268,11 +271,11 @@ proc create_p2id_note # => [P2ID_SCRIPT_ROOT] # Increment swap count (ensures unique serial per P2ID note in chained fills) - mem_load.SWAPP_COUNT_INPUT add.1 mem_store.SWAPP_COUNT_INPUT + mem_load.PSWAP_COUNT_ITEM add.1 mem_store.PSWAP_COUNT_ITEM # => [P2ID_SCRIPT_ROOT] # Load swap count word from memory - padw mem_loadw_le.SWAPP_COUNT_INPUT + padw mem_loadw_le.PSWAP_COUNT_ITEM # => [SWAP_COUNT_WORD, P2ID_SCRIPT_ROOT] # Get serial number from active note @@ -293,7 +296,7 @@ proc create_p2id_note mem_load.NOTE_TYPE # => [note_type, P2ID_RECIPIENT] - mem_load.P2ID_TAG_INPUT + mem_load.P2ID_TAG_ITEM # => [tag, note_type, RECIPIENT] exec.output_note::create @@ -384,7 +387,7 @@ end #! proc create_remainder_note # Update note storage with new requested amount - mem_store.REQUESTED_ASSET_VALUE_INPUT + mem_store.REQUESTED_ASSET_VALUE_ITEM # => [] # Build PSWAP remainder recipient using the same script as the active note @@ -403,19 +406,19 @@ proc create_remainder_note # => [RECIPIENT_SWAPP] mem_load.NOTE_TYPE - mem_load.SWAPP_TAG_INPUT + mem_load.PSWAP_TAG_ITEM exec.output_note::create # => [note_idx] - mem_store.SWAPP_NOTE_IDX + mem_store.PSWAP_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 + push.0 mem_load.PSWAP_NOTE_IDX # => [note_idx, attachment_scheme, ATTACHMENT] exec.output_note::set_word_attachment @@ -423,7 +426,7 @@ proc create_remainder_note # Add remaining offered asset to remainder note # remainder_amount = total_offered - offered_out - mem_load.SWAPP_NOTE_IDX + mem_load.PSWAP_NOTE_IDX # => [note_idx] # Build ASSET_VALUE = [amount, 0, 0, 0] (amount on top) @@ -451,7 +454,7 @@ 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 + mem_load.PSWAP_CREATOR_SUFFIX_ITEM mem_load.PSWAP_CREATOR_PREFIX_ITEM # => [creator_prefix, creator_suffix, acct_id_suffix, acct_id_prefix] movup.3 eq @@ -536,7 +539,7 @@ proc execute_pswap # => [] # Load requested asset from note storage (ASSET_KEY + ASSET_VALUE, 8 cells) - push.REQUESTED_ASSET_WORD_INPUT exec.asset::load + push.REQUESTED_ASSET_WORD_ITEM exec.asset::load # => [ASSET_KEY, ASSET_VALUE] # Extract requested amount @@ -552,12 +555,27 @@ proc execute_pswap dropw # => [] - # If both input and inflight are 0, default to full fill (input = requested) + # If both input and inflight are 0, default to a full fill (input = requested). + # This enables consumption by network accounts, which execute without note_args + # (the kernel defaults note_args to [0, 0, 0, 0] when none are provided). mem_load.AMT_REQUESTED_IN mem_load.AMT_REQUESTED_INFLIGHT add push.0 eq if.true mem_load.AMT_REQUESTED mem_store.AMT_REQUESTED_IN end + # Validate: fill amount (input + inflight) must not exceed total requested + mem_load.AMT_REQUESTED_IN mem_load.AMT_REQUESTED_INFLIGHT add + mem_load.AMT_REQUESTED + # => [requested, fill_amount] + # lte pops [b=requested, a=fill_amount] and pushes (fill_amount <= requested) + lte assert.err=ERR_PSWAP_FILL_EXCEEDS_REQUESTED + # => [] + + # Calculate offered_out for input and inflight amounts separately rather than + # summing them first, because the input portion (offered_out_input) must be sent + # to the consumer's vault individually, while the total (input + inflight) is + # needed to determine the remainder note's offered amount. + # # Calculate offered_out for input_amount mem_load.AMT_REQUESTED_IN mem_load.AMT_REQUESTED @@ -628,6 +646,9 @@ 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) + # + # In network transactions, note_args are not provided by the executor and default + # to [0, 0, 0, 0]. The script handles this by defaulting to a full fill. mem_store.AMT_REQUESTED_IN # => [inflight_amount, 0, 0] diff --git a/crates/miden-standards/src/note/pswap.rs b/crates/miden-standards/src/note/pswap.rs index 615efaf05e..2db75ae2ba 100644 --- a/crates/miden-standards/src/note/pswap.rs +++ b/crates/miden-standards/src/note/pswap.rs @@ -5,19 +5,11 @@ use miden_protocol::assembly::Path; use miden_protocol::asset::{Asset, FungibleAsset}; use miden_protocol::errors::NoteError; use miden_protocol::note::{ - Note, - NoteAssets, - NoteAttachment, - NoteAttachmentScheme, - NoteMetadata, - NoteRecipient, - NoteScript, - NoteStorage, - NoteTag, - NoteType, + Note, NoteAssets, NoteAttachment, NoteAttachmentScheme, NoteMetadata, NoteRecipient, + NoteScript, NoteStorage, NoteTag, NoteType, }; use miden_protocol::utils::sync::LazyLock; -use miden_protocol::{Felt, Hasher, Word, ZERO}; +use miden_protocol::{Felt, Hasher, ONE, Word, ZERO}; use crate::StandardsLib; use crate::note::P2idNoteStorage; @@ -55,8 +47,7 @@ static PSWAP_SCRIPT: LazyLock = LazyLock::new(|| { /// | `[16-17]` | Creator account ID (prefix, suffix) | #[derive(Debug, Clone, PartialEq, Eq, bon::Builder)] pub struct PswapNoteStorage { - requested_asset_key: Word, - requested_asset_value: Word, + requested_asset: FungibleAsset, #[builder(default)] pswap_tag: NoteTag, @@ -89,18 +80,19 @@ impl PswapNoteStorage { // PUBLIC ACCESSORS // -------------------------------------------------------------------------------------------- - pub fn requested_asset_key(&self) -> Word { - self.requested_asset_key - } - - pub fn requested_asset_value(&self) -> Word { - self.requested_asset_value + /// Returns a reference to the requested [`FungibleAsset`]. + pub fn requested_asset(&self) -> &FungibleAsset { + &self.requested_asset } + /// Returns the PSWAP note tag. This may be the default (zero) tag until the note + /// is converted into a [`Note`], at which point the tag is derived from the + /// offered/requested asset pair. pub fn pswap_tag(&self) -> NoteTag { self.pswap_tag } + /// Returns the payback note routing tag, derived from the creator's account ID. pub fn payback_note_tag(&self) -> NoteTag { NoteTag::with_account_target(self.creator_account_id) } @@ -110,49 +102,40 @@ impl PswapNoteStorage { self.swap_count } + /// Returns the account ID of the note creator. pub fn creator_account_id(&self) -> AccountId { self.creator_account_id } - /// 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_asset_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 of the requested asset from the key word. - pub fn requested_faucet_id(&self) -> Result { - AccountId::try_from_elements(self.requested_asset_key[2], self.requested_asset_key[3]) - .map_err(|e| NoteError::other_with_source("failed to parse faucet ID from key", e)) + /// Returns the faucet ID of the requested asset. + pub fn requested_faucet_id(&self) -> AccountId { + self.requested_asset.faucet_id() } - /// Extracts the requested token amount from the value word. + /// Returns the requested token amount. pub fn requested_asset_amount(&self) -> u64 { - self.requested_asset_value[0].as_canonical_u64() + self.requested_asset.amount() } } /// Serializes [`PswapNoteStorage`] into an 18-element [`NoteStorage`]. impl From for NoteStorage { fn from(storage: PswapNoteStorage) -> Self { + let asset = Asset::Fungible(storage.requested_asset); + let key_word = asset.to_key_word(); + let value_word = asset.to_value_word(); + let storage_items = vec![ // ASSET_KEY [0-3] - storage.requested_asset_key[0], - storage.requested_asset_key[1], - storage.requested_asset_key[2], - storage.requested_asset_key[3], + key_word[0], + key_word[1], + key_word[2], + key_word[3], // ASSET_VALUE [4-7] - storage.requested_asset_value[0], - storage.requested_asset_value[1], - storage.requested_asset_value[2], - storage.requested_asset_value[3], + value_word[0], + value_word[1], + value_word[2], + value_word[3], // Tags [8-9] Felt::from(storage.pswap_tag), Felt::from(storage.payback_note_tag()), @@ -189,7 +172,16 @@ impl TryFrom<&[Felt]> for PswapNoteStorage { Word::from([note_storage[0], note_storage[1], note_storage[2], note_storage[3]]); let requested_asset_value = Word::from([note_storage[4], note_storage[5], note_storage[6], note_storage[7]]); - let pswap_tag = NoteTag::new(note_storage[8].as_canonical_u64() as u32); + let requested_asset = + FungibleAsset::from_key_value_words(requested_asset_key, requested_asset_value) + .map_err(|e| { + NoteError::other_with_source("failed to parse requested asset from storage", e) + })?; + + let pswap_tag = NoteTag::new( + u32::try_from(note_storage[8].as_canonical_u64()) + .map_err(|_| NoteError::other("pswap_tag exceeds u32"))?, + ); let swap_count: u16 = note_storage[12] .as_canonical_u64() .try_into() @@ -201,8 +193,7 @@ impl TryFrom<&[Felt]> for PswapNoteStorage { })?; Ok(Self { - requested_asset_key, - requested_asset_value, + requested_asset, pswap_tag, swap_count, creator_account_id, @@ -219,6 +210,12 @@ impl TryFrom<&[Felt]> for PswapNoteStorage { /// 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 payback note. +/// +/// The note can be consumed both in local transactions (where the consumer provides +/// fill amounts via note_args) and in network transactions (where note_args default to +/// `[0, 0, 0, 0]`, triggering a full fill). To route a PSWAP note to a network account, +/// set the `attachment` to a [`NetworkAccountTarget`](crate::note::NetworkAccountTarget) +/// via the builder. #[derive(Debug, Clone, bon::Builder)] #[builder(finish_fn(vis = "", name = build_internal))] pub struct PswapNote { @@ -229,7 +226,7 @@ pub struct PswapNote { #[builder(default = NoteType::Private)] note_type: NoteType, - assets: NoteAssets, + offered_asset: FungibleAsset, #[builder(default)] attachment: NoteAttachment, @@ -239,16 +236,15 @@ impl PswapNoteBuilder where S: pswap_note_builder::IsComplete, { + /// Validates and builds the [`PswapNote`]. + /// + /// # Errors + /// + /// Returns an error if the offered and requested assets have the same faucet ID. pub fn build(self) -> Result { let note = self.build_internal(); - if note.assets.num_assets() != 1 { - return Err(NoteError::other("Swap note must have exactly 1 offered asset")); - } - - let offered_asset = note.assets.iter().next().unwrap(); - let requested_asset = note.storage.requested_asset()?; - if offered_asset.faucet_id() == requested_asset.faucet_id() { + if note.offered_asset.faucet_id() == note.storage.requested_faucet_id() { return Err(NoteError::other( "offered and requested assets must have different faucets", )); @@ -259,12 +255,6 @@ where } impl PswapNote { - // CONSTANTS - // -------------------------------------------------------------------------------------------- - - /// Expected number of storage items for the PSWAP note. - pub const NUM_STORAGE_ITEMS: usize = PswapNoteStorage::NUM_STORAGE_ITEMS; - // PUBLIC ACCESSORS // -------------------------------------------------------------------------------------------- @@ -278,26 +268,36 @@ impl PswapNote { PSWAP_SCRIPT.root() } + /// Returns the account ID of the note sender. pub fn sender(&self) -> AccountId { self.sender } + /// Returns a reference to the PSWAP note storage. pub fn storage(&self) -> &PswapNoteStorage { &self.storage } + /// Returns the serial number of this note. pub fn serial_number(&self) -> Word { self.serial_number } + /// Returns the note type (public or private). pub fn note_type(&self) -> NoteType { self.note_type } - pub fn assets(&self) -> &NoteAssets { - &self.assets + /// Returns a reference to the offered [`FungibleAsset`]. + pub fn offered_asset(&self) -> &FungibleAsset { + &self.offered_asset } + /// Returns a reference to the note attachment. + /// + /// For notes targeting a network account, this may contain a + /// [`NetworkAccountTarget`](crate::note::NetworkAccountTarget) with scheme = 1. + /// For local-only notes, this is typically `NoteAttachmentScheme::none()`. pub fn attachment(&self) -> &NoteAttachment { &self.attachment } @@ -305,33 +305,71 @@ impl PswapNote { // INSTANCE METHODS // -------------------------------------------------------------------------------------------- + /// Executes the swap as a full fill, intended for network transactions. + /// + /// In network transactions, note_args are unavailable (the kernel defaults them to + /// `[0, 0, 0, 0]`), so the MASM script fills the entire requested amount. This method + /// mirrors that behavior. Returns only the payback note — no remainder is produced. + /// + /// # Errors + /// + /// Returns an error if the swap count overflows `u16::MAX`. + pub fn execute_full_fill_network( + &self, + network_account_id: AccountId, + ) -> Result { + let requested_faucet_id = self.storage.requested_faucet_id(); + let total_requested_amount = self.storage.requested_asset_amount(); + + let fill_asset = FungibleAsset::new(requested_faucet_id, total_requested_amount) + .map_err(|e| NoteError::other_with_source("failed to create full fill asset", e))?; + + self.create_payback_note(network_account_id, fill_asset, total_requested_amount) + } + /// Executes the swap, producing the output notes for a given fill. /// - /// `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. + /// `input_asset` is debited from the consumer's vault; `inflight_asset` arrives + /// from another note in the same transaction (cross-swap). At least one must be + /// provided. /// /// Returns `(payback_note, Option)`. The remainder is /// `None` when the fill equals the total requested amount (full fill). + /// + /// # Errors + /// + /// Returns an error if: + /// - Both assets are `None`. + /// - The fill amount is zero. + /// - The fill amount exceeds the total requested amount. + /// - The swap count overflows `u16::MAX`. pub fn execute( &self, consumer_account_id: AccountId, - input_amount: u64, - inflight_amount: u64, + input_asset: Option, + inflight_asset: Option, ) -> Result<(Note, Option), NoteError> { - let fill_amount = input_amount + inflight_amount; - - let requested_faucet_id = self.storage.requested_faucet_id()?; - let total_requested_amount = self.storage.requested_asset_amount(); - - // Ensure offered asset exists and is fungible - if self.assets.num_assets() != 1 { - return Err(NoteError::other("Swap note must have exactly 1 offered asset")); + if input_asset.is_none() && inflight_asset.is_none() { + return Err(NoteError::other( + "at least one of input_asset or inflight_asset must be provided", + )); } - let total_offered_amount = self.offered_asset_amount()?; - let offered_faucet_id = match self.assets.iter().next().unwrap() { - Asset::Fungible(fa) => fa.faucet_id(), - _ => unreachable!(), + + // Combine input and inflight into a single payback asset + let input_amount = input_asset.as_ref().map_or(0, |a| a.amount()); + let inflight_amount = inflight_asset.as_ref().map_or(0, |a| a.amount()); + let payback_asset = match (input_asset, inflight_asset) { + (Some(input), Some(inflight)) => input.add(inflight).map_err(|e| { + NoteError::other_with_source("failed to combine input and inflight assets", e) + })?, + (Some(asset), None) | (None, Some(asset)) => asset, + (None, None) => unreachable!("validated above"), }; + let fill_amount = payback_asset.amount(); + + let total_offered_amount = self.offered_asset.amount(); + let requested_faucet_id = self.storage.requested_faucet_id(); + let total_requested_amount = self.storage.requested_asset_amount(); // Validate fill amount if fill_amount == 0 { @@ -346,7 +384,9 @@ impl PswapNote { } // Calculate offered amounts separately for input and inflight, matching the MASM - // which calls calculate_tokens_offered_for_requested twice. + // which calls calculate_tokens_offered_for_requested twice. This is necessary + // because the input portion goes to the consumer's vault while the total determines + // the remainder note's offered amount. let offered_for_input = Self::calculate_output_amount( total_offered_amount, total_requested_amount, @@ -359,12 +399,6 @@ impl PswapNote { ); let offered_amount_for_fill = offered_for_input + offered_for_inflight; - // Build the payback note - let payback_asset = Asset::Fungible( - FungibleAsset::new(requested_faucet_id, fill_amount) - .map_err(|e| NoteError::other_with_source("failed to create payback asset", e))?, - ); - let payback_note = self.create_payback_note(consumer_account_id, payback_asset, fill_amount)?; @@ -374,14 +408,19 @@ impl PswapNote { let remaining_requested = total_requested_amount - fill_amount; let remaining_offered_asset = - Asset::Fungible(FungibleAsset::new(offered_faucet_id, remaining_offered).map_err( + FungibleAsset::new(self.offered_asset.faucet_id(), remaining_offered).map_err( |e| NoteError::other_with_source("failed to create remainder asset", e), - )?); + )?; + + let remaining_requested_asset = + FungibleAsset::new(requested_faucet_id, remaining_requested).map_err(|e| { + NoteError::other_with_source("failed to create remaining requested asset", e) + })?; Some(self.create_remainder_pswap_note( consumer_account_id, remaining_offered_asset, - remaining_requested, + remaining_requested_asset, offered_amount_for_fill, )?) } else { @@ -391,26 +430,13 @@ impl PswapNote { Ok((payback_note, remainder)) } - /// Returns the amount of the offered fungible asset in this note. - /// - /// # Errors - /// - /// Returns an error if the note has no assets or the asset is non-fungible. - pub fn offered_asset_amount(&self) -> Result { - let asset = self.assets.iter().next().ok_or(NoteError::other("No offered asset found"))?; - match asset { - Asset::Fungible(fa) => Ok(fa.amount()), - _ => Err(NoteError::other("Non-fungible offered asset not supported")), - } - } - /// 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) -> Result { + pub fn calculate_offered_for_requested(&self, input_amount: u64) -> u64 { let total_requested = self.storage.requested_asset_amount(); - let total_offered = self.offered_asset_amount()?; + let total_offered = self.offered_asset.amount(); - Ok(Self::calculate_output_amount(total_offered, total_requested, input_amount)) + Self::calculate_output_amount(total_offered, total_requested, input_amount) } // ASSOCIATED FUNCTIONS @@ -426,8 +452,8 @@ impl PswapNote { /// ``` pub fn create_tag( note_type: NoteType, - offered_asset: &Asset, - requested_asset: &Asset, + offered_asset: &FungibleAsset, + requested_asset: &FungibleAsset, ) -> NoteTag { let pswap_root_bytes = Self::script().root().as_bytes(); @@ -455,11 +481,14 @@ impl PswapNote { /// 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, - input_amount: u64, - ) -> u64 { + /// + /// The formula is implemented in two branches to maximize precision: + /// - When `offered > requested`: the ratio `offered/requested` is >= 1, so we compute + /// `(offered * FACTOR / requested) * input / FACTOR` to avoid losing the fractional part. + /// - When `requested >= offered`: the ratio `offered/requested` is < 1, so computing it + /// directly would truncate to zero. Instead we compute the inverse ratio + /// `(requested * FACTOR / offered)` and divide: `(input * FACTOR) / inverse_ratio`. + 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 { @@ -475,31 +504,42 @@ impl PswapNote { } } - /// Builds a payback note that delivers the filled assets to the swap creator. + /// Builds a payback note (P2ID) that delivers the filled assets to the swap creator. /// /// The note inherits its type (public/private) from this PSWAP note and derives a /// deterministic serial number via `hmerge(swap_count + 1, serial_num)`. + /// + /// The attachment carries the fill amount as auxiliary data with + /// `NoteAttachmentScheme::none()`, matching the on-chain MASM behavior. fn create_payback_note( &self, consumer_account_id: AccountId, - payback_asset: Asset, + payback_asset: FungibleAsset, fill_amount: u64, ) -> Result { let payback_note_tag = self.storage.payback_note_tag(); // Derive P2ID serial matching PSWAP.masm - let swap_count_word = - Word::from([Felt::from(self.storage.swap_count + 1), ZERO, ZERO, ZERO]); - let p2id_serial_digest = Hasher::merge(&[swap_count_word, self.serial_number]); - let p2id_serial_num: Word = p2id_serial_digest; + let next_swap_count = self + .storage + .swap_count + .checked_add(1) + .ok_or_else(|| NoteError::other("swap count overflow"))?; + let swap_count_word = Word::from([Felt::from(next_swap_count), ZERO, ZERO, ZERO]); + let p2id_serial_num = Hasher::merge(&[swap_count_word, self.serial_number]); // P2ID recipient targets the creator let recipient = P2idNoteStorage::new(self.storage.creator_account_id).into_recipient(p2id_serial_num); - let attachment_word = Word::from([Felt::new(fill_amount), ZERO, ZERO, ZERO]); + let attachment_word = Word::from([ + Felt::try_from(fill_amount).expect("fill amount should fit in a felt"), + ZERO, + ZERO, + ZERO, + ]); let attachment = NoteAttachment::new_word(NoteAttachmentScheme::none(), attachment_word); - let p2id_assets = NoteAssets::new(vec![payback_asset])?; + let p2id_assets = NoteAssets::new(vec![Asset::Fungible(payback_asset)])?; let p2id_metadata = NoteMetadata::new(consumer_account_id, self.note_type) .with_tag(payback_note_tag) .with_attachment(attachment); @@ -511,50 +551,51 @@ impl PswapNote { /// /// The remainder inherits the original creator, tags, and note type, but has an /// incremented swap count and an updated serial number (`serial[0] + 1`). + /// + /// The attachment carries the total offered amount for the fill as auxiliary data + /// with `NoteAttachmentScheme::none()`, matching the on-chain MASM behavior. fn create_remainder_pswap_note( &self, consumer_account_id: AccountId, - remaining_offered_asset: Asset, - remaining_requested_amount: u64, + remaining_offered_asset: FungibleAsset, + remaining_requested_asset: FungibleAsset, 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 key_word = remaining_requested_asset.to_key_word(); - let value_word = remaining_requested_asset.to_value_word(); - + let next_swap_count = self + .storage + .swap_count + .checked_add(1) + .ok_or_else(|| NoteError::other("swap count overflow"))?; let new_storage = PswapNoteStorage::builder() - .requested_asset_key(key_word) - .requested_asset_value(value_word) + .requested_asset(remaining_requested_asset) .pswap_tag(self.storage.pswap_tag) - .swap_count(self.storage.swap_count + 1) + .swap_count(next_swap_count) .creator_account_id(self.storage.creator_account_id) .build(); // 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[0] + ONE, self.serial_number[1], self.serial_number[2], self.serial_number[3], ]); - let attachment_word = Word::from([Felt::new(offered_amount_for_fill), ZERO, ZERO, ZERO]); + let attachment_word = Word::from([ + Felt::try_from(offered_amount_for_fill) + .expect("offered amount for fill should fit in a felt"), + ZERO, + ZERO, + ZERO, + ]); let attachment = NoteAttachment::new_word(NoteAttachmentScheme::none(), attachment_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, + offered_asset: remaining_offered_asset, attachment, }) } @@ -566,28 +607,23 @@ impl PswapNote { /// 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.assets.iter().next().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::create_tag(pswap.note_type, offered_asset, &requested_asset); + let tag = PswapNote::create_tag( + pswap.note_type, + &pswap.offered_asset, + pswap.storage.requested_asset(), + ); let storage = pswap.storage.with_pswap_tag(tag); let recipient = storage.into_recipient(pswap.serial_number); + let assets = NoteAssets::new(vec![Asset::Fungible(pswap.offered_asset)]) + .expect("single fungible asset should be valid"); + 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()) + Note::new(assets, metadata, recipient) } } @@ -596,14 +632,28 @@ impl TryFrom<&Note> for PswapNote { type Error = NoteError; fn try_from(note: &Note) -> Result { + if note.recipient().script().root() != PswapNote::script_root() { + return Err(NoteError::other("note script root does not match PSWAP script root")); + } + let storage = PswapNoteStorage::try_from(note.recipient().storage().items())?; + if note.assets().num_assets() != 1 { + return Err(NoteError::other("PSWAP note must have exactly one asset")); + } + let offered_asset = match note.assets().iter().next().unwrap() { + Asset::Fungible(fa) => *fa, + Asset::NonFungible(_) => { + return Err(NoteError::other("PSWAP note asset must be fungible")); + }, + }; + Ok(Self { sender: note.metadata().sender(), storage, serial_number: note.recipient().serial_num(), note_type: note.metadata().note_type(), - assets: note.assets().clone(), + offered_asset, attachment: note.metadata().attachment().clone(), }) } @@ -616,6 +666,7 @@ impl TryFrom<&Note> for PswapNote { mod tests { use miden_protocol::account::{AccountId, AccountIdVersion, AccountStorageMode, AccountType}; use miden_protocol::asset::FungibleAsset; + use miden_protocol::crypto::rand::{FeltRng, RandomCoin}; use super::*; @@ -648,19 +699,16 @@ mod tests { 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()); + let offered_asset = FungibleAsset::new(offered_faucet_id, 1000).unwrap(); + let requested_asset = FungibleAsset::new(requested_faucet_id, 500).unwrap(); - use miden_protocol::crypto::rand::{FeltRng, RandomCoin}; let mut rng = RandomCoin::new(Word::default()); let script = PswapNote::script(); assert!(script.root() != Word::default(), "Script root should not be zero"); let storage = PswapNoteStorage::builder() - .requested_asset_key(requested_asset.to_key_word()) - .requested_asset_value(requested_asset.to_value_word()) + .requested_asset(requested_asset) .creator_account_id(creator_id) .build(); let pswap = PswapNote::builder() @@ -668,7 +716,7 @@ mod tests { .storage(storage) .serial_number(rng.draw_word()) .note_type(NoteType::Public) - .assets(NoteAssets::new(vec![offered_asset]).unwrap()) + .offered_asset(offered_asset) .build() .unwrap(); @@ -680,7 +728,10 @@ 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(), + PswapNoteStorage::NUM_STORAGE_ITEMS as u16, + ); } #[test] @@ -712,16 +763,13 @@ mod tests { 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()); + let offered_asset = FungibleAsset::new(offered_faucet_id, 1000).unwrap(); + let requested_asset = FungibleAsset::new(requested_faucet_id, 500).unwrap(); - use miden_protocol::crypto::rand::{FeltRng, RandomCoin}; let mut rng = RandomCoin::new(Word::default()); let storage = PswapNoteStorage::builder() - .requested_asset_key(requested_asset.to_key_word()) - .requested_asset_value(requested_asset.to_value_word()) + .requested_asset(requested_asset) .creator_account_id(creator_id) .build(); let pswap = PswapNote::builder() @@ -729,20 +777,22 @@ mod tests { .storage(storage) .serial_number(rng.draw_word()) .note_type(NoteType::Public) - .assets(NoteAssets::new(vec![offered_asset]).unwrap()) + .offered_asset(offered_asset) .build() .unwrap(); 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,); + assert_eq!( + note.recipient().storage().num_items(), + PswapNoteStorage::NUM_STORAGE_ITEMS as u16, + ); } #[test] @@ -755,30 +805,26 @@ mod tests { 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 offered_asset = FungibleAsset::new( + AccountId::dummy( + offered_faucet_bytes, + AccountIdVersion::Version0, + AccountType::FungibleFaucet, + AccountStorageMode::Public, + ), + 100, + ) + .unwrap(); + let requested_asset = FungibleAsset::new( + AccountId::dummy( + requested_faucet_bytes, + AccountIdVersion::Version0, + AccountType::FungibleFaucet, + AccountStorageMode::Public, + ), + 200, + ) + .unwrap(); let tag = PswapNote::create_tag(NoteType::Public, &offered_asset, &requested_asset); let tag_u32 = u32::from(tag); @@ -848,10 +894,6 @@ mod tests { let parsed = PswapNoteStorage::try_from(storage_items.as_slice()).unwrap(); assert_eq!(parsed.swap_count(), 3); assert_eq!(parsed.creator_account_id(), creator_id); - assert_eq!( - parsed.requested_asset_key(), - Word::from([key_word[0], key_word[1], key_word[2], key_word[3]]) - ); assert_eq!(parsed.requested_asset_amount(), 500); } @@ -871,10 +913,9 @@ mod tests { AccountStorageMode::Public, ); - let requested_asset = Asset::Fungible(FungibleAsset::new(faucet_id, 500).unwrap()); + let requested_asset = FungibleAsset::new(faucet_id, 500).unwrap(); let storage = PswapNoteStorage::builder() - .requested_asset_key(requested_asset.to_key_word()) - .requested_asset_value(requested_asset.to_value_word()) + .requested_asset(requested_asset) .creator_account_id(creator_id) .build(); diff --git a/crates/miden-testing/tests/scripts/pswap.rs b/crates/miden-testing/tests/scripts/pswap.rs index 735a6dcbf9..2abee51df7 100644 --- a/crates/miden-testing/tests/scripts/pswap.rs +++ b/crates/miden-testing/tests/scripts/pswap.rs @@ -1,11 +1,13 @@ use std::collections::BTreeMap; use miden_protocol::account::auth::AuthScheme; +use miden_protocol::account::{Account, AccountStorageMode}; use miden_protocol::asset::{Asset, FungibleAsset}; use miden_protocol::crypto::rand::{FeltRng, RandomCoin}; use miden_protocol::note::{Note, NoteAssets, NoteMetadata, NoteRecipient, NoteStorage, NoteType}; use miden_protocol::transaction::RawOutputNote; -use miden_protocol::{Felt, Word, ZERO}; +use miden_protocol::{Felt, Word, ONE, ZERO}; +use miden_standards::account::wallets::BasicWallet; use miden_standards::note::{PswapNote, PswapNoteStorage}; use miden_testing::{Auth, MockChain}; @@ -35,13 +37,12 @@ async fn pswap_note_full_fill_test() -> anyhow::Result<()> { [FungibleAsset::new(eth_faucet.id(), 25)?.into()], )?; - let offered_asset = Asset::Fungible(FungibleAsset::new(usdc_faucet.id(), 50)?); - let requested_asset = Asset::Fungible(FungibleAsset::new(eth_faucet.id(), 25)?); + let offered_asset = FungibleAsset::new(usdc_faucet.id(), 50)?; + let requested_asset = FungibleAsset::new(eth_faucet.id(), 25)?; let mut rng = RandomCoin::new(Word::default()); let storage = PswapNoteStorage::builder() - .requested_asset_key(requested_asset.to_key_word()) - .requested_asset_value(requested_asset.to_value_word()) + .requested_asset(requested_asset) .creator_account_id(alice.id()) .build(); let pswap_note: Note = PswapNote::builder() @@ -49,7 +50,7 @@ async fn pswap_note_full_fill_test() -> anyhow::Result<()> { .storage(storage) .serial_number(rng.draw_word()) .note_type(NoteType::Public) - .assets(NoteAssets::new(vec![offered_asset])?) + .offered_asset(offered_asset) .build()? .into(); builder.add_output_note(RawOutputNote::Full(pswap_note.clone())); @@ -57,10 +58,11 @@ async fn pswap_note_full_fill_test() -> anyhow::Result<()> { let mut mock_chain = builder.build()?; let mut note_args_map = BTreeMap::new(); - note_args_map.insert(pswap_note.id(), Word::from([Felt::new(25), Felt::new(0), ZERO, ZERO])); + note_args_map.insert(pswap_note.id(), Word::from([Felt::from(25u32), Felt::from(0u32), ZERO, ZERO])); let pswap = PswapNote::try_from(&pswap_note)?; - let (p2id_note, _remainder) = pswap.execute(bob.id(), 25, 0)?; + let (p2id_note, _remainder) = + pswap.execute(bob.id(), Some(FungibleAsset::new(eth_faucet.id(), 25)?), None)?; let tx_context = mock_chain .build_tx_context(bob.id(), &[pswap_note.id()], &[])? @@ -125,14 +127,13 @@ async fn pswap_note_private_full_fill_test() -> anyhow::Result<()> { [FungibleAsset::new(eth_faucet.id(), 25)?.into()], )?; - let offered_asset = Asset::Fungible(FungibleAsset::new(usdc_faucet.id(), 50)?); - let requested_asset = Asset::Fungible(FungibleAsset::new(eth_faucet.id(), 25)?); + let offered_asset = FungibleAsset::new(usdc_faucet.id(), 50)?; + let requested_asset = FungibleAsset::new(eth_faucet.id(), 25)?; let mut rng = RandomCoin::new(Word::default()); // Create a PRIVATE swap note (output notes should also be Private) let storage = PswapNoteStorage::builder() - .requested_asset_key(requested_asset.to_key_word()) - .requested_asset_value(requested_asset.to_value_word()) + .requested_asset(requested_asset) .creator_account_id(alice.id()) .build(); let pswap_note: Note = PswapNote::builder() @@ -140,7 +141,7 @@ async fn pswap_note_private_full_fill_test() -> anyhow::Result<()> { .storage(storage) .serial_number(rng.draw_word()) .note_type(NoteType::Private) - .assets(NoteAssets::new(vec![offered_asset])?) + .offered_asset(offered_asset) .build()? .into(); builder.add_output_note(RawOutputNote::Full(pswap_note.clone())); @@ -148,11 +149,12 @@ async fn pswap_note_private_full_fill_test() -> anyhow::Result<()> { let mut mock_chain = builder.build()?; let mut note_args_map = BTreeMap::new(); - note_args_map.insert(pswap_note.id(), Word::from([Felt::new(25), Felt::new(0), ZERO, ZERO])); + note_args_map.insert(pswap_note.id(), Word::from([Felt::from(25u32), Felt::from(0u32), ZERO, ZERO])); // Expected P2ID note should inherit Private type from swap note let pswap = PswapNote::try_from(&pswap_note)?; - let (p2id_note, _remainder) = pswap.execute(bob.id(), 25, 0)?; + let (p2id_note, _remainder) = + pswap.execute(bob.id(), Some(FungibleAsset::new(eth_faucet.id(), 25)?), None)?; let tx_context = mock_chain .build_tx_context(bob.id(), &[pswap_note.id()], &[])? @@ -213,13 +215,12 @@ async fn pswap_note_partial_fill_test() -> anyhow::Result<()> { [FungibleAsset::new(eth_faucet.id(), 20)?.into()], )?; - let offered_asset = Asset::Fungible(FungibleAsset::new(usdc_faucet.id(), 50)?); - let requested_asset = Asset::Fungible(FungibleAsset::new(eth_faucet.id(), 25)?); + let offered_asset = FungibleAsset::new(usdc_faucet.id(), 50)?; + let requested_asset = FungibleAsset::new(eth_faucet.id(), 25)?; let mut rng = RandomCoin::new(Word::default()); let storage = PswapNoteStorage::builder() - .requested_asset_key(requested_asset.to_key_word()) - .requested_asset_value(requested_asset.to_value_word()) + .requested_asset(requested_asset) .creator_account_id(alice.id()) .build(); let pswap_note: Note = PswapNote::builder() @@ -227,7 +228,7 @@ async fn pswap_note_partial_fill_test() -> anyhow::Result<()> { .storage(storage) .serial_number(rng.draw_word()) .note_type(NoteType::Public) - .assets(NoteAssets::new(vec![offered_asset])?) + .offered_asset(offered_asset) .build()? .into(); builder.add_output_note(RawOutputNote::Full(pswap_note.clone())); @@ -235,10 +236,11 @@ async fn pswap_note_partial_fill_test() -> anyhow::Result<()> { let mut mock_chain = builder.build()?; let mut note_args_map = BTreeMap::new(); - note_args_map.insert(pswap_note.id(), Word::from([Felt::new(20), Felt::new(0), ZERO, ZERO])); + note_args_map.insert(pswap_note.id(), Word::from([Felt::from(20u32), Felt::from(0u32), ZERO, ZERO])); let pswap = PswapNote::try_from(&pswap_note)?; - let (p2id_note, remainder_pswap) = pswap.execute(bob.id(), 20, 0)?; + let (p2id_note, remainder_pswap) = + pswap.execute(bob.id(), Some(FungibleAsset::new(eth_faucet.id(), 20)?), None)?; let remainder_note = Note::from(remainder_pswap.expect("partial fill should produce remainder")); @@ -310,10 +312,9 @@ async fn pswap_note_inflight_cross_swap_test() -> anyhow::Result<()> { let mut rng = RandomCoin::new(Word::default()); // Alice's note: offers 50 USDC, requests 25 ETH - let alice_requested_asset = Asset::Fungible(FungibleAsset::new(eth_faucet.id(), 25)?); + let alice_requested_asset = FungibleAsset::new(eth_faucet.id(), 25)?; let storage = PswapNoteStorage::builder() - .requested_asset_key(alice_requested_asset.to_key_word()) - .requested_asset_value(alice_requested_asset.to_value_word()) + .requested_asset(alice_requested_asset) .creator_account_id(alice.id()) .build(); let alice_pswap_note: Note = PswapNote::builder() @@ -321,19 +322,15 @@ async fn pswap_note_inflight_cross_swap_test() -> anyhow::Result<()> { .storage(storage) .serial_number(rng.draw_word()) .note_type(NoteType::Public) - .assets(NoteAssets::new(vec![Asset::Fungible(FungibleAsset::new( - usdc_faucet.id(), - 50, - )?)])?) + .offered_asset(FungibleAsset::new(usdc_faucet.id(), 50)?) .build()? .into(); builder.add_output_note(RawOutputNote::Full(alice_pswap_note.clone())); // Bob's note: offers 25 ETH, requests 50 USDC - let bob_requested_asset = Asset::Fungible(FungibleAsset::new(usdc_faucet.id(), 50)?); + let bob_requested_asset = FungibleAsset::new(usdc_faucet.id(), 50)?; let storage = PswapNoteStorage::builder() - .requested_asset_key(bob_requested_asset.to_key_word()) - .requested_asset_value(bob_requested_asset.to_value_word()) + .requested_asset(bob_requested_asset) .creator_account_id(bob.id()) .build(); let bob_pswap_note: Note = PswapNote::builder() @@ -341,10 +338,7 @@ async fn pswap_note_inflight_cross_swap_test() -> anyhow::Result<()> { .storage(storage) .serial_number(rng.draw_word()) .note_type(NoteType::Public) - .assets(NoteAssets::new(vec![Asset::Fungible(FungibleAsset::new( - eth_faucet.id(), - 25, - )?)])?) + .offered_asset(FungibleAsset::new(eth_faucet.id(), 25)?) .build()? .into(); builder.add_output_note(RawOutputNote::Full(bob_pswap_note.clone())); @@ -354,16 +348,18 @@ async fn pswap_note_inflight_cross_swap_test() -> anyhow::Result<()> { // Note args: pure inflight (input=0, inflight=full amount) let mut note_args_map = BTreeMap::new(); note_args_map - .insert(alice_pswap_note.id(), Word::from([Felt::new(0), Felt::new(25), ZERO, ZERO])); + .insert(alice_pswap_note.id(), Word::from([Felt::from(0u32), Felt::from(25u32), ZERO, ZERO])); note_args_map - .insert(bob_pswap_note.id(), Word::from([Felt::new(0), Felt::new(50), ZERO, ZERO])); + .insert(bob_pswap_note.id(), Word::from([Felt::from(0u32), Felt::from(50u32), ZERO, ZERO])); // Expected P2ID notes let alice_pswap = PswapNote::try_from(&alice_pswap_note)?; - let (alice_p2id_note, _) = alice_pswap.execute(charlie.id(), 0, 25)?; + let (alice_p2id_note, _) = + alice_pswap.execute(charlie.id(), None, Some(FungibleAsset::new(eth_faucet.id(), 25)?))?; let bob_pswap = PswapNote::try_from(&bob_pswap_note)?; - let (bob_p2id_note, _) = bob_pswap.execute(charlie.id(), 0, 50)?; + let (bob_p2id_note, _) = + bob_pswap.execute(charlie.id(), None, Some(FungibleAsset::new(usdc_faucet.id(), 50)?))?; let tx_context = mock_chain .build_tx_context(charlie.id(), &[alice_pswap_note.id(), bob_pswap_note.id()], &[])? @@ -416,10 +412,9 @@ async fn pswap_note_creator_reclaim_test() -> anyhow::Result<()> { )?; let mut rng = RandomCoin::new(Word::default()); - let requested_asset = Asset::Fungible(FungibleAsset::new(eth_faucet.id(), 25)?); + let requested_asset = FungibleAsset::new(eth_faucet.id(), 25)?; let storage = PswapNoteStorage::builder() - .requested_asset_key(requested_asset.to_key_word()) - .requested_asset_value(requested_asset.to_value_word()) + .requested_asset(requested_asset) .creator_account_id(alice.id()) .build(); let pswap_note: Note = PswapNote::builder() @@ -427,10 +422,7 @@ async fn pswap_note_creator_reclaim_test() -> anyhow::Result<()> { .storage(storage) .serial_number(rng.draw_word()) .note_type(NoteType::Public) - .assets(NoteAssets::new(vec![Asset::Fungible(FungibleAsset::new( - usdc_faucet.id(), - 50, - )?)])?) + .offered_asset(FungibleAsset::new(usdc_faucet.id(), 50)?) .build()? .into(); builder.add_output_note(RawOutputNote::Full(pswap_note.clone())); @@ -477,10 +469,9 @@ async fn pswap_note_invalid_input_test() -> anyhow::Result<()> { )?; let mut rng = RandomCoin::new(Word::default()); - let requested_asset = Asset::Fungible(FungibleAsset::new(eth_faucet.id(), 25)?); + let requested_asset = FungibleAsset::new(eth_faucet.id(), 25)?; let storage = PswapNoteStorage::builder() - .requested_asset_key(requested_asset.to_key_word()) - .requested_asset_value(requested_asset.to_value_word()) + .requested_asset(requested_asset) .creator_account_id(alice.id()) .build(); let pswap_note: Note = PswapNote::builder() @@ -488,10 +479,7 @@ async fn pswap_note_invalid_input_test() -> anyhow::Result<()> { .storage(storage) .serial_number(rng.draw_word()) .note_type(NoteType::Public) - .assets(NoteAssets::new(vec![Asset::Fungible(FungibleAsset::new( - usdc_faucet.id(), - 50, - )?)])?) + .offered_asset(FungibleAsset::new(usdc_faucet.id(), 50)?) .build()? .into(); builder.add_output_note(RawOutputNote::Full(pswap_note.clone())); @@ -499,7 +487,7 @@ async fn pswap_note_invalid_input_test() -> anyhow::Result<()> { // Try to fill with 30 ETH when only 25 is requested - should fail let mut note_args_map = BTreeMap::new(); - note_args_map.insert(pswap_note.id(), Word::from([Felt::new(30), Felt::new(0), ZERO, ZERO])); + note_args_map.insert(pswap_note.id(), Word::from([Felt::from(30u32), Felt::from(0u32), ZERO, ZERO])); let tx_context = mock_chain .build_tx_context(bob.id(), &[pswap_note.id()], &[])? @@ -545,10 +533,9 @@ async fn pswap_note_multiple_partial_fills_test() -> anyhow::Result<()> { )?; let mut rng = RandomCoin::new(Word::default()); - let requested_asset = Asset::Fungible(FungibleAsset::new(eth_faucet.id(), 25)?); + let requested_asset = FungibleAsset::new(eth_faucet.id(), 25)?; let storage = PswapNoteStorage::builder() - .requested_asset_key(requested_asset.to_key_word()) - .requested_asset_value(requested_asset.to_value_word()) + .requested_asset(requested_asset) .creator_account_id(alice.id()) .build(); let pswap_note: Note = PswapNote::builder() @@ -556,26 +543,26 @@ async fn pswap_note_multiple_partial_fills_test() -> anyhow::Result<()> { .storage(storage) .serial_number(rng.draw_word()) .note_type(NoteType::Public) - .assets(NoteAssets::new(vec![Asset::Fungible(FungibleAsset::new( - usdc_faucet.id(), - 50, - )?)])?) + .offered_asset(FungibleAsset::new(usdc_faucet.id(), 50)?) .build()? .into(); builder.add_output_note(RawOutputNote::Full(pswap_note.clone())); let mock_chain = builder.build()?; - let offered_out = PswapNote::calculate_output_amount(50, 25, input_amount); - let mut note_args_map = BTreeMap::new(); note_args_map.insert( pswap_note.id(), - Word::from([Felt::new(input_amount), Felt::new(0), ZERO, ZERO]), + Word::from([Felt::try_from(input_amount).unwrap(), Felt::from(0u32), ZERO, ZERO]), ); let pswap = PswapNote::try_from(&pswap_note)?; - let (p2id_note, remainder_pswap) = pswap.execute(bob.id(), input_amount, 0)?; + let offered_out = pswap.calculate_offered_for_requested(input_amount); + let (p2id_note, remainder_pswap) = pswap.execute( + bob.id(), + Some(FungibleAsset::new(eth_faucet.id(), input_amount)?), + None, + )?; let mut expected_notes = vec![RawOutputNote::Full(p2id_note)]; @@ -612,8 +599,6 @@ 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 = - 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))?; @@ -629,10 +614,9 @@ async fn pswap_note_non_exact_ratio_partial_fill_test() -> anyhow::Result<()> { )?; let mut rng = RandomCoin::new(Word::default()); - let requested_asset = Asset::Fungible(FungibleAsset::new(eth_faucet.id(), requested_total)?); + let requested_asset = FungibleAsset::new(eth_faucet.id(), requested_total)?; let storage = PswapNoteStorage::builder() - .requested_asset_key(requested_asset.to_key_word()) - .requested_asset_value(requested_asset.to_value_word()) + .requested_asset(requested_asset) .creator_account_id(alice.id()) .build(); let pswap_note: Note = PswapNote::builder() @@ -640,10 +624,7 @@ async fn pswap_note_non_exact_ratio_partial_fill_test() -> anyhow::Result<()> { .storage(storage) .serial_number(rng.draw_word()) .note_type(NoteType::Public) - .assets(NoteAssets::new(vec![Asset::Fungible(FungibleAsset::new( - usdc_faucet.id(), - offered_total, - )?)])?) + .offered_asset(FungibleAsset::new(usdc_faucet.id(), offered_total)?) .build()? .into(); builder.add_output_note(RawOutputNote::Full(pswap_note.clone())); @@ -652,10 +633,15 @@ async fn pswap_note_non_exact_ratio_partial_fill_test() -> anyhow::Result<()> { let mut note_args_map = BTreeMap::new(); note_args_map - .insert(pswap_note.id(), Word::from([Felt::new(input_amount), Felt::new(0), ZERO, ZERO])); + .insert(pswap_note.id(), Word::from([Felt::try_from(input_amount).unwrap(), Felt::from(0u32), ZERO, ZERO])); let pswap = PswapNote::try_from(&pswap_note)?; - let (p2id_note, remainder_pswap) = pswap.execute(bob.id(), input_amount, 0)?; + let expected_output = pswap.calculate_offered_for_requested(input_amount); + let (p2id_note, remainder_pswap) = pswap.execute( + bob.id(), + Some(FungibleAsset::new(eth_faucet.id(), input_amount)?), + None, + )?; let remainder = Note::from(remainder_pswap.expect("partial fill should produce remainder")); let tx_context = mock_chain @@ -715,14 +701,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 = - PswapNote::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; @@ -745,10 +725,9 @@ async fn pswap_note_partial_fill_non_integer_ratio_fuzz_test() -> anyhow::Result )?; let mut rng = RandomCoin::new(Word::default()); - let requested_asset = Asset::Fungible(FungibleAsset::new(eth_faucet.id(), *requested_eth)?); + let requested_asset = FungibleAsset::new(eth_faucet.id(), *requested_eth)?; let storage = PswapNoteStorage::builder() - .requested_asset_key(requested_asset.to_key_word()) - .requested_asset_value(requested_asset.to_value_word()) + .requested_asset(requested_asset) .creator_account_id(alice.id()) .build(); let pswap_note: Note = PswapNote::builder() @@ -756,10 +735,7 @@ async fn pswap_note_partial_fill_non_integer_ratio_fuzz_test() -> anyhow::Result .storage(storage) .serial_number(rng.draw_word()) .note_type(NoteType::Public) - .assets(NoteAssets::new(vec![Asset::Fungible(FungibleAsset::new( - usdc_faucet.id(), - *offered_usdc, - )?)])?) + .offered_asset(FungibleAsset::new(usdc_faucet.id(), *offered_usdc)?) .build()? .into(); builder.add_output_note(RawOutputNote::Full(pswap_note.clone())); @@ -768,10 +744,19 @@ async fn pswap_note_partial_fill_non_integer_ratio_fuzz_test() -> anyhow::Result let mut note_args_map = BTreeMap::new(); note_args_map - .insert(pswap_note.id(), Word::from([Felt::new(*fill_eth), Felt::new(0), ZERO, ZERO])); + .insert(pswap_note.id(), Word::from([Felt::try_from(*fill_eth).unwrap(), Felt::from(0u32), ZERO, ZERO])); let pswap = PswapNote::try_from(&pswap_note)?; - let (p2id_note, remainder_pswap) = pswap.execute(bob.id(), *fill_eth, 0)?; + let offered_out = pswap.calculate_offered_for_requested(*fill_eth); + let remaining_offered = offered_usdc - offered_out; + + assert!(offered_out > 0, "Case {}: offered_out must be > 0", i + 1); + assert!(offered_out <= *offered_usdc, "Case {}: offered_out > offered", i + 1); + let (p2id_note, remainder_pswap) = pswap.execute( + bob.id(), + Some(FungibleAsset::new(eth_faucet.id(), *fill_eth)?), + None, + )?; let mut expected_notes = vec![RawOutputNote::Full(p2id_note)]; if remaining_requested > 0 { @@ -844,12 +829,6 @@ async fn pswap_note_chained_partial_fills_non_integer_ratio_test() -> anyhow::Re let mut current_serial = rng.draw_word(); for (current_swap_count, fill_amount) in fills.iter().enumerate() { - let offered_out = PswapNote::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(); @@ -878,17 +857,17 @@ async fn pswap_note_chained_partial_fills_non_integer_ratio_test() -> anyhow::Re )?; // 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 offered_fungible = + FungibleAsset::new(usdc_faucet.id(), current_offered)?; + let requested_fungible = + FungibleAsset::new(eth_faucet.id(), current_requested)?; let pswap_tag = - PswapNote::create_tag(NoteType::Public, &offered_asset, &requested_asset); + PswapNote::create_tag(NoteType::Public, &offered_fungible, &requested_fungible); + let offered_asset = Asset::Fungible(offered_fungible); let storage = PswapNoteStorage::builder() - .requested_asset_key(requested_asset.to_key_word()) - .requested_asset_value(requested_asset.to_value_word()) + .requested_asset(requested_fungible) .pswap_tag(pswap_tag) .swap_count(current_swap_count as u16) .creator_account_id(alice.id()) @@ -907,11 +886,17 @@ async fn pswap_note_chained_partial_fills_non_integer_ratio_test() -> anyhow::Re let mut note_args_map = BTreeMap::new(); note_args_map.insert( pswap_note.id(), - Word::from([Felt::new(*fill_amount), Felt::new(0), ZERO, ZERO]), + Word::from([Felt::try_from(*fill_amount).unwrap(), Felt::from(0u32), ZERO, ZERO]), ); let pswap = PswapNote::try_from(&pswap_note)?; - let (p2id_note, remainder_pswap) = pswap.execute(bob.id(), *fill_amount, 0)?; + let offered_out = pswap.calculate_offered_for_requested(*fill_amount); + let remaining_offered = current_offered - offered_out; + let (p2id_note, remainder_pswap) = pswap.execute( + bob.id(), + Some(FungibleAsset::new(eth_faucet.id(), *fill_amount)?), + None, + )?; let mut expected_notes = vec![RawOutputNote::Full(p2id_note)]; if remaining_requested > 0 { @@ -969,7 +954,7 @@ async fn pswap_note_chained_partial_fills_non_integer_ratio_test() -> anyhow::Re current_requested = remaining_requested; // Remainder serial: [0] + 1 (matching MASM LE orientation) current_serial = Word::from([ - Felt::new(current_serial[0].as_canonical_u64() + 1), + current_serial[0] + ONE, current_serial[1], current_serial[2], current_serial[3], @@ -1012,10 +997,9 @@ fn compare_pswap_create_output_notes_vs_test_helper() { // Create swap note using PswapNote builder let mut rng = RandomCoin::new(Word::default()); - let requested_asset = Asset::Fungible(FungibleAsset::new(eth_faucet.id(), 25).unwrap()); + let requested_asset = FungibleAsset::new(eth_faucet.id(), 25).unwrap(); let storage = PswapNoteStorage::builder() - .requested_asset_key(requested_asset.to_key_word()) - .requested_asset_value(requested_asset.to_value_word()) + .requested_asset(requested_asset) .creator_account_id(alice.id()) .build(); let pswap_note: Note = PswapNote::builder() @@ -1023,12 +1007,7 @@ fn compare_pswap_create_output_notes_vs_test_helper() { .storage(storage) .serial_number(rng.draw_word()) .note_type(NoteType::Public) - .assets( - NoteAssets::new(vec![Asset::Fungible( - FungibleAsset::new(usdc_faucet.id(), 50).unwrap(), - )]) - .unwrap(), - ) + .offered_asset(FungibleAsset::new(usdc_faucet.id(), 50).unwrap()) .build() .unwrap() .into(); @@ -1039,13 +1018,13 @@ fn compare_pswap_create_output_notes_vs_test_helper() { // 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_asset_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(); + let (p2id_note, remainder) = + pswap.execute(bob.id(), Some(FungibleAsset::new(eth_faucet.id(), 25).unwrap()), None).unwrap(); assert!(remainder.is_none(), "Full fill should not produce remainder"); // Verify P2ID note properties @@ -1060,7 +1039,8 @@ fn compare_pswap_create_output_notes_vs_test_helper() { } // Partial fill: should produce P2ID note + remainder - let (p2id_partial, remainder_partial) = pswap.execute(bob.id(), 10, 0).unwrap(); + let (p2id_partial, remainder_partial) = + pswap.execute(bob.id(), Some(FungibleAsset::new(eth_faucet.id(), 10).unwrap()), None).unwrap(); let remainder_pswap = remainder_partial.expect("Partial fill should produce remainder"); assert_eq!(p2id_partial.assets().num_assets(), 1); @@ -1095,10 +1075,9 @@ fn pswap_parse_inputs_roundtrip() { .unwrap(); let mut rng = RandomCoin::new(Word::default()); - let requested_asset = Asset::Fungible(FungibleAsset::new(eth_faucet.id(), 25).unwrap()); + let requested_asset = FungibleAsset::new(eth_faucet.id(), 25).unwrap(); let storage = PswapNoteStorage::builder() - .requested_asset_key(requested_asset.to_key_word()) - .requested_asset_value(requested_asset.to_value_word()) + .requested_asset(requested_asset) .creator_account_id(alice.id()) .build(); let pswap_note: Note = PswapNote::builder() @@ -1106,12 +1085,7 @@ fn pswap_parse_inputs_roundtrip() { .storage(storage) .serial_number(rng.draw_word()) .note_type(NoteType::Public) - .assets( - NoteAssets::new(vec![Asset::Fungible( - FungibleAsset::new(usdc_faucet.id(), 50).unwrap(), - )]) - .unwrap(), - ) + .offered_asset(FungibleAsset::new(usdc_faucet.id(), 50).unwrap()) .build() .unwrap() .into(); @@ -1127,3 +1101,100 @@ fn pswap_parse_inputs_roundtrip() { // Verify requested amount from value word assert_eq!(parsed.requested_asset_amount(), 25, "Requested amount should be 25"); } + +/// Test that a PSWAP note can be consumed by a network account (full fill, no note_args). +/// +/// Alice (local) creates a PSWAP note offering 50 USDC for 25 ETH. A network account with a +/// BasicWallet consumes it. Since no note_args are provided, the script defaults to a full fill. +#[tokio::test] +async fn pswap_note_network_account_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()], + )?; + + // Create a network account with BasicWallet that holds 25 ETH + let seed: [u8; 32] = builder.rng_mut().draw_word().into(); + let network_consumer = builder.add_account_from_builder( + BASIC_AUTH, + Account::builder(seed) + .storage_mode(AccountStorageMode::Network) + .with_component(BasicWallet) + .with_assets([FungibleAsset::new(eth_faucet.id(), 25)?.into()]), + miden_testing::AccountState::Exists, + )?; + + let requested_asset = FungibleAsset::new(eth_faucet.id(), 25)?; + + let mut rng = RandomCoin::new(Word::default()); + let storage = PswapNoteStorage::builder() + .requested_asset(requested_asset) + .creator_account_id(alice.id()) + .build(); + let pswap_note: Note = PswapNote::builder() + .sender(alice.id()) + .storage(storage) + .serial_number(rng.draw_word()) + .note_type(NoteType::Public) + .offered_asset(FungibleAsset::new(usdc_faucet.id(), 50)?) + .build()? + .into(); + builder.add_output_note(RawOutputNote::Full(pswap_note.clone())); + + let mut mock_chain = builder.build()?; + + // No note_args — simulates a network transaction where args default to [0, 0, 0, 0]. + // The PSWAP script defaults to a full fill when both input and inflight are 0. + let pswap = PswapNote::try_from(&pswap_note)?; + let p2id_note = pswap.execute_full_fill_network(network_consumer.id())?; + + let tx_context = mock_chain + .build_tx_context(network_consumer.id(), &[pswap_note.id()], &[])? + .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 for Alice + 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 network consumer'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(()) +} From 7894fffa089d4bf011722c15f638a20f0d0066bc Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Mon, 6 Apr 2026 22:49:07 +0530 Subject: [PATCH 17/66] refactor: pass creator ID via stack to is_consumer_creator and use account_id::is_equal --- .../asm/standards/notes/pswap.masm | 25 ++++++------------- 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/crates/miden-standards/asm/standards/notes/pswap.masm b/crates/miden-standards/asm/standards/notes/pswap.masm index 6cde177530..2d4c4c8fa3 100644 --- a/crates/miden-standards/asm/standards/notes/pswap.masm +++ b/crates/miden-standards/asm/standards/notes/pswap.masm @@ -4,6 +4,7 @@ use miden::protocol::note use miden::standards::wallets::basic->wallet use miden::core::sys use miden::protocol::active_account +use miden::protocol::account_id use miden::core::math::u64 use miden::protocol::asset use miden::standards::notes::p2id @@ -444,29 +445,14 @@ 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: [] +#! Inputs: [creator_id_suffix, creator_id_prefix] #! Outputs: [is_creator] #! proc is_consumer_creator exec.active_account::get_id - # => [acct_id_suffix, acct_id_prefix] - - mem_load.PSWAP_CREATOR_SUFFIX_ITEM mem_load.PSWAP_CREATOR_PREFIX_ITEM - # => [creator_prefix, creator_suffix, acct_id_suffix, acct_id_prefix] - - movup.3 eq - # => [prefix_eq, creator_suffix, acct_id_suffix] + # => [acct_id_suffix, acct_id_prefix, creator_id_suffix, creator_id_prefix] - movdn.2 - # => [creator_suffix, acct_id_suffix, prefix_eq] - - eq - # => [suffix_eq, prefix_eq] - - and + exec.account_id::is_equal # => [is_creator] end @@ -672,6 +658,9 @@ pub proc main exec.extract_note_type # => [] + mem_load.PSWAP_CREATOR_SUFFIX_ITEM mem_load.PSWAP_CREATOR_PREFIX_ITEM swap + # => [creator_id_suffix, creator_id_prefix] + exec.is_consumer_creator # => [is_creator] From d7223de6cc59ff3e105bb23ee3498042311e2b09 Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Tue, 7 Apr 2026 11:40:02 +0530 Subject: [PATCH 18/66] refactor: optimize requested asset storage to 4 felts and add NUM_STORAGE_ITEMS constant Store requested asset as individual felts [enable_callbacks, faucet_suffix, faucet_prefix, amount] instead of full ASSET_KEY + ASSET_VALUE (8 felts). Use create_fungible_asset to reconstruct when needed. Reduces note storage items from 18 to 14. Add NUM_STORAGE_ITEMS constant and execute_pswap doc comment. --- .../asm/standards/notes/pswap.masm | 108 ++++++++--------- crates/miden-standards/src/note/pswap.rs | 112 +++++++++--------- 2 files changed, 104 insertions(+), 116 deletions(-) diff --git a/crates/miden-standards/asm/standards/notes/pswap.masm b/crates/miden-standards/asm/standards/notes/pswap.masm index 2d4c4c8fa3..d00f5c176e 100644 --- a/crates/miden-standards/asm/standards/notes/pswap.masm +++ b/crates/miden-standards/asm/standards/notes/pswap.masm @@ -12,29 +12,32 @@ use miden::standards::notes::p2id # CONSTANTS # ================================================================================================= +const NUM_STORAGE_ITEMS=14 const NOTE_TYPE_MASK=0x03 -const FACTOR=0x000186A0 # 1e5 +const FACTOR=0x000186A0 # 1e5 const MAX_U32=0x0000000100000000 # Memory Addresses # ================================================================================================= # Memory Address Layout: -# - PSWAP Note Storage: addresses 0x0000 - 0x0011 (loaded from note storage) +# - PSWAP Note Storage: addresses 0x0000 - 0x000D (loaded from note storage) # - Price Calculation: addresses 0x0028 - 0x0036 -# - Asset Keys: addresses 0x0038 - 0x003B, 0x003C - 0x003F (word-aligned) +# - Asset Keys: addresses 0x0038 - 0x003B (word-aligned) # - Full Word (word-aligned): addresses 0x0018 - 0x001F -# PSWAP Note Storage (18 items loaded at address 0) -# REQUESTED_ASSET_WORD_ITEM is the base address of an 8-cell block: -# addresses 0x0000-0x0003 = ASSET_KEY, 0x0004-0x0007 = ASSET_VALUE -const REQUESTED_ASSET_WORD_ITEM = 0x0000 -const REQUESTED_ASSET_VALUE_ITEM = 0x0004 -const PSWAP_TAG_ITEM = 0x0008 -const P2ID_TAG_ITEM = 0x0009 -const PSWAP_COUNT_ITEM = 0x000C -const PSWAP_CREATOR_PREFIX_ITEM = 0x0010 -const PSWAP_CREATOR_SUFFIX_ITEM = 0x0011 +# PSWAP Note Storage (14 items loaded at address 0) +# Requested asset is stored as individual felts (fungible assets only): +# [enable_callbacks, faucet_id_suffix, faucet_id_prefix, requested_amount] +const REQUESTED_ENABLE_CALLBACKS_ITEM = 0x0000 +const REQUESTED_FAUCET_SUFFIX_ITEM = 0x0001 +const REQUESTED_FAUCET_PREFIX_ITEM = 0x0002 +const REQUESTED_AMOUNT_ITEM = 0x0003 +const PSWAP_TAG_ITEM = 0x0004 +const P2ID_TAG_ITEM = 0x0005 +const PSWAP_COUNT_ITEM = 0x0008 +const PSWAP_CREATOR_PREFIX_ITEM = 0x000C +const PSWAP_CREATOR_SUFFIX_ITEM = 0x000D # Memory Addresses for Price Calculation Procedure const AMT_OFFERED = 0x0028 @@ -51,7 +54,6 @@ const AMT_OFFERED_OUT_INFLIGHT = 0x0036 # Asset Key Memory Addresses (word-aligned, 4 cells each) # ASSET_KEY = [asset_id_suffix, asset_id_prefix, faucet_id_suffix_and_metadata, faucet_id_prefix] const OFFERED_ASSET_KEY = 0x0038 -const REQUESTED_ASSET_KEY = 0x003C # Full Word Memory Addresses # Asset storage (8 cells each, word-aligned) @@ -70,8 +72,8 @@ const P2ID_RECIPIENT_PREFIX = 0x0FA1 # ERRORS # ================================================================================================= -# PSWAP script expects exactly 18 note storage items -const ERR_PSWAP_WRONG_NUMBER_OF_INPUTS="PSWAP script expects exactly 18 note storage items" +# PSWAP script expects exactly 14 note storage items +const ERR_PSWAP_WRONG_NUMBER_OF_INPUTS="PSWAP script expects exactly 14 note storage items" # PSWAP script requires exactly one note asset const ERR_PSWAP_WRONG_NUMBER_OF_ASSETS="PSWAP script requires exactly one note asset" @@ -332,12 +334,13 @@ proc create_p2id_note mem_load.P2ID_NOTE_IDX # => [note_idx, pad(7)] - # Build ASSET_VALUE = [amount, 0, 0, 0] (amount on top) - push.0.0.0 mem_load.AMT_REQUESTED_IN - # => [amount, 0, 0, 0, note_idx, pad(7)] - - # Load stored ASSET_KEY - padw mem_loadw_le.REQUESTED_ASSET_KEY + # Create requested asset with input amount using create_fungible_asset + # Stack order: [enable_callbacks, faucet_id_suffix, faucet_id_prefix, amount] + mem_load.AMT_REQUESTED_IN + mem_load.REQUESTED_FAUCET_PREFIX_ITEM + mem_load.REQUESTED_FAUCET_SUFFIX_ITEM + mem_load.REQUESTED_ENABLE_CALLBACKS_ITEM + exec.asset::create_fungible_asset # => [ASSET_KEY, ASSET_VALUE, note_idx, pad(7)] call.wallet::move_asset_to_note @@ -358,12 +361,13 @@ proc create_p2id_note mem_load.P2ID_NOTE_IDX # => [note_idx] - # Build ASSET_VALUE = [amount, 0, 0, 0] (amount on top) - push.0.0.0 mem_load.AMT_REQUESTED_INFLIGHT - # => [amount, 0, 0, 0, note_idx] - - # Load stored ASSET_KEY - padw mem_loadw_le.REQUESTED_ASSET_KEY + # Create requested asset with inflight amount using create_fungible_asset + # Stack order: [enable_callbacks, faucet_id_suffix, faucet_id_prefix, amount] + mem_load.AMT_REQUESTED_INFLIGHT + mem_load.REQUESTED_FAUCET_PREFIX_ITEM + mem_load.REQUESTED_FAUCET_SUFFIX_ITEM + mem_load.REQUESTED_ENABLE_CALLBACKS_ITEM + exec.asset::create_fungible_asset # => [ASSET_KEY, ASSET_VALUE, note_idx] exec.output_note::add_asset @@ -388,7 +392,7 @@ end #! proc create_remainder_note # Update note storage with new requested amount - mem_store.REQUESTED_ASSET_VALUE_ITEM + mem_store.REQUESTED_AMOUNT_ITEM # => [] # Build PSWAP remainder recipient using the same script as the active note @@ -399,9 +403,9 @@ proc create_remainder_note 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] + # Build recipient from all note storage items (now with updated requested amount) + push.NUM_STORAGE_ITEMS.0 + # => [storage_ptr=0, num_storage_items, SERIAL_NUM', SCRIPT_ROOT] exec.note::build_recipient # => [RECIPIENT_SWAPP] @@ -484,18 +488,14 @@ proc handle_reclaim # => [] 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) -# - +#! Executes the partially-fillable swap. +#! +#! Sends offered tokens to consumer, requested tokens to creator via P2ID, +#! and creates a remainder note if partially filled. +#! +#! Inputs: [] +#! Outputs: [] +#! proc execute_pswap # Load note assets to OFFERED_ASSET_WORD push.OFFERED_ASSET_WORD exec.active_note::get_assets @@ -524,21 +524,9 @@ proc execute_pswap dropw # => [] - # Load requested asset from note storage (ASSET_KEY + ASSET_VALUE, 8 cells) - push.REQUESTED_ASSET_WORD_ITEM exec.asset::load - # => [ASSET_KEY, ASSET_VALUE] - - # Extract requested amount - exec.asset::fungible_to_amount - # => [amount, ASSET_KEY, ASSET_VALUE] - + # Read requested amount directly from note storage + mem_load.REQUESTED_AMOUNT_ITEM mem_store.AMT_REQUESTED - # => [ASSET_KEY, ASSET_VALUE] - - # Store requested ASSET_KEY (preserves faucet ID + callback metadata) - mem_storew_le.REQUESTED_ASSET_KEY dropw - # => [ASSET_VALUE] - dropw # => [] # If both input and inflight are 0, default to a full fill (input = requested). @@ -644,11 +632,11 @@ pub proc main drop drop # => [] - # Load all 18 note storage items to memory starting at address 0 + # Load all 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 + eq.NUM_STORAGE_ITEMS assert.err=ERR_PSWAP_WRONG_NUMBER_OF_INPUTS # => [storage_ptr] drop diff --git a/crates/miden-standards/src/note/pswap.rs b/crates/miden-standards/src/note/pswap.rs index 2db75ae2ba..1b1577a83b 100644 --- a/crates/miden-standards/src/note/pswap.rs +++ b/crates/miden-standards/src/note/pswap.rs @@ -2,7 +2,7 @@ use alloc::vec; use miden_protocol::account::AccountId; use miden_protocol::assembly::Path; -use miden_protocol::asset::{Asset, FungibleAsset}; +use miden_protocol::asset::{Asset, AssetCallbackFlag, FungibleAsset}; use miden_protocol::errors::NoteError; use miden_protocol::note::{ Note, NoteAssets, NoteAttachment, NoteAttachmentScheme, NoteMetadata, NoteRecipient, @@ -33,18 +33,20 @@ static PSWAP_SCRIPT: LazyLock = LazyLock::new(|| { /// Canonical storage representation for a PSWAP note. /// -/// Maps to the 18-element [`NoteStorage`] layout consumed by the on-chain MASM script: +/// Maps to the 14-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]` | Payback note 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) | +/// | `[0]` | Requested asset enable_callbacks flag | +/// | `[1]` | Requested asset faucet ID suffix | +/// | `[2]` | Requested asset faucet ID prefix | +/// | `[3]` | Requested asset amount | +/// | `[4]` | PSWAP note tag | +/// | `[5]` | Payback note routing tag (targets the creator) | +/// | `[6-7]` | Reserved (zero) | +/// | `[8]` | Swap count (incremented on each partial fill) | +/// | `[9-11]` | Reserved (zero) | +/// | `[12-13]` | Creator account ID (prefix, suffix) | #[derive(Debug, Clone, PartialEq, Eq, bon::Builder)] pub struct PswapNoteStorage { requested_asset: FungibleAsset, @@ -63,7 +65,7 @@ impl PswapNoteStorage { // -------------------------------------------------------------------------------------------- /// Expected number of storage items for the PSWAP note. - pub const NUM_STORAGE_ITEMS: usize = 18; + pub const NUM_STORAGE_ITEMS: usize = 14; /// Consumes the storage and returns a PSWAP [`NoteRecipient`] with the provided serial number. pub fn into_recipient(self, serial_num: Word) -> NoteRecipient { @@ -118,36 +120,28 @@ impl PswapNoteStorage { } } -/// Serializes [`PswapNoteStorage`] into an 18-element [`NoteStorage`]. +/// Serializes [`PswapNoteStorage`] into a 14-element [`NoteStorage`]. impl From for NoteStorage { fn from(storage: PswapNoteStorage) -> Self { - let asset = Asset::Fungible(storage.requested_asset); - let key_word = asset.to_key_word(); - let value_word = asset.to_value_word(); - let storage_items = 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] + // Requested asset (individual felts) [0-3] + Felt::from(storage.requested_asset.callbacks().as_u8()), + storage.requested_asset.faucet_id().suffix(), + storage.requested_asset.faucet_id().prefix().as_felt(), + Felt::try_from(storage.requested_asset.amount()) + .expect("asset amount should fit in a felt"), + // Tags [4-5] Felt::from(storage.pswap_tag), Felt::from(storage.payback_note_tag()), - // Padding [10-11] + // Padding [6-7] ZERO, ZERO, - // Swap count [12-15] + // Swap count [8-11] Felt::from(storage.swap_count), ZERO, ZERO, ZERO, - // Creator ID [16-17] + // Creator ID [12-13] storage.creator_account_id.prefix().as_felt(), storage.creator_account_id.suffix(), ]; @@ -156,7 +150,7 @@ impl From for NoteStorage { } } -/// Deserializes [`PswapNoteStorage`] from a slice of exactly 18 [`Felt`]s. +/// Deserializes [`PswapNoteStorage`] from a slice of exactly 14 [`Felt`]s. impl TryFrom<&[Felt]> for PswapNoteStorage { type Error = NoteError; @@ -168,26 +162,38 @@ impl TryFrom<&[Felt]> for PswapNoteStorage { }); } - let requested_asset_key = - Word::from([note_storage[0], note_storage[1], note_storage[2], note_storage[3]]); - let requested_asset_value = - Word::from([note_storage[4], note_storage[5], note_storage[6], note_storage[7]]); - let requested_asset = - FungibleAsset::from_key_value_words(requested_asset_key, requested_asset_value) - .map_err(|e| { - NoteError::other_with_source("failed to parse requested asset from storage", e) - })?; + // Reconstruct requested asset from individual felts: + // [0] = enable_callbacks, [1] = faucet_id_suffix, [2] = faucet_id_prefix, [3] = amount + let callbacks = AssetCallbackFlag::try_from( + u8::try_from(note_storage[0].as_canonical_u64()) + .map_err(|_| NoteError::other("enable_callbacks exceeds u8"))?, + ) + .map_err(|e| { + NoteError::other_with_source("failed to parse asset callback flag", e) + })?; + + let faucet_id = + AccountId::try_from_elements(note_storage[1], note_storage[2]).map_err(|e| { + NoteError::other_with_source("failed to parse requested faucet ID", e) + })?; + + let amount = note_storage[3].as_canonical_u64(); + let requested_asset = FungibleAsset::new(faucet_id, amount) + .map_err(|e| { + NoteError::other_with_source("failed to create requested asset", e) + })? + .with_callbacks(callbacks); let pswap_tag = NoteTag::new( - u32::try_from(note_storage[8].as_canonical_u64()) + u32::try_from(note_storage[4].as_canonical_u64()) .map_err(|_| NoteError::other("pswap_tag exceeds u32"))?, ); - let swap_count: u16 = note_storage[12] + let swap_count: u16 = note_storage[8] .as_canonical_u64() .try_into() .map_err(|_| NoteError::other("swap_count exceeds u16"))?; - let creator_account_id = AccountId::try_from_elements(note_storage[17], note_storage[16]) + let creator_account_id = AccountId::try_from_elements(note_storage[13], note_storage[12]) .map_err(|e| { NoteError::other_with_source("failed to parse creator account ID", e) })?; @@ -866,21 +872,15 @@ mod tests { 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 requested_asset = FungibleAsset::new(faucet_id, 500).unwrap(); let storage_items = 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::from(0xc0000000u32), // pswap_tag - Felt::from(0x80000001u32), // payback_note_tag + Felt::from(requested_asset.callbacks().as_u8()), // enable_callbacks + requested_asset.faucet_id().suffix(), // faucet_id_suffix + requested_asset.faucet_id().prefix().as_felt(), // faucet_id_prefix + Felt::try_from(requested_asset.amount()).unwrap(), // amount + Felt::from(0xc0000000u32), // pswap_tag + Felt::from(0x80000001u32), // payback_note_tag ZERO, ZERO, Felt::from(3u16), // swap_count From 840dca302c0c088377318d40c07267886a203e7d Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Tue, 7 Apr 2026 14:55:08 +0530 Subject: [PATCH 19/66] refactor: use p2id::new, pass inputs via stack, unify serial derivation - Replace manual recipient building with p2id::new in create_p2id_note - Pass all inputs via stack, store intermediates in @locals - Remove build_p2id_recipient_hash, P2ID_RECIPIENT_*, P2ID_NOTE_IDX - Payback serial: serial[0] + 1, remainder serial: serial[3] + 1 - Update Rust side to match new serial derivation, remove Hasher import --- .../asm/standards/notes/pswap.masm | 167 ++++++------------ crates/miden-standards/src/note/pswap.rs | 28 +-- 2 files changed, 72 insertions(+), 123 deletions(-) diff --git a/crates/miden-standards/asm/standards/notes/pswap.masm b/crates/miden-standards/asm/standards/notes/pswap.masm index d00f5c176e..6bae4e4b47 100644 --- a/crates/miden-standards/asm/standards/notes/pswap.masm +++ b/crates/miden-standards/asm/standards/notes/pswap.masm @@ -60,14 +60,9 @@ const OFFERED_ASSET_KEY = 0x0038 const OFFERED_ASSET_WORD = 0x0018 # Note indices and type -const P2ID_NOTE_IDX = 0x007C const PSWAP_NOTE_IDX = 0x0080 const NOTE_TYPE = 0x0084 -# P2ID recipient storage (creator account ID written here for build_recipient) -# Layout matches P2ID note storage: [suffix, prefix] -const P2ID_RECIPIENT_SUFFIX = 0x0FA0 -const P2ID_RECIPIENT_PREFIX = 0x0FA1 # ERRORS # ================================================================================================= @@ -230,116 +225,64 @@ proc extract_note_type # => [] end -# HASHING PROCEDURES -# ================================================================================================= - -#! Builds the P2ID recipient hash for the swap creator. -#! -#! Loads the creator's account ID from note storage (PSWAP_CREATOR_SUFFIX/PREFIX_ITEM), -#! 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] for P2ID recipient hashing - mem_load.PSWAP_CREATOR_SUFFIX_ITEM mem_store.P2ID_RECIPIENT_SUFFIX - mem_load.PSWAP_CREATOR_PREFIX_ITEM mem_store.P2ID_RECIPIENT_PREFIX - # => [SERIAL_NUM, SCRIPT_ROOT] - - # note::build_recipient: [storage_ptr, num_storage_items, SERIAL_NUM, SCRIPT_ROOT] => [RECIPIENT] - push.2.P2ID_RECIPIENT_SUFFIX - # => [storage_ptr, 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). +#! Derives a unique serial number by incrementing the least significant element +#! of the provided serial, creates the output note using p2id::new, sets the +#! attachment, and adds the requested assets (from vault and/or inflight). #! -#! Inputs: [] +#! Inputs: [creator_suffix, creator_prefix, tag, note_type, SERIAL_NUM, +#! enable_callbacks, faucet_suffix, faucet_prefix, amt_in, amt_inflight] #! Outputs: [] #! +@locals(6) +# loc.0 = note_idx +# loc.1 = enable_callbacks +# loc.2 = faucet_suffix +# loc.3 = faucet_prefix +# loc.4 = amt_in +# loc.5 = amt_inflight proc create_p2id_note - # Get P2ID script root at compile time via procref - procref.p2id::main - # => [P2ID_SCRIPT_ROOT] + # Derive P2ID serial: increment least significant element + movup.4 add.1 movdn.4 + # => [creator_suffix, creator_prefix, tag, note_type, P2ID_SERIAL_NUM, + # enable_callbacks, faucet_suffix, faucet_prefix, amt_in, amt_inflight] - # Increment swap count (ensures unique serial per P2ID note in chained fills) - mem_load.PSWAP_COUNT_ITEM add.1 mem_store.PSWAP_COUNT_ITEM - # => [P2ID_SCRIPT_ROOT] + exec.p2id::new + # => [note_idx, enable_callbacks, faucet_suffix, faucet_prefix, amt_in, amt_inflight] - # Load swap count word from memory - padw mem_loadw_le.PSWAP_COUNT_ITEM - # => [SWAP_COUNT_WORD, P2ID_SCRIPT_ROOT] - - # Get serial number from active note - exec.active_note::get_serial_number - # => [SERIAL_NUM, SWAP_COUNT_WORD, P2ID_SCRIPT_ROOT] - - # 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] - - # Build P2ID recipient - exec.build_p2id_recipient_hash - # => [P2ID_RECIPIENT] - - # Create output note (note_type inherited from active note metadata) - mem_load.NOTE_TYPE - # => [note_type, P2ID_RECIPIENT] - - mem_load.P2ID_TAG_ITEM - # => [tag, note_type, RECIPIENT] - - exec.output_note::create - # => [note_idx] - - mem_store.P2ID_NOTE_IDX + loc_store.0 loc_store.1 loc_store.2 loc_store.3 loc_store.4 loc_store.5 # => [] - # Set attachment: aux = input_amount + inflight_amount (total fill) + # Set attachment: aux = amt_in + amt_inflight # attachment_scheme = 0 (NoteAttachmentScheme::none) - # See: output_note::set_word_attachment in miden-protocol/asm/protocol/output_note.masm - mem_load.AMT_REQUESTED_IN mem_load.AMT_REQUESTED_INFLIGHT add + loc_load.4 loc_load.5 add + # => [total_fill] push.0.0.0 - # => [0, 0, 0, aux] - - push.0 mem_load.P2ID_NOTE_IDX - # => [note_idx, attachment_scheme=0, ATTACHMENT] + # => [0, 0, 0, total_fill] + push.0 loc_load.0 + # => [note_idx, attachment_scheme=0, 0, 0, 0, total_fill] exec.output_note::set_word_attachment # => [] # Move input_amount from consumer's vault to P2ID note (if > 0) - mem_load.AMT_REQUESTED_IN dup push.0 neq - # => [amt != 0, amt] + loc_load.4 push.0 gt + # => [amt > 0] 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 + padw push.0.0.0 loc_load.0 # => [note_idx, pad(7)] - # Create requested asset with input amount using create_fungible_asset - # Stack order: [enable_callbacks, faucet_id_suffix, faucet_id_prefix, amount] - mem_load.AMT_REQUESTED_IN - mem_load.REQUESTED_FAUCET_PREFIX_ITEM - mem_load.REQUESTED_FAUCET_SUFFIX_ITEM - mem_load.REQUESTED_ENABLE_CALLBACKS_ITEM + push.0.0.0 loc_load.4 + # => [amt_in, 0, 0, 0, note_idx, pad(7)] + + loc_load.3 loc_load.2 loc_load.1 + # => [enable_cb, faucet_suffix, faucet_prefix, amt_in, 0, 0, 0, note_idx, pad(7)] + exec.asset::create_fungible_asset # => [ASSET_KEY, ASSET_VALUE, note_idx, pad(7)] @@ -348,32 +291,23 @@ proc create_p2id_note dropw dropw dropw dropw # => [] - else - drop end # Add inflight_amount directly to P2ID note (no vault debit, if > 0) - mem_load.AMT_REQUESTED_INFLIGHT dup push.0 neq - # => [amt != 0, amt] + loc_load.5 push.0 gt + # => [amt > 0] if.true - drop - - mem_load.P2ID_NOTE_IDX + loc_load.0 # => [note_idx] - # Create requested asset with inflight amount using create_fungible_asset - # Stack order: [enable_callbacks, faucet_id_suffix, faucet_id_prefix, amount] - mem_load.AMT_REQUESTED_INFLIGHT - mem_load.REQUESTED_FAUCET_PREFIX_ITEM - mem_load.REQUESTED_FAUCET_SUFFIX_ITEM - mem_load.REQUESTED_ENABLE_CALLBACKS_ITEM + loc_load.5 loc_load.3 loc_load.2 loc_load.1 + # => [enable_cb, faucet_suffix, faucet_prefix, amt_inflight, note_idx] + exec.asset::create_fungible_asset # => [ASSET_KEY, ASSET_VALUE, note_idx] exec.output_note::add_asset # => [] - else - drop end end @@ -399,9 +333,11 @@ proc create_remainder_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] + # Derive remainder serial: increment most significant element of active note's serial + exec.active_note::get_serial_number + # => [s0, s1, s2, s3, SCRIPT_ROOT] + movup.3 add.1 movdn.3 + # => [s0, s1, s2, s3+1, SCRIPT_ROOT] # Build recipient from all note storage items (now with updated requested amount) push.NUM_STORAGE_ITEMS.0 @@ -580,6 +516,19 @@ proc execute_pswap # => [] # Create P2ID note for creator + mem_load.AMT_REQUESTED_INFLIGHT + mem_load.AMT_REQUESTED_IN + mem_load.REQUESTED_FAUCET_PREFIX_ITEM + mem_load.REQUESTED_FAUCET_SUFFIX_ITEM + mem_load.REQUESTED_ENABLE_CALLBACKS_ITEM + exec.active_note::get_serial_number + mem_load.NOTE_TYPE + mem_load.P2ID_TAG_ITEM + mem_load.PSWAP_CREATOR_PREFIX_ITEM + mem_load.PSWAP_CREATOR_SUFFIX_ITEM + # => [creator_suffix, creator_prefix, tag, note_type, SERIAL_NUM, + # enable_callbacks, faucet_suffix, faucet_prefix, amt_in, amt_inflight] + exec.create_p2id_note # => [] diff --git a/crates/miden-standards/src/note/pswap.rs b/crates/miden-standards/src/note/pswap.rs index 1b1577a83b..f2d0c10576 100644 --- a/crates/miden-standards/src/note/pswap.rs +++ b/crates/miden-standards/src/note/pswap.rs @@ -9,7 +9,7 @@ use miden_protocol::note::{ NoteScript, NoteStorage, NoteTag, NoteType, }; use miden_protocol::utils::sync::LazyLock; -use miden_protocol::{Felt, Hasher, ONE, Word, ZERO}; +use miden_protocol::{Felt, ONE, Word, ZERO}; use crate::StandardsLib; use crate::note::P2idNoteStorage; @@ -513,7 +513,8 @@ impl PswapNote { /// Builds a payback note (P2ID) that delivers the filled assets to the swap creator. /// /// The note inherits its type (public/private) from this PSWAP note and derives a - /// deterministic serial number via `hmerge(swap_count + 1, serial_num)`. + /// deterministic serial number by incrementing the least significant element of the + /// serial number (`serial[0] + 1`). /// /// The attachment carries the fill amount as auxiliary data with /// `NoteAttachmentScheme::none()`, matching the on-chain MASM behavior. @@ -524,14 +525,13 @@ impl PswapNote { fill_amount: u64, ) -> Result { let payback_note_tag = self.storage.payback_note_tag(); - // Derive P2ID serial matching PSWAP.masm - let next_swap_count = self - .storage - .swap_count - .checked_add(1) - .ok_or_else(|| NoteError::other("swap count overflow"))?; - let swap_count_word = Word::from([Felt::from(next_swap_count), ZERO, ZERO, ZERO]); - let p2id_serial_num = Hasher::merge(&[swap_count_word, self.serial_number]); + // Derive P2ID serial: increment least significant element (matching MASM add.1) + let p2id_serial_num = Word::from([ + self.serial_number[0] + ONE, + self.serial_number[1], + self.serial_number[2], + self.serial_number[3], + ]); // P2ID recipient targets the creator let recipient = @@ -556,7 +556,7 @@ impl PswapNote { /// Builds a remainder PSWAP note carrying the unfilled portion of the swap. /// /// The remainder inherits the original creator, tags, and note type, but has an - /// incremented swap count and an updated serial number (`serial[0] + 1`). + /// incremented swap count and an updated serial number (`serial[3] + 1`). /// /// The attachment carries the total offered amount for the fill as auxiliary data /// with `NoteAttachmentScheme::none()`, matching the on-chain MASM behavior. @@ -579,12 +579,12 @@ impl PswapNote { .creator_account_id(self.storage.creator_account_id) .build(); - // Remainder serial: increment top element (matching MASM add.1 on Word[0]) + // Remainder serial: increment most significant element (matching MASM movup.3 add.1 movdn.3) let remainder_serial_num = Word::from([ - self.serial_number[0] + ONE, + self.serial_number[0], self.serial_number[1], self.serial_number[2], - self.serial_number[3], + self.serial_number[3] + ONE, ]); let attachment_word = Word::from([ From 9c185a6287bcba28cd441e065c508cada5cc3454 Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Tue, 7 Apr 2026 15:48:02 +0530 Subject: [PATCH 20/66] refactor: pass all inputs via stack to create_remainder_note, eliminate global memory reads - create_remainder_note takes 8 stack inputs, stores to @locals(6) - Extract offered faucet info from ASSET_KEY in execute_pswap - Use create_fungible_asset for consumer receive_asset and remainder asset - Remove OFFERED_ASSET_KEY, PSWAP_NOTE_IDX constants --- .../asm/standards/notes/pswap.masm | 128 ++++++++++++------ 1 file changed, 85 insertions(+), 43 deletions(-) diff --git a/crates/miden-standards/asm/standards/notes/pswap.masm b/crates/miden-standards/asm/standards/notes/pswap.masm index 6bae4e4b47..ffc73910ee 100644 --- a/crates/miden-standards/asm/standards/notes/pswap.masm +++ b/crates/miden-standards/asm/standards/notes/pswap.masm @@ -51,16 +51,17 @@ const AMT_REQUESTED_INFLIGHT = 0x0033 const AMT_OFFERED_OUT_INPUT = 0x0034 const AMT_OFFERED_OUT_INFLIGHT = 0x0036 -# Asset Key Memory Addresses (word-aligned, 4 cells each) -# ASSET_KEY = [asset_id_suffix, asset_id_prefix, faucet_id_suffix_and_metadata, faucet_id_prefix] -const OFFERED_ASSET_KEY = 0x0038 +# Offered asset faucet info (extracted from ASSET_KEY) +const OFFERED_ENABLE_CB = 0x0040 +const OFFERED_FAUCET_SUFFIX = 0x0041 +const OFFERED_FAUCET_PREFIX = 0x0042 + # Full Word Memory Addresses # Asset storage (8 cells each, word-aligned) const OFFERED_ASSET_WORD = 0x0018 # Note indices and type -const PSWAP_NOTE_IDX = 0x0080 const NOTE_TYPE = 0x0084 @@ -318,65 +319,82 @@ end #! #! 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. +#! most significant element of the active note's serial number), creates the output +#! note, sets the attachment, and adds the remaining offered asset. #! -#! Inputs: [remaining_requested] +#! Inputs: [offered_faucet_prefix, offered_faucet_suffix, offered_enable_cb, +#! amt_offered_out, amt_offered, +#! remaining_requested, note_type, tag] #! Outputs: [] #! +@locals(6) +# loc.0 = note_idx +# loc.1 = offered_faucet_prefix +# loc.2 = offered_faucet_suffix +# loc.3 = offered_enable_cb +# loc.4 = amt_offered_out +# loc.5 = amt_offered proc create_remainder_note - # Update note storage with new requested amount - mem_store.REQUESTED_AMOUNT_ITEM - # => [] + # => [offered_faucet_prefix, offered_faucet_suffix, offered_enable_cb, + # amt_offered_out, amt_offered, + # remaining_requested, note_type, tag] + + # Store offered asset info to locals (top element stored first) + loc_store.1 loc_store.2 loc_store.3 loc_store.4 loc_store.5 + # => [remaining_requested, note_type, tag] + + # Update note storage with new requested amount (needed by build_recipient) + movdn.2 + # => [note_type, tag, remaining_requested] + movup.2 mem_store.REQUESTED_AMOUNT_ITEM + # => [note_type, tag] # Build PSWAP remainder recipient using the same script as the active note exec.active_note::get_script_root - # => [SCRIPT_ROOT] + # => [SCRIPT_ROOT, note_type, tag] - # Derive remainder serial: increment most significant element of active note's serial + # Derive remainder serial: increment most significant element exec.active_note::get_serial_number - # => [s0, s1, s2, s3, SCRIPT_ROOT] + # => [s0, s1, s2, s3, SCRIPT_ROOT, note_type, tag] movup.3 add.1 movdn.3 - # => [s0, s1, s2, s3+1, SCRIPT_ROOT] + # => [s0, s1, s2, s3+1, SCRIPT_ROOT, note_type, tag] # Build recipient from all note storage items (now with updated requested amount) push.NUM_STORAGE_ITEMS.0 - # => [storage_ptr=0, num_storage_items, SERIAL_NUM', SCRIPT_ROOT] + # => [storage_ptr=0, num_storage_items, SERIAL_NUM', SCRIPT_ROOT, note_type, tag] exec.note::build_recipient - # => [RECIPIENT_SWAPP] + # => [RECIPIENT, note_type, tag] - mem_load.NOTE_TYPE - mem_load.PSWAP_TAG_ITEM + movup.4 movup.5 + # => [tag, note_type, RECIPIENT] exec.output_note::create # => [note_idx] - mem_store.PSWAP_NOTE_IDX + loc_store.0 # => [] - # Set attachment: aux = total offered_out amount - mem_load.AMT_OFFERED_OUT push.0.0.0 - # => [0, 0, 0, aux] - - push.0 mem_load.PSWAP_NOTE_IDX - # => [note_idx, attachment_scheme, ATTACHMENT] + # Set attachment: aux = amt_offered_out + loc_load.4 push.0.0.0 + # => [0, 0, 0, amt_offered_out] + push.0 loc_load.0 + # => [note_idx, attachment_scheme=0, ATTACHMENT] exec.output_note::set_word_attachment # => [] - # Add remaining offered asset to remainder note - # remainder_amount = total_offered - offered_out - mem_load.PSWAP_NOTE_IDX + # Add remaining offered asset: remainder_amount = amt_offered - amt_offered_out + loc_load.0 # => [note_idx] - # Build ASSET_VALUE = [amount, 0, 0, 0] (amount on top) - push.0.0.0 - mem_load.AMT_OFFERED mem_load.AMT_OFFERED_OUT sub - # => [remainder_amount, 0, 0, 0, note_idx] + loc_load.5 loc_load.4 sub + # => [remainder_amount, note_idx] + + loc_load.1 loc_load.2 loc_load.3 + # => [offered_enable_cb, offered_faucet_suffix, offered_faucet_prefix, remainder_amount, note_idx] - # Load stored ASSET_KEY - padw mem_loadw_le.OFFERED_ASSET_KEY + exec.asset::create_fungible_asset # => [ASSET_KEY, ASSET_VALUE, note_idx] exec.output_note::add_asset @@ -454,8 +472,12 @@ proc execute_pswap mem_store.AMT_OFFERED # => [ASSET_KEY, ASSET_VALUE] - # Store offered ASSET_KEY (preserves faucet ID + callback metadata) - mem_storew_le.OFFERED_ASSET_KEY dropw + # Extract and store offered faucet info from ASSET_KEY + exec.asset::key_to_callbacks_enabled mem_store.OFFERED_ENABLE_CB + # => [ASSET_KEY, ASSET_VALUE] + exec.asset::key_into_faucet_id + # => [faucet_id_suffix, faucet_id_prefix, ASSET_VALUE] + mem_store.OFFERED_FAUCET_SUFFIX mem_store.OFFERED_FAUCET_PREFIX # => [ASSET_VALUE] dropw # => [] @@ -534,15 +556,19 @@ proc execute_pswap # 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 + # => [pad(8)] + mem_load.AMT_OFFERED_OUT_INPUT - movdn.4 - # => [ASSET_KEY, ASSET_VALUE', pad(8)] + mem_load.OFFERED_FAUCET_PREFIX + mem_load.OFFERED_FAUCET_SUFFIX + mem_load.OFFERED_ENABLE_CB + # => [enable_cb, faucet_suffix, faucet_prefix, amt_offered_out_input, pad(8)] + + exec.asset::create_fungible_asset + # => [ASSET_KEY, ASSET_VALUE, pad(8)] + call.wallet::receive_asset + # => [pad(16)] dropw dropw dropw dropw # => [] @@ -556,6 +582,22 @@ proc execute_pswap swap sub # => [remaining_requested] + mem_load.PSWAP_TAG_ITEM + mem_load.NOTE_TYPE + # => [note_type, tag, remaining_requested] + + movup.2 + # => [remaining_requested, note_type, tag] + + mem_load.AMT_OFFERED + mem_load.AMT_OFFERED_OUT + mem_load.OFFERED_ENABLE_CB + mem_load.OFFERED_FAUCET_SUFFIX + mem_load.OFFERED_FAUCET_PREFIX + # => [offered_faucet_prefix, offered_faucet_suffix, offered_enable_cb, + # amt_offered_out, amt_offered, + # remaining_requested, note_type, tag] + exec.create_remainder_note else drop drop From 7b10e6c830bcdc49d6999f57374bdafc8f51d4ef Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Tue, 7 Apr 2026 18:26:00 +0530 Subject: [PATCH 21/66] feat: add P2ID reconstruction test, overfill docs, and stack comments - Add test verifying Alice can reconstruct and consume P2ID payback note from her PSWAP data after Bob's partial fill - Document why overfill is not allowed (likely unintentional error) - Add missing stack comment on partial fill check --- .../asm/standards/notes/pswap.masm | 6 +- crates/miden-testing/tests/scripts/pswap.rs | 124 ++++++++++++++++++ 2 files changed, 129 insertions(+), 1 deletion(-) diff --git a/crates/miden-standards/asm/standards/notes/pswap.masm b/crates/miden-standards/asm/standards/notes/pswap.masm index ffc73910ee..88073001d3 100644 --- a/crates/miden-standards/asm/standards/notes/pswap.masm +++ b/crates/miden-standards/asm/standards/notes/pswap.masm @@ -495,7 +495,9 @@ proc execute_pswap mem_load.AMT_REQUESTED mem_store.AMT_REQUESTED_IN end - # Validate: fill amount (input + inflight) must not exceed total requested + # Validate: fill amount (input + inflight) must not exceed total requested. + # Overfilling is not allowed because it is likely due to an error and mostly + # unintentional — the consumer would lose the excess tokens with no change returned. mem_load.AMT_REQUESTED_IN mem_load.AMT_REQUESTED_INFLIGHT add mem_load.AMT_REQUESTED # => [requested, fill_amount] @@ -575,7 +577,9 @@ proc execute_pswap # Check if partial fill: total_in < total_requested mem_load.AMT_REQUESTED_IN mem_load.AMT_REQUESTED_INFLIGHT add mem_load.AMT_REQUESTED + # => [total_requested, total_in] dup.1 dup.1 lt + # => [is_partial, total_requested, total_in] if.true # remaining_requested = total_requested - total_in diff --git a/crates/miden-testing/tests/scripts/pswap.rs b/crates/miden-testing/tests/scripts/pswap.rs index 2abee51df7..8a38e59c7c 100644 --- a/crates/miden-testing/tests/scripts/pswap.rs +++ b/crates/miden-testing/tests/scripts/pswap.rs @@ -21,6 +21,130 @@ const BASIC_AUTH: Auth = Auth::BasicAuth { // TESTS // ================================================================================================ +/// Verifies that Alice can independently reconstruct and consume the P2ID payback note +/// using only her original PSWAP note data and the aux data from Bob's transaction output. +/// +/// Flow: +/// 1. Alice creates a PSWAP note (50 USDC for 25 ETH) +/// 2. Bob partially fills it (20 ETH) → produces P2ID payback + remainder +/// 3. Alice reconstructs the P2ID note from her PSWAP data + fill amount from aux +/// 4. Alice consumes the reconstructed P2ID note and receives 20 ETH +#[tokio::test] +async fn pswap_note_alice_reconstructs_and_consumes_p2id() -> anyhow::Result<()> { + use miden_standards::note::P2idNoteStorage; + + 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 offered_asset = FungibleAsset::new(usdc_faucet.id(), 50)?; + let requested_asset = FungibleAsset::new(eth_faucet.id(), 25)?; + + let mut rng = RandomCoin::new(Word::default()); + let serial_number = rng.draw_word(); + let storage = PswapNoteStorage::builder() + .requested_asset(requested_asset) + .creator_account_id(alice.id()) + .build(); + let pswap_note: Note = PswapNote::builder() + .sender(alice.id()) + .storage(storage) + .serial_number(serial_number) + .note_type(NoteType::Public) + .offered_asset(offered_asset) + .build()? + .into(); + builder.add_output_note(RawOutputNote::Full(pswap_note.clone())); + + let mut mock_chain = builder.build()?; + + // --- Step 1: Bob partially fills the PSWAP note (20 out of 25 ETH) --- + + let fill_amount = 20u32; + let mut note_args_map = BTreeMap::new(); + note_args_map.insert( + pswap_note.id(), + Word::from([Felt::from(fill_amount), Felt::from(0u32), ZERO, ZERO]), + ); + + let pswap = PswapNote::try_from(&pswap_note)?; + let (p2id_note, remainder_pswap) = + pswap.execute(bob.id(), Some(FungibleAsset::new(eth_faucet.id(), 20)?), None)?; + let remainder_note = + Note::from(remainder_pswap.expect("partial fill should produce remainder")); + + let tx_context = mock_chain + .build_tx_context(bob.id(), &[pswap_note.id()], &[])? + .extend_note_args(note_args_map) + .extend_expected_output_notes(vec![ + RawOutputNote::Full(p2id_note.clone()), + RawOutputNote::Full(remainder_note), + ]) + .build()?; + + let executed_transaction = tx_context.execute().await?; + mock_chain.add_pending_executed_transaction(&executed_transaction)?; + let _ = mock_chain.prove_next_block(); + + // --- Step 2: Alice reconstructs the P2ID note from her PSWAP data --- + + // Alice knows the fill amount from the P2ID note's assets (visible on chain for public notes) + let output_notes = executed_transaction.output_notes(); + let p2id_output_assets = output_notes.get_note(0).assets(); + let fill_asset = match p2id_output_assets.iter().next().unwrap() { + Asset::Fungible(f) => *f, + _ => panic!("Expected fungible asset in P2ID note"), + }; + assert_eq!(fill_asset.amount(), 20, "Fill amount should be 20 ETH"); + + // Alice reconstructs the recipient using her serial number and account ID + let p2id_serial = Word::from([ + serial_number[0] + ONE, + serial_number[1], + serial_number[2], + serial_number[3], + ]); + let reconstructed_recipient = P2idNoteStorage::new(alice.id()).into_recipient(p2id_serial); + + // Verify the reconstructed recipient matches the actual output + assert_eq!( + reconstructed_recipient.digest(), + p2id_note.recipient().digest(), + "Alice's reconstructed P2ID recipient does not match the actual output" + ); + + // --- Step 3: Alice consumes the P2ID payback note --- + + let tx_context = mock_chain + .build_tx_context(alice.id(), &[p2id_note.id()], &[])? + .build()?; + + let executed_transaction = tx_context.execute().await?; + + // Verify Alice received 20 ETH + 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.faucet_id(), eth_faucet.id()); + assert_eq!(f.amount(), 20); + } else { + panic!("Expected fungible asset in Alice's vault"); + } + + Ok(()) +} + #[tokio::test] async fn pswap_note_full_fill_test() -> anyhow::Result<()> { let mut builder = MockChain::builder(); From edce92c46146b649d9e9ff5940437f9115578841 Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Tue, 7 Apr 2026 18:34:56 +0530 Subject: [PATCH 22/66] fix: use Rust-predicted note for aux data in P2ID reconstruction test The test framework doesn't preserve word attachment content in executed transaction outputs. Use the Rust-predicted note to read the fill amount from aux data, which mirrors production behavior where aux is visible. --- crates/miden-testing/tests/scripts/pswap.rs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/crates/miden-testing/tests/scripts/pswap.rs b/crates/miden-testing/tests/scripts/pswap.rs index 8a38e59c7c..c8574f7620 100644 --- a/crates/miden-testing/tests/scripts/pswap.rs +++ b/crates/miden-testing/tests/scripts/pswap.rs @@ -96,16 +96,15 @@ async fn pswap_note_alice_reconstructs_and_consumes_p2id() -> anyhow::Result<()> mock_chain.add_pending_executed_transaction(&executed_transaction)?; let _ = mock_chain.prove_next_block(); - // --- Step 2: Alice reconstructs the P2ID note from her PSWAP data --- - - // Alice knows the fill amount from the P2ID note's assets (visible on chain for public notes) - let output_notes = executed_transaction.output_notes(); - let p2id_output_assets = output_notes.get_note(0).assets(); - let fill_asset = match p2id_output_assets.iter().next().unwrap() { - Asset::Fungible(f) => *f, - _ => panic!("Expected fungible asset in P2ID note"), - }; - assert_eq!(fill_asset.amount(), 20, "Fill amount should be 20 ETH"); + // --- Step 2: Alice reconstructs the P2ID note from her PSWAP data + aux --- + + // In production, Alice reads the fill amount from the P2ID note's attachment (aux data), + // which is visible for both public and private notes. Here we read it from the + // Rust-predicted note since the test framework doesn't preserve word attachment content + // in executed transaction outputs. + let aux_word = p2id_note.metadata().attachment().content().to_word(); + let fill_amount_from_aux = aux_word[0].as_canonical_u64(); + assert_eq!(fill_amount_from_aux, 20, "Fill amount from aux should be 20 ETH"); // Alice reconstructs the recipient using her serial number and account ID let p2id_serial = Word::from([ From 1ea42ab75f4873e1e4e18177ad6c3af663ff863b Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Tue, 7 Apr 2026 22:25:02 +0530 Subject: [PATCH 23/66] refactor: rename input to account fill, extract load_offered_asset proc - Rename AMT_REQUESTED_IN to AMT_REQUESTED_ACCT_FILL and related variables - Extract load_offered_asset proc centralizing asset loading and validation - Remove OFFERED_ASSET_WORD global memory constant --- .../asm/standards/notes/pswap.masm | 110 +++++++++--------- 1 file changed, 54 insertions(+), 56 deletions(-) diff --git a/crates/miden-standards/asm/standards/notes/pswap.masm b/crates/miden-standards/asm/standards/notes/pswap.masm index 88073001d3..215d249c4c 100644 --- a/crates/miden-standards/asm/standards/notes/pswap.masm +++ b/crates/miden-standards/asm/standards/notes/pswap.masm @@ -42,13 +42,13 @@ const PSWAP_CREATOR_SUFFIX_ITEM = 0x000D # Memory Addresses for Price Calculation Procedure const AMT_OFFERED = 0x0028 const AMT_REQUESTED = 0x0029 -const AMT_REQUESTED_IN = 0x002A +const AMT_REQUESTED_ACCT_FILL = 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_ACCT_FILL = 0x0034 const AMT_OFFERED_OUT_INFLIGHT = 0x0036 # Offered asset faucet info (extracted from ASSET_KEY) @@ -57,10 +57,6 @@ const OFFERED_FAUCET_SUFFIX = 0x0041 const OFFERED_FAUCET_PREFIX = 0x0042 -# Full Word Memory Addresses -# Asset storage (8 cells each, word-aligned) -const OFFERED_ASSET_WORD = 0x0018 - # Note indices and type const NOTE_TYPE = 0x0084 @@ -74,7 +70,7 @@ const ERR_PSWAP_WRONG_NUMBER_OF_INPUTS="PSWAP script expects exactly 14 note sto # PSWAP script requires exactly one note asset const ERR_PSWAP_WRONG_NUMBER_OF_ASSETS="PSWAP script requires exactly one note asset" -# PSWAP fill amount (input + inflight) exceeds the total requested amount +# PSWAP fill amount (account fill + inflight) exceeds the total requested amount const ERR_PSWAP_FILL_EXCEEDS_REQUESTED="PSWAP fill amount exceeds requested amount" # PRICE CALCULATION @@ -236,7 +232,7 @@ end #! attachment, and adds the requested assets (from vault and/or inflight). #! #! Inputs: [creator_suffix, creator_prefix, tag, note_type, SERIAL_NUM, -#! enable_callbacks, faucet_suffix, faucet_prefix, amt_in, amt_inflight] +#! enable_callbacks, faucet_suffix, faucet_prefix, amt_acct_fill, amt_inflight] #! Outputs: [] #! @locals(6) @@ -244,21 +240,21 @@ end # loc.1 = enable_callbacks # loc.2 = faucet_suffix # loc.3 = faucet_prefix -# loc.4 = amt_in +# loc.4 = amt_acct_fill # loc.5 = amt_inflight proc create_p2id_note # Derive P2ID serial: increment least significant element movup.4 add.1 movdn.4 # => [creator_suffix, creator_prefix, tag, note_type, P2ID_SERIAL_NUM, - # enable_callbacks, faucet_suffix, faucet_prefix, amt_in, amt_inflight] + # enable_callbacks, faucet_suffix, faucet_prefix, amt_acct_fill, amt_inflight] exec.p2id::new - # => [note_idx, enable_callbacks, faucet_suffix, faucet_prefix, amt_in, amt_inflight] + # => [note_idx, enable_callbacks, faucet_suffix, faucet_prefix, amt_acct_fill, amt_inflight] loc_store.0 loc_store.1 loc_store.2 loc_store.3 loc_store.4 loc_store.5 # => [] - # Set attachment: aux = amt_in + amt_inflight + # Set attachment: aux = amt_acct_fill + amt_inflight # attachment_scheme = 0 (NoteAttachmentScheme::none) loc_load.4 loc_load.5 add # => [total_fill] @@ -270,7 +266,7 @@ proc create_p2id_note exec.output_note::set_word_attachment # => [] - # Move input_amount from consumer's vault to P2ID note (if > 0) + # Move acct_fill_amount from consumer's vault to P2ID note (if > 0) loc_load.4 push.0 gt # => [amt > 0] if.true @@ -279,10 +275,10 @@ proc create_p2id_note # => [note_idx, pad(7)] push.0.0.0 loc_load.4 - # => [amt_in, 0, 0, 0, note_idx, pad(7)] + # => [amt_acct_fill, 0, 0, 0, note_idx, pad(7)] loc_load.3 loc_load.2 loc_load.1 - # => [enable_cb, faucet_suffix, faucet_prefix, amt_in, 0, 0, 0, note_idx, pad(7)] + # => [enable_cb, faucet_suffix, faucet_prefix, amt_acct_fill, 0, 0, 0, note_idx, pad(7)] exec.asset::create_fungible_asset # => [ASSET_KEY, ASSET_VALUE, note_idx, pad(7)] @@ -414,6 +410,23 @@ proc is_consumer_creator # => [is_creator] end +#! Loads the offered asset from the active note and validates there is exactly one asset. +#! +#! Inputs: [] +#! Outputs: [ASSET_KEY, ASSET_VALUE] +#! +@locals(2) +proc load_offered_asset + locaddr.0 exec.active_note::get_assets + # => [num_assets, dest_ptr] + + push.1 eq assert.err=ERR_PSWAP_WRONG_NUMBER_OF_ASSETS + # => [dest_ptr] + + exec.asset::load + # => [ASSET_KEY, ASSET_VALUE] +end + #! Reclaims all assets from the note back to the creator's vault. #! #! Called when the consumer IS the creator (cancel/reclaim path). @@ -422,13 +435,7 @@ end #! 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 + exec.load_offered_asset # => [ASSET_KEY, ASSET_VALUE] # Build 16-element call frame: [ASSET_KEY, ASSET_VALUE, pad(8)] @@ -451,18 +458,7 @@ end #! Outputs: [] #! 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 + exec.load_offered_asset # => [ASSET_KEY, ASSET_VALUE] # Extract offered amount @@ -487,38 +483,38 @@ proc execute_pswap mem_store.AMT_REQUESTED # => [] - # If both input and inflight are 0, default to a full fill (input = requested). + # If both account fill and inflight are 0, default to a full fill (account fill = requested). # This enables consumption by network accounts, which execute without note_args # (the kernel defaults note_args to [0, 0, 0, 0] when none are provided). - mem_load.AMT_REQUESTED_IN mem_load.AMT_REQUESTED_INFLIGHT add push.0 eq + mem_load.AMT_REQUESTED_ACCT_FILL mem_load.AMT_REQUESTED_INFLIGHT add push.0 eq if.true - mem_load.AMT_REQUESTED mem_store.AMT_REQUESTED_IN + mem_load.AMT_REQUESTED mem_store.AMT_REQUESTED_ACCT_FILL end - # Validate: fill amount (input + inflight) must not exceed total requested. + # Validate: fill amount (account fill + inflight) must not exceed total requested. # Overfilling is not allowed because it is likely due to an error and mostly # unintentional — the consumer would lose the excess tokens with no change returned. - mem_load.AMT_REQUESTED_IN mem_load.AMT_REQUESTED_INFLIGHT add + mem_load.AMT_REQUESTED_ACCT_FILL mem_load.AMT_REQUESTED_INFLIGHT add mem_load.AMT_REQUESTED # => [requested, fill_amount] # lte pops [b=requested, a=fill_amount] and pushes (fill_amount <= requested) lte assert.err=ERR_PSWAP_FILL_EXCEEDS_REQUESTED # => [] - # Calculate offered_out for input and inflight amounts separately rather than - # summing them first, because the input portion (offered_out_input) must be sent - # to the consumer's vault individually, while the total (input + inflight) is + # Calculate offered_out for account fill and inflight amounts separately rather than + # summing them first, because the account fill portion must be sent + # to the consumer's vault individually, while the total (account fill + inflight) is # needed to determine the remainder note's offered amount. # - # Calculate offered_out for input_amount - mem_load.AMT_REQUESTED_IN + # Calculate offered_out for acct_fill_amount + mem_load.AMT_REQUESTED_ACCT_FILL mem_load.AMT_REQUESTED mem_load.AMT_OFFERED - # => [offered, requested, input_amount] + # => [offered, requested, acct_fill_amount] exec.calculate_tokens_offered_for_requested - # => [input_offered_out] + # => [acct_fill_offered_out] - mem_store.AMT_OFFERED_OUT_INPUT + mem_store.AMT_OFFERED_OUT_ACCT_FILL # => [] # Calculate offered_out for inflight_amount @@ -532,8 +528,8 @@ proc execute_pswap 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 = acct_fill_offered_out + inflight_offered_out + mem_load.AMT_OFFERED_OUT_ACCT_FILL mem_load.AMT_OFFERED_OUT_INFLIGHT add # => [total_offered_out] mem_store.AMT_OFFERED_OUT @@ -541,7 +537,7 @@ proc execute_pswap # Create P2ID note for creator mem_load.AMT_REQUESTED_INFLIGHT - mem_load.AMT_REQUESTED_IN + mem_load.AMT_REQUESTED_ACCT_FILL mem_load.REQUESTED_FAUCET_PREFIX_ITEM mem_load.REQUESTED_FAUCET_SUFFIX_ITEM mem_load.REQUESTED_ENABLE_CALLBACKS_ITEM @@ -551,16 +547,16 @@ proc execute_pswap mem_load.PSWAP_CREATOR_PREFIX_ITEM mem_load.PSWAP_CREATOR_SUFFIX_ITEM # => [creator_suffix, creator_prefix, tag, note_type, SERIAL_NUM, - # enable_callbacks, faucet_suffix, faucet_prefix, amt_in, amt_inflight] + # enable_callbacks, faucet_suffix, faucet_prefix, amt_acct_fill, amt_inflight] exec.create_p2id_note # => [] - # Consumer receives only input_offered_out into vault (not inflight portion) + # Consumer receives only acct_fill_offered_out into vault (not the inflight portion) padw padw # => [pad(8)] - mem_load.AMT_OFFERED_OUT_INPUT + mem_load.AMT_OFFERED_OUT_ACCT_FILL mem_load.OFFERED_FAUCET_PREFIX mem_load.OFFERED_FAUCET_SUFFIX mem_load.OFFERED_ENABLE_CB @@ -575,7 +571,7 @@ proc execute_pswap # => [] # Check if partial fill: total_in < total_requested - mem_load.AMT_REQUESTED_IN mem_load.AMT_REQUESTED_INFLIGHT add + mem_load.AMT_REQUESTED_ACCT_FILL mem_load.AMT_REQUESTED_INFLIGHT add mem_load.AMT_REQUESTED # => [total_requested, total_in] dup.1 dup.1 lt @@ -613,13 +609,15 @@ end @note_script pub proc main # => [NOTE_ARGS] - # Stack (top to bottom): [input_amount, inflight_amount, 0, 0] + # Stack (top to bottom): [acct_fill_amount, inflight_amount, 0, 0] + # acct_fill_amount: debited from consumer's account vault + # inflight_amount: added directly from another note in the same transaction (no vault debit) # (Word[0] on top after mem_loadw_le in kernel prologue) # # In network transactions, note_args are not provided by the executor and default # to [0, 0, 0, 0]. The script handles this by defaulting to a full fill. - mem_store.AMT_REQUESTED_IN + mem_store.AMT_REQUESTED_ACCT_FILL # => [inflight_amount, 0, 0] mem_store.AMT_REQUESTED_INFLIGHT From 1cbbecf465acf4b77403365a58299f3228dedb3d Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Tue, 7 Apr 2026 22:51:14 +0530 Subject: [PATCH 24/66] refactor: extract test helpers and reduce verbosity in pswap unit tests --- crates/miden-standards/src/note/pswap.rs | 172 +++++++---------------- 1 file changed, 53 insertions(+), 119 deletions(-) diff --git a/crates/miden-standards/src/note/pswap.rs b/crates/miden-standards/src/note/pswap.rs index f2d0c10576..ed00f937a6 100644 --- a/crates/miden-standards/src/note/pswap.rs +++ b/crates/miden-standards/src/note/pswap.rs @@ -676,43 +676,35 @@ mod tests { 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, - ); + // TEST HELPERS + // -------------------------------------------------------------------------------------------- - let requested_faucet_id = AccountId::dummy( - requested_faucet_bytes, + fn dummy_faucet_id(byte: u8) -> AccountId { + let mut bytes = [0; 15]; + bytes[0] = byte; + AccountId::dummy( + bytes, AccountIdVersion::Version0, AccountType::FungibleFaucet, AccountStorageMode::Public, - ); + ) + } - let creator_id = AccountId::dummy( + fn dummy_creator_id() -> AccountId { + AccountId::dummy( [1; 15], AccountIdVersion::Version0, AccountType::RegularAccountImmutableCode, AccountStorageMode::Public, - ); - - let offered_asset = FungibleAsset::new(offered_faucet_id, 1000).unwrap(); - let requested_asset = FungibleAsset::new(requested_faucet_id, 500).unwrap(); + ) + } + fn build_pswap_note( + offered_asset: FungibleAsset, + requested_asset: FungibleAsset, + creator_id: AccountId, + ) -> (PswapNote, Note) { let mut rng = RandomCoin::new(Word::default()); - - let script = PswapNote::script(); - assert!(script.root() != Word::default(), "Script root should not be zero"); - let storage = PswapNoteStorage::builder() .requested_asset(requested_asset) .creator_account_id(creator_id) @@ -725,15 +717,30 @@ mod tests { .offered_asset(offered_asset) .build() .unwrap(); + let note: Note = pswap.clone().into(); + (pswap, note) + } - let note: Note = pswap.into(); + // TESTS + // -------------------------------------------------------------------------------------------- + + #[test] + fn pswap_note_creation_and_script() { + let creator_id = dummy_creator_id(); + let offered_asset = FungibleAsset::new(dummy_faucet_id(0xaa), 1000).unwrap(); + let requested_asset = FungibleAsset::new(dummy_faucet_id(0xbb), 500).unwrap(); + + let (pswap, note) = build_pswap_note(offered_asset, requested_asset, creator_id); + + assert_eq!(pswap.sender(), creator_id); + assert_eq!(pswap.note_type(), NoteType::Public); + let script = PswapNote::script(); + assert!(script.root() != Word::default(), "Script root should not be zero"); 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(), PswapNoteStorage::NUM_STORAGE_ITEMS as u16, @@ -742,56 +749,14 @@ mod tests { #[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 = FungibleAsset::new(offered_faucet_id, 1000).unwrap(); - let requested_asset = FungibleAsset::new(requested_faucet_id, 500).unwrap(); + let creator_id = dummy_creator_id(); + let offered_asset = FungibleAsset::new(dummy_faucet_id(0xaa), 1000).unwrap(); + let requested_asset = FungibleAsset::new(dummy_faucet_id(0xbb), 500).unwrap(); - let mut rng = RandomCoin::new(Word::default()); - - let storage = PswapNoteStorage::builder() - .requested_asset(requested_asset) - .creator_account_id(creator_id) - .build(); - let pswap = PswapNote::builder() - .sender(creator_id) - .storage(storage) - .serial_number(rng.draw_word()) - .note_type(NoteType::Public) - .offered_asset(offered_asset) - .build() - .unwrap(); + let (pswap, note) = build_pswap_note(offered_asset, requested_asset, creator_id); assert_eq!(pswap.sender(), creator_id); assert_eq!(pswap.note_type(), NoteType::Public); - - // 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); @@ -842,14 +807,9 @@ mod tests { #[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); + assert_eq!(PswapNote::calculate_output_amount(100, 100, 50), 50); // Equal ratio + assert_eq!(PswapNote::calculate_output_amount(200, 100, 50), 100); // 2:1 ratio + assert_eq!(PswapNote::calculate_output_amount(100, 200, 50), 25); // 1:2 ratio // Non-integer ratio (100/73) let result = PswapNote::calculate_output_amount(100, 73, 7); @@ -858,29 +818,16 @@ mod tests { #[test] fn pswap_note_storage_try_from() { - 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 = FungibleAsset::new(faucet_id, 500).unwrap(); + let creator_id = dummy_creator_id(); + let requested_asset = FungibleAsset::new(dummy_faucet_id(0xaa), 500).unwrap(); let storage_items = vec![ - Felt::from(requested_asset.callbacks().as_u8()), // enable_callbacks - requested_asset.faucet_id().suffix(), // faucet_id_suffix - requested_asset.faucet_id().prefix().as_felt(), // faucet_id_prefix - Felt::try_from(requested_asset.amount()).unwrap(), // amount - Felt::from(0xc0000000u32), // pswap_tag - Felt::from(0x80000001u32), // payback_note_tag + Felt::from(requested_asset.callbacks().as_u8()), + requested_asset.faucet_id().suffix(), + requested_asset.faucet_id().prefix().as_felt(), + Felt::try_from(requested_asset.amount()).unwrap(), + Felt::from(0xc0000000u32), // pswap_tag + Felt::from(0x80000001u32), // payback_note_tag ZERO, ZERO, Felt::from(3u16), // swap_count @@ -899,27 +846,14 @@ mod tests { #[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 creator_id = dummy_creator_id(); + let requested_asset = FungibleAsset::new(dummy_faucet_id(0xaa), 500).unwrap(); - let requested_asset = FungibleAsset::new(faucet_id, 500).unwrap(); let storage = PswapNoteStorage::builder() .requested_asset(requested_asset) .creator_account_id(creator_id) .build(); - // Convert to NoteStorage and back let note_storage = NoteStorage::from(storage.clone()); let parsed = PswapNoteStorage::try_from(note_storage.items()).unwrap(); From e27b0a11624d8a4eec58c186d8e18ec9d52c2d65 Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Tue, 7 Apr 2026 23:11:35 +0530 Subject: [PATCH 25/66] refactor: guard zero-amount receive_asset in MASM, add NUM_STORAGE_ITEMS alias on PswapNote --- .../asm/standards/notes/pswap.masm | 32 +++++++++++-------- crates/miden-standards/src/note/mod.rs | 2 +- crates/miden-standards/src/note/pswap.rs | 6 ++++ 3 files changed, 25 insertions(+), 15 deletions(-) diff --git a/crates/miden-standards/asm/standards/notes/pswap.masm b/crates/miden-standards/asm/standards/notes/pswap.masm index 215d249c4c..10d1697705 100644 --- a/crates/miden-standards/asm/standards/notes/pswap.masm +++ b/crates/miden-standards/asm/standards/notes/pswap.masm @@ -552,23 +552,27 @@ proc execute_pswap exec.create_p2id_note # => [] - # Consumer receives only acct_fill_offered_out into vault (not the inflight portion) - padw padw - # => [pad(8)] + # Consumer receives only acct_fill_offered_out into vault (not the inflight portion, if > 0) + mem_load.AMT_OFFERED_OUT_ACCT_FILL push.0 gt + # => [amt > 0] + if.true + padw padw + # => [pad(8)] - mem_load.AMT_OFFERED_OUT_ACCT_FILL - mem_load.OFFERED_FAUCET_PREFIX - mem_load.OFFERED_FAUCET_SUFFIX - mem_load.OFFERED_ENABLE_CB - # => [enable_cb, faucet_suffix, faucet_prefix, amt_offered_out_input, pad(8)] + mem_load.AMT_OFFERED_OUT_ACCT_FILL + mem_load.OFFERED_FAUCET_PREFIX + mem_load.OFFERED_FAUCET_SUFFIX + mem_load.OFFERED_ENABLE_CB + # => [enable_cb, faucet_suffix, faucet_prefix, amt_offered_out_input, pad(8)] - exec.asset::create_fungible_asset - # => [ASSET_KEY, ASSET_VALUE, pad(8)] + exec.asset::create_fungible_asset + # => [ASSET_KEY, ASSET_VALUE, pad(8)] - call.wallet::receive_asset - # => [pad(16)] - dropw dropw dropw dropw - # => [] + call.wallet::receive_asset + # => [pad(16)] + dropw dropw dropw dropw + # => [] + end # Check if partial fill: total_in < total_requested mem_load.AMT_REQUESTED_ACCT_FILL mem_load.AMT_REQUESTED_INFLIGHT add diff --git a/crates/miden-standards/src/note/mod.rs b/crates/miden-standards/src/note/mod.rs index eaef854e68..3c42769a6c 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_STORAGE_ITEMS, + Self::PSWAP => PswapNote::NUM_STORAGE_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 ed00f937a6..ebc74a6088 100644 --- a/crates/miden-standards/src/note/pswap.rs +++ b/crates/miden-standards/src/note/pswap.rs @@ -261,6 +261,12 @@ where } impl PswapNote { + // CONSTANTS + // -------------------------------------------------------------------------------------------- + + /// Expected number of storage items for the PSWAP note. + pub const NUM_STORAGE_ITEMS: usize = PswapNoteStorage::NUM_STORAGE_ITEMS; + // PUBLIC ACCESSORS // -------------------------------------------------------------------------------------------- From a329ae3e5bd4836f30406597b5dd8da8294f8f6f Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Tue, 7 Apr 2026 23:29:04 +0530 Subject: [PATCH 26/66] refactor: rename ERR_PSWAP_WRONG_NUMBER_OF_INPUTS to ERR_PSWAP_WRONG_NUMBER_OF_STORAGE_ITEMS --- crates/miden-standards/asm/standards/notes/pswap.masm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/miden-standards/asm/standards/notes/pswap.masm b/crates/miden-standards/asm/standards/notes/pswap.masm index 10d1697705..89cefc8871 100644 --- a/crates/miden-standards/asm/standards/notes/pswap.masm +++ b/crates/miden-standards/asm/standards/notes/pswap.masm @@ -65,7 +65,7 @@ const NOTE_TYPE = 0x0084 # ================================================================================================= # PSWAP script expects exactly 14 note storage items -const ERR_PSWAP_WRONG_NUMBER_OF_INPUTS="PSWAP script expects exactly 14 note storage items" +const ERR_PSWAP_WRONG_NUMBER_OF_STORAGE_ITEMS="PSWAP script expects exactly 14 note storage items" # PSWAP script requires exactly one note asset const ERR_PSWAP_WRONG_NUMBER_OF_ASSETS="PSWAP script requires exactly one note asset" @@ -633,7 +633,7 @@ pub proc main push.0 exec.active_note::get_storage # => [num_storage_items, storage_ptr] - eq.NUM_STORAGE_ITEMS assert.err=ERR_PSWAP_WRONG_NUMBER_OF_INPUTS + eq.NUM_STORAGE_ITEMS assert.err=ERR_PSWAP_WRONG_NUMBER_OF_STORAGE_ITEMS # => [storage_ptr] drop From 2ae6bd00253022be409de368ecc1d928a7889a23 Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Wed, 8 Apr 2026 13:37:42 +0530 Subject: [PATCH 27/66] refactor: replace global memory with @locals in execute_pswap and pass fill amounts via stack --- .../asm/standards/notes/pswap.masm | 161 +++++++++--------- 1 file changed, 80 insertions(+), 81 deletions(-) diff --git a/crates/miden-standards/asm/standards/notes/pswap.masm b/crates/miden-standards/asm/standards/notes/pswap.masm index 89cefc8871..1ac0cb8209 100644 --- a/crates/miden-standards/asm/standards/notes/pswap.masm +++ b/crates/miden-standards/asm/standards/notes/pswap.masm @@ -20,11 +20,13 @@ const MAX_U32=0x0000000100000000 # Memory Addresses # ================================================================================================= -# Memory Address Layout: +# Global Memory Address Layout: # - PSWAP Note Storage: addresses 0x0000 - 0x000D (loaded from note storage) -# - Price Calculation: addresses 0x0028 - 0x0036 -# - Asset Keys: addresses 0x0038 - 0x003B (word-aligned) -# - Full Word (word-aligned): addresses 0x0018 - 0x001F +# - NOTE_TYPE: 0x0084 (set by extract_note_type) +# +# Procedure-local values use @locals instead of global memory: +# - execute_pswap: @locals(8) for offered asset info and calculation intermediates +# - calculate_tokens_offered_for_requested: @locals(1) for input amount # PSWAP Note Storage (14 items loaded at address 0) # Requested asset is stored as individual felts (fungible assets only): @@ -39,25 +41,7 @@ const PSWAP_COUNT_ITEM = 0x0008 const PSWAP_CREATOR_PREFIX_ITEM = 0x000C const PSWAP_CREATOR_SUFFIX_ITEM = 0x000D -# Memory Addresses for Price Calculation Procedure -const AMT_OFFERED = 0x0028 -const AMT_REQUESTED = 0x0029 -const AMT_REQUESTED_ACCT_FILL = 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_ACCT_FILL = 0x0034 -const AMT_OFFERED_OUT_INFLIGHT = 0x0036 - -# Offered asset faucet info (extracted from ASSET_KEY) -const OFFERED_ENABLE_CB = 0x0040 -const OFFERED_FAUCET_SUFFIX = 0x0041 -const OFFERED_FAUCET_PREFIX = 0x0042 - - -# Note indices and type +# Global Memory Address (shared across procedures) const NOTE_TYPE = 0x0084 @@ -89,6 +73,8 @@ const ERR_PSWAP_FILL_EXCEEDS_REQUESTED="PSWAP fill amount exceeds requested amou #! Inputs: [offered, requested, input] (offered on top) #! Outputs: [offered_out] #! +@locals(1) +# loc.0 = input amount proc calculate_tokens_offered_for_requested # u64 convention: lo is on TOP after u32split # u32split(a) => [lo (top), hi] @@ -96,12 +82,12 @@ proc calculate_tokens_offered_for_requested # 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 + movup.2 loc_store.0 # => [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 + dup.1 loc_load.0 eq # => [requested == input, offered, requested] if.true @@ -137,7 +123,7 @@ proc calculate_tokens_offered_for_requested exec.u64::div # => [ratio_lo, ratio_hi] - mem_load.CALC_AMT_IN u32split push.FACTOR u32split + loc_load.0 u32split push.FACTOR u32split # => [F_lo, F_hi, in_lo, in_hi, ratio_lo, ratio_hi] exec.u64::wrapping_mul @@ -168,7 +154,7 @@ proc calculate_tokens_offered_for_requested exec.u64::div # => [ratio_lo, ratio_hi] - mem_load.CALC_AMT_IN u32split + loc_load.0 u32split # => [in_lo, in_hi, ratio_lo, ratio_hi] exec.u64::wrapping_mul @@ -454,10 +440,24 @@ end #! Sends offered tokens to consumer, requested tokens to creator via P2ID, #! and creates a remainder note if partially filled. #! -#! Inputs: [] +#! Inputs: [acct_fill_amount, inflight_amount] #! Outputs: [] #! +@locals(10) +# loc.0 = amt_offered +# loc.1 = amt_requested +# loc.2 = amt_offered_out (total) +# loc.3 = amt_offered_out_acct_fill +# loc.4 = amt_offered_out_inflight +# loc.5 = offered_enable_cb +# loc.6 = offered_faucet_suffix +# loc.7 = offered_faucet_prefix +# loc.8 = amt_requested_acct_fill +# loc.9 = amt_requested_inflight proc execute_pswap + loc_store.8 loc_store.9 + # => [] + exec.load_offered_asset # => [ASSET_KEY, ASSET_VALUE] @@ -465,37 +465,29 @@ proc execute_pswap exec.asset::fungible_to_amount # => [amount, ASSET_KEY, ASSET_VALUE] - mem_store.AMT_OFFERED + loc_store.0 # => [ASSET_KEY, ASSET_VALUE] # Extract and store offered faucet info from ASSET_KEY - exec.asset::key_to_callbacks_enabled mem_store.OFFERED_ENABLE_CB + exec.asset::key_to_callbacks_enabled loc_store.5 # => [ASSET_KEY, ASSET_VALUE] exec.asset::key_into_faucet_id # => [faucet_id_suffix, faucet_id_prefix, ASSET_VALUE] - mem_store.OFFERED_FAUCET_SUFFIX mem_store.OFFERED_FAUCET_PREFIX + loc_store.6 loc_store.7 # => [ASSET_VALUE] dropw # => [] # Read requested amount directly from note storage mem_load.REQUESTED_AMOUNT_ITEM - mem_store.AMT_REQUESTED + loc_store.1 # => [] - # If both account fill and inflight are 0, default to a full fill (account fill = requested). - # This enables consumption by network accounts, which execute without note_args - # (the kernel defaults note_args to [0, 0, 0, 0] when none are provided). - mem_load.AMT_REQUESTED_ACCT_FILL mem_load.AMT_REQUESTED_INFLIGHT add push.0 eq - if.true - mem_load.AMT_REQUESTED mem_store.AMT_REQUESTED_ACCT_FILL - end - # Validate: fill amount (account fill + inflight) must not exceed total requested. # Overfilling is not allowed because it is likely due to an error and mostly # unintentional — the consumer would lose the excess tokens with no change returned. - mem_load.AMT_REQUESTED_ACCT_FILL mem_load.AMT_REQUESTED_INFLIGHT add - mem_load.AMT_REQUESTED + loc_load.8 loc_load.9 add + loc_load.1 # => [requested, fill_amount] # lte pops [b=requested, a=fill_amount] and pushes (fill_amount <= requested) lte assert.err=ERR_PSWAP_FILL_EXCEEDS_REQUESTED @@ -507,37 +499,37 @@ proc execute_pswap # needed to determine the remainder note's offered amount. # # Calculate offered_out for acct_fill_amount - mem_load.AMT_REQUESTED_ACCT_FILL - mem_load.AMT_REQUESTED - mem_load.AMT_OFFERED + loc_load.8 + loc_load.1 + loc_load.0 # => [offered, requested, acct_fill_amount] exec.calculate_tokens_offered_for_requested # => [acct_fill_offered_out] - mem_store.AMT_OFFERED_OUT_ACCT_FILL + loc_store.3 # => [] # Calculate offered_out for inflight_amount - mem_load.AMT_REQUESTED_INFLIGHT - mem_load.AMT_REQUESTED - mem_load.AMT_OFFERED + loc_load.9 + loc_load.1 + loc_load.0 # => [offered, requested, inflight_amount] exec.calculate_tokens_offered_for_requested # => [inflight_offered_out] - mem_store.AMT_OFFERED_OUT_INFLIGHT + loc_store.4 # => [] # total_offered_out = acct_fill_offered_out + inflight_offered_out - mem_load.AMT_OFFERED_OUT_ACCT_FILL mem_load.AMT_OFFERED_OUT_INFLIGHT add + loc_load.3 loc_load.4 add # => [total_offered_out] - mem_store.AMT_OFFERED_OUT + loc_store.2 # => [] # Create P2ID note for creator - mem_load.AMT_REQUESTED_INFLIGHT - mem_load.AMT_REQUESTED_ACCT_FILL + loc_load.9 + loc_load.8 mem_load.REQUESTED_FAUCET_PREFIX_ITEM mem_load.REQUESTED_FAUCET_SUFFIX_ITEM mem_load.REQUESTED_ENABLE_CALLBACKS_ITEM @@ -553,17 +545,17 @@ proc execute_pswap # => [] # Consumer receives only acct_fill_offered_out into vault (not the inflight portion, if > 0) - mem_load.AMT_OFFERED_OUT_ACCT_FILL push.0 gt + loc_load.3 push.0 gt # => [amt > 0] if.true padw padw # => [pad(8)] - mem_load.AMT_OFFERED_OUT_ACCT_FILL - mem_load.OFFERED_FAUCET_PREFIX - mem_load.OFFERED_FAUCET_SUFFIX - mem_load.OFFERED_ENABLE_CB - # => [enable_cb, faucet_suffix, faucet_prefix, amt_offered_out_input, pad(8)] + loc_load.3 + loc_load.7 + loc_load.6 + loc_load.5 + # => [enable_cb, faucet_suffix, faucet_prefix, amt_offered_out_acct_fill, pad(8)] exec.asset::create_fungible_asset # => [ASSET_KEY, ASSET_VALUE, pad(8)] @@ -575,8 +567,8 @@ proc execute_pswap end # Check if partial fill: total_in < total_requested - mem_load.AMT_REQUESTED_ACCT_FILL mem_load.AMT_REQUESTED_INFLIGHT add - mem_load.AMT_REQUESTED + loc_load.8 loc_load.9 add + loc_load.1 # => [total_requested, total_in] dup.1 dup.1 lt # => [is_partial, total_requested, total_in] @@ -593,11 +585,11 @@ proc execute_pswap movup.2 # => [remaining_requested, note_type, tag] - mem_load.AMT_OFFERED - mem_load.AMT_OFFERED_OUT - mem_load.OFFERED_ENABLE_CB - mem_load.OFFERED_FAUCET_SUFFIX - mem_load.OFFERED_FAUCET_PREFIX + loc_load.0 + loc_load.2 + loc_load.5 + loc_load.6 + loc_load.7 # => [offered_faucet_prefix, offered_faucet_suffix, offered_enable_cb, # amt_offered_out, amt_offered, # remaining_requested, note_type, tag] @@ -621,35 +613,42 @@ pub proc main # In network transactions, note_args are not provided by the executor and default # to [0, 0, 0, 0]. The script handles this by defaulting to a full fill. - mem_store.AMT_REQUESTED_ACCT_FILL - # => [inflight_amount, 0, 0] - - mem_store.AMT_REQUESTED_INFLIGHT - # => [0, 0] - drop drop - # => [] + # => [acct_fill_amount, inflight_amount, 0, 0] + movdn.3 movdn.3 drop drop + # => [acct_fill_amount, inflight_amount] # Load all note storage items to memory starting at address 0 push.0 exec.active_note::get_storage - # => [num_storage_items, storage_ptr] + # => [num_storage_items, storage_ptr, acct_fill_amount, inflight_amount] eq.NUM_STORAGE_ITEMS assert.err=ERR_PSWAP_WRONG_NUMBER_OF_STORAGE_ITEMS - # => [storage_ptr] + # => [storage_ptr, acct_fill_amount, inflight_amount] drop - # => [] + # => [acct_fill_amount, inflight_amount] + + # If both account fill and inflight are 0, default to a full fill (account fill = requested). + # This enables consumption by network accounts, which execute without note_args + # (the kernel defaults note_args to [0, 0, 0, 0] when none are provided). + dup.1 dup.1 add push.0 eq + # => [both_zero, acct_fill_amount, inflight_amount] + if.true + drop mem_load.REQUESTED_AMOUNT_ITEM + # => [acct_fill_amount=requested, inflight_amount=0] + end # Extract and store note_type from active note metadata exec.extract_note_type - # => [] + # => [acct_fill_amount, inflight_amount] - mem_load.PSWAP_CREATOR_SUFFIX_ITEM mem_load.PSWAP_CREATOR_PREFIX_ITEM swap - # => [creator_id_suffix, creator_id_prefix] + mem_load.PSWAP_CREATOR_PREFIX_ITEM mem_load.PSWAP_CREATOR_SUFFIX_ITEM + # => [creator_id_suffix, creator_id_prefix, acct_fill_amount, inflight_amount] exec.is_consumer_creator - # => [is_creator] + # => [is_creator, acct_fill_amount, inflight_amount] if.true + drop drop exec.handle_reclaim else exec.execute_pswap From 82cae63a3444e0dc28640d4209f6c4847b52b82c Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Mon, 13 Apr 2026 18:24:44 +0530 Subject: [PATCH 28/66] refactor(pswap): apply PR review nits from #2636 - Drop duplicated u64 stdlib docs inside calculate_tokens_offered_for_requested - Replace `swap push.MAX_U32 mul add` with `swap mul.MAX_U32 add` - Fold redundant empty-input guard in PswapNote::execute into the match arm --- .../miden-standards/asm/standards/notes/pswap.masm | 10 ++-------- crates/miden-standards/src/note/pswap.rs | 12 +++++------- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/crates/miden-standards/asm/standards/notes/pswap.masm b/crates/miden-standards/asm/standards/notes/pswap.masm index 1ac0cb8209..ab7669dde2 100644 --- a/crates/miden-standards/asm/standards/notes/pswap.masm +++ b/crates/miden-standards/asm/standards/notes/pswap.masm @@ -76,12 +76,6 @@ const ERR_PSWAP_FILL_EXCEEDS_REQUESTED="PSWAP fill amount exceeds requested amou @locals(1) # loc.0 = input amount 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 loc_store.0 # => [offered, requested] @@ -135,7 +129,7 @@ proc calculate_tokens_offered_for_requested exec.u64::div # => [result_lo, result_hi] - swap push.MAX_U32 mul add + swap mul.MAX_U32 add # => [result] else @@ -166,7 +160,7 @@ proc calculate_tokens_offered_for_requested exec.u64::div # => [result_lo, result_hi] - swap push.MAX_U32 mul add + swap mul.MAX_U32 add # => [result] end diff --git a/crates/miden-standards/src/note/pswap.rs b/crates/miden-standards/src/note/pswap.rs index ebc74a6088..11b19fe356 100644 --- a/crates/miden-standards/src/note/pswap.rs +++ b/crates/miden-standards/src/note/pswap.rs @@ -361,12 +361,6 @@ impl PswapNote { input_asset: Option, inflight_asset: Option, ) -> Result<(Note, Option), NoteError> { - if input_asset.is_none() && inflight_asset.is_none() { - return Err(NoteError::other( - "at least one of input_asset or inflight_asset must be provided", - )); - } - // Combine input and inflight into a single payback asset let input_amount = input_asset.as_ref().map_or(0, |a| a.amount()); let inflight_amount = inflight_asset.as_ref().map_or(0, |a| a.amount()); @@ -375,7 +369,11 @@ impl PswapNote { NoteError::other_with_source("failed to combine input and inflight assets", e) })?, (Some(asset), None) | (None, Some(asset)) => asset, - (None, None) => unreachable!("validated above"), + (None, None) => { + return Err(NoteError::other( + "at least one of input_asset or inflight_asset must be provided", + )); + }, }; let fill_amount = payback_asset.amount(); From 30188530dd18be1901344a92cacf928794819dc1 Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Mon, 13 Apr 2026 18:49:06 +0530 Subject: [PATCH 29/66] feat(pswap): make payback note type configurable via PswapNoteStorage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a `payback_note_type: NoteType` field on `PswapNoteStorage` (default `Private`) so the payback note produced on fill can be private even when the pswap note itself is public. Private payback notes are cheaper in fees and bandwidth and don't lose information — the fill amount is already recorded in the executed transaction's output. Stored in the previously-reserved storage slot [6]. MASM now loads the type from that slot when building the payback p2id note instead of using the active note's own type. Remainder pswap note continues to inherit the parent pswap's note type (unchanged). Addresses PhilippGackstatter's review comment on PR #2636. --- .../asm/standards/notes/pswap.masm | 3 +- crates/miden-standards/src/note/pswap.rs | 38 +++++++++++++++---- crates/miden-testing/tests/scripts/pswap.rs | 1 + 3 files changed, 34 insertions(+), 8 deletions(-) diff --git a/crates/miden-standards/asm/standards/notes/pswap.masm b/crates/miden-standards/asm/standards/notes/pswap.masm index ab7669dde2..33a496a0e5 100644 --- a/crates/miden-standards/asm/standards/notes/pswap.masm +++ b/crates/miden-standards/asm/standards/notes/pswap.masm @@ -37,6 +37,7 @@ const REQUESTED_FAUCET_PREFIX_ITEM = 0x0002 const REQUESTED_AMOUNT_ITEM = 0x0003 const PSWAP_TAG_ITEM = 0x0004 const P2ID_TAG_ITEM = 0x0005 +const PAYBACK_NOTE_TYPE_ITEM = 0x0006 const PSWAP_COUNT_ITEM = 0x0008 const PSWAP_CREATOR_PREFIX_ITEM = 0x000C const PSWAP_CREATOR_SUFFIX_ITEM = 0x000D @@ -528,7 +529,7 @@ proc execute_pswap mem_load.REQUESTED_FAUCET_SUFFIX_ITEM mem_load.REQUESTED_ENABLE_CALLBACKS_ITEM exec.active_note::get_serial_number - mem_load.NOTE_TYPE + mem_load.PAYBACK_NOTE_TYPE_ITEM mem_load.P2ID_TAG_ITEM mem_load.PSWAP_CREATOR_PREFIX_ITEM mem_load.PSWAP_CREATOR_SUFFIX_ITEM diff --git a/crates/miden-standards/src/note/pswap.rs b/crates/miden-standards/src/note/pswap.rs index 11b19fe356..dfff09fd75 100644 --- a/crates/miden-standards/src/note/pswap.rs +++ b/crates/miden-standards/src/note/pswap.rs @@ -43,7 +43,8 @@ static PSWAP_SCRIPT: LazyLock = LazyLock::new(|| { /// | `[3]` | Requested asset amount | /// | `[4]` | PSWAP note tag | /// | `[5]` | Payback note routing tag (targets the creator) | -/// | `[6-7]` | Reserved (zero) | +/// | `[6]` | Payback note type (0 = private, 1 = public) | +/// | `[7]` | Reserved (zero) | /// | `[8]` | Swap count (incremented on each partial fill) | /// | `[9-11]` | Reserved (zero) | /// | `[12-13]` | Creator account ID (prefix, suffix) | @@ -58,6 +59,14 @@ pub struct PswapNoteStorage { swap_count: u16, creator_account_id: AccountId, + + /// Note type of the payback note produced when the pswap is filled. Defaults to + /// [`NoteType::Private`] because the payback carries the fill asset and is typically + /// consumed directly by the creator — a private note is cheaper in fees and bandwidth + /// and offers the same information (the fill amount is already recorded in the + /// executed transaction's output). + #[builder(default = NoteType::Private)] + payback_note_type: NoteType, } impl PswapNoteStorage { @@ -109,6 +118,11 @@ impl PswapNoteStorage { self.creator_account_id } + /// Returns the [`NoteType`] used when creating the payback note. + pub fn payback_note_type(&self) -> NoteType { + self.payback_note_type + } + /// Returns the faucet ID of the requested asset. pub fn requested_faucet_id(&self) -> AccountId { self.requested_asset.faucet_id() @@ -133,8 +147,8 @@ impl From for NoteStorage { // Tags [4-5] Felt::from(storage.pswap_tag), Felt::from(storage.payback_note_tag()), - // Padding [6-7] - ZERO, + // Payback note type [6] + reserved [7] + Felt::from(storage.payback_note_type.as_u8()), ZERO, // Swap count [8-11] Felt::from(storage.swap_count), @@ -188,6 +202,13 @@ impl TryFrom<&[Felt]> for PswapNoteStorage { u32::try_from(note_storage[4].as_canonical_u64()) .map_err(|_| NoteError::other("pswap_tag exceeds u32"))?, ); + + let payback_note_type = NoteType::try_from( + u8::try_from(note_storage[6].as_canonical_u64()) + .map_err(|_| NoteError::other("payback_note_type exceeds u8"))?, + ) + .map_err(|e| NoteError::other_with_source("failed to parse payback note type", e))?; + let swap_count: u16 = note_storage[8] .as_canonical_u64() .try_into() @@ -203,6 +224,7 @@ impl TryFrom<&[Felt]> for PswapNoteStorage { pswap_tag, swap_count, creator_account_id, + payback_note_type, }) } } @@ -550,9 +572,10 @@ impl PswapNote { let attachment = NoteAttachment::new_word(NoteAttachmentScheme::none(), attachment_word); let p2id_assets = NoteAssets::new(vec![Asset::Fungible(payback_asset)])?; - let p2id_metadata = NoteMetadata::new(consumer_account_id, self.note_type) - .with_tag(payback_note_tag) - .with_attachment(attachment); + let p2id_metadata = + NoteMetadata::new(consumer_account_id, self.storage.payback_note_type) + .with_tag(payback_note_tag) + .with_attachment(attachment); Ok(Note::new(p2id_assets, p2id_metadata, recipient)) } @@ -581,6 +604,7 @@ impl PswapNote { .pswap_tag(self.storage.pswap_tag) .swap_count(next_swap_count) .creator_account_id(self.storage.creator_account_id) + .payback_note_type(self.storage.payback_note_type) .build(); // Remainder serial: increment most significant element (matching MASM movup.3 add.1 movdn.3) @@ -832,7 +856,7 @@ mod tests { Felt::try_from(requested_asset.amount()).unwrap(), Felt::from(0xc0000000u32), // pswap_tag Felt::from(0x80000001u32), // payback_note_tag - ZERO, + Felt::from(NoteType::Private.as_u8()), // payback_note_type ZERO, Felt::from(3u16), // swap_count ZERO, diff --git a/crates/miden-testing/tests/scripts/pswap.rs b/crates/miden-testing/tests/scripts/pswap.rs index c8574f7620..1f396357dd 100644 --- a/crates/miden-testing/tests/scripts/pswap.rs +++ b/crates/miden-testing/tests/scripts/pswap.rs @@ -1124,6 +1124,7 @@ fn compare_pswap_create_output_notes_vs_test_helper() { let storage = PswapNoteStorage::builder() .requested_asset(requested_asset) .creator_account_id(alice.id()) + .payback_note_type(NoteType::Public) .build(); let pswap_note: Note = PswapNote::builder() .sender(alice.id()) From 2629b8cdeb85ecd134f36c0bcd6c98da696d2df6 Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Mon, 13 Apr 2026 19:10:03 +0530 Subject: [PATCH 30/66] perf(pswap): compress note storage layout from 14 to 10 items Drops four reserved/zero padding slots ([7] and [9-11] in the old layout) that served no purpose. The compressed layout is contiguous: [0-3] requested asset (callbacks, faucet suffix/prefix, amount) [4] pswap_tag [5] payback_note_tag [6] payback_note_type [7] swap_count [8-9] creator account id (prefix, suffix) Saves 4 felts per pswap note on-chain, reducing fees and bandwidth for every pswap created. No information is lost. Addresses PR #2636 review comment from PhilippGackstatter on compressing the storage layout. --- .../asm/standards/notes/pswap.masm | 14 ++++----- crates/miden-standards/src/note/pswap.rs | 30 +++++++------------ 2 files changed, 17 insertions(+), 27 deletions(-) diff --git a/crates/miden-standards/asm/standards/notes/pswap.masm b/crates/miden-standards/asm/standards/notes/pswap.masm index 33a496a0e5..136a4f66f3 100644 --- a/crates/miden-standards/asm/standards/notes/pswap.masm +++ b/crates/miden-standards/asm/standards/notes/pswap.masm @@ -12,7 +12,7 @@ use miden::standards::notes::p2id # CONSTANTS # ================================================================================================= -const NUM_STORAGE_ITEMS=14 +const NUM_STORAGE_ITEMS=10 const NOTE_TYPE_MASK=0x03 const FACTOR=0x000186A0 # 1e5 const MAX_U32=0x0000000100000000 @@ -28,7 +28,7 @@ const MAX_U32=0x0000000100000000 # - execute_pswap: @locals(8) for offered asset info and calculation intermediates # - calculate_tokens_offered_for_requested: @locals(1) for input amount -# PSWAP Note Storage (14 items loaded at address 0) +# PSWAP Note Storage (10 items loaded at address 0) # Requested asset is stored as individual felts (fungible assets only): # [enable_callbacks, faucet_id_suffix, faucet_id_prefix, requested_amount] const REQUESTED_ENABLE_CALLBACKS_ITEM = 0x0000 @@ -38,9 +38,9 @@ const REQUESTED_AMOUNT_ITEM = 0x0003 const PSWAP_TAG_ITEM = 0x0004 const P2ID_TAG_ITEM = 0x0005 const PAYBACK_NOTE_TYPE_ITEM = 0x0006 -const PSWAP_COUNT_ITEM = 0x0008 -const PSWAP_CREATOR_PREFIX_ITEM = 0x000C -const PSWAP_CREATOR_SUFFIX_ITEM = 0x000D +const PSWAP_COUNT_ITEM = 0x0007 +const PSWAP_CREATOR_PREFIX_ITEM = 0x0008 +const PSWAP_CREATOR_SUFFIX_ITEM = 0x0009 # Global Memory Address (shared across procedures) const NOTE_TYPE = 0x0084 @@ -49,8 +49,8 @@ const NOTE_TYPE = 0x0084 # ERRORS # ================================================================================================= -# PSWAP script expects exactly 14 note storage items -const ERR_PSWAP_WRONG_NUMBER_OF_STORAGE_ITEMS="PSWAP script expects exactly 14 note storage items" +# PSWAP script expects exactly 10 note storage items +const ERR_PSWAP_WRONG_NUMBER_OF_STORAGE_ITEMS="PSWAP script expects exactly 10 note storage items" # PSWAP script requires exactly one note asset const ERR_PSWAP_WRONG_NUMBER_OF_ASSETS="PSWAP script requires exactly one note asset" diff --git a/crates/miden-standards/src/note/pswap.rs b/crates/miden-standards/src/note/pswap.rs index dfff09fd75..3057a743ac 100644 --- a/crates/miden-standards/src/note/pswap.rs +++ b/crates/miden-standards/src/note/pswap.rs @@ -44,10 +44,8 @@ static PSWAP_SCRIPT: LazyLock = LazyLock::new(|| { /// | `[4]` | PSWAP note tag | /// | `[5]` | Payback note routing tag (targets the creator) | /// | `[6]` | Payback note type (0 = private, 1 = public) | -/// | `[7]` | Reserved (zero) | -/// | `[8]` | Swap count (incremented on each partial fill) | -/// | `[9-11]` | Reserved (zero) | -/// | `[12-13]` | Creator account ID (prefix, suffix) | +/// | `[7]` | Swap count (incremented on each partial fill) | +/// | `[8-9]` | Creator account ID (prefix, suffix) | #[derive(Debug, Clone, PartialEq, Eq, bon::Builder)] pub struct PswapNoteStorage { requested_asset: FungibleAsset, @@ -74,7 +72,7 @@ impl PswapNoteStorage { // -------------------------------------------------------------------------------------------- /// Expected number of storage items for the PSWAP note. - pub const NUM_STORAGE_ITEMS: usize = 14; + pub const NUM_STORAGE_ITEMS: usize = 10; /// Consumes the storage and returns a PSWAP [`NoteRecipient`] with the provided serial number. pub fn into_recipient(self, serial_num: Word) -> NoteRecipient { @@ -134,7 +132,7 @@ impl PswapNoteStorage { } } -/// Serializes [`PswapNoteStorage`] into a 14-element [`NoteStorage`]. +/// Serializes [`PswapNoteStorage`] into a 10-element [`NoteStorage`]. impl From for NoteStorage { fn from(storage: PswapNoteStorage) -> Self { let storage_items = vec![ @@ -147,15 +145,11 @@ impl From for NoteStorage { // Tags [4-5] Felt::from(storage.pswap_tag), Felt::from(storage.payback_note_tag()), - // Payback note type [6] + reserved [7] + // Payback note type [6] Felt::from(storage.payback_note_type.as_u8()), - ZERO, - // Swap count [8-11] + // Swap count [7] Felt::from(storage.swap_count), - ZERO, - ZERO, - ZERO, - // Creator ID [12-13] + // Creator ID [8-9] storage.creator_account_id.prefix().as_felt(), storage.creator_account_id.suffix(), ]; @@ -164,7 +158,7 @@ impl From for NoteStorage { } } -/// Deserializes [`PswapNoteStorage`] from a slice of exactly 14 [`Felt`]s. +/// Deserializes [`PswapNoteStorage`] from a slice of exactly 10 [`Felt`]s. impl TryFrom<&[Felt]> for PswapNoteStorage { type Error = NoteError; @@ -209,12 +203,12 @@ impl TryFrom<&[Felt]> for PswapNoteStorage { ) .map_err(|e| NoteError::other_with_source("failed to parse payback note type", e))?; - let swap_count: u16 = note_storage[8] + let swap_count: u16 = note_storage[7] .as_canonical_u64() .try_into() .map_err(|_| NoteError::other("swap_count exceeds u16"))?; - let creator_account_id = AccountId::try_from_elements(note_storage[13], note_storage[12]) + let creator_account_id = AccountId::try_from_elements(note_storage[9], note_storage[8]) .map_err(|e| { NoteError::other_with_source("failed to parse creator account ID", e) })?; @@ -857,11 +851,7 @@ mod tests { Felt::from(0xc0000000u32), // pswap_tag Felt::from(0x80000001u32), // payback_note_tag Felt::from(NoteType::Private.as_u8()), // payback_note_type - ZERO, Felt::from(3u16), // swap_count - ZERO, - ZERO, - ZERO, creator_id.prefix().as_felt(), creator_id.suffix(), ]; From 971b5ae382f2e71b9e71cabd3096af0ae61d50a5 Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Mon, 13 Apr 2026 19:51:28 +0530 Subject: [PATCH 31/66] test(pswap): cover combined input+inflight partial and full fill paths Adds two unit tests against `PswapNote::execute` for the input+inflight code path flagged in PR #2636 review: - partial fill: account fill 10 + inflight 20 of a 50-requested pswap. Asserts payback carries 30 of the requested asset and a remainder pswap is produced with 20 requested / 40 offered remaining. - full fill: account fill 30 + inflight 20 of a 50-requested pswap. Asserts payback carries 50 of the requested asset and no remainder. Also factors out a `dummy_consumer_id()` helper alongside `dummy_creator_id()` to keep the new tests readable. --- crates/miden-standards/src/note/pswap.rs | 82 ++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/crates/miden-standards/src/note/pswap.rs b/crates/miden-standards/src/note/pswap.rs index 3057a743ac..e268b67b37 100644 --- a/crates/miden-standards/src/note/pswap.rs +++ b/crates/miden-standards/src/note/pswap.rs @@ -721,6 +721,15 @@ mod tests { ) } + fn dummy_consumer_id() -> AccountId { + AccountId::dummy( + [2; 15], + AccountIdVersion::Version0, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Public, + ) + } + fn build_pswap_note( offered_asset: FungibleAsset, requested_asset: FungibleAsset, @@ -879,4 +888,77 @@ mod tests { assert_eq!(parsed.swap_count(), 0); assert_eq!(parsed.requested_asset_amount(), 500); } + + /// Consumer supplies both an account fill and an inflight fill, and the sum is below + /// the requested amount → `execute` must combine them into a single payback note + /// carrying input+inflight of the requested asset and emit a remainder pswap note + /// for the unfilled portion. + #[test] + fn pswap_execute_combined_input_and_inflight_partial_fill() { + let creator_id = dummy_creator_id(); + let consumer_id = dummy_consumer_id(); + let offered_faucet = dummy_faucet_id(0xaa); + let requested_faucet = dummy_faucet_id(0xbb); + + // Offer 100 offered, request 50 requested → 2:1 ratio. + let offered_asset = FungibleAsset::new(offered_faucet, 100).unwrap(); + let requested_asset = FungibleAsset::new(requested_faucet, 50).unwrap(); + let (pswap, _) = build_pswap_note(offered_asset, requested_asset, creator_id); + + // Account fill = 10, inflight = 20 → total fill = 30 (< 50, so partial). + let input = FungibleAsset::new(requested_faucet, 10).unwrap(); + let inflight = FungibleAsset::new(requested_faucet, 20).unwrap(); + + let (payback, remainder) = pswap.execute(consumer_id, Some(input), Some(inflight)).unwrap(); + + // Payback note must carry the combined 30 of requested asset. + assert_eq!(payback.assets().num_assets(), 1); + let payback_asset = payback.assets().iter().next().unwrap(); + let Asset::Fungible(fa) = payback_asset else { + panic!("expected fungible payback asset"); + }; + assert_eq!(fa.faucet_id(), requested_faucet); + assert_eq!(fa.amount(), 30); + + // Remainder must exist with the unfilled 50 - 30 = 20 of requested, and the + // offered amount reduced proportionally (100 - 30*2 = 40). + let remainder = remainder.expect("partial fill should produce remainder"); + assert_eq!(remainder.storage().requested_asset_amount(), 20); + assert_eq!(remainder.offered_asset().amount(), 40); + assert_eq!(remainder.storage().swap_count(), 1); + assert_eq!(remainder.storage().creator_account_id(), creator_id); + } + + /// Consumer supplies both an account fill and an inflight fill, and the sum exactly + /// matches the requested amount → `execute` must produce a single payback note for + /// the full amount and no remainder. + #[test] + fn pswap_execute_combined_input_and_inflight_full_fill() { + let creator_id = dummy_creator_id(); + let consumer_id = dummy_consumer_id(); + let offered_faucet = dummy_faucet_id(0xaa); + let requested_faucet = dummy_faucet_id(0xbb); + + let offered_asset = FungibleAsset::new(offered_faucet, 100).unwrap(); + let requested_asset = FungibleAsset::new(requested_faucet, 50).unwrap(); + let (pswap, _) = build_pswap_note(offered_asset, requested_asset, creator_id); + + // Account fill = 30, inflight = 20 → total fill = 50 (exactly requested). + let input = FungibleAsset::new(requested_faucet, 30).unwrap(); + let inflight = FungibleAsset::new(requested_faucet, 20).unwrap(); + + let (payback, remainder) = pswap.execute(consumer_id, Some(input), Some(inflight)).unwrap(); + + // Payback note must carry the full 50 of requested asset. + assert_eq!(payback.assets().num_assets(), 1); + let payback_asset = payback.assets().iter().next().unwrap(); + let Asset::Fungible(fa) = payback_asset else { + panic!("expected fungible payback asset"); + }; + assert_eq!(fa.faucet_id(), requested_faucet); + assert_eq!(fa.amount(), 50); + + // Full fill → no remainder note. + assert!(remainder.is_none(), "full fill must not produce a remainder"); + } } From 22d5b6390247118ee39f8797a78cd0db5b2b23c6 Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Mon, 13 Apr 2026 22:53:32 +0530 Subject: [PATCH 32/66] refactor(pswap): rename input/inflight to account_fill/note_fill MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses the naming feedback on PR #2636. The old `input` / `inflight` terminology was ambiguous — "input" could mean the total fill amount, the note_args input word, or the account-side fill, while "inflight" conflated the transport mechanism with its role. The new names are explicit about where the fill comes from: - offered_out -> payout_amount - input / input_amount -> fill_amount (when referring to the total fill amount) or account_fill_amount (when referring to the native-account portion specifically) - inflight_amount -> note_fill_amount - input_asset -> account_fill_asset - inflight_asset -> note_fill_asset Touches the public `PswapNote::execute` parameters, all internal Rust locals and doc comments, the MASM stack annotations / doc blocks / local-var labels, and the integration test callers. --- .../asm/standards/notes/pswap.masm | 133 +++++++++--------- crates/miden-standards/src/note/pswap.rs | 97 +++++++------ crates/miden-testing/tests/scripts/pswap.rs | 48 +++---- 3 files changed, 143 insertions(+), 135 deletions(-) diff --git a/crates/miden-standards/asm/standards/notes/pswap.masm b/crates/miden-standards/asm/standards/notes/pswap.masm index 136a4f66f3..4b64a899ed 100644 --- a/crates/miden-standards/asm/standards/notes/pswap.masm +++ b/crates/miden-standards/asm/standards/notes/pswap.masm @@ -26,7 +26,7 @@ const MAX_U32=0x0000000100000000 # # Procedure-local values use @locals instead of global memory: # - execute_pswap: @locals(8) for offered asset info and calculation intermediates -# - calculate_tokens_offered_for_requested: @locals(1) for input amount +# - calculate_tokens_offered_for_requested: @locals(1) for fill amount # PSWAP Note Storage (10 items loaded at address 0) # Requested asset is stored as individual felts (fungible assets only): @@ -55,35 +55,35 @@ const ERR_PSWAP_WRONG_NUMBER_OF_STORAGE_ITEMS="PSWAP script expects exactly 10 n # PSWAP script requires exactly one note asset const ERR_PSWAP_WRONG_NUMBER_OF_ASSETS="PSWAP script requires exactly one note asset" -# PSWAP fill amount (account fill + inflight) exceeds the total requested amount +# PSWAP fill amount (account fill + note fill) exceeds the total requested amount const ERR_PSWAP_FILL_EXCEEDS_REQUESTED="PSWAP fill amount exceeds requested amount" # PRICE CALCULATION # ================================================================================================= -#! Computes the proportional amount of offered tokens for a given requested input. +#! Computes the proportional amount of offered tokens for a given fill amount. #! #! 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) +#! if fill_amount == requested: result = offered (full fill, avoids precision loss) +#! if offered >= requested: result = (offered * FACTOR / requested) * fill_amount / FACTOR +#! if requested > offered: result = (fill_amount * FACTOR) / (requested * FACTOR / offered) #! -#! Inputs: [offered, requested, input] (offered on top) -#! Outputs: [offered_out] +#! Inputs: [offered, requested, fill_amount] (offered on top) +#! Outputs: [payout_amount] #! @locals(1) -# loc.0 = input amount +# loc.0 = fill amount proc calculate_tokens_offered_for_requested movup.2 loc_store.0 # => [offered, requested] - # Early return: if input == requested (full fill), return offered directly. + # Early return: if fill_amount == requested (full fill), return offered directly. # This avoids precision loss from integer division with the FACTOR. dup.1 loc_load.0 eq - # => [requested == input, offered, requested] + # => [requested == fill_amount, offered, requested] if.true # Full fill: consumer provides all requested, gets all offered @@ -101,7 +101,7 @@ proc calculate_tokens_offered_for_requested if.true # Case: requested > offered # ratio = (requested * FACTOR) / offered - # result = (input * FACTOR) / ratio + # result = (fill_amount * FACTOR) / ratio swap # => [requested, offered] @@ -135,7 +135,7 @@ proc calculate_tokens_offered_for_requested else # Case: offered >= requested - # result = ((offered * FACTOR) / requested) * input / FACTOR + # result = ((offered * FACTOR) / requested) * fill_amount / FACTOR u32split push.FACTOR u32split # => [F_lo, F_hi, off_lo, off_hi, requested] @@ -210,10 +210,11 @@ end #! #! Derives a unique serial number by incrementing the least significant element #! of the provided serial, creates the output note using p2id::new, sets the -#! attachment, and adds the requested assets (from vault and/or inflight). +#! attachment, and adds the requested assets (from the consumer's account vault +#! and/or from another note in the same transaction). #! #! Inputs: [creator_suffix, creator_prefix, tag, note_type, SERIAL_NUM, -#! enable_callbacks, faucet_suffix, faucet_prefix, amt_acct_fill, amt_inflight] +#! enable_callbacks, faucet_suffix, faucet_prefix, amt_account_fill, amt_note_fill] #! Outputs: [] #! @locals(6) @@ -221,21 +222,21 @@ end # loc.1 = enable_callbacks # loc.2 = faucet_suffix # loc.3 = faucet_prefix -# loc.4 = amt_acct_fill -# loc.5 = amt_inflight +# loc.4 = amt_account_fill +# loc.5 = amt_note_fill proc create_p2id_note # Derive P2ID serial: increment least significant element movup.4 add.1 movdn.4 # => [creator_suffix, creator_prefix, tag, note_type, P2ID_SERIAL_NUM, - # enable_callbacks, faucet_suffix, faucet_prefix, amt_acct_fill, amt_inflight] + # enable_callbacks, faucet_suffix, faucet_prefix, amt_account_fill, amt_note_fill] exec.p2id::new - # => [note_idx, enable_callbacks, faucet_suffix, faucet_prefix, amt_acct_fill, amt_inflight] + # => [note_idx, enable_callbacks, faucet_suffix, faucet_prefix, amt_account_fill, amt_note_fill] loc_store.0 loc_store.1 loc_store.2 loc_store.3 loc_store.4 loc_store.5 # => [] - # Set attachment: aux = amt_acct_fill + amt_inflight + # Set attachment: aux = amt_account_fill + amt_note_fill # attachment_scheme = 0 (NoteAttachmentScheme::none) loc_load.4 loc_load.5 add # => [total_fill] @@ -247,7 +248,7 @@ proc create_p2id_note exec.output_note::set_word_attachment # => [] - # Move acct_fill_amount from consumer's vault to P2ID note (if > 0) + # Move account_fill_amount from consumer's vault to P2ID note (if > 0) loc_load.4 push.0 gt # => [amt > 0] if.true @@ -256,10 +257,10 @@ proc create_p2id_note # => [note_idx, pad(7)] push.0.0.0 loc_load.4 - # => [amt_acct_fill, 0, 0, 0, note_idx, pad(7)] + # => [amt_account_fill, 0, 0, 0, note_idx, pad(7)] loc_load.3 loc_load.2 loc_load.1 - # => [enable_cb, faucet_suffix, faucet_prefix, amt_acct_fill, 0, 0, 0, note_idx, pad(7)] + # => [enable_cb, faucet_suffix, faucet_prefix, amt_account_fill, 0, 0, 0, note_idx, pad(7)] exec.asset::create_fungible_asset # => [ASSET_KEY, ASSET_VALUE, note_idx, pad(7)] @@ -271,7 +272,7 @@ proc create_p2id_note # => [] end - # Add inflight_amount directly to P2ID note (no vault debit, if > 0) + # Add note_fill_amount directly to P2ID note (no vault debit, if > 0) loc_load.5 push.0 gt # => [amt > 0] if.true @@ -279,7 +280,7 @@ proc create_p2id_note # => [note_idx] loc_load.5 loc_load.3 loc_load.2 loc_load.1 - # => [enable_cb, faucet_suffix, faucet_prefix, amt_inflight, note_idx] + # => [enable_cb, faucet_suffix, faucet_prefix, amt_note_fill, note_idx] exec.asset::create_fungible_asset # => [ASSET_KEY, ASSET_VALUE, note_idx] @@ -300,7 +301,7 @@ end #! note, sets the attachment, and adds the remaining offered asset. #! #! Inputs: [offered_faucet_prefix, offered_faucet_suffix, offered_enable_cb, -#! amt_offered_out, amt_offered, +#! amt_payout, amt_offered, #! remaining_requested, note_type, tag] #! Outputs: [] #! @@ -309,11 +310,11 @@ end # loc.1 = offered_faucet_prefix # loc.2 = offered_faucet_suffix # loc.3 = offered_enable_cb -# loc.4 = amt_offered_out +# loc.4 = amt_payout # loc.5 = amt_offered proc create_remainder_note # => [offered_faucet_prefix, offered_faucet_suffix, offered_enable_cb, - # amt_offered_out, amt_offered, + # amt_payout, amt_offered, # remaining_requested, note_type, tag] # Store offered asset info to locals (top element stored first) @@ -352,16 +353,16 @@ proc create_remainder_note loc_store.0 # => [] - # Set attachment: aux = amt_offered_out + # Set attachment: aux = amt_payout loc_load.4 push.0.0.0 - # => [0, 0, 0, amt_offered_out] + # => [0, 0, 0, amt_payout] push.0 loc_load.0 # => [note_idx, attachment_scheme=0, ATTACHMENT] exec.output_note::set_word_attachment # => [] - # Add remaining offered asset: remainder_amount = amt_offered - amt_offered_out + # Add remaining offered asset: remainder_amount = amt_offered - amt_payout loc_load.0 # => [note_idx] @@ -435,20 +436,20 @@ end #! Sends offered tokens to consumer, requested tokens to creator via P2ID, #! and creates a remainder note if partially filled. #! -#! Inputs: [acct_fill_amount, inflight_amount] +#! Inputs: [account_fill_amount, note_fill_amount] #! Outputs: [] #! @locals(10) # loc.0 = amt_offered # loc.1 = amt_requested -# loc.2 = amt_offered_out (total) -# loc.3 = amt_offered_out_acct_fill -# loc.4 = amt_offered_out_inflight +# loc.2 = amt_payout (total) +# loc.3 = amt_payout_account_fill +# loc.4 = amt_payout_note_fill # loc.5 = offered_enable_cb # loc.6 = offered_faucet_suffix # loc.7 = offered_faucet_prefix -# loc.8 = amt_requested_acct_fill -# loc.9 = amt_requested_inflight +# loc.8 = amt_requested_account_fill +# loc.9 = amt_requested_note_fill proc execute_pswap loc_store.8 loc_store.9 # => [] @@ -478,7 +479,7 @@ proc execute_pswap loc_store.1 # => [] - # Validate: fill amount (account fill + inflight) must not exceed total requested. + # Validate: fill amount (account fill + note fill) must not exceed total requested. # Overfilling is not allowed because it is likely due to an error and mostly # unintentional — the consumer would lose the excess tokens with no change returned. loc_load.8 loc_load.9 add @@ -488,36 +489,36 @@ proc execute_pswap lte assert.err=ERR_PSWAP_FILL_EXCEEDS_REQUESTED # => [] - # Calculate offered_out for account fill and inflight amounts separately rather than + # Calculate payout_amount for account fill and note fill amounts separately rather than # summing them first, because the account fill portion must be sent - # to the consumer's vault individually, while the total (account fill + inflight) is + # to the consumer's vault individually, while the total (account fill + note fill) is # needed to determine the remainder note's offered amount. # - # Calculate offered_out for acct_fill_amount + # Calculate payout_amount for account_fill_amount loc_load.8 loc_load.1 loc_load.0 - # => [offered, requested, acct_fill_amount] + # => [offered, requested, account_fill_amount] exec.calculate_tokens_offered_for_requested - # => [acct_fill_offered_out] + # => [account_fill_payout] loc_store.3 # => [] - # Calculate offered_out for inflight_amount + # Calculate payout_amount for note_fill_amount loc_load.9 loc_load.1 loc_load.0 - # => [offered, requested, inflight_amount] + # => [offered, requested, note_fill_amount] exec.calculate_tokens_offered_for_requested - # => [inflight_offered_out] + # => [note_fill_payout] loc_store.4 # => [] - # total_offered_out = acct_fill_offered_out + inflight_offered_out + # total_payout = account_fill_payout + note_fill_payout loc_load.3 loc_load.4 add - # => [total_offered_out] + # => [total_payout] loc_store.2 # => [] @@ -534,12 +535,12 @@ proc execute_pswap mem_load.PSWAP_CREATOR_PREFIX_ITEM mem_load.PSWAP_CREATOR_SUFFIX_ITEM # => [creator_suffix, creator_prefix, tag, note_type, SERIAL_NUM, - # enable_callbacks, faucet_suffix, faucet_prefix, amt_acct_fill, amt_inflight] + # enable_callbacks, faucet_suffix, faucet_prefix, amt_account_fill, amt_note_fill] exec.create_p2id_note # => [] - # Consumer receives only acct_fill_offered_out into vault (not the inflight portion, if > 0) + # Consumer receives only account_fill_payout into vault (not the note_fill portion, if > 0) loc_load.3 push.0 gt # => [amt > 0] if.true @@ -550,7 +551,7 @@ proc execute_pswap loc_load.7 loc_load.6 loc_load.5 - # => [enable_cb, faucet_suffix, faucet_prefix, amt_offered_out_acct_fill, pad(8)] + # => [enable_cb, faucet_suffix, faucet_prefix, amt_payout_account_fill, pad(8)] exec.asset::create_fungible_asset # => [ASSET_KEY, ASSET_VALUE, pad(8)] @@ -586,7 +587,7 @@ proc execute_pswap loc_load.6 loc_load.7 # => [offered_faucet_prefix, offered_faucet_suffix, offered_enable_cb, - # amt_offered_out, amt_offered, + # amt_payout, amt_offered, # remaining_requested, note_type, tag] exec.create_remainder_note @@ -600,47 +601,47 @@ end @note_script pub proc main # => [NOTE_ARGS] - # Stack (top to bottom): [acct_fill_amount, inflight_amount, 0, 0] - # acct_fill_amount: debited from consumer's account vault - # inflight_amount: added directly from another note in the same transaction (no vault debit) + # Stack (top to bottom): [account_fill_amount, note_fill_amount, 0, 0] + # account_fill_amount: debited from consumer's account vault + # note_fill_amount: added directly from another note in the same transaction (no vault debit) # (Word[0] on top after mem_loadw_le in kernel prologue) # # In network transactions, note_args are not provided by the executor and default # to [0, 0, 0, 0]. The script handles this by defaulting to a full fill. - # => [acct_fill_amount, inflight_amount, 0, 0] + # => [account_fill_amount, note_fill_amount, 0, 0] movdn.3 movdn.3 drop drop - # => [acct_fill_amount, inflight_amount] + # => [account_fill_amount, note_fill_amount] # Load all note storage items to memory starting at address 0 push.0 exec.active_note::get_storage - # => [num_storage_items, storage_ptr, acct_fill_amount, inflight_amount] + # => [num_storage_items, storage_ptr, account_fill_amount, note_fill_amount] eq.NUM_STORAGE_ITEMS assert.err=ERR_PSWAP_WRONG_NUMBER_OF_STORAGE_ITEMS - # => [storage_ptr, acct_fill_amount, inflight_amount] + # => [storage_ptr, account_fill_amount, note_fill_amount] drop - # => [acct_fill_amount, inflight_amount] + # => [account_fill_amount, note_fill_amount] - # If both account fill and inflight are 0, default to a full fill (account fill = requested). + # If both account fill and note fill are 0, default to a full fill (account fill = requested). # This enables consumption by network accounts, which execute without note_args # (the kernel defaults note_args to [0, 0, 0, 0] when none are provided). dup.1 dup.1 add push.0 eq - # => [both_zero, acct_fill_amount, inflight_amount] + # => [both_zero, account_fill_amount, note_fill_amount] if.true drop mem_load.REQUESTED_AMOUNT_ITEM - # => [acct_fill_amount=requested, inflight_amount=0] + # => [account_fill_amount=requested, note_fill_amount=0] end # Extract and store note_type from active note metadata exec.extract_note_type - # => [acct_fill_amount, inflight_amount] + # => [account_fill_amount, note_fill_amount] mem_load.PSWAP_CREATOR_PREFIX_ITEM mem_load.PSWAP_CREATOR_SUFFIX_ITEM - # => [creator_id_suffix, creator_id_prefix, acct_fill_amount, inflight_amount] + # => [creator_id_suffix, creator_id_prefix, account_fill_amount, note_fill_amount] exec.is_consumer_creator - # => [is_creator, acct_fill_amount, inflight_amount] + # => [is_creator, account_fill_amount, note_fill_amount] if.true drop drop diff --git a/crates/miden-standards/src/note/pswap.rs b/crates/miden-standards/src/note/pswap.rs index e268b67b37..87b9acf050 100644 --- a/crates/miden-standards/src/note/pswap.rs +++ b/crates/miden-standards/src/note/pswap.rs @@ -357,7 +357,7 @@ impl PswapNote { /// Executes the swap, producing the output notes for a given fill. /// - /// `input_asset` is debited from the consumer's vault; `inflight_asset` arrives + /// `account_fill_asset` is debited from the consumer's vault; `note_fill_asset` arrives /// from another note in the same transaction (cross-swap). At least one must be /// provided. /// @@ -374,20 +374,23 @@ impl PswapNote { pub fn execute( &self, consumer_account_id: AccountId, - input_asset: Option, - inflight_asset: Option, + account_fill_asset: Option, + note_fill_asset: Option, ) -> Result<(Note, Option), NoteError> { - // Combine input and inflight into a single payback asset - let input_amount = input_asset.as_ref().map_or(0, |a| a.amount()); - let inflight_amount = inflight_asset.as_ref().map_or(0, |a| a.amount()); - let payback_asset = match (input_asset, inflight_asset) { - (Some(input), Some(inflight)) => input.add(inflight).map_err(|e| { - NoteError::other_with_source("failed to combine input and inflight assets", e) - })?, + // Combine account fill and note fill into a single payback asset. + let payback_asset = match (account_fill_asset, note_fill_asset) { + (Some(account_fill), Some(note_fill)) => { + account_fill.add(note_fill).map_err(|e| { + NoteError::other_with_source( + "failed to combine account fill and note fill assets", + e, + ) + })? + }, (Some(asset), None) | (None, Some(asset)) => asset, (None, None) => { return Err(NoteError::other( - "at least one of input_asset or inflight_asset must be provided", + "at least one of account_fill_asset or note_fill_asset must be provided", )); }, }; @@ -409,21 +412,23 @@ impl PswapNote { ))); } - // Calculate offered amounts separately for input and inflight, matching the MASM - // which calls calculate_tokens_offered_for_requested twice. This is necessary - // because the input portion goes to the consumer's vault while the total determines - // the remainder note's offered amount. - let offered_for_input = Self::calculate_output_amount( + // Calculate payout amounts separately for account fill and note fill, matching the + // MASM which calls calculate_tokens_offered_for_requested twice. This is necessary + // because the account fill portion goes to the consumer's vault while the total + // determines the remainder note's offered amount. + let account_fill_amount = account_fill_asset.as_ref().map_or(0, |a| a.amount()); + let note_fill_amount = note_fill_asset.as_ref().map_or(0, |a| a.amount()); + let payout_for_account_fill = Self::calculate_output_amount( total_offered_amount, total_requested_amount, - input_amount, + account_fill_amount, ); - let offered_for_inflight = Self::calculate_output_amount( + let payout_for_note_fill = Self::calculate_output_amount( total_offered_amount, total_requested_amount, - inflight_amount, + note_fill_amount, ); - let offered_amount_for_fill = offered_for_input + offered_for_inflight; + let offered_amount_for_fill = payout_for_account_fill + payout_for_note_fill; let payback_note = self.create_payback_note(consumer_account_id, payback_asset, fill_amount)?; @@ -456,13 +461,13 @@ impl PswapNote { Ok((payback_note, remainder)) } - /// Returns how many offered tokens a consumer receives for `input_amount` of the + /// Returns how many offered tokens a consumer receives for `fill_amount` of the /// requested asset, based on this note's current offered/requested ratio. - pub fn calculate_offered_for_requested(&self, input_amount: u64) -> u64 { + pub fn calculate_offered_for_requested(&self, fill_amount: u64) -> u64 { let total_requested = self.storage.requested_asset_amount(); let total_offered = self.offered_asset.amount(); - Self::calculate_output_amount(total_offered, total_requested, input_amount) + Self::calculate_output_amount(total_offered, total_requested, fill_amount) } // ASSOCIATED FUNCTIONS @@ -504,29 +509,29 @@ impl PswapNote { NoteTag::new(tag) } - /// Computes `offered_total * input_amount / requested_total` using fixed-point + /// Computes `offered_total * fill_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`. + /// calculation. Returns the full `offered_total` when `fill_amount == requested_total`. /// /// The formula is implemented in two branches to maximize precision: /// - When `offered > requested`: the ratio `offered/requested` is >= 1, so we compute - /// `(offered * FACTOR / requested) * input / FACTOR` to avoid losing the fractional part. + /// `(offered * FACTOR / requested) * fill_amount / FACTOR` to avoid losing the fractional part. /// - When `requested >= offered`: the ratio `offered/requested` is < 1, so computing it /// directly would truncate to zero. Instead we compute the inverse ratio - /// `(requested * FACTOR / offered)` and divide: `(input * FACTOR) / inverse_ratio`. - fn calculate_output_amount(offered_total: u64, requested_total: u64, input_amount: u64) -> u64 { + /// `(requested * FACTOR / offered)` and divide: `(fill_amount * FACTOR) / inverse_ratio`. + fn calculate_output_amount(offered_total: u64, requested_total: u64, fill_amount: u64) -> u64 { const PRECISION_FACTOR: u64 = 100_000; - if requested_total == input_amount { + if requested_total == fill_amount { return offered_total; } if offered_total > requested_total { let ratio = (offered_total * PRECISION_FACTOR) / requested_total; - (input_amount * ratio) / PRECISION_FACTOR + (fill_amount * ratio) / PRECISION_FACTOR } else { let ratio = (requested_total * PRECISION_FACTOR) / offered_total; - (input_amount * PRECISION_FACTOR) / ratio + (fill_amount * PRECISION_FACTOR) / ratio } } @@ -889,12 +894,12 @@ mod tests { assert_eq!(parsed.requested_asset_amount(), 500); } - /// Consumer supplies both an account fill and an inflight fill, and the sum is below + /// Consumer supplies both an account fill and a note fill, and the sum is below /// the requested amount → `execute` must combine them into a single payback note - /// carrying input+inflight of the requested asset and emit a remainder pswap note - /// for the unfilled portion. + /// carrying account_fill+note_fill of the requested asset and emit a remainder + /// pswap note for the unfilled portion. #[test] - fn pswap_execute_combined_input_and_inflight_partial_fill() { + fn pswap_execute_combined_account_fill_and_note_fill_partial_fill() { let creator_id = dummy_creator_id(); let consumer_id = dummy_consumer_id(); let offered_faucet = dummy_faucet_id(0xaa); @@ -905,11 +910,12 @@ mod tests { let requested_asset = FungibleAsset::new(requested_faucet, 50).unwrap(); let (pswap, _) = build_pswap_note(offered_asset, requested_asset, creator_id); - // Account fill = 10, inflight = 20 → total fill = 30 (< 50, so partial). - let input = FungibleAsset::new(requested_faucet, 10).unwrap(); - let inflight = FungibleAsset::new(requested_faucet, 20).unwrap(); + // Account fill = 10, note fill = 20 → total fill = 30 (< 50, so partial). + let account_fill = FungibleAsset::new(requested_faucet, 10).unwrap(); + let note_fill = FungibleAsset::new(requested_faucet, 20).unwrap(); - let (payback, remainder) = pswap.execute(consumer_id, Some(input), Some(inflight)).unwrap(); + let (payback, remainder) = + pswap.execute(consumer_id, Some(account_fill), Some(note_fill)).unwrap(); // Payback note must carry the combined 30 of requested asset. assert_eq!(payback.assets().num_assets(), 1); @@ -929,11 +935,11 @@ mod tests { assert_eq!(remainder.storage().creator_account_id(), creator_id); } - /// Consumer supplies both an account fill and an inflight fill, and the sum exactly + /// Consumer supplies both an account fill and a note fill, and the sum exactly /// matches the requested amount → `execute` must produce a single payback note for /// the full amount and no remainder. #[test] - fn pswap_execute_combined_input_and_inflight_full_fill() { + fn pswap_execute_combined_account_fill_and_note_fill_full_fill() { let creator_id = dummy_creator_id(); let consumer_id = dummy_consumer_id(); let offered_faucet = dummy_faucet_id(0xaa); @@ -943,11 +949,12 @@ mod tests { let requested_asset = FungibleAsset::new(requested_faucet, 50).unwrap(); let (pswap, _) = build_pswap_note(offered_asset, requested_asset, creator_id); - // Account fill = 30, inflight = 20 → total fill = 50 (exactly requested). - let input = FungibleAsset::new(requested_faucet, 30).unwrap(); - let inflight = FungibleAsset::new(requested_faucet, 20).unwrap(); + // Account fill = 30, note fill = 20 → total fill = 50 (exactly requested). + let account_fill = FungibleAsset::new(requested_faucet, 30).unwrap(); + let note_fill = FungibleAsset::new(requested_faucet, 20).unwrap(); - let (payback, remainder) = pswap.execute(consumer_id, Some(input), Some(inflight)).unwrap(); + let (payback, remainder) = + pswap.execute(consumer_id, Some(account_fill), Some(note_fill)).unwrap(); // Payback note must carry the full 50 of requested asset. assert_eq!(payback.assets().num_assets(), 1); diff --git a/crates/miden-testing/tests/scripts/pswap.rs b/crates/miden-testing/tests/scripts/pswap.rs index 1f396357dd..f5ec75717e 100644 --- a/crates/miden-testing/tests/scripts/pswap.rs +++ b/crates/miden-testing/tests/scripts/pswap.rs @@ -620,7 +620,7 @@ async fn pswap_note_invalid_input_test() -> anyhow::Result<()> { let result = tx_context.execute().await; assert!( result.is_err(), - "Transaction should fail when input_amount > requested_asset_total" + "Transaction should fail when fill_amount > requested_asset_total" ); Ok(()) @@ -640,7 +640,7 @@ async fn pswap_note_multiple_partial_fills_test() -> anyhow::Result<()> { (25, "25 ETH - 100% fill (full)"), ]; - for (input_amount, _description) in test_scenarios { + for (fill_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))?; @@ -652,7 +652,7 @@ async fn pswap_note_multiple_partial_fills_test() -> anyhow::Result<()> { let bob = builder.add_existing_wallet_with_assets( BASIC_AUTH, - [FungibleAsset::new(eth_faucet.id(), input_amount)?.into()], + [FungibleAsset::new(eth_faucet.id(), fill_amount)?.into()], )?; let mut rng = RandomCoin::new(Word::default()); @@ -676,14 +676,14 @@ async fn pswap_note_multiple_partial_fills_test() -> anyhow::Result<()> { let mut note_args_map = BTreeMap::new(); note_args_map.insert( pswap_note.id(), - Word::from([Felt::try_from(input_amount).unwrap(), Felt::from(0u32), ZERO, ZERO]), + Word::from([Felt::try_from(fill_amount).unwrap(), Felt::from(0u32), ZERO, ZERO]), ); let pswap = PswapNote::try_from(&pswap_note)?; - let offered_out = pswap.calculate_offered_for_requested(input_amount); + let payout_amount = pswap.calculate_offered_for_requested(fill_amount); let (p2id_note, remainder_pswap) = pswap.execute( bob.id(), - Some(FungibleAsset::new(eth_faucet.id(), input_amount)?), + Some(FungibleAsset::new(eth_faucet.id(), fill_amount)?), None, )?; @@ -702,7 +702,7 @@ async fn pswap_note_multiple_partial_fills_test() -> anyhow::Result<()> { let executed_transaction = tx_context.execute().await?; let output_notes = executed_transaction.output_notes(); - let expected_count = if input_amount < 25 { 2 } else { 1 }; + let expected_count = if fill_amount < 25 { 2 } else { 1 }; assert_eq!(output_notes.num_notes(), expected_count); // Verify Bob's vault @@ -710,7 +710,7 @@ async fn pswap_note_multiple_partial_fills_test() -> anyhow::Result<()> { 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); + assert_eq!(f.amount(), payout_amount); } } @@ -721,7 +721,7 @@ async fn pswap_note_multiple_partial_fills_test() -> anyhow::Result<()> { 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 fill_amount = 7u64; let mut builder = MockChain::builder(); let usdc_faucet = builder.add_existing_basic_faucet(BASIC_AUTH, "USDC", 10000, Some(1000))?; @@ -733,7 +733,7 @@ async fn pswap_note_non_exact_ratio_partial_fill_test() -> anyhow::Result<()> { )?; let bob = builder.add_existing_wallet_with_assets( BASIC_AUTH, - [FungibleAsset::new(eth_faucet.id(), input_amount)?.into()], + [FungibleAsset::new(eth_faucet.id(), fill_amount)?.into()], )?; let mut rng = RandomCoin::new(Word::default()); @@ -756,13 +756,13 @@ async fn pswap_note_non_exact_ratio_partial_fill_test() -> anyhow::Result<()> { let mut note_args_map = BTreeMap::new(); note_args_map - .insert(pswap_note.id(), Word::from([Felt::try_from(input_amount).unwrap(), Felt::from(0u32), ZERO, ZERO])); + .insert(pswap_note.id(), Word::from([Felt::try_from(fill_amount).unwrap(), Felt::from(0u32), ZERO, ZERO])); let pswap = PswapNote::try_from(&pswap_note)?; - let expected_output = pswap.calculate_offered_for_requested(input_amount); + let expected_output = pswap.calculate_offered_for_requested(fill_amount); let (p2id_note, remainder_pswap) = pswap.execute( bob.id(), - Some(FungibleAsset::new(eth_faucet.id(), input_amount)?), + Some(FungibleAsset::new(eth_faucet.id(), fill_amount)?), None, )?; let remainder = Note::from(remainder_pswap.expect("partial fill should produce remainder")); @@ -870,11 +870,11 @@ async fn pswap_note_partial_fill_non_integer_ratio_fuzz_test() -> anyhow::Result .insert(pswap_note.id(), Word::from([Felt::try_from(*fill_eth).unwrap(), Felt::from(0u32), ZERO, ZERO])); let pswap = PswapNote::try_from(&pswap_note)?; - let offered_out = pswap.calculate_offered_for_requested(*fill_eth); - let remaining_offered = offered_usdc - offered_out; + let payout_amount = pswap.calculate_offered_for_requested(*fill_eth); + let remaining_offered = offered_usdc - payout_amount; - assert!(offered_out > 0, "Case {}: offered_out must be > 0", i + 1); - assert!(offered_out <= *offered_usdc, "Case {}: offered_out > offered", i + 1); + assert!(payout_amount > 0, "Case {}: payout_amount must be > 0", i + 1); + assert!(payout_amount <= *offered_usdc, "Case {}: payout_amount > offered", i + 1); let (p2id_note, remainder_pswap) = pswap.execute( bob.id(), Some(FungibleAsset::new(eth_faucet.id(), *fill_eth)?), @@ -914,14 +914,14 @@ async fn pswap_note_partial_fill_non_integer_ratio_fuzz_test() -> anyhow::Result 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!(f.amount(), payout_amount, "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); + assert_eq!(payout_amount + remaining_offered, *offered_usdc, "Case {}: conservation", i + 1); } Ok(()) @@ -1013,8 +1013,8 @@ async fn pswap_note_chained_partial_fills_non_integer_ratio_test() -> anyhow::Re ); let pswap = PswapNote::try_from(&pswap_note)?; - let offered_out = pswap.calculate_offered_for_requested(*fill_amount); - let remaining_offered = current_offered - offered_out; + let payout_amount = pswap.calculate_offered_for_requested(*fill_amount); + let remaining_offered = current_offered - payout_amount; let (p2id_note, remainder_pswap) = pswap.execute( bob.id(), Some(FungibleAsset::new(eth_faucet.id(), *fill_amount)?), @@ -1062,16 +1062,16 @@ async fn pswap_note_chained_partial_fills_non_integer_ratio_test() -> anyhow::Re if let Asset::Fungible(f) = &added[0] { assert_eq!( f.amount(), - offered_out, + payout_amount, "Chain {} fill {}: Bob should get {} USDC", chain_idx + 1, current_swap_count + 1, - offered_out + payout_amount ); } // Update state for next fill - total_usdc_to_bob += offered_out; + total_usdc_to_bob += payout_amount; total_eth_from_bob += fill_amount; current_offered = remaining_offered; current_requested = remaining_requested; From 7dc90ffff40966dc8f23880367a7d1b7db0992cb Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Mon, 13 Apr 2026 23:16:24 +0530 Subject: [PATCH 33/66] refactor(pswap): fetch active note_type inline, drop global NOTE_TYPE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Upstream PR #2738 added `miden::protocol::note::metadata_into_note_type` which does the bit-correct extraction against the new 1-bit NoteType encoding. The pswap script no longer needs its own `extract_note_type` proc or the `NOTE_TYPE` global slot. Changes: - Drop the `extract_note_type` proc and the `NOTE_TYPE_MASK` / `NOTE_TYPE` consts. - The payback note already reads its type from `PAYBACK_NOTE_TYPE_ITEM` in storage (no active-note fetch needed). - The remainder branch inlines `active_note::get_metadata dropw exec.note::metadata_into_note_type` where the type is actually used, instead of eagerly computing it once and stashing it in global memory. - Update `pswap_note_alice_reconstructs_and_consumes_p2id` to set `payback_note_type(NoteType::Public)` so the reconstructed P2ID payback matches — the previous test implicitly relied on the payback inheriting the pswap's Public type (broken since the bit-encoding change). This also fixes the previously-failing `pswap_note_alice_reconstructs_and_consumes_p2id` test, which was hitting the broken extract_note_type mask (always returning 0 under the new encoding). --- .../asm/standards/notes/pswap.masm | 56 ++++--------------- crates/miden-testing/tests/scripts/pswap.rs | 1 + 2 files changed, 11 insertions(+), 46 deletions(-) diff --git a/crates/miden-standards/asm/standards/notes/pswap.masm b/crates/miden-standards/asm/standards/notes/pswap.masm index 4b64a899ed..5848577c8c 100644 --- a/crates/miden-standards/asm/standards/notes/pswap.masm +++ b/crates/miden-standards/asm/standards/notes/pswap.masm @@ -13,7 +13,6 @@ use miden::standards::notes::p2id # ================================================================================================= const NUM_STORAGE_ITEMS=10 -const NOTE_TYPE_MASK=0x03 const FACTOR=0x000186A0 # 1e5 const MAX_U32=0x0000000100000000 @@ -21,11 +20,10 @@ const MAX_U32=0x0000000100000000 # ================================================================================================= # Global Memory Address Layout: -# - PSWAP Note Storage: addresses 0x0000 - 0x000D (loaded from note storage) -# - NOTE_TYPE: 0x0084 (set by extract_note_type) +# - PSWAP Note Storage: addresses 0x0000 - 0x0009 (loaded from note storage) # # Procedure-local values use @locals instead of global memory: -# - execute_pswap: @locals(8) for offered asset info and calculation intermediates +# - execute_pswap: @locals(10) for offered asset info and calculation intermediates # - calculate_tokens_offered_for_requested: @locals(1) for fill amount # PSWAP Note Storage (10 items loaded at address 0) @@ -42,10 +40,6 @@ const PSWAP_COUNT_ITEM = 0x0007 const PSWAP_CREATOR_PREFIX_ITEM = 0x0008 const PSWAP_CREATOR_SUFFIX_ITEM = 0x0009 -# Global Memory Address (shared across procedures) -const NOTE_TYPE = 0x0084 - - # ERRORS # ================================================================================================= @@ -169,40 +163,6 @@ proc calculate_tokens_offered_for_requested end end -# METADATA PROCEDURES -# ================================================================================================= - -#! Extracts the note_type from the active note's metadata and stores it at NOTE_TYPE. -#! -#! get_metadata returns [NOTE_ATTACHMENT(4), METADATA_HEADER(4)]. -#! METADATA_HEADER word layout (see NoteMetadataHeader in miden-protocol/src/note/metadata.rs): -#! word[0] = sender_suffix_and_note_type (note_type in bits 0-1) -#! word[1] = sender_id_prefix -#! word[2] = tag -#! word[3] = attachment_kind_scheme -#! -#! After dropw and mem_loadw_le ordering, word[0] is on top of the stack. -#! -#! Inputs: [] -#! Outputs: [] -#! -proc extract_note_type - exec.active_note::get_metadata - # => [NOTE_ATTACHMENT(4), METADATA_HEADER(4)] - dropw - # => [word[0]=sender_suffix_and_note_type, word[1]=prefix, word[2]=tag, word[3]=attachment] - # Keep word[0] (top), move it to bottom and drop the rest - movdn.3 drop drop drop - # => [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 - # P2ID NOTE CREATION PROCEDURE # ================================================================================================= @@ -575,7 +535,14 @@ proc execute_pswap # => [remaining_requested] mem_load.PSWAP_TAG_ITEM - mem_load.NOTE_TYPE + # => [tag, remaining_requested] + + # Fetch the active note's type to inherit it for the remainder pswap note. + exec.active_note::get_metadata + # => [NOTE_ATTACHMENT(4), METADATA_HEADER(4), tag, remaining_requested] + dropw + # => [METADATA_HEADER(4), tag, remaining_requested] + exec.note::metadata_into_note_type # => [note_type, tag, remaining_requested] movup.2 @@ -632,9 +599,6 @@ pub proc main drop mem_load.REQUESTED_AMOUNT_ITEM # => [account_fill_amount=requested, note_fill_amount=0] end - - # Extract and store note_type from active note metadata - exec.extract_note_type # => [account_fill_amount, note_fill_amount] mem_load.PSWAP_CREATOR_PREFIX_ITEM mem_load.PSWAP_CREATOR_SUFFIX_ITEM diff --git a/crates/miden-testing/tests/scripts/pswap.rs b/crates/miden-testing/tests/scripts/pswap.rs index f5ec75717e..553ee97bca 100644 --- a/crates/miden-testing/tests/scripts/pswap.rs +++ b/crates/miden-testing/tests/scripts/pswap.rs @@ -55,6 +55,7 @@ async fn pswap_note_alice_reconstructs_and_consumes_p2id() -> anyhow::Result<()> let storage = PswapNoteStorage::builder() .requested_asset(requested_asset) .creator_account_id(alice.id()) + .payback_note_type(NoteType::Public) .build(); let pswap_note: Note = PswapNote::builder() .sender(alice.id()) From cbfbedc5b02a4ea945af900b8202e9ac036067c4 Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Tue, 14 Apr 2026 01:38:17 +0530 Subject: [PATCH 34/66] refactor(pswap): switch storage consts to STORAGE_PTR base pattern Matches the convention used in p2id.masm: declare a single `STORAGE_PTR` base and derive every note-storage item offset relative to it. This makes the storage layout easier to read and relocate. Also use `push.STORAGE_PTR` at the load site instead of a bare `0`. --- .../asm/standards/notes/pswap.masm | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/crates/miden-standards/asm/standards/notes/pswap.masm b/crates/miden-standards/asm/standards/notes/pswap.masm index 5848577c8c..25f3cf393f 100644 --- a/crates/miden-standards/asm/standards/notes/pswap.masm +++ b/crates/miden-standards/asm/standards/notes/pswap.masm @@ -26,19 +26,20 @@ const MAX_U32=0x0000000100000000 # - execute_pswap: @locals(10) for offered asset info and calculation intermediates # - calculate_tokens_offered_for_requested: @locals(1) for fill amount -# PSWAP Note Storage (10 items loaded at address 0) +# PSWAP Note Storage (10 items loaded at STORAGE_PTR) # Requested asset is stored as individual felts (fungible assets only): # [enable_callbacks, faucet_id_suffix, faucet_id_prefix, requested_amount] -const REQUESTED_ENABLE_CALLBACKS_ITEM = 0x0000 -const REQUESTED_FAUCET_SUFFIX_ITEM = 0x0001 -const REQUESTED_FAUCET_PREFIX_ITEM = 0x0002 -const REQUESTED_AMOUNT_ITEM = 0x0003 -const PSWAP_TAG_ITEM = 0x0004 -const P2ID_TAG_ITEM = 0x0005 -const PAYBACK_NOTE_TYPE_ITEM = 0x0006 -const PSWAP_COUNT_ITEM = 0x0007 -const PSWAP_CREATOR_PREFIX_ITEM = 0x0008 -const PSWAP_CREATOR_SUFFIX_ITEM = 0x0009 +const STORAGE_PTR = 0 +const REQUESTED_ENABLE_CALLBACKS_ITEM = STORAGE_PTR +const REQUESTED_FAUCET_SUFFIX_ITEM = STORAGE_PTR + 1 +const REQUESTED_FAUCET_PREFIX_ITEM = STORAGE_PTR + 2 +const REQUESTED_AMOUNT_ITEM = STORAGE_PTR + 3 +const PSWAP_TAG_ITEM = STORAGE_PTR + 4 +const P2ID_TAG_ITEM = STORAGE_PTR + 5 +const PAYBACK_NOTE_TYPE_ITEM = STORAGE_PTR + 6 +const PSWAP_COUNT_ITEM = STORAGE_PTR + 7 +const PSWAP_CREATOR_PREFIX_ITEM = STORAGE_PTR + 8 +const PSWAP_CREATOR_SUFFIX_ITEM = STORAGE_PTR + 9 # ERRORS # ================================================================================================= @@ -581,7 +582,7 @@ pub proc main # => [account_fill_amount, note_fill_amount] # Load all note storage items to memory starting at address 0 - push.0 exec.active_note::get_storage + push.STORAGE_PTR exec.active_note::get_storage # => [num_storage_items, storage_ptr, account_fill_amount, note_fill_amount] eq.NUM_STORAGE_ITEMS assert.err=ERR_PSWAP_WRONG_NUMBER_OF_STORAGE_ITEMS From 1e4d2f806bee00d5a2a7d69c95982b362374cf30 Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Tue, 14 Apr 2026 01:56:39 +0530 Subject: [PATCH 35/66] docs(pswap): add #! doc block to @note_script main procedure Closes PR #2636 review comment: `pub proc main` lacked a proper `#!` doc block with `Inputs`/`Outputs`/storage layout/panic conditions. Adds one modelled after the canonical style in p2id.masm / swap.masm. --- .../asm/standards/notes/pswap.masm | 49 +++++++++++++++---- 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/crates/miden-standards/asm/standards/notes/pswap.masm b/crates/miden-standards/asm/standards/notes/pswap.masm index 25f3cf393f..fbb5b1f02f 100644 --- a/crates/miden-standards/asm/standards/notes/pswap.masm +++ b/crates/miden-standards/asm/standards/notes/pswap.masm @@ -566,18 +566,47 @@ proc execute_pswap exec.sys::truncate_stack end +#! Partially-fillable swap note script: exchanges a portion of the offered asset for a +#! proportional amount of the requested asset, producing a P2ID payback note for the creator +#! and, on partial fills, a remainder PSWAP note carrying the unfilled balance. +#! +#! The consumer specifies two fill amounts via `NOTE_ARGS`: +#! - `account_fill_amount`: portion of the requested asset debited from the consumer's vault. +#! - `note_fill_amount`: portion of the requested asset sourced from another note in the +#! same transaction (cross-swap / net-zero flow, no vault debit). +#! +#! At least one of the two must be non-zero. If both are zero — which is the default in +#! network transactions where the executor does not provide note_args — the script falls back +#! to a full fill (`account_fill_amount = requested`). If the consuming account is the note's +#! creator, the script reclaims the offered asset back to the creator's vault instead. +#! +#! Requires that the account exposes: +#! - `miden::standards::wallets::basic::receive_asset` procedure. +#! - `miden::standards::wallets::basic::move_asset_to_note` procedure. +#! +#! Inputs: [NOTE_ARGS] +#! Outputs: [] +#! +#! Where NOTE_ARGS = [account_fill_amount, note_fill_amount, 0, 0] (top of stack first). +#! +#! Note storage is assumed to be as follows (10 items at STORAGE_PTR): +#! - [0] requested_asset enable_callbacks flag +#! - [1-2] requested_asset faucet ID (suffix, prefix) +#! - [3] requested_asset amount +#! - [4] PSWAP note tag +#! - [5] payback note routing tag (targets the creator) +#! - [6] payback note type (0 = private, 1 = public) +#! - [7] swap count (incremented on each partial fill) +#! - [8-9] creator account ID (prefix, suffix) +#! +#! Panics if: +#! - the number of note storage items is not `NUM_STORAGE_ITEMS`. +#! - the note does not carry exactly one offered asset. +#! - the total fill amount (account fill + note fill) exceeds the requested amount. +#! - the account does not expose `receive_asset` / `move_asset_to_note`. @note_script pub proc main - # => [NOTE_ARGS] - # Stack (top to bottom): [account_fill_amount, note_fill_amount, 0, 0] - # account_fill_amount: debited from consumer's account vault - # note_fill_amount: added directly from another note in the same transaction (no vault debit) - # (Word[0] on top after mem_loadw_le in kernel prologue) - # - # In network transactions, note_args are not provided by the executor and default - # to [0, 0, 0, 0]. The script handles this by defaulting to a full fill. - - # => [account_fill_amount, note_fill_amount, 0, 0] + # => [NOTE_ARGS = [account_fill_amount, note_fill_amount, 0, 0]] movdn.3 movdn.3 drop drop # => [account_fill_amount, note_fill_amount] From 974c8d981a08ae5b15802ce4d8ef6dcfba365bfa Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Tue, 14 Apr 2026 02:07:17 +0530 Subject: [PATCH 36/66] refactor(pswap): scrub last inflight/input references in tests Rename `pswap_note_inflight_cross_swap_test` to `pswap_note_note_fill_cross_swap_test` and update two stale comments that still said "input" / "inflight" to match the new account_fill / note_fill terminology used throughout the module. --- crates/miden-testing/tests/scripts/pswap.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/miden-testing/tests/scripts/pswap.rs b/crates/miden-testing/tests/scripts/pswap.rs index 553ee97bca..96f5296c90 100644 --- a/crates/miden-testing/tests/scripts/pswap.rs +++ b/crates/miden-testing/tests/scripts/pswap.rs @@ -417,7 +417,7 @@ async fn pswap_note_partial_fill_test() -> anyhow::Result<()> { } #[tokio::test] -async fn pswap_note_inflight_cross_swap_test() -> anyhow::Result<()> { +async fn pswap_note_note_fill_cross_swap_test() -> anyhow::Result<()> { let mut builder = MockChain::builder(); let usdc_faucet = builder.add_existing_basic_faucet(BASIC_AUTH, "USDC", 1000, Some(150))?; @@ -469,7 +469,7 @@ async fn pswap_note_inflight_cross_swap_test() -> anyhow::Result<()> { let mock_chain = builder.build()?; - // Note args: pure inflight (input=0, inflight=full amount) + // Note args: pure note fill (account_fill = 0, note_fill = full amount) let mut note_args_map = BTreeMap::new(); note_args_map .insert(alice_pswap_note.id(), Word::from([Felt::from(0u32), Felt::from(25u32), ZERO, ZERO])); @@ -1274,7 +1274,7 @@ async fn pswap_note_network_account_full_fill_test() -> anyhow::Result<()> { let mut mock_chain = builder.build()?; // No note_args — simulates a network transaction where args default to [0, 0, 0, 0]. - // The PSWAP script defaults to a full fill when both input and inflight are 0. + // The PSWAP script defaults to a full fill when both account_fill and note_fill are 0. let pswap = PswapNote::try_from(&pswap_note)?; let p2id_note = pswap.execute_full_fill_network(network_consumer.id())?; From 2e4813022c59e5214bf27641dc71974dba595ca9 Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Wed, 15 Apr 2026 01:08:05 +0530 Subject: [PATCH 37/66] style(pswap): readability nits across pswap.masm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Minor cleanups surfaced during review, no behavior changes: - Sort imports alphabetically by path. - Reshape storage layout doc block to `[i..j] : N felts` + `Where:` form and drop the duplicate listing in the main proc docstring. - Drop the stale "Memory Addresses" meta-block. - Name local slots via top-of-file consts (e.g. CALC_FILL_AMOUNT, P2ID_NOTE_IDX, REMAINDER_AMT_OFFERED, EXEC_AMT_OFFERED, ...) so loc_store/loc_load sites read like variables rather than loc.N. - Expand the rationale for the FACTOR=1e5 choice (precision vs u64 overflow headroom) and switch the literal to decimal. - Rename boolean stack-comment markers from operator syntax (`amt > 0`, `requested > offered`, `requested == fill_amount`) to readable predicates (`has_account_fill`, `has_note_fill`, `has_account_fill_payout`, `requested_exceeds_offered`, `is_full_fill`). - Drop obvious/redundant comments (error-const headers, `gt pops…` primitive explanation, proc-start stack echoes, validate/calculate rationale moved into the execute_pswap docstring). - Split `push.NUM_STORAGE_ITEMS.0` into two pushes using the STORAGE_PTR constant to match the `bridge_out.masm` pattern. - Add converged stack markers (`# => [...]`) after every if/else `end` and before every proc return. --- .../asm/standards/notes/pswap.masm | 339 +++++++++++------- 1 file changed, 201 insertions(+), 138 deletions(-) diff --git a/crates/miden-standards/asm/standards/notes/pswap.masm b/crates/miden-standards/asm/standards/notes/pswap.masm index fbb5b1f02f..02e95092d2 100644 --- a/crates/miden-standards/asm/standards/notes/pswap.masm +++ b/crates/miden-standards/asm/standards/notes/pswap.masm @@ -1,34 +1,57 @@ -use miden::protocol::active_note -use miden::protocol::output_note -use miden::protocol::note -use miden::standards::wallets::basic->wallet +use miden::core::math::u64 use miden::core::sys -use miden::protocol::active_account use miden::protocol::account_id -use miden::core::math::u64 +use miden::protocol::active_account +use miden::protocol::active_note use miden::protocol::asset +use miden::protocol::note +use miden::protocol::output_note use miden::standards::notes::p2id +use miden::standards::wallets::basic->wallet # CONSTANTS # ================================================================================================= const NUM_STORAGE_ITEMS=10 -const FACTOR=0x000186A0 # 1e5 -const MAX_U32=0x0000000100000000 -# Memory Addresses -# ================================================================================================= - -# Global Memory Address Layout: -# - PSWAP Note Storage: addresses 0x0000 - 0x0009 (loaded from note storage) +# Fixed-point scaling factor (1e5) used by calculate_tokens_offered_for_requested +# to approximate the offered/requested ratio with u64 integer math. +# +# Precision: ~5 decimal digits on the ratio; rounding is always toward zero +# (integer division), so a filler never receives more than proportional. # -# Procedure-local values use @locals instead of global memory: -# - execute_pswap: @locals(10) for offered asset info and calculation intermediates -# - calculate_tokens_offered_for_requested: @locals(1) for fill amount +# Overflow headroom: the hottest intermediate is `offered * FACTOR` (and +# symmetrically `requested * FACTOR`, `fill_amount * FACTOR`), which must fit +# in u64. With FACTOR = 100000 that caps each side at ~1.8e14 (2^64 / 1e5), +# well above any realistic fungible-asset amount. +const FACTOR=100000 +const MAX_U32=0x0000000100000000 -# PSWAP Note Storage (10 items loaded at STORAGE_PTR) -# Requested asset is stored as individual felts (fungible assets only): -# [enable_callbacks, faucet_id_suffix, faucet_id_prefix, requested_amount] +# Note storage layout (10 felts, loaded at STORAGE_PTR by get_storage): +# - requested_enable_callbacks [0] : 1 felt +# - requested_faucet_suffix [1] : 1 felt +# - requested_faucet_prefix [2] : 1 felt +# - requested_amount [3] : 1 felt +# - pswap_tag [4] : 1 felt +# - p2id_tag [5] : 1 felt +# - payback_note_type [6] : 1 felt +# - pswap_count [7] : 1 felt +# - creator_id_prefix [8] : 1 felt +# - creator_id_suffix [9] : 1 felt +# +# Where: +# - requested_enable_callbacks: Callback-enabled flag for the requested fungible asset +# (FungibleAsset::callbacks() as u8) +# - requested_faucet_suffix: Suffix of the requested asset's faucet AccountId (Felt) +# - requested_faucet_prefix: Prefix of the requested asset's faucet AccountId +# (AccountIdPrefix as Felt) +# - requested_amount: Amount of the requested fungible asset (Felt) +# - pswap_tag: The NoteTag for remainder PSWAP notes, derived from the offered/requested asset pair +# - p2id_tag: The NoteTag for P2ID payback notes, derived from the creator's account ID +# - payback_note_type: The NoteType propagated to the P2ID payback and remainder PSWAP notes +# - pswap_count: Number of times this note has been partially filled and re-created +# - creator_id_prefix: The prefix of the creator's AccountId (AccountIdPrefix as Felt) +# - creator_id_suffix: The suffix of the creator's AccountId (Felt) const STORAGE_PTR = 0 const REQUESTED_ENABLE_CALLBACKS_ITEM = STORAGE_PTR const REQUESTED_FAUCET_SUFFIX_ITEM = STORAGE_PTR + 1 @@ -41,16 +64,45 @@ const PSWAP_COUNT_ITEM = STORAGE_PTR + 7 const PSWAP_CREATOR_PREFIX_ITEM = STORAGE_PTR + 8 const PSWAP_CREATOR_SUFFIX_ITEM = STORAGE_PTR + 9 +# Local memory offsets +# ------------------------------------------------------------------------------------------------- + +# calculate_tokens_offered_for_requested locals +const CALC_FILL_AMOUNT = 0 + +# create_p2id_note locals +const P2ID_NOTE_IDX = 0 +const P2ID_REQUESTED_ENABLE_CB = 1 +const P2ID_REQUESTED_FAUCET_SUFFIX = 2 +const P2ID_REQUESTED_FAUCET_PREFIX = 3 +const P2ID_AMT_ACCOUNT_FILL = 4 +const P2ID_AMT_NOTE_FILL = 5 + +# create_remainder_note locals +const REMAINDER_NOTE_IDX = 0 +const REMAINDER_OFFERED_FAUCET_PREFIX = 1 +const REMAINDER_OFFERED_FAUCET_SUFFIX = 2 +const REMAINDER_OFFERED_ENABLE_CB = 3 +const REMAINDER_AMT_PAYOUT = 4 +const REMAINDER_AMT_OFFERED = 5 + +# execute_pswap locals +const EXEC_AMT_OFFERED = 0 +const EXEC_AMT_REQUESTED = 1 +const EXEC_AMT_PAYOUT_TOTAL = 2 +const EXEC_AMT_PAYOUT_ACCOUNT_FILL = 3 +const EXEC_AMT_PAYOUT_NOTE_FILL = 4 +const EXEC_OFFERED_ENABLE_CB = 5 +const EXEC_OFFERED_FAUCET_SUFFIX = 6 +const EXEC_OFFERED_FAUCET_PREFIX = 7 +const EXEC_AMT_REQUESTED_ACCOUNT_FILL = 8 +const EXEC_AMT_REQUESTED_NOTE_FILL = 9 + # ERRORS # ================================================================================================= -# PSWAP script expects exactly 10 note storage items const ERR_PSWAP_WRONG_NUMBER_OF_STORAGE_ITEMS="PSWAP script expects exactly 10 note storage items" - -# PSWAP script requires exactly one note asset const ERR_PSWAP_WRONG_NUMBER_OF_ASSETS="PSWAP script requires exactly one note asset" - -# PSWAP fill amount (account fill + note fill) exceeds the total requested amount const ERR_PSWAP_FILL_EXCEEDS_REQUESTED="PSWAP fill amount exceeds requested amount" # PRICE CALCULATION @@ -70,15 +122,15 @@ const ERR_PSWAP_FILL_EXCEEDS_REQUESTED="PSWAP fill amount exceeds requested amou #! Outputs: [payout_amount] #! @locals(1) -# loc.0 = fill amount +# CALC_FILL_AMOUNT: fill amount proc calculate_tokens_offered_for_requested - movup.2 loc_store.0 + movup.2 loc_store.CALC_FILL_AMOUNT # => [offered, requested] # Early return: if fill_amount == requested (full fill), return offered directly. # This avoids precision loss from integer division with the FACTOR. - dup.1 loc_load.0 eq - # => [requested == fill_amount, offered, requested] + dup.1 loc_load.CALC_FILL_AMOUNT eq + # => [is_full_fill, offered, requested] if.true # Full fill: consumer provides all requested, gets all offered @@ -90,8 +142,7 @@ proc calculate_tokens_offered_for_requested # => [offered, requested, offered, requested] gt - # gt pops [b=offered, a=requested], pushes (a > b) i.e. (requested > offered) - # => [requested > offered, offered, requested] + # => [requested_exceeds_offered, offered, requested] if.true # Case: requested > offered @@ -113,7 +164,7 @@ proc calculate_tokens_offered_for_requested exec.u64::div # => [ratio_lo, ratio_hi] - loc_load.0 u32split push.FACTOR u32split + loc_load.CALC_FILL_AMOUNT u32split push.FACTOR u32split # => [F_lo, F_hi, in_lo, in_hi, ratio_lo, ratio_hi] exec.u64::wrapping_mul @@ -144,7 +195,7 @@ proc calculate_tokens_offered_for_requested exec.u64::div # => [ratio_lo, ratio_hi] - loc_load.0 u32split + loc_load.CALC_FILL_AMOUNT u32split # => [in_lo, in_hi, ratio_lo, ratio_hi] exec.u64::wrapping_mul @@ -158,10 +209,10 @@ proc calculate_tokens_offered_for_requested swap mul.MAX_U32 add # => [result] - end - + # => [result] end + # => [payout_amount] end # P2ID NOTE CREATION PROCEDURE @@ -179,12 +230,12 @@ end #! Outputs: [] #! @locals(6) -# loc.0 = note_idx -# loc.1 = enable_callbacks -# loc.2 = faucet_suffix -# loc.3 = faucet_prefix -# loc.4 = amt_account_fill -# loc.5 = amt_note_fill +# P2ID_NOTE_IDX : note_idx +# P2ID_REQUESTED_ENABLE_CB : enable_callbacks +# P2ID_REQUESTED_FAUCET_SUFFIX : faucet_suffix +# P2ID_REQUESTED_FAUCET_PREFIX : faucet_prefix +# P2ID_AMT_ACCOUNT_FILL : amt_account_fill +# P2ID_AMT_NOTE_FILL : amt_note_fill proc create_p2id_note # Derive P2ID serial: increment least significant element movup.4 add.1 movdn.4 @@ -194,33 +245,40 @@ proc create_p2id_note exec.p2id::new # => [note_idx, enable_callbacks, faucet_suffix, faucet_prefix, amt_account_fill, amt_note_fill] - loc_store.0 loc_store.1 loc_store.2 loc_store.3 loc_store.4 loc_store.5 + loc_store.P2ID_NOTE_IDX + loc_store.P2ID_REQUESTED_ENABLE_CB + loc_store.P2ID_REQUESTED_FAUCET_SUFFIX + loc_store.P2ID_REQUESTED_FAUCET_PREFIX + loc_store.P2ID_AMT_ACCOUNT_FILL + loc_store.P2ID_AMT_NOTE_FILL # => [] # Set attachment: aux = amt_account_fill + amt_note_fill # attachment_scheme = 0 (NoteAttachmentScheme::none) - loc_load.4 loc_load.5 add + loc_load.P2ID_AMT_ACCOUNT_FILL loc_load.P2ID_AMT_NOTE_FILL add # => [total_fill] push.0.0.0 # => [0, 0, 0, total_fill] - push.0 loc_load.0 + push.0 loc_load.P2ID_NOTE_IDX # => [note_idx, attachment_scheme=0, 0, 0, 0, total_fill] exec.output_note::set_word_attachment # => [] # Move account_fill_amount from consumer's vault to P2ID note (if > 0) - loc_load.4 push.0 gt - # => [amt > 0] + loc_load.P2ID_AMT_ACCOUNT_FILL push.0 gt + # => [has_account_fill] if.true # Build 16-element call frame: [ASSET_KEY, ASSET_VALUE, note_idx, pad(7)] - padw push.0.0.0 loc_load.0 + padw push.0.0.0 loc_load.P2ID_NOTE_IDX # => [note_idx, pad(7)] - push.0.0.0 loc_load.4 + push.0.0.0 loc_load.P2ID_AMT_ACCOUNT_FILL # => [amt_account_fill, 0, 0, 0, note_idx, pad(7)] - loc_load.3 loc_load.2 loc_load.1 + loc_load.P2ID_REQUESTED_FAUCET_PREFIX + loc_load.P2ID_REQUESTED_FAUCET_SUFFIX + loc_load.P2ID_REQUESTED_ENABLE_CB # => [enable_cb, faucet_suffix, faucet_prefix, amt_account_fill, 0, 0, 0, note_idx, pad(7)] exec.asset::create_fungible_asset @@ -232,15 +290,19 @@ proc create_p2id_note dropw dropw dropw dropw # => [] end + # => [] # Add note_fill_amount directly to P2ID note (no vault debit, if > 0) - loc_load.5 push.0 gt - # => [amt > 0] + loc_load.P2ID_AMT_NOTE_FILL push.0 gt + # => [has_note_fill] if.true - loc_load.0 + loc_load.P2ID_NOTE_IDX # => [note_idx] - loc_load.5 loc_load.3 loc_load.2 loc_load.1 + loc_load.P2ID_AMT_NOTE_FILL + loc_load.P2ID_REQUESTED_FAUCET_PREFIX + loc_load.P2ID_REQUESTED_FAUCET_SUFFIX + loc_load.P2ID_REQUESTED_ENABLE_CB # => [enable_cb, faucet_suffix, faucet_prefix, amt_note_fill, note_idx] exec.asset::create_fungible_asset @@ -249,6 +311,7 @@ proc create_p2id_note exec.output_note::add_asset # => [] end + # => [] end # REMAINDER NOTE CREATION PROCEDURE @@ -267,19 +330,19 @@ end #! Outputs: [] #! @locals(6) -# loc.0 = note_idx -# loc.1 = offered_faucet_prefix -# loc.2 = offered_faucet_suffix -# loc.3 = offered_enable_cb -# loc.4 = amt_payout -# loc.5 = amt_offered +# REMAINDER_NOTE_IDX : note_idx +# REMAINDER_OFFERED_FAUCET_PREFIX : offered_faucet_prefix +# REMAINDER_OFFERED_FAUCET_SUFFIX : offered_faucet_suffix +# REMAINDER_OFFERED_ENABLE_CB : offered_enable_cb +# REMAINDER_AMT_PAYOUT : amt_payout +# REMAINDER_AMT_OFFERED : amt_offered proc create_remainder_note - # => [offered_faucet_prefix, offered_faucet_suffix, offered_enable_cb, - # amt_payout, amt_offered, - # remaining_requested, note_type, tag] - # Store offered asset info to locals (top element stored first) - loc_store.1 loc_store.2 loc_store.3 loc_store.4 loc_store.5 + loc_store.REMAINDER_OFFERED_FAUCET_PREFIX + loc_store.REMAINDER_OFFERED_FAUCET_SUFFIX + loc_store.REMAINDER_OFFERED_ENABLE_CB + loc_store.REMAINDER_AMT_PAYOUT + loc_store.REMAINDER_AMT_OFFERED # => [remaining_requested, note_type, tag] # Update note storage with new requested amount (needed by build_recipient) @@ -299,8 +362,8 @@ proc create_remainder_note # => [s0, s1, s2, s3+1, SCRIPT_ROOT, note_type, tag] # Build recipient from all note storage items (now with updated requested amount) - push.NUM_STORAGE_ITEMS.0 - # => [storage_ptr=0, num_storage_items, SERIAL_NUM', SCRIPT_ROOT, note_type, tag] + push.NUM_STORAGE_ITEMS push.STORAGE_PTR + # => [storage_ptr, num_storage_items, SERIAL_NUM', SCRIPT_ROOT, note_type, tag] exec.note::build_recipient # => [RECIPIENT, note_type, tag] @@ -311,26 +374,28 @@ proc create_remainder_note exec.output_note::create # => [note_idx] - loc_store.0 + loc_store.REMAINDER_NOTE_IDX # => [] # Set attachment: aux = amt_payout - loc_load.4 push.0.0.0 + loc_load.REMAINDER_AMT_PAYOUT push.0.0.0 # => [0, 0, 0, amt_payout] - push.0 loc_load.0 + push.0 loc_load.REMAINDER_NOTE_IDX # => [note_idx, attachment_scheme=0, ATTACHMENT] exec.output_note::set_word_attachment # => [] # Add remaining offered asset: remainder_amount = amt_offered - amt_payout - loc_load.0 + loc_load.REMAINDER_NOTE_IDX # => [note_idx] - loc_load.5 loc_load.4 sub + loc_load.REMAINDER_AMT_OFFERED loc_load.REMAINDER_AMT_PAYOUT sub # => [remainder_amount, note_idx] - loc_load.1 loc_load.2 loc_load.3 + loc_load.REMAINDER_OFFERED_FAUCET_PREFIX + loc_load.REMAINDER_OFFERED_FAUCET_SUFFIX + loc_load.REMAINDER_OFFERED_ENABLE_CB # => [offered_enable_cb, offered_faucet_suffix, offered_faucet_prefix, remainder_amount, note_idx] exec.asset::create_fungible_asset @@ -397,22 +462,31 @@ end #! Sends offered tokens to consumer, requested tokens to creator via P2ID, #! and creates a remainder note if partially filled. #! +#! The total fill (account fill + note fill) must not exceed the requested amount: +#! overfilling is disallowed because the consumer would lose the excess tokens with +#! no change returned, and it is almost always unintentional. +#! +#! Account fill and note fill payouts are computed separately (rather than summing +#! first) because the account fill portion must be credited to the consumer's vault +#! on its own, while the combined total is needed to size the remainder note. +#! #! Inputs: [account_fill_amount, note_fill_amount] #! Outputs: [] #! @locals(10) -# loc.0 = amt_offered -# loc.1 = amt_requested -# loc.2 = amt_payout (total) -# loc.3 = amt_payout_account_fill -# loc.4 = amt_payout_note_fill -# loc.5 = offered_enable_cb -# loc.6 = offered_faucet_suffix -# loc.7 = offered_faucet_prefix -# loc.8 = amt_requested_account_fill -# loc.9 = amt_requested_note_fill +# EXEC_AMT_OFFERED : amt_offered +# EXEC_AMT_REQUESTED : amt_requested +# EXEC_AMT_PAYOUT_TOTAL : amt_payout (total) +# EXEC_AMT_PAYOUT_ACCOUNT_FILL : amt_payout_account_fill +# EXEC_AMT_PAYOUT_NOTE_FILL : amt_payout_note_fill +# EXEC_OFFERED_ENABLE_CB : offered_enable_cb +# EXEC_OFFERED_FAUCET_SUFFIX : offered_faucet_suffix +# EXEC_OFFERED_FAUCET_PREFIX : offered_faucet_prefix +# EXEC_AMT_REQUESTED_ACCOUNT_FILL : amt_requested_account_fill +# EXEC_AMT_REQUESTED_NOTE_FILL : amt_requested_note_fill proc execute_pswap - loc_store.8 loc_store.9 + loc_store.EXEC_AMT_REQUESTED_ACCOUNT_FILL + loc_store.EXEC_AMT_REQUESTED_NOTE_FILL # => [] exec.load_offered_asset @@ -422,71 +496,63 @@ proc execute_pswap exec.asset::fungible_to_amount # => [amount, ASSET_KEY, ASSET_VALUE] - loc_store.0 + loc_store.EXEC_AMT_OFFERED # => [ASSET_KEY, ASSET_VALUE] # Extract and store offered faucet info from ASSET_KEY - exec.asset::key_to_callbacks_enabled loc_store.5 + exec.asset::key_to_callbacks_enabled loc_store.EXEC_OFFERED_ENABLE_CB # => [ASSET_KEY, ASSET_VALUE] exec.asset::key_into_faucet_id # => [faucet_id_suffix, faucet_id_prefix, ASSET_VALUE] - loc_store.6 loc_store.7 + loc_store.EXEC_OFFERED_FAUCET_SUFFIX loc_store.EXEC_OFFERED_FAUCET_PREFIX # => [ASSET_VALUE] dropw # => [] # Read requested amount directly from note storage mem_load.REQUESTED_AMOUNT_ITEM - loc_store.1 + loc_store.EXEC_AMT_REQUESTED # => [] - # Validate: fill amount (account fill + note fill) must not exceed total requested. - # Overfilling is not allowed because it is likely due to an error and mostly - # unintentional — the consumer would lose the excess tokens with no change returned. - loc_load.8 loc_load.9 add - loc_load.1 + # Assert (account_fill + note_fill) <= requested + loc_load.EXEC_AMT_REQUESTED_ACCOUNT_FILL loc_load.EXEC_AMT_REQUESTED_NOTE_FILL add + loc_load.EXEC_AMT_REQUESTED # => [requested, fill_amount] - # lte pops [b=requested, a=fill_amount] and pushes (fill_amount <= requested) lte assert.err=ERR_PSWAP_FILL_EXCEEDS_REQUESTED # => [] - # Calculate payout_amount for account fill and note fill amounts separately rather than - # summing them first, because the account fill portion must be sent - # to the consumer's vault individually, while the total (account fill + note fill) is - # needed to determine the remainder note's offered amount. - # - # Calculate payout_amount for account_fill_amount - loc_load.8 - loc_load.1 - loc_load.0 + # Payout for account_fill_amount + loc_load.EXEC_AMT_REQUESTED_ACCOUNT_FILL + loc_load.EXEC_AMT_REQUESTED + loc_load.EXEC_AMT_OFFERED # => [offered, requested, account_fill_amount] exec.calculate_tokens_offered_for_requested # => [account_fill_payout] - loc_store.3 + loc_store.EXEC_AMT_PAYOUT_ACCOUNT_FILL # => [] - # Calculate payout_amount for note_fill_amount - loc_load.9 - loc_load.1 - loc_load.0 + # Payout for note_fill_amount + loc_load.EXEC_AMT_REQUESTED_NOTE_FILL + loc_load.EXEC_AMT_REQUESTED + loc_load.EXEC_AMT_OFFERED # => [offered, requested, note_fill_amount] exec.calculate_tokens_offered_for_requested # => [note_fill_payout] - loc_store.4 + loc_store.EXEC_AMT_PAYOUT_NOTE_FILL # => [] # total_payout = account_fill_payout + note_fill_payout - loc_load.3 loc_load.4 add + loc_load.EXEC_AMT_PAYOUT_ACCOUNT_FILL loc_load.EXEC_AMT_PAYOUT_NOTE_FILL add # => [total_payout] - loc_store.2 + loc_store.EXEC_AMT_PAYOUT_TOTAL # => [] # Create P2ID note for creator - loc_load.9 - loc_load.8 + loc_load.EXEC_AMT_REQUESTED_NOTE_FILL + loc_load.EXEC_AMT_REQUESTED_ACCOUNT_FILL mem_load.REQUESTED_FAUCET_PREFIX_ITEM mem_load.REQUESTED_FAUCET_SUFFIX_ITEM mem_load.REQUESTED_ENABLE_CALLBACKS_ITEM @@ -502,16 +568,16 @@ proc execute_pswap # => [] # Consumer receives only account_fill_payout into vault (not the note_fill portion, if > 0) - loc_load.3 push.0 gt - # => [amt > 0] + loc_load.EXEC_AMT_PAYOUT_ACCOUNT_FILL push.0 gt + # => [has_account_fill_payout] if.true padw padw # => [pad(8)] - loc_load.3 - loc_load.7 - loc_load.6 - loc_load.5 + loc_load.EXEC_AMT_PAYOUT_ACCOUNT_FILL + loc_load.EXEC_OFFERED_FAUCET_PREFIX + loc_load.EXEC_OFFERED_FAUCET_SUFFIX + loc_load.EXEC_OFFERED_ENABLE_CB # => [enable_cb, faucet_suffix, faucet_prefix, amt_payout_account_fill, pad(8)] exec.asset::create_fungible_asset @@ -522,10 +588,11 @@ proc execute_pswap dropw dropw dropw dropw # => [] end + # => [] # Check if partial fill: total_in < total_requested - loc_load.8 loc_load.9 add - loc_load.1 + loc_load.EXEC_AMT_REQUESTED_ACCOUNT_FILL loc_load.EXEC_AMT_REQUESTED_NOTE_FILL add + loc_load.EXEC_AMT_REQUESTED # => [total_requested, total_in] dup.1 dup.1 lt # => [is_partial, total_requested, total_in] @@ -549,21 +616,25 @@ proc execute_pswap movup.2 # => [remaining_requested, note_type, tag] - loc_load.0 - loc_load.2 - loc_load.5 - loc_load.6 - loc_load.7 + loc_load.EXEC_AMT_OFFERED + loc_load.EXEC_AMT_PAYOUT_TOTAL + loc_load.EXEC_OFFERED_ENABLE_CB + loc_load.EXEC_OFFERED_FAUCET_SUFFIX + loc_load.EXEC_OFFERED_FAUCET_PREFIX # => [offered_faucet_prefix, offered_faucet_suffix, offered_enable_cb, # amt_payout, amt_offered, # remaining_requested, note_type, tag] exec.create_remainder_note + # => [] else drop drop + # => [] end + # => [] exec.sys::truncate_stack + # => [] end #! Partially-fillable swap note script: exchanges a portion of the offered asset for a @@ -589,15 +660,8 @@ end #! #! Where NOTE_ARGS = [account_fill_amount, note_fill_amount, 0, 0] (top of stack first). #! -#! Note storage is assumed to be as follows (10 items at STORAGE_PTR): -#! - [0] requested_asset enable_callbacks flag -#! - [1-2] requested_asset faucet ID (suffix, prefix) -#! - [3] requested_asset amount -#! - [4] PSWAP note tag -#! - [5] payback note routing tag (targets the creator) -#! - [6] payback note type (0 = private, 1 = public) -#! - [7] swap count (incremented on each partial fill) -#! - [8-9] creator account ID (prefix, suffix) +#! See the `# Note storage layout` block near the top of this file for the full +#! storage layout consumed by this script. #! #! Panics if: #! - the number of note storage items is not `NUM_STORAGE_ITEMS`. @@ -606,7 +670,6 @@ end #! - the account does not expose `receive_asset` / `move_asset_to_note`. @note_script pub proc main - # => [NOTE_ARGS = [account_fill_amount, note_fill_amount, 0, 0]] movdn.3 movdn.3 drop drop # => [account_fill_amount, note_fill_amount] @@ -620,14 +683,11 @@ pub proc main drop # => [account_fill_amount, note_fill_amount] - # If both account fill and note fill are 0, default to a full fill (account fill = requested). - # This enables consumption by network accounts, which execute without note_args - # (the kernel defaults note_args to [0, 0, 0, 0] when none are provided). dup.1 dup.1 add push.0 eq # => [both_zero, account_fill_amount, note_fill_amount] if.true drop mem_load.REQUESTED_AMOUNT_ITEM - # => [account_fill_amount=requested, note_fill_amount=0] + # => [account_fill_amount, note_fill_amount] end # => [account_fill_amount, note_fill_amount] @@ -640,7 +700,10 @@ pub proc main if.true drop drop exec.handle_reclaim + # => [] else exec.execute_pswap + # => [] end + # => [] end From 9717b9b67c69ad753e324a16ec662a0231e4d564 Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Wed, 15 Apr 2026 01:40:40 +0530 Subject: [PATCH 38/66] test(pswap): dedupe integration tests via helpers + rstest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add shared helpers at the top of the test file: `build_pswap_note`, `note_args`, `assert_fungible_asset`, `assert_vault_added_removed`, `assert_vault_single_added`. Removes the repeated 13-line pswap-note setup and the `if let Asset::Fungible(f) = …` / vault-delta boilerplate. - Consolidate variant-heavy tests with `#[rstest]`: - `pswap_fill_test` merges full/private/partial/network fill cases into one parameterized async test (4 cases). - `pswap_multiple_partial_fills_test` replaces the inner for-loop with 9 `#[case]`s, one per fill amount. - `pswap_partial_fill_ratio_test` merges the non-exact-ratio and hand-listed fuzz suites into one rstest with 27 regression cases, delegating to a new `run_partial_fill_ratio_case` helper. - `pswap_chained_partial_fills_test` parameterizes the outer chain loop with 10 `#[case]`s; the stateful per-chain inner fill loop is preserved intact. - Add `pswap_partial_fill_ratio_fuzz` as a seeded-random coverage sibling: two `#[case]`s (seed 42, seed 1337) × 30 random `(offered, requested, fill)` triples each, drawn from `SmallRng`. Failure message includes seed, iter, and the failing triple so any regression is reproducible from the rstest case name alone. The regression `#[case]` block stays as a permanent edge-case suite. - Leave `alice_reconstructs`, `cross_swap`, `creator_reclaim`, `invalid_input`, `compare`, and `parse_inputs` as standalone tests but rewrite their bodies to use the shared helpers. Net: 1325 → 956 non-helper lines (-369), test count 14 → 58. --- crates/miden-testing/tests/scripts/pswap.rs | 1296 +++++++------------ 1 file changed, 485 insertions(+), 811 deletions(-) diff --git a/crates/miden-testing/tests/scripts/pswap.rs b/crates/miden-testing/tests/scripts/pswap.rs index 96f5296c90..446fa7ca55 100644 --- a/crates/miden-testing/tests/scripts/pswap.rs +++ b/crates/miden-testing/tests/scripts/pswap.rs @@ -1,15 +1,16 @@ use std::collections::BTreeMap; use miden_protocol::account::auth::AuthScheme; -use miden_protocol::account::{Account, AccountStorageMode}; +use miden_protocol::account::{Account, AccountId, AccountStorageMode, AccountVaultDelta}; use miden_protocol::asset::{Asset, FungibleAsset}; use miden_protocol::crypto::rand::{FeltRng, RandomCoin}; use miden_protocol::note::{Note, NoteAssets, NoteMetadata, NoteRecipient, NoteStorage, NoteType}; use miden_protocol::transaction::RawOutputNote; -use miden_protocol::{Felt, Word, ONE, ZERO}; +use miden_protocol::{Felt, ONE, Word, ZERO}; use miden_standards::account::wallets::BasicWallet; use miden_standards::note::{PswapNote, PswapNoteStorage}; -use miden_testing::{Auth, MockChain}; +use miden_testing::{Auth, MockChain, MockChainBuilder}; +use rstest::rstest; // CONSTANTS // ================================================================================================ @@ -18,6 +19,85 @@ const BASIC_AUTH: Auth = Auth::BasicAuth { auth_scheme: AuthScheme::Falcon512Poseidon2, }; +// HELPERS +// ================================================================================================ + +/// Builds a PswapNote, registers it on the builder as an output note, returns the Note. +fn build_pswap_note( + builder: &mut MockChainBuilder, + sender: AccountId, + offered_asset: FungibleAsset, + requested_asset: FungibleAsset, + note_type: NoteType, + rng: &mut RandomCoin, +) -> anyhow::Result { + let storage = PswapNoteStorage::builder() + .requested_asset(requested_asset) + .creator_account_id(sender) + .build(); + let note: Note = PswapNote::builder() + .sender(sender) + .storage(storage) + .serial_number(rng.draw_word()) + .note_type(note_type) + .offered_asset(offered_asset) + .build()? + .into(); + builder.add_output_note(RawOutputNote::Full(note.clone())); + Ok(note) +} + +/// Note-args Word `[account_fill, note_fill, 0, 0]`. +fn note_args(account_fill: u64, note_fill: u64) -> Word { + Word::from([ + Felt::try_from(account_fill).expect("account_fill fits in a felt"), + Felt::try_from(note_fill).expect("note_fill fits in a felt"), + ZERO, + ZERO, + ]) +} + +#[track_caller] +fn assert_fungible_asset(asset: &Asset, expected_faucet: AccountId, expected_amount: u64) { + match asset { + Asset::Fungible(f) => { + assert_eq!(f.faucet_id(), expected_faucet, "faucet id mismatch"); + assert_eq!( + f.amount(), + expected_amount, + "amount mismatch (expected {expected_amount}, got {})", + f.amount() + ); + }, + _ => panic!("expected fungible asset, got non-fungible"), + } +} + +#[track_caller] +fn assert_vault_added_removed( + vault_delta: &AccountVaultDelta, + expected_added: (AccountId, u64), + expected_removed: (AccountId, u64), +) { + let added: Vec = vault_delta.added_assets().collect(); + let removed: Vec = vault_delta.removed_assets().collect(); + assert_eq!(added.len(), 1, "expected exactly 1 added asset"); + assert_eq!(removed.len(), 1, "expected exactly 1 removed asset"); + assert_fungible_asset(&added[0], expected_added.0, expected_added.1); + assert_fungible_asset(&removed[0], expected_removed.0, expected_removed.1); +} + +#[track_caller] +fn assert_vault_single_added( + vault_delta: &AccountVaultDelta, + expected_faucet: AccountId, + expected_amount: u64, +) { + let added: Vec = vault_delta.added_assets().collect(); + assert_eq!(added.len(), 1, "expected exactly 1 added asset"); + assert_fungible_asset(&added[0], expected_faucet, expected_amount); +} + // TESTS // ================================================================================================ @@ -71,12 +151,9 @@ async fn pswap_note_alice_reconstructs_and_consumes_p2id() -> anyhow::Result<()> // --- Step 1: Bob partially fills the PSWAP note (20 out of 25 ETH) --- - let fill_amount = 20u32; + let fill_amount = 20u64; let mut note_args_map = BTreeMap::new(); - note_args_map.insert( - pswap_note.id(), - Word::from([Felt::from(fill_amount), Felt::from(0u32), ZERO, ZERO]), - ); + note_args_map.insert(pswap_note.id(), note_args(fill_amount, 0)); let pswap = PswapNote::try_from(&pswap_note)?; let (p2id_note, remainder_pswap) = @@ -108,12 +185,8 @@ async fn pswap_note_alice_reconstructs_and_consumes_p2id() -> anyhow::Result<()> assert_eq!(fill_amount_from_aux, 20, "Fill amount from aux should be 20 ETH"); // Alice reconstructs the recipient using her serial number and account ID - let p2id_serial = Word::from([ - serial_number[0] + ONE, - serial_number[1], - serial_number[2], - serial_number[3], - ]); + let p2id_serial = + Word::from([serial_number[0] + ONE, serial_number[1], serial_number[2], serial_number[3]]); let reconstructed_recipient = P2idNoteStorage::new(alice.id()).into_recipient(p2id_serial); // Verify the reconstructed recipient matches the actual output @@ -125,28 +198,33 @@ async fn pswap_note_alice_reconstructs_and_consumes_p2id() -> anyhow::Result<()> // --- Step 3: Alice consumes the P2ID payback note --- - let tx_context = mock_chain - .build_tx_context(alice.id(), &[p2id_note.id()], &[])? - .build()?; + let tx_context = mock_chain.build_tx_context(alice.id(), &[p2id_note.id()], &[])?.build()?; let executed_transaction = tx_context.execute().await?; // Verify Alice received 20 ETH 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.faucet_id(), eth_faucet.id()); - assert_eq!(f.amount(), 20); - } else { - panic!("Expected fungible asset in Alice's vault"); - } + assert_vault_single_added(vault_delta, eth_faucet.id(), 20); Ok(()) } +/// Parameterized fill test covering: +/// - full public fill +/// - full private fill +/// - partial public fill (offered=50 USDC / requested=25 ETH / fill=20 ETH → payout=40 USDC, remainder=10 USDC) +/// - full fill via a network account (no note_args → script defaults to full fill) +#[rstest] +#[case::full_public(25, NoteType::Public, false)] +#[case::full_private(25, NoteType::Private, false)] +#[case::partial_public(20, NoteType::Public, false)] +#[case::network_full_fill(25, NoteType::Public, true)] #[tokio::test] -async fn pswap_note_full_fill_test() -> anyhow::Result<()> { +async fn pswap_fill_test( + #[case] fill_amount: u64, + #[case] note_type: NoteType, + #[case] use_network_account: bool, +) -> anyhow::Result<()> { let mut builder = MockChain::builder(); let usdc_faucet = builder.add_existing_basic_faucet(BASIC_AUTH, "USDC", 1000, Some(150))?; @@ -156,259 +234,108 @@ async fn pswap_note_full_fill_test() -> anyhow::Result<()> { 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 offered_asset = FungibleAsset::new(usdc_faucet.id(), 50)?; - let requested_asset = FungibleAsset::new(eth_faucet.id(), 25)?; - - let mut rng = RandomCoin::new(Word::default()); - let storage = PswapNoteStorage::builder() - .requested_asset(requested_asset) - .creator_account_id(alice.id()) - .build(); - let pswap_note: Note = PswapNote::builder() - .sender(alice.id()) - .storage(storage) - .serial_number(rng.draw_word()) - .note_type(NoteType::Public) - .offered_asset(offered_asset) - .build()? - .into(); - builder.add_output_note(RawOutputNote::Full(pswap_note.clone())); - let mut mock_chain = builder.build()?; - - let mut note_args_map = BTreeMap::new(); - note_args_map.insert(pswap_note.id(), Word::from([Felt::from(25u32), Felt::from(0u32), ZERO, ZERO])); - - let pswap = PswapNote::try_from(&pswap_note)?; - let (p2id_note, _remainder) = - pswap.execute(bob.id(), Some(FungibleAsset::new(eth_faucet.id(), 25)?), None)?; - - let tx_context = mock_chain - .build_tx_context(bob.id(), &[pswap_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); + let consumer_id = if use_network_account { + let seed: [u8; 32] = builder.rng_mut().draw_word().into(); + let network_consumer = builder.add_account_from_builder( + BASIC_AUTH, + Account::builder(seed) + .storage_mode(AccountStorageMode::Network) + .with_component(BasicWallet) + .with_assets([FungibleAsset::new(eth_faucet.id(), fill_amount)?.into()]), + miden_testing::AccountState::Exists, + )?; + network_consumer.id() } 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 bob = builder.add_existing_wallet_with_assets( + BASIC_AUTH, + [FungibleAsset::new(eth_faucet.id(), fill_amount)?.into()], + )?; + bob.id() + }; let offered_asset = FungibleAsset::new(usdc_faucet.id(), 50)?; let requested_asset = FungibleAsset::new(eth_faucet.id(), 25)?; let mut rng = RandomCoin::new(Word::default()); - // Create a PRIVATE swap note (output notes should also be Private) - let storage = PswapNoteStorage::builder() - .requested_asset(requested_asset) - .creator_account_id(alice.id()) - .build(); - let pswap_note: Note = PswapNote::builder() - .sender(alice.id()) - .storage(storage) - .serial_number(rng.draw_word()) - .note_type(NoteType::Private) - .offered_asset(offered_asset) - .build()? - .into(); - builder.add_output_note(RawOutputNote::Full(pswap_note.clone())); + let pswap_note = build_pswap_note( + &mut builder, + alice.id(), + offered_asset, + requested_asset, + note_type, + &mut rng, + )?; let mut mock_chain = builder.build()?; - let mut note_args_map = BTreeMap::new(); - note_args_map.insert(pswap_note.id(), Word::from([Felt::from(25u32), Felt::from(0u32), ZERO, ZERO])); - - // Expected P2ID note should inherit Private type from swap note let pswap = PswapNote::try_from(&pswap_note)?; - let (p2id_note, _remainder) = - pswap.execute(bob.id(), Some(FungibleAsset::new(eth_faucet.id(), 25)?), None)?; - - let tx_context = mock_chain - .build_tx_context(bob.id(), &[pswap_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 fill_asset = FungibleAsset::new(eth_faucet.id(), fill_amount)?; - 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); + let (p2id_note, remainder_pswap) = if use_network_account { + let p2id = pswap.execute_full_fill_network(consumer_id)?; + (p2id, None) } else { - panic!("Expected fungible asset in P2ID note"); - } + pswap.execute(consumer_id, Some(fill_asset), None)? + }; - // 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(); + let is_partial = fill_amount < 25; + let payout_amount = pswap.calculate_offered_for_requested(fill_amount); - 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); + let mut expected_notes = vec![RawOutputNote::Full(p2id_note.clone())]; + if let Some(remainder) = remainder_pswap { + expected_notes.push(RawOutputNote::Full(Note::from(remainder))); } - 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 offered_asset = FungibleAsset::new(usdc_faucet.id(), 50)?; - let requested_asset = FungibleAsset::new(eth_faucet.id(), 25)?; - - let mut rng = RandomCoin::new(Word::default()); - let storage = PswapNoteStorage::builder() - .requested_asset(requested_asset) - .creator_account_id(alice.id()) - .build(); - let pswap_note: Note = PswapNote::builder() - .sender(alice.id()) - .storage(storage) - .serial_number(rng.draw_word()) - .note_type(NoteType::Public) - .offered_asset(offered_asset) - .build()? - .into(); - builder.add_output_note(RawOutputNote::Full(pswap_note.clone())); + let mut tx_builder = mock_chain + .build_tx_context(consumer_id, &[pswap_note.id()], &[])? + .extend_expected_output_notes(expected_notes); - let mut mock_chain = builder.build()?; - - let mut note_args_map = BTreeMap::new(); - note_args_map.insert(pswap_note.id(), Word::from([Felt::from(20u32), Felt::from(0u32), ZERO, ZERO])); - - let pswap = PswapNote::try_from(&pswap_note)?; - let (p2id_note, remainder_pswap) = - pswap.execute(bob.id(), Some(FungibleAsset::new(eth_faucet.id(), 20)?), None)?; - let remainder_note = - Note::from(remainder_pswap.expect("partial fill should produce remainder")); - - let tx_context = mock_chain - .build_tx_context(bob.id(), &[pswap_note.id()], &[])? - .extend_note_args(note_args_map) - .extend_expected_output_notes(vec![ - RawOutputNote::Full(p2id_note), - RawOutputNote::Full(remainder_note), - ]) - .build()?; + if !use_network_account { + let mut note_args_map = BTreeMap::new(); + note_args_map.insert(pswap_note.id(), note_args(fill_amount, 0)); + tx_builder = tx_builder.extend_note_args(note_args_map); + } + let tx_context = tx_builder.build()?; let executed_transaction = tx_context.execute().await?; - // Verify: 2 output notes (P2ID + remainder) + // Verify output note count let output_notes = executed_transaction.output_notes(); - assert_eq!(output_notes.num_notes(), 2); + let expected_count = if is_partial { 2 } else { 1 }; + assert_eq!( + output_notes.num_notes(), + expected_count, + "expected {expected_count} output notes" + ); - // 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); - } + // Verify the P2ID recipient matches our Rust prediction + 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!"); - // 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); + // P2ID note carries fill_amount ETH + let p2id_assets = output_notes.get_note(0).assets(); + assert_eq!(p2id_assets.num_assets(), 1); + assert_fungible_asset(p2id_assets.iter().next().unwrap(), eth_faucet.id(), fill_amount); + + // On partial fill, assert remainder note has offered - payout USDC + if is_partial { + let remainder_assets = output_notes.get_note(1).assets(); + assert_fungible_asset( + remainder_assets.iter().next().unwrap(), + usdc_faucet.id(), + 50 - payout_amount, + ); } - // Bob's vault: +40 USDC, -20 ETH + // Consumer's vault delta: +payout USDC, -fill 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); - } + assert_vault_added_removed( + vault_delta, + (usdc_faucet.id(), payout_amount), + (eth_faucet.id(), fill_amount), + ); mock_chain.add_pending_executed_transaction(&executed_transaction)?; let _ = mock_chain.prove_next_block(); @@ -436,45 +363,31 @@ async fn pswap_note_note_fill_cross_swap_test() -> anyhow::Result<()> { let mut rng = RandomCoin::new(Word::default()); // Alice's note: offers 50 USDC, requests 25 ETH - let alice_requested_asset = FungibleAsset::new(eth_faucet.id(), 25)?; - let storage = PswapNoteStorage::builder() - .requested_asset(alice_requested_asset) - .creator_account_id(alice.id()) - .build(); - let alice_pswap_note: Note = PswapNote::builder() - .sender(alice.id()) - .storage(storage) - .serial_number(rng.draw_word()) - .note_type(NoteType::Public) - .offered_asset(FungibleAsset::new(usdc_faucet.id(), 50)?) - .build()? - .into(); - builder.add_output_note(RawOutputNote::Full(alice_pswap_note.clone())); + let alice_pswap_note = build_pswap_note( + &mut builder, + alice.id(), + FungibleAsset::new(usdc_faucet.id(), 50)?, + FungibleAsset::new(eth_faucet.id(), 25)?, + NoteType::Public, + &mut rng, + )?; // Bob's note: offers 25 ETH, requests 50 USDC - let bob_requested_asset = FungibleAsset::new(usdc_faucet.id(), 50)?; - let storage = PswapNoteStorage::builder() - .requested_asset(bob_requested_asset) - .creator_account_id(bob.id()) - .build(); - let bob_pswap_note: Note = PswapNote::builder() - .sender(bob.id()) - .storage(storage) - .serial_number(rng.draw_word()) - .note_type(NoteType::Public) - .offered_asset(FungibleAsset::new(eth_faucet.id(), 25)?) - .build()? - .into(); - builder.add_output_note(RawOutputNote::Full(bob_pswap_note.clone())); + let bob_pswap_note = build_pswap_note( + &mut builder, + bob.id(), + FungibleAsset::new(eth_faucet.id(), 25)?, + FungibleAsset::new(usdc_faucet.id(), 50)?, + NoteType::Public, + &mut rng, + )?; let mock_chain = builder.build()?; // Note args: pure note fill (account_fill = 0, note_fill = full amount) let mut note_args_map = BTreeMap::new(); - note_args_map - .insert(alice_pswap_note.id(), Word::from([Felt::from(0u32), Felt::from(25u32), ZERO, ZERO])); - note_args_map - .insert(bob_pswap_note.id(), Word::from([Felt::from(0u32), Felt::from(50u32), ZERO, ZERO])); + note_args_map.insert(alice_pswap_note.id(), note_args(0, 25)); + note_args_map.insert(bob_pswap_note.id(), note_args(0, 50)); // Expected P2ID notes let alice_pswap = PswapNote::try_from(&alice_pswap_note)?; @@ -536,20 +449,14 @@ async fn pswap_note_creator_reclaim_test() -> anyhow::Result<()> { )?; let mut rng = RandomCoin::new(Word::default()); - let requested_asset = FungibleAsset::new(eth_faucet.id(), 25)?; - let storage = PswapNoteStorage::builder() - .requested_asset(requested_asset) - .creator_account_id(alice.id()) - .build(); - let pswap_note: Note = PswapNote::builder() - .sender(alice.id()) - .storage(storage) - .serial_number(rng.draw_word()) - .note_type(NoteType::Public) - .offered_asset(FungibleAsset::new(usdc_faucet.id(), 50)?) - .build()? - .into(); - builder.add_output_note(RawOutputNote::Full(pswap_note.clone())); + let pswap_note = build_pswap_note( + &mut builder, + alice.id(), + FungibleAsset::new(usdc_faucet.id(), 50)?, + FungibleAsset::new(eth_faucet.id(), 25)?, + NoteType::Public, + &mut rng, + )?; let mock_chain = builder.build()?; @@ -561,17 +468,8 @@ async fn pswap_note_creator_reclaim_test() -> anyhow::Result<()> { 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); + let vault_delta = executed_transaction.account_delta().vault(); + assert_vault_single_added(vault_delta, usdc_faucet.id(), 50); Ok(()) } @@ -593,25 +491,19 @@ async fn pswap_note_invalid_input_test() -> anyhow::Result<()> { )?; let mut rng = RandomCoin::new(Word::default()); - let requested_asset = FungibleAsset::new(eth_faucet.id(), 25)?; - let storage = PswapNoteStorage::builder() - .requested_asset(requested_asset) - .creator_account_id(alice.id()) - .build(); - let pswap_note: Note = PswapNote::builder() - .sender(alice.id()) - .storage(storage) - .serial_number(rng.draw_word()) - .note_type(NoteType::Public) - .offered_asset(FungibleAsset::new(usdc_faucet.id(), 50)?) - .build()? - .into(); - builder.add_output_note(RawOutputNote::Full(pswap_note.clone())); + let pswap_note = build_pswap_note( + &mut builder, + alice.id(), + FungibleAsset::new(usdc_faucet.id(), 50)?, + FungibleAsset::new(eth_faucet.id(), 25)?, + NoteType::Public, + &mut rng, + )?; 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(pswap_note.id(), Word::from([Felt::from(30u32), Felt::from(0u32), ZERO, ZERO])); + note_args_map.insert(pswap_note.id(), note_args(30, 0)); let tx_context = mock_chain .build_tx_context(bob.id(), &[pswap_note.id()], &[])? @@ -627,205 +519,256 @@ async fn pswap_note_invalid_input_test() -> anyhow::Result<()> { Ok(()) } +#[rstest] +#[case(5)] +#[case(7)] +#[case(10)] +#[case(13)] +#[case(15)] +#[case(19)] +#[case(20)] +#[case(23)] +#[case(25)] #[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 (fill_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()], - )?; +async fn pswap_multiple_partial_fills_test(#[case] fill_amount: u64) -> 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 bob = builder.add_existing_wallet_with_assets( - BASIC_AUTH, - [FungibleAsset::new(eth_faucet.id(), fill_amount)?.into()], - )?; + let alice = builder.add_existing_wallet_with_assets( + BASIC_AUTH, + [FungibleAsset::new(usdc_faucet.id(), 50)?.into()], + )?; - let mut rng = RandomCoin::new(Word::default()); - let requested_asset = FungibleAsset::new(eth_faucet.id(), 25)?; - let storage = PswapNoteStorage::builder() - .requested_asset(requested_asset) - .creator_account_id(alice.id()) - .build(); - let pswap_note: Note = PswapNote::builder() - .sender(alice.id()) - .storage(storage) - .serial_number(rng.draw_word()) - .note_type(NoteType::Public) - .offered_asset(FungibleAsset::new(usdc_faucet.id(), 50)?) - .build()? - .into(); - builder.add_output_note(RawOutputNote::Full(pswap_note.clone())); + let bob = builder.add_existing_wallet_with_assets( + BASIC_AUTH, + [FungibleAsset::new(eth_faucet.id(), fill_amount)?.into()], + )?; - let mock_chain = builder.build()?; + let mut rng = RandomCoin::new(Word::default()); + let pswap_note = build_pswap_note( + &mut builder, + alice.id(), + FungibleAsset::new(usdc_faucet.id(), 50)?, + FungibleAsset::new(eth_faucet.id(), 25)?, + NoteType::Public, + &mut rng, + )?; - let mut note_args_map = BTreeMap::new(); - note_args_map.insert( - pswap_note.id(), - Word::from([Felt::try_from(fill_amount).unwrap(), Felt::from(0u32), ZERO, ZERO]), - ); + let mock_chain = builder.build()?; - let pswap = PswapNote::try_from(&pswap_note)?; - let payout_amount = pswap.calculate_offered_for_requested(fill_amount); - let (p2id_note, remainder_pswap) = pswap.execute( - bob.id(), - Some(FungibleAsset::new(eth_faucet.id(), fill_amount)?), - None, - )?; + let mut note_args_map = BTreeMap::new(); + note_args_map.insert(pswap_note.id(), note_args(fill_amount, 0)); - let mut expected_notes = vec![RawOutputNote::Full(p2id_note)]; + let pswap = PswapNote::try_from(&pswap_note)?; + let payout_amount = pswap.calculate_offered_for_requested(fill_amount); + let (p2id_note, remainder_pswap) = + pswap.execute(bob.id(), Some(FungibleAsset::new(eth_faucet.id(), fill_amount)?), None)?; - if let Some(remainder) = remainder_pswap { - expected_notes.push(RawOutputNote::Full(Note::from(remainder))); - } + let mut expected_notes = vec![RawOutputNote::Full(p2id_note)]; + if let Some(remainder) = remainder_pswap { + expected_notes.push(RawOutputNote::Full(Note::from(remainder))); + } - let tx_context = mock_chain - .build_tx_context(bob.id(), &[pswap_note.id()], &[])? - .extend_expected_output_notes(expected_notes) - .extend_note_args(note_args_map) - .build()?; + let tx_context = mock_chain + .build_tx_context(bob.id(), &[pswap_note.id()], &[])? + .extend_expected_output_notes(expected_notes) + .extend_note_args(note_args_map) + .build()?; - let executed_transaction = tx_context.execute().await?; + let executed_transaction = tx_context.execute().await?; - let output_notes = executed_transaction.output_notes(); - let expected_count = if fill_amount < 25 { 2 } else { 1 }; - assert_eq!(output_notes.num_notes(), expected_count); + let output_notes = executed_transaction.output_notes(); + let expected_count = if fill_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(), payout_amount); - } - } + // Verify Bob's vault + let vault_delta = executed_transaction.account_delta().vault(); + assert_vault_single_added(vault_delta, usdc_faucet.id(), payout_amount); Ok(()) } -#[tokio::test] -async fn pswap_note_non_exact_ratio_partial_fill_test() -> anyhow::Result<()> { - let offered_total = 100u64; - let requested_total = 30u64; - let fill_amount = 7u64; +/// Runs one full partial-fill scenario for a `(offered, requested, fill)` triple. +/// +/// Shared between the hand-picked `pswap_partial_fill_ratio_test` regression suite and the +/// seeded random `pswap_partial_fill_ratio_fuzz` coverage test. +async fn run_partial_fill_ratio_case( + offered_usdc: u64, + requested_eth: u64, + fill_eth: u64, +) -> anyhow::Result<()> { + let remaining_requested = requested_eth - fill_eth; 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 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_total)?.into()], + [FungibleAsset::new(usdc_faucet.id(), offered_usdc)?.into()], )?; let bob = builder.add_existing_wallet_with_assets( BASIC_AUTH, - [FungibleAsset::new(eth_faucet.id(), fill_amount)?.into()], + [FungibleAsset::new(eth_faucet.id(), fill_eth)?.into()], )?; let mut rng = RandomCoin::new(Word::default()); - let requested_asset = FungibleAsset::new(eth_faucet.id(), requested_total)?; - let storage = PswapNoteStorage::builder() - .requested_asset(requested_asset) - .creator_account_id(alice.id()) - .build(); - let pswap_note: Note = PswapNote::builder() - .sender(alice.id()) - .storage(storage) - .serial_number(rng.draw_word()) - .note_type(NoteType::Public) - .offered_asset(FungibleAsset::new(usdc_faucet.id(), offered_total)?) - .build()? - .into(); - builder.add_output_note(RawOutputNote::Full(pswap_note.clone())); + let pswap_note = build_pswap_note( + &mut builder, + alice.id(), + FungibleAsset::new(usdc_faucet.id(), offered_usdc)?, + FungibleAsset::new(eth_faucet.id(), requested_eth)?, + NoteType::Public, + &mut rng, + )?; let mock_chain = builder.build()?; let mut note_args_map = BTreeMap::new(); - note_args_map - .insert(pswap_note.id(), Word::from([Felt::try_from(fill_amount).unwrap(), Felt::from(0u32), ZERO, ZERO])); + note_args_map.insert(pswap_note.id(), note_args(fill_eth, 0)); let pswap = PswapNote::try_from(&pswap_note)?; - let expected_output = pswap.calculate_offered_for_requested(fill_amount); - let (p2id_note, remainder_pswap) = pswap.execute( - bob.id(), - Some(FungibleAsset::new(eth_faucet.id(), fill_amount)?), - None, - )?; - let remainder = Note::from(remainder_pswap.expect("partial fill should produce remainder")); + let payout_amount = pswap.calculate_offered_for_requested(fill_eth); + let remaining_offered = offered_usdc - payout_amount; + + assert!(payout_amount > 0, "payout_amount must be > 0"); + assert!(payout_amount <= offered_usdc, "payout_amount > offered"); + + let (p2id_note, remainder_pswap) = + pswap.execute(bob.id(), Some(FungibleAsset::new(eth_faucet.id(), fill_eth)?), None)?; + + let mut expected_notes = vec![RawOutputNote::Full(p2id_note)]; + if remaining_requested > 0 { + let remainder = Note::from(remainder_pswap.expect("partial fill should produce remainder")); + expected_notes.push(RawOutputNote::Full(remainder)); + } let tx_context = mock_chain .build_tx_context(bob.id(), &[pswap_note.id()], &[])? - .extend_expected_output_notes(vec![ - RawOutputNote::Full(p2id_note), - RawOutputNote::Full(remainder), - ]) + .extend_expected_output_notes(expected_notes) .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 expected_count = if remaining_requested > 0 { 2 } else { 1 }; + assert_eq!(output_notes.num_notes(), expected_count); 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); - } + assert_vault_added_removed( + vault_delta, + (usdc_faucet.id(), payout_amount), + (eth_faucet.id(), fill_eth), + ); + + assert_eq!(payout_amount + remaining_offered, offered_usdc, "conservation"); + + Ok(()) +} + +#[rstest] +// Single non-exact-ratio partial fill. +#[case(100, 30, 7)] +// Non-integer ratio regression cases. +#[case(23, 20, 7)] +#[case(23, 20, 13)] +#[case(23, 20, 19)] +#[case(17, 13, 5)] +#[case(97, 89, 37)] +#[case(53, 47, 23)] +#[case(7, 5, 3)] +#[case(7, 5, 1)] +#[case(7, 5, 4)] +#[case(89, 55, 21)] +#[case(233, 144, 55)] +#[case(34, 21, 8)] +#[case(50, 97, 30)] +#[case(13, 47, 20)] +#[case(3, 7, 5)] +#[case(101, 100, 50)] +#[case(100, 99, 50)] +#[case(997, 991, 500)] +#[case(1000, 3, 1)] +#[case(1000, 3, 2)] +#[case(3, 1000, 500)] +#[case(9999, 7777, 3333)] +#[case(5000, 3333, 1111)] +#[case(127, 63, 31)] +#[case(255, 127, 63)] +#[case(511, 255, 100)] +#[tokio::test] +async fn pswap_partial_fill_ratio_test( + #[case] offered_usdc: u64, + #[case] requested_eth: u64, + #[case] fill_eth: u64, +) -> anyhow::Result<()> { + run_partial_fill_ratio_case(offered_usdc, requested_eth, fill_eth).await +} + +/// Seeded-random coverage for the `calculate_offered_for_requested` math + full execute path. +/// +/// Each seed draws `FUZZ_ITERATIONS` random `(offered, requested, fill)` triples and runs them +/// through `run_partial_fill_ratio_case`. Seeds are baked into the case names so a failure like +/// `pswap_partial_fill_ratio_fuzz::seed_1337` is reproducible with one command: rerun that case, +/// the error message pinpoints the exact iteration and triple that broke. +#[rstest] +#[case::seed_42(42)] +#[case::seed_1337(1337)] +#[tokio::test] +async fn pswap_partial_fill_ratio_fuzz(#[case] seed: u64) -> anyhow::Result<()> { + use rand::rngs::SmallRng; + use rand::{Rng, SeedableRng}; + const FUZZ_ITERATIONS: usize = 30; + + let mut rng = SmallRng::seed_from_u64(seed); + for iter in 0..FUZZ_ITERATIONS { + let offered_usdc = rng.random_range(2u64..10_000); + let requested_eth = rng.random_range(2u64..10_000); + let fill_eth = rng.random_range(1u64..=requested_eth); + + run_partial_fill_ratio_case(offered_usdc, requested_eth, fill_eth).await.map_err(|e| { + anyhow::anyhow!( + "seed={seed} iter={iter} (offered={offered_usdc}, requested={requested_eth}, fill={fill_eth}): {e}" + ) + })?; + } Ok(()) } +#[rstest] +#[case(100, 73, vec![17, 23, 19])] +#[case(53, 47, vec![7, 11, 13, 5])] +#[case(200, 137, vec![41, 37, 29])] +#[case(7, 5, vec![2, 1])] +#[case(1000, 777, vec![100, 200, 150, 100])] +#[case(50, 97, vec![20, 30, 15])] +#[case(89, 55, vec![13, 8, 21])] +#[case(23, 20, vec![3, 5, 4, 3])] +#[case(997, 991, vec![300, 300, 200])] +#[case(3, 2, vec![1])] #[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 remaining_requested = requested_eth - fill_eth; +async fn pswap_chained_partial_fills_test( + #[case] initial_offered: u64, + #[case] initial_requested: u64, + #[case] fills: Vec, +) -> anyhow::Result<()> { + 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; + // Track serial for remainder chain + let mut rng = RandomCoin::new(Word::default()); + let mut current_serial = rng.draw_word(); + + for (current_swap_count, fill_amount) in fills.iter().enumerate() { + let remaining_requested = current_requested - fill_amount; let mut builder = MockChain::builder(); let max_supply = 100_000u64; @@ -834,51 +777,54 @@ async fn pswap_note_partial_fill_non_integer_ratio_fuzz_test() -> anyhow::Result BASIC_AUTH, "USDC", max_supply, - Some(*offered_usdc), + Some(current_offered), )?; let eth_faucet = - builder.add_existing_basic_faucet(BASIC_AUTH, "ETH", max_supply, Some(*fill_eth))?; + 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(), *offered_usdc)?.into()], + [FungibleAsset::new(usdc_faucet.id(), current_offered)?.into()], )?; let bob = builder.add_existing_wallet_with_assets( BASIC_AUTH, - [FungibleAsset::new(eth_faucet.id(), *fill_eth)?.into()], + [FungibleAsset::new(eth_faucet.id(), *fill_amount)?.into()], )?; - let mut rng = RandomCoin::new(Word::default()); - let requested_asset = FungibleAsset::new(eth_faucet.id(), *requested_eth)?; + // Build storage and note manually to use the correct serial for chain position + let offered_fungible = FungibleAsset::new(usdc_faucet.id(), current_offered)?; + let requested_fungible = FungibleAsset::new(eth_faucet.id(), current_requested)?; + + let pswap_tag = + PswapNote::create_tag(NoteType::Public, &offered_fungible, &requested_fungible); + let offered_asset = Asset::Fungible(offered_fungible); + let storage = PswapNoteStorage::builder() - .requested_asset(requested_asset) + .requested_asset(requested_fungible) + .pswap_tag(pswap_tag) + .swap_count(current_swap_count as u16) .creator_account_id(alice.id()) .build(); - let pswap_note: Note = PswapNote::builder() - .sender(alice.id()) - .storage(storage) - .serial_number(rng.draw_word()) - .note_type(NoteType::Public) - .offered_asset(FungibleAsset::new(usdc_faucet.id(), *offered_usdc)?) - .build()? - .into(); - builder.add_output_note(RawOutputNote::Full(pswap_note.clone())); + let note_assets = NoteAssets::new(vec![offered_asset])?; + + // Create note with the correct serial for this chain position + 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 pswap_note = Note::new(note_assets, metadata, recipient); + builder.add_output_note(RawOutputNote::Full(pswap_note.clone())); let mock_chain = builder.build()?; let mut note_args_map = BTreeMap::new(); - note_args_map - .insert(pswap_note.id(), Word::from([Felt::try_from(*fill_eth).unwrap(), Felt::from(0u32), ZERO, ZERO])); + note_args_map.insert(pswap_note.id(), note_args(*fill_amount, 0)); let pswap = PswapNote::try_from(&pswap_note)?; - let payout_amount = pswap.calculate_offered_for_requested(*fill_eth); - let remaining_offered = offered_usdc - payout_amount; - - assert!(payout_amount > 0, "Case {}: payout_amount must be > 0", i + 1); - assert!(payout_amount <= *offered_usdc, "Case {}: payout_amount > offered", i + 1); + let payout_amount = pswap.calculate_offered_for_requested(*fill_amount); + let remaining_offered = current_offered - payout_amount; let (p2id_note, remainder_pswap) = pswap.execute( bob.id(), - Some(FungibleAsset::new(eth_faucet.id(), *fill_eth)?), + Some(FungibleAsset::new(eth_faucet.id(), *fill_amount)?), None, )?; @@ -897,204 +843,40 @@ async fn pswap_note_partial_fill_non_integer_ratio_fuzz_test() -> anyhow::Result let executed_tx = tx_context.execute().await.map_err(|e| { anyhow::anyhow!( - "Case {} failed: {} (offered={}, requested={}, fill={})", - i + 1, + "fill {} failed: {} (offered={}, requested={}, fill={})", + current_swap_count + 1, e, - offered_usdc, - requested_eth, - fill_eth + 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, "Case {}", i + 1); + assert_eq!(output_notes.num_notes(), expected_count, "fill {}", current_swap_count + 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(), payout_amount, "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!(payout_amount + remaining_offered, *offered_usdc, "Case {}: conservation", i + 1); + assert_vault_single_added(vault_delta, usdc_faucet.id(), payout_amount); + + // Update state for next fill + total_usdc_to_bob += payout_amount; + total_eth_from_bob += fill_amount; + current_offered = remaining_offered; + current_requested = remaining_requested; + // Remainder serial: [0] + 1 (matching MASM LE orientation) + current_serial = Word::from([ + current_serial[0] + ONE, + current_serial[1], + current_serial[2], + current_serial[3], + ]); } - 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; - // Track serial for remainder chain - let mut rng = RandomCoin::new(Word::default()); - let mut current_serial = rng.draw_word(); - - for (current_swap_count, fill_amount) in fills.iter().enumerate() { - 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()], - )?; - - // Build storage and note manually to use the correct serial for chain position - let offered_fungible = - FungibleAsset::new(usdc_faucet.id(), current_offered)?; - let requested_fungible = - FungibleAsset::new(eth_faucet.id(), current_requested)?; - - let pswap_tag = - PswapNote::create_tag(NoteType::Public, &offered_fungible, &requested_fungible); - let offered_asset = Asset::Fungible(offered_fungible); - - let storage = PswapNoteStorage::builder() - .requested_asset(requested_fungible) - .pswap_tag(pswap_tag) - .swap_count(current_swap_count as u16) - .creator_account_id(alice.id()) - .build(); - let note_assets = NoteAssets::new(vec![offered_asset])?; - - // Create note with the correct serial for this chain position - 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 pswap_note = Note::new(note_assets, metadata, recipient); - - builder.add_output_note(RawOutputNote::Full(pswap_note.clone())); - let mock_chain = builder.build()?; - - let mut note_args_map = BTreeMap::new(); - note_args_map.insert( - pswap_note.id(), - Word::from([Felt::try_from(*fill_amount).unwrap(), Felt::from(0u32), ZERO, ZERO]), - ); - - let pswap = PswapNote::try_from(&pswap_note)?; - let payout_amount = pswap.calculate_offered_for_requested(*fill_amount); - let remaining_offered = current_offered - payout_amount; - let (p2id_note, remainder_pswap) = pswap.execute( - bob.id(), - Some(FungibleAsset::new(eth_faucet.id(), *fill_amount)?), - None, - )?; - - let mut expected_notes = vec![RawOutputNote::Full(p2id_note)]; - if remaining_requested > 0 { - let remainder = - Note::from(remainder_pswap.expect("partial fill should produce remainder")); - expected_notes.push(RawOutputNote::Full(remainder)); - } - - let tx_context = mock_chain - .build_tx_context(bob.id(), &[pswap_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, - current_swap_count + 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, - current_swap_count + 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, current_swap_count + 1); - if let Asset::Fungible(f) = &added[0] { - assert_eq!( - f.amount(), - payout_amount, - "Chain {} fill {}: Bob should get {} USDC", - chain_idx + 1, - current_swap_count + 1, - payout_amount - ); - } - - // Update state for next fill - total_usdc_to_bob += payout_amount; - total_eth_from_bob += fill_amount; - current_offered = remaining_offered; - current_requested = remaining_requested; - // Remainder serial: [0] + 1 (matching MASM LE orientation) - current_serial = Word::from([ - current_serial[0] + ONE, - 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 - ); - } + // Verify conservation + let total_fills: u64 = fills.iter().sum(); + assert_eq!(total_eth_from_bob, total_fills, "ETH conservation"); + assert_eq!(total_usdc_to_bob + current_offered, initial_offered, "USDC conservation"); Ok(()) } @@ -1148,31 +930,25 @@ fn compare_pswap_create_output_notes_vs_test_helper() { 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(), Some(FungibleAsset::new(eth_faucet.id(), 25).unwrap()), None).unwrap(); + let (p2id_note, remainder) = pswap + .execute(bob.id(), Some(FungibleAsset::new(eth_faucet.id(), 25).unwrap()), None) + .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"); - } + assert_fungible_asset(p2id_note.assets().iter().next().unwrap(), eth_faucet.id(), 25); // Partial fill: should produce P2ID note + remainder - let (p2id_partial, remainder_partial) = - pswap.execute(bob.id(), Some(FungibleAsset::new(eth_faucet.id(), 10).unwrap()), None).unwrap(); + let (p2id_partial, remainder_partial) = pswap + .execute(bob.id(), Some(FungibleAsset::new(eth_faucet.id(), 10).unwrap()), None) + .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); - } + assert_fungible_asset(p2id_partial.assets().iter().next().unwrap(), eth_faucet.id(), 10); // Verify remainder properties assert_eq!(remainder_pswap.storage().swap_count(), 1, "Remainder swap count should be 1"); @@ -1200,20 +976,15 @@ fn pswap_parse_inputs_roundtrip() { .unwrap(); let mut rng = RandomCoin::new(Word::default()); - let requested_asset = FungibleAsset::new(eth_faucet.id(), 25).unwrap(); - let storage = PswapNoteStorage::builder() - .requested_asset(requested_asset) - .creator_account_id(alice.id()) - .build(); - let pswap_note: Note = PswapNote::builder() - .sender(alice.id()) - .storage(storage) - .serial_number(rng.draw_word()) - .note_type(NoteType::Public) - .offered_asset(FungibleAsset::new(usdc_faucet.id(), 50).unwrap()) - .build() - .unwrap() - .into(); + let pswap_note = build_pswap_note( + &mut builder, + alice.id(), + FungibleAsset::new(usdc_faucet.id(), 50).unwrap(), + FungibleAsset::new(eth_faucet.id(), 25).unwrap(), + NoteType::Public, + &mut rng, + ) + .unwrap(); let storage = pswap_note.recipient().storage(); let items = storage.items(); @@ -1226,100 +997,3 @@ fn pswap_parse_inputs_roundtrip() { // Verify requested amount from value word assert_eq!(parsed.requested_asset_amount(), 25, "Requested amount should be 25"); } - -/// Test that a PSWAP note can be consumed by a network account (full fill, no note_args). -/// -/// Alice (local) creates a PSWAP note offering 50 USDC for 25 ETH. A network account with a -/// BasicWallet consumes it. Since no note_args are provided, the script defaults to a full fill. -#[tokio::test] -async fn pswap_note_network_account_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()], - )?; - - // Create a network account with BasicWallet that holds 25 ETH - let seed: [u8; 32] = builder.rng_mut().draw_word().into(); - let network_consumer = builder.add_account_from_builder( - BASIC_AUTH, - Account::builder(seed) - .storage_mode(AccountStorageMode::Network) - .with_component(BasicWallet) - .with_assets([FungibleAsset::new(eth_faucet.id(), 25)?.into()]), - miden_testing::AccountState::Exists, - )?; - - let requested_asset = FungibleAsset::new(eth_faucet.id(), 25)?; - - let mut rng = RandomCoin::new(Word::default()); - let storage = PswapNoteStorage::builder() - .requested_asset(requested_asset) - .creator_account_id(alice.id()) - .build(); - let pswap_note: Note = PswapNote::builder() - .sender(alice.id()) - .storage(storage) - .serial_number(rng.draw_word()) - .note_type(NoteType::Public) - .offered_asset(FungibleAsset::new(usdc_faucet.id(), 50)?) - .build()? - .into(); - builder.add_output_note(RawOutputNote::Full(pswap_note.clone())); - - let mut mock_chain = builder.build()?; - - // No note_args — simulates a network transaction where args default to [0, 0, 0, 0]. - // The PSWAP script defaults to a full fill when both account_fill and note_fill are 0. - let pswap = PswapNote::try_from(&pswap_note)?; - let p2id_note = pswap.execute_full_fill_network(network_consumer.id())?; - - let tx_context = mock_chain - .build_tx_context(network_consumer.id(), &[pswap_note.id()], &[])? - .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 for Alice - 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 network consumer'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(()) -} From 789a938229dfc48155aefed6c14c28cac0c93574 Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Wed, 15 Apr 2026 03:37:34 +0530 Subject: [PATCH 39/66] style(pswap): apply nightly rustfmt --- crates/miden-standards/src/note/pswap.rs | 68 ++++++++++----------- crates/miden-testing/tests/scripts/pswap.rs | 3 +- 2 files changed, 35 insertions(+), 36 deletions(-) diff --git a/crates/miden-standards/src/note/pswap.rs b/crates/miden-standards/src/note/pswap.rs index 87b9acf050..dcfbbbbd71 100644 --- a/crates/miden-standards/src/note/pswap.rs +++ b/crates/miden-standards/src/note/pswap.rs @@ -5,8 +5,16 @@ use miden_protocol::assembly::Path; use miden_protocol::asset::{Asset, AssetCallbackFlag, FungibleAsset}; use miden_protocol::errors::NoteError; use miden_protocol::note::{ - Note, NoteAssets, NoteAttachment, NoteAttachmentScheme, NoteMetadata, NoteRecipient, - NoteScript, NoteStorage, NoteTag, NoteType, + Note, + NoteAssets, + NoteAttachment, + NoteAttachmentScheme, + NoteMetadata, + NoteRecipient, + NoteScript, + NoteStorage, + NoteTag, + NoteType, }; use miden_protocol::utils::sync::LazyLock; use miden_protocol::{Felt, ONE, Word, ZERO}; @@ -176,20 +184,14 @@ impl TryFrom<&[Felt]> for PswapNoteStorage { u8::try_from(note_storage[0].as_canonical_u64()) .map_err(|_| NoteError::other("enable_callbacks exceeds u8"))?, ) - .map_err(|e| { - NoteError::other_with_source("failed to parse asset callback flag", e) - })?; + .map_err(|e| NoteError::other_with_source("failed to parse asset callback flag", e))?; - let faucet_id = - AccountId::try_from_elements(note_storage[1], note_storage[2]).map_err(|e| { - NoteError::other_with_source("failed to parse requested faucet ID", e) - })?; + let faucet_id = AccountId::try_from_elements(note_storage[1], note_storage[2]) + .map_err(|e| NoteError::other_with_source("failed to parse requested faucet ID", e))?; let amount = note_storage[3].as_canonical_u64(); let requested_asset = FungibleAsset::new(faucet_id, amount) - .map_err(|e| { - NoteError::other_with_source("failed to create requested asset", e) - })? + .map_err(|e| NoteError::other_with_source("failed to create requested asset", e))? .with_callbacks(callbacks); let pswap_tag = NoteTag::new( @@ -209,9 +211,7 @@ impl TryFrom<&[Felt]> for PswapNoteStorage { .map_err(|_| NoteError::other("swap_count exceeds u16"))?; let creator_account_id = AccountId::try_from_elements(note_storage[9], note_storage[8]) - .map_err(|e| { - NoteError::other_with_source("failed to parse creator account ID", e) - })?; + .map_err(|e| NoteError::other_with_source("failed to parse creator account ID", e))?; Ok(Self { requested_asset, @@ -379,14 +379,12 @@ impl PswapNote { ) -> Result<(Note, Option), NoteError> { // Combine account fill and note fill into a single payback asset. let payback_asset = match (account_fill_asset, note_fill_asset) { - (Some(account_fill), Some(note_fill)) => { - account_fill.add(note_fill).map_err(|e| { - NoteError::other_with_source( - "failed to combine account fill and note fill assets", - e, - ) - })? - }, + (Some(account_fill), Some(note_fill)) => account_fill.add(note_fill).map_err(|e| { + NoteError::other_with_source( + "failed to combine account fill and note fill assets", + e, + ) + })?, (Some(asset), None) | (None, Some(asset)) => asset, (None, None) => { return Err(NoteError::other( @@ -514,11 +512,11 @@ impl PswapNote { /// calculation. Returns the full `offered_total` when `fill_amount == requested_total`. /// /// The formula is implemented in two branches to maximize precision: - /// - When `offered > requested`: the ratio `offered/requested` is >= 1, so we compute - /// `(offered * FACTOR / requested) * fill_amount / FACTOR` to avoid losing the fractional part. + /// - When `offered > requested`: the ratio `offered/requested` is >= 1, so we compute `(offered + /// * FACTOR / requested) * fill_amount / FACTOR` to avoid losing the fractional part. /// - When `requested >= offered`: the ratio `offered/requested` is < 1, so computing it - /// directly would truncate to zero. Instead we compute the inverse ratio - /// `(requested * FACTOR / offered)` and divide: `(fill_amount * FACTOR) / inverse_ratio`. + /// directly would truncate to zero. Instead we compute the inverse ratio `(requested * FACTOR + /// / offered)` and divide: `(fill_amount * FACTOR) / inverse_ratio`. fn calculate_output_amount(offered_total: u64, requested_total: u64, fill_amount: u64) -> u64 { const PRECISION_FACTOR: u64 = 100_000; @@ -571,10 +569,9 @@ impl PswapNote { let attachment = NoteAttachment::new_word(NoteAttachmentScheme::none(), attachment_word); let p2id_assets = NoteAssets::new(vec![Asset::Fungible(payback_asset)])?; - let p2id_metadata = - NoteMetadata::new(consumer_account_id, self.storage.payback_note_type) - .with_tag(payback_note_tag) - .with_attachment(attachment); + let p2id_metadata = NoteMetadata::new(consumer_account_id, self.storage.payback_note_type) + .with_tag(payback_note_tag) + .with_attachment(attachment); Ok(Note::new(p2id_assets, p2id_metadata, recipient)) } @@ -606,7 +603,8 @@ impl PswapNote { .payback_note_type(self.storage.payback_note_type) .build(); - // Remainder serial: increment most significant element (matching MASM movup.3 add.1 movdn.3) + // Remainder serial: increment most significant element (matching MASM movup.3 add.1 + // movdn.3) let remainder_serial_num = Word::from([ self.serial_number[0], self.serial_number[1], @@ -862,10 +860,10 @@ mod tests { requested_asset.faucet_id().suffix(), requested_asset.faucet_id().prefix().as_felt(), Felt::try_from(requested_asset.amount()).unwrap(), - Felt::from(0xc0000000u32), // pswap_tag - Felt::from(0x80000001u32), // payback_note_tag + Felt::from(0xc0000000u32), // pswap_tag + Felt::from(0x80000001u32), // payback_note_tag Felt::from(NoteType::Private.as_u8()), // payback_note_type - Felt::from(3u16), // swap_count + Felt::from(3u16), // swap_count creator_id.prefix().as_felt(), creator_id.suffix(), ]; diff --git a/crates/miden-testing/tests/scripts/pswap.rs b/crates/miden-testing/tests/scripts/pswap.rs index 446fa7ca55..4f3831faf5 100644 --- a/crates/miden-testing/tests/scripts/pswap.rs +++ b/crates/miden-testing/tests/scripts/pswap.rs @@ -212,7 +212,8 @@ async fn pswap_note_alice_reconstructs_and_consumes_p2id() -> anyhow::Result<()> /// Parameterized fill test covering: /// - full public fill /// - full private fill -/// - partial public fill (offered=50 USDC / requested=25 ETH / fill=20 ETH → payout=40 USDC, remainder=10 USDC) +/// - partial public fill (offered=50 USDC / requested=25 ETH / fill=20 ETH → payout=40 USDC, +/// remainder=10 USDC) /// - full fill via a network account (no note_args → script defaults to full fill) #[rstest] #[case::full_public(25, NoteType::Public, false)] From 60577b34e16e1823ce432efe13eedcdb6568cde2 Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Wed, 15 Apr 2026 17:09:38 +0530 Subject: [PATCH 40/66] fix(pswap): guard user-provided fill sum against u64 overflow Switch `execute_pswap`'s `account_fill + note_fill` check to `u64::overflowing_add` + `ERR_PSWAP_FILL_SUM_OVERFLOW`, so a malicious consumer cannot pick operands whose felt sum wraps past u64 and spuriously satisfies the `lte requested` guard. Also document why the matching sums/subs in `create_p2id_note` and `create_remainder_note` cannot over/underflow, and drop a stray trailing space on `FACTOR`. --- .../asm/standards/notes/pswap.masm | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/crates/miden-standards/asm/standards/notes/pswap.masm b/crates/miden-standards/asm/standards/notes/pswap.masm index 02e95092d2..b4b99cb1df 100644 --- a/crates/miden-standards/asm/standards/notes/pswap.masm +++ b/crates/miden-standards/asm/standards/notes/pswap.masm @@ -24,7 +24,7 @@ const NUM_STORAGE_ITEMS=10 # symmetrically `requested * FACTOR`, `fill_amount * FACTOR`), which must fit # in u64. With FACTOR = 100000 that caps each side at ~1.8e14 (2^64 / 1e5), # well above any realistic fungible-asset amount. -const FACTOR=100000 +const FACTOR=100000 const MAX_U32=0x0000000100000000 # Note storage layout (10 felts, loaded at STORAGE_PTR by get_storage): @@ -104,6 +104,7 @@ const EXEC_AMT_REQUESTED_NOTE_FILL = 9 const ERR_PSWAP_WRONG_NUMBER_OF_STORAGE_ITEMS="PSWAP script expects exactly 10 note storage items" const ERR_PSWAP_WRONG_NUMBER_OF_ASSETS="PSWAP script requires exactly one note asset" const ERR_PSWAP_FILL_EXCEEDS_REQUESTED="PSWAP fill amount exceeds requested amount" +const ERR_PSWAP_FILL_SUM_OVERFLOW="PSWAP account_fill + note_fill overflows u64" # PRICE CALCULATION # ================================================================================================= @@ -253,8 +254,12 @@ proc create_p2id_note loc_store.P2ID_AMT_NOTE_FILL # => [] - # Set attachment: aux = amt_account_fill + amt_note_fill - # attachment_scheme = 0 (NoteAttachmentScheme::none) + # Set attachment: aux = amt_account_fill + amt_note_fill. + # attachment_scheme = 0 (NoteAttachmentScheme::none). + # + # The add cannot overflow: `execute_pswap` asserts + # `amt_account_fill + amt_note_fill <= requested_amount` before calling + # this procedure, and `requested_amount` itself fits in a felt. loc_load.P2ID_AMT_ACCOUNT_FILL loc_load.P2ID_AMT_NOTE_FILL add # => [total_fill] push.0.0.0 @@ -386,7 +391,9 @@ proc create_remainder_note exec.output_note::set_word_attachment # => [] - # Add remaining offered asset: remainder_amount = amt_offered - amt_payout + # Add remaining offered asset: remainder_amount = amt_offered - amt_payout. + # Sub cannot underflow: amt_payout <= amt_offered by construction in + # `calculate_tokens_offered_for_requested`. loc_load.REMAINDER_NOTE_IDX # => [note_idx] @@ -514,8 +521,17 @@ proc execute_pswap loc_store.EXEC_AMT_REQUESTED # => [] - # Assert (account_fill + note_fill) <= requested - loc_load.EXEC_AMT_REQUESTED_ACCOUNT_FILL loc_load.EXEC_AMT_REQUESTED_NOTE_FILL add + # Assert (account_fill + note_fill) <= requested. + # account_fill and note_fill are user-provided, so use overflow-checked add. + loc_load.EXEC_AMT_REQUESTED_ACCOUNT_FILL u32split + loc_load.EXEC_AMT_REQUESTED_NOTE_FILL u32split + # => [nf_lo, nf_hi, af_lo, af_hi] + exec.u64::overflowing_add + # => [overflow, sum_lo, sum_hi] + eq.0 assert.err=ERR_PSWAP_FILL_SUM_OVERFLOW + # => [sum_lo, sum_hi] + swap mul.MAX_U32 add + # => [fill_amount] loc_load.EXEC_AMT_REQUESTED # => [requested, fill_amount] lte assert.err=ERR_PSWAP_FILL_EXCEEDS_REQUESTED @@ -683,7 +699,7 @@ pub proc main drop # => [account_fill_amount, note_fill_amount] - dup.1 dup.1 add push.0 eq + dup.1 eq.0 dup.1 eq.0 and # => [both_zero, account_fill_amount, note_fill_amount] if.true drop mem_load.REQUESTED_AMOUNT_ITEM From 0b6ab74d97660619be9e89c0debcbdfd1652fc7d Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Wed, 15 Apr 2026 18:17:18 +0530 Subject: [PATCH 41/66] fix(pswap): correctness + layout cleanups from PR review theme 2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - create_p2id_note: drop three stray zero pads that shoved the real `note_idx` to stack depth 11 in the `has_account_fill` branch, so `move_asset_to_note` was reading a pad zero as the note index. The bug was masked in every existing test because `note_idx == 0`; add a SPAWN-note regression test that forces P2ID to `note_idx == 1`. - load_offered_asset: bump `@locals(2)` to `@locals(8)` — we store two words (8 felts), not two felts. - Drop `pswap_tag` from note storage (10 -> 9 items). The remainder path now lifts the tag out of the active note's metadata via `dup.2 movdn.4` + `metadata_into_note_type`, since the asset pair is unchanged across remainder creation. `PswapNoteStorage` loses the field, `with_pswap_tag`, and the getter; storage offsets and `NUM_STORAGE_ITEMS` shift accordingly. - Document why `create_p2id_note`'s add (P2ID attachment), why `create_remainder_note`'s sub (amt_offered - amt_payout), and why `create_p2id_note` itself are reachable but safe (invariants come from the caller, not local checks). - Correct the `payback_note_type` storage-layout docstring (only P2ID payback uses it; remainder inherits from active-note metadata). - Note in a comment why `procref.main` cannot replace the runtime `active_note::get_script_root` call in `create_remainder_note` (compile-time call-graph cycle via main -> execute_pswap -> create_remainder_note -> procref.main). --- .../asm/standards/notes/pswap.masm | 69 ++++++----- crates/miden-standards/src/note/pswap.rs | 66 ++++------ crates/miden-testing/tests/scripts/pswap.rs | 114 ++++++++++++++++-- 3 files changed, 168 insertions(+), 81 deletions(-) diff --git a/crates/miden-standards/asm/standards/notes/pswap.masm b/crates/miden-standards/asm/standards/notes/pswap.masm index b4b99cb1df..1a71425faa 100644 --- a/crates/miden-standards/asm/standards/notes/pswap.masm +++ b/crates/miden-standards/asm/standards/notes/pswap.masm @@ -12,7 +12,7 @@ use miden::standards::wallets::basic->wallet # CONSTANTS # ================================================================================================= -const NUM_STORAGE_ITEMS=10 +const NUM_STORAGE_ITEMS=9 # Fixed-point scaling factor (1e5) used by calculate_tokens_offered_for_requested # to approximate the offered/requested ratio with u64 integer math. @@ -27,17 +27,16 @@ const NUM_STORAGE_ITEMS=10 const FACTOR=100000 const MAX_U32=0x0000000100000000 -# Note storage layout (10 felts, loaded at STORAGE_PTR by get_storage): +# Note storage layout (9 felts, loaded at STORAGE_PTR by get_storage): # - requested_enable_callbacks [0] : 1 felt # - requested_faucet_suffix [1] : 1 felt # - requested_faucet_prefix [2] : 1 felt # - requested_amount [3] : 1 felt -# - pswap_tag [4] : 1 felt -# - p2id_tag [5] : 1 felt -# - payback_note_type [6] : 1 felt -# - pswap_count [7] : 1 felt -# - creator_id_prefix [8] : 1 felt -# - creator_id_suffix [9] : 1 felt +# - p2id_tag [4] : 1 felt +# - payback_note_type [5] : 1 felt +# - pswap_count [6] : 1 felt +# - creator_id_prefix [7] : 1 felt +# - creator_id_suffix [8] : 1 felt # # Where: # - requested_enable_callbacks: Callback-enabled flag for the requested fungible asset @@ -46,23 +45,25 @@ const MAX_U32=0x0000000100000000 # - requested_faucet_prefix: Prefix of the requested asset's faucet AccountId # (AccountIdPrefix as Felt) # - requested_amount: Amount of the requested fungible asset (Felt) -# - pswap_tag: The NoteTag for remainder PSWAP notes, derived from the offered/requested asset pair # - p2id_tag: The NoteTag for P2ID payback notes, derived from the creator's account ID -# - payback_note_type: The NoteType propagated to the P2ID payback and remainder PSWAP notes +# - payback_note_type: The NoteType used for the P2ID payback note # - pswap_count: Number of times this note has been partially filled and re-created # - creator_id_prefix: The prefix of the creator's AccountId (AccountIdPrefix as Felt) # - creator_id_suffix: The suffix of the creator's AccountId (Felt) +# +# The remainder PSWAP note's own tag is not stored — it is lifted from the +# active note's metadata at remainder-creation time (same asset pair => same +# tag), saving one storage slot. const STORAGE_PTR = 0 const REQUESTED_ENABLE_CALLBACKS_ITEM = STORAGE_PTR const REQUESTED_FAUCET_SUFFIX_ITEM = STORAGE_PTR + 1 const REQUESTED_FAUCET_PREFIX_ITEM = STORAGE_PTR + 2 const REQUESTED_AMOUNT_ITEM = STORAGE_PTR + 3 -const PSWAP_TAG_ITEM = STORAGE_PTR + 4 -const P2ID_TAG_ITEM = STORAGE_PTR + 5 -const PAYBACK_NOTE_TYPE_ITEM = STORAGE_PTR + 6 -const PSWAP_COUNT_ITEM = STORAGE_PTR + 7 -const PSWAP_CREATOR_PREFIX_ITEM = STORAGE_PTR + 8 -const PSWAP_CREATOR_SUFFIX_ITEM = STORAGE_PTR + 9 +const P2ID_TAG_ITEM = STORAGE_PTR + 4 +const PAYBACK_NOTE_TYPE_ITEM = STORAGE_PTR + 5 +const PSWAP_COUNT_ITEM = STORAGE_PTR + 6 +const PSWAP_CREATOR_PREFIX_ITEM = STORAGE_PTR + 7 +const PSWAP_CREATOR_SUFFIX_ITEM = STORAGE_PTR + 8 # Local memory offsets # ------------------------------------------------------------------------------------------------- @@ -101,7 +102,7 @@ const EXEC_AMT_REQUESTED_NOTE_FILL = 9 # ERRORS # ================================================================================================= -const ERR_PSWAP_WRONG_NUMBER_OF_STORAGE_ITEMS="PSWAP script expects exactly 10 note storage items" +const ERR_PSWAP_WRONG_NUMBER_OF_STORAGE_ITEMS="PSWAP script expects exactly 9 note storage items" const ERR_PSWAP_WRONG_NUMBER_OF_ASSETS="PSWAP script requires exactly one note asset" const ERR_PSWAP_FILL_EXCEEDS_REQUESTED="PSWAP fill amount exceeds requested amount" const ERR_PSWAP_FILL_SUM_OVERFLOW="PSWAP account_fill + note_fill overflows u64" @@ -278,13 +279,11 @@ proc create_p2id_note padw push.0.0.0 loc_load.P2ID_NOTE_IDX # => [note_idx, pad(7)] - push.0.0.0 loc_load.P2ID_AMT_ACCOUNT_FILL - # => [amt_account_fill, 0, 0, 0, note_idx, pad(7)] - + loc_load.P2ID_AMT_ACCOUNT_FILL loc_load.P2ID_REQUESTED_FAUCET_PREFIX loc_load.P2ID_REQUESTED_FAUCET_SUFFIX loc_load.P2ID_REQUESTED_ENABLE_CB - # => [enable_cb, faucet_suffix, faucet_prefix, amt_account_fill, 0, 0, 0, note_idx, pad(7)] + # => [enable_cb, faucet_suffix, faucet_prefix, amt_account_fill, note_idx, pad(7)] exec.asset::create_fungible_asset # => [ASSET_KEY, ASSET_VALUE, note_idx, pad(7)] @@ -356,7 +355,7 @@ proc create_remainder_note movup.2 mem_store.REQUESTED_AMOUNT_ITEM # => [note_type, tag] - # Build PSWAP 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, note_type, tag] @@ -430,7 +429,7 @@ end #! Inputs: [] #! Outputs: [ASSET_KEY, ASSET_VALUE] #! -@locals(2) +@locals(8) proc load_offered_asset locaddr.0 exec.active_note::get_assets # => [num_assets, dest_ptr] @@ -566,7 +565,9 @@ proc execute_pswap loc_store.EXEC_AMT_PAYOUT_TOTAL # => [] - # Create P2ID note for creator + # Create P2ID note for creator. Content is keyed off the fill amounts + # (`account_fill + note_fill` of the requested asset), not `total_payout`, + # and fills are guaranteed > 0 by the `both_zero` fallback in `main`. loc_load.EXEC_AMT_REQUESTED_NOTE_FILL loc_load.EXEC_AMT_REQUESTED_ACCOUNT_FILL mem_load.REQUESTED_FAUCET_PREFIX_ITEM @@ -618,14 +619,22 @@ proc execute_pswap swap sub # => [remaining_requested] - mem_load.PSWAP_TAG_ITEM - # => [tag, remaining_requested] - - # Fetch the active note's type to inherit it for the remainder pswap note. + # Fetch active note metadata: the remainder note inherits both the + # note type and the pswap tag from it. The asset pair is unchanged, + # so the tag derived in Rust at creation time still applies, and we + # avoid spending a storage slot on `pswap_tag`. exec.active_note::get_metadata - # => [NOTE_ATTACHMENT(4), METADATA_HEADER(4), tag, remaining_requested] + # => [NOTE_ATTACHMENT(4), METADATA_HEADER(4), remaining_requested] + # where METADATA_HEADER = [sid_suf_ver, sid_pre, tag, att_ks] dropw - # => [METADATA_HEADER(4), tag, remaining_requested] + # => [sid_suf_ver, sid_pre, tag, att_ks, remaining_requested] + + # Dup the tag out of METADATA_HEADER (at depth 2) and park it just + # below the header so `metadata_into_note_type` can consume the + # header intact. + dup.2 movdn.4 + # => [sid_suf_ver, sid_pre, tag, att_ks, tag, remaining_requested] + exec.note::metadata_into_note_type # => [note_type, tag, remaining_requested] diff --git a/crates/miden-standards/src/note/pswap.rs b/crates/miden-standards/src/note/pswap.rs index dcfbbbbd71..9bda7e747c 100644 --- a/crates/miden-standards/src/note/pswap.rs +++ b/crates/miden-standards/src/note/pswap.rs @@ -41,7 +41,7 @@ static PSWAP_SCRIPT: LazyLock = LazyLock::new(|| { /// Canonical storage representation for a PSWAP note. /// -/// Maps to the 14-element [`NoteStorage`] layout consumed by the on-chain MASM script: +/// Maps to the 9-element [`NoteStorage`] layout consumed by the on-chain MASM script: /// /// | Slot | Field | /// |---------|-------| @@ -49,18 +49,18 @@ static PSWAP_SCRIPT: LazyLock = LazyLock::new(|| { /// | `[1]` | Requested asset faucet ID suffix | /// | `[2]` | Requested asset faucet ID prefix | /// | `[3]` | Requested asset amount | -/// | `[4]` | PSWAP note tag | -/// | `[5]` | Payback note routing tag (targets the creator) | -/// | `[6]` | Payback note type (0 = private, 1 = public) | -/// | `[7]` | Swap count (incremented on each partial fill) | -/// | `[8-9]` | Creator account ID (prefix, suffix) | +/// | `[4]` | Payback note routing tag (targets the creator) | +/// | `[5]` | Payback note type (0 = private, 1 = public) | +/// | `[6]` | Swap count (incremented on each partial fill) | +/// | `[7-8]` | Creator account ID (prefix, suffix) | +/// +/// The PSWAP note's own tag is not stored: it lives in the note's metadata and +/// is lifted from there by the on-chain script when a remainder note is created +/// (the asset pair is unchanged, so the tag carries over unchanged). #[derive(Debug, Clone, PartialEq, Eq, bon::Builder)] pub struct PswapNoteStorage { requested_asset: FungibleAsset, - #[builder(default)] - pswap_tag: NoteTag, - #[builder(default)] swap_count: u16, @@ -80,20 +80,13 @@ impl PswapNoteStorage { // -------------------------------------------------------------------------------------------- /// Expected number of storage items for the PSWAP note. - pub const NUM_STORAGE_ITEMS: usize = 10; + pub const NUM_STORAGE_ITEMS: usize = 9; /// 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)) } - /// 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 - } - // PUBLIC ACCESSORS // -------------------------------------------------------------------------------------------- @@ -102,13 +95,6 @@ impl PswapNoteStorage { &self.requested_asset } - /// Returns the PSWAP note tag. This may be the default (zero) tag until the note - /// is converted into a [`Note`], at which point the tag is derived from the - /// offered/requested asset pair. - pub fn pswap_tag(&self) -> NoteTag { - self.pswap_tag - } - /// Returns the payback note routing tag, derived from the creator's account ID. pub fn payback_note_tag(&self) -> NoteTag { NoteTag::with_account_target(self.creator_account_id) @@ -140,7 +126,7 @@ impl PswapNoteStorage { } } -/// Serializes [`PswapNoteStorage`] into a 10-element [`NoteStorage`]. +/// Serializes [`PswapNoteStorage`] into a 9-element [`NoteStorage`]. impl From for NoteStorage { fn from(storage: PswapNoteStorage) -> Self { let storage_items = vec![ @@ -150,14 +136,13 @@ impl From for NoteStorage { storage.requested_asset.faucet_id().prefix().as_felt(), Felt::try_from(storage.requested_asset.amount()) .expect("asset amount should fit in a felt"), - // Tags [4-5] - Felt::from(storage.pswap_tag), + // Payback tag [4] Felt::from(storage.payback_note_tag()), - // Payback note type [6] + // Payback note type [5] Felt::from(storage.payback_note_type.as_u8()), - // Swap count [7] + // Swap count [6] Felt::from(storage.swap_count), - // Creator ID [8-9] + // Creator ID [7-8] storage.creator_account_id.prefix().as_felt(), storage.creator_account_id.suffix(), ]; @@ -166,7 +151,7 @@ impl From for NoteStorage { } } -/// Deserializes [`PswapNoteStorage`] from a slice of exactly 10 [`Felt`]s. +/// Deserializes [`PswapNoteStorage`] from a slice of exactly 9 [`Felt`]s. impl TryFrom<&[Felt]> for PswapNoteStorage { type Error = NoteError; @@ -194,28 +179,24 @@ impl TryFrom<&[Felt]> for PswapNoteStorage { .map_err(|e| NoteError::other_with_source("failed to create requested asset", e))? .with_callbacks(callbacks); - let pswap_tag = NoteTag::new( - u32::try_from(note_storage[4].as_canonical_u64()) - .map_err(|_| NoteError::other("pswap_tag exceeds u32"))?, - ); - + // [4] is the payback_note_tag, which is derived from the creator ID at + // serialization time and not re-parsed here. let payback_note_type = NoteType::try_from( - u8::try_from(note_storage[6].as_canonical_u64()) + u8::try_from(note_storage[5].as_canonical_u64()) .map_err(|_| NoteError::other("payback_note_type exceeds u8"))?, ) .map_err(|e| NoteError::other_with_source("failed to parse payback note type", e))?; - let swap_count: u16 = note_storage[7] + let swap_count: u16 = note_storage[6] .as_canonical_u64() .try_into() .map_err(|_| NoteError::other("swap_count exceeds u16"))?; - let creator_account_id = AccountId::try_from_elements(note_storage[9], note_storage[8]) + let creator_account_id = AccountId::try_from_elements(note_storage[8], note_storage[7]) .map_err(|e| NoteError::other_with_source("failed to parse creator account ID", e))?; Ok(Self { requested_asset, - pswap_tag, swap_count, creator_account_id, payback_note_type, @@ -597,7 +578,6 @@ impl PswapNote { .ok_or_else(|| NoteError::other("swap count overflow"))?; let new_storage = PswapNoteStorage::builder() .requested_asset(remaining_requested_asset) - .pswap_tag(self.storage.pswap_tag) .swap_count(next_swap_count) .creator_account_id(self.storage.creator_account_id) .payback_note_type(self.storage.payback_note_type) @@ -644,8 +624,7 @@ impl From for Note { pswap.storage.requested_asset(), ); - let storage = pswap.storage.with_pswap_tag(tag); - let recipient = storage.into_recipient(pswap.serial_number); + let recipient = pswap.storage.into_recipient(pswap.serial_number); let assets = NoteAssets::new(vec![Asset::Fungible(pswap.offered_asset)]) .expect("single fungible asset should be valid"); @@ -860,7 +839,6 @@ mod tests { requested_asset.faucet_id().suffix(), requested_asset.faucet_id().prefix().as_felt(), Felt::try_from(requested_asset.amount()).unwrap(), - Felt::from(0xc0000000u32), // pswap_tag Felt::from(0x80000001u32), // payback_note_tag Felt::from(NoteType::Private.as_u8()), // payback_note_type Felt::from(3u16), // swap_count diff --git a/crates/miden-testing/tests/scripts/pswap.rs b/crates/miden-testing/tests/scripts/pswap.rs index 4f3831faf5..366405890b 100644 --- a/crates/miden-testing/tests/scripts/pswap.rs +++ b/crates/miden-testing/tests/scripts/pswap.rs @@ -9,7 +9,10 @@ use miden_protocol::transaction::RawOutputNote; use miden_protocol::{Felt, ONE, Word, ZERO}; use miden_standards::account::wallets::BasicWallet; use miden_standards::note::{PswapNote, PswapNoteStorage}; +use miden_standards::testing::note::NoteBuilder; use miden_testing::{Auth, MockChain, MockChainBuilder}; +use rand::SeedableRng; +use rand::rngs::SmallRng; use rstest::rstest; // CONSTANTS @@ -176,12 +179,15 @@ async fn pswap_note_alice_reconstructs_and_consumes_p2id() -> anyhow::Result<()> // --- Step 2: Alice reconstructs the P2ID note from her PSWAP data + aux --- - // In production, Alice reads the fill amount from the P2ID note's attachment (aux data), - // which is visible for both public and private notes. Here we read it from the - // Rust-predicted note since the test framework doesn't preserve word attachment content - // in executed transaction outputs. - let aux_word = p2id_note.metadata().attachment().content().to_word(); - let fill_amount_from_aux = aux_word[0].as_canonical_u64(); + //let aux_word = p2id_note.metadata().attachment().content().to_word(); + let aux_word = executed_transaction + .output_notes() + .get_note(0) + .metadata() + .attachment() + .content() + .to_word(); + let fill_amount_from_aux = aux_word[3].as_canonical_u64(); assert_eq!(fill_amount_from_aux, 20, "Fill amount from aux should be 20 ETH"); // Alice reconstructs the recipient using her serial number and account ID @@ -520,6 +526,101 @@ async fn pswap_note_invalid_input_test() -> anyhow::Result<()> { Ok(()) } +/// Regression test for the `note_idx` stack-layout bug in `create_p2id_note`'s +/// `has_account_fill` branch. +/// +/// The buggy frame setup left three stray zeros between `ASSET_VALUE` and the +/// real `note_idx` on the stack, so `move_asset_to_note` read a pad zero as the +/// note index. Every existing pswap test masked this because the PSWAP note +/// was always the only output-note emitter in the transaction, so `note_idx` +/// was 0 and happened to match one of the pad zeros by coincidence. +/// +/// This test consumes a SPAWN note *first*, which emits an (empty) dummy note +/// at `note_idx == 0`. The subsequent PSWAP note therefore creates its P2ID at +/// `note_idx == 1`. If the bug is reintroduced, bob's 25 ETH will be routed to +/// the dummy at idx 0 instead of the P2ID at idx 1, and the asset assertions +/// below will fail. +#[tokio::test] +async fn pswap_note_idx_nonzero_regression_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 bob = builder.add_existing_wallet_with_assets( + BASIC_AUTH, + [FungibleAsset::new(eth_faucet.id(), 25)?.into()], + )?; + + let mut rng = RandomCoin::new(Word::default()); + let pswap_note = build_pswap_note( + &mut builder, + alice.id(), + FungibleAsset::new(usdc_faucet.id(), 50)?, + FungibleAsset::new(eth_faucet.id(), 25)?, + NoteType::Public, + &mut rng, + )?; + + // Dummy output note to be emitted by the SPAWN note. Sender must equal + // the transaction's native account (bob) per `create_spawn_note`'s check. + // No assets — keeps the spawn script trivial. + let dummy_note = NoteBuilder::new(bob.id(), SmallRng::seed_from_u64(7777)).build()?; + let spawn_note = builder.add_spawn_note([&dummy_note])?; + + let mock_chain = builder.build()?; + + // Full account-fill: 25 ETH out of bob's vault. Exercises the + // `has_account_fill` branch where the `note_idx` bug lives. + let mut note_args_map = BTreeMap::new(); + note_args_map.insert(pswap_note.id(), note_args(25, 0)); + + let pswap = PswapNote::try_from(&pswap_note)?; + let (expected_p2id, _) = + pswap.execute(bob.id(), Some(FungibleAsset::new(eth_faucet.id(), 25)?), None)?; + + // Consume spawn first so the PSWAP-created P2ID gets note_idx == 1. + let tx_context = mock_chain + .build_tx_context(bob.id(), &[spawn_note.id(), pswap_note.id()], &[])? + .extend_note_args(note_args_map) + .extend_expected_output_notes(vec![ + RawOutputNote::Full(dummy_note.clone()), + RawOutputNote::Full(expected_p2id), + ]) + .build()?; + + let executed = tx_context.execute().await?; + + // Exactly 2 output notes: dummy (from spawn) at idx 0, P2ID (from pswap) at idx 1. + let output_notes = executed.output_notes(); + assert_eq!(output_notes.num_notes(), 2, "expected dummy + p2id"); + + // Dummy at idx 0 must be empty. If the note_idx bug is reintroduced, + // bob's 25 ETH would land here instead of on the P2ID. + let dummy_out = output_notes.get_note(0); + assert_eq!( + dummy_out.assets().num_assets(), + 0, + "SPAWN dummy should be empty; non-empty means `create_p2id_note` \ + wrote its asset to the wrong output note_idx", + ); + + // P2ID at idx 1 must carry the full 25 ETH. + let p2id_out = output_notes.get_note(1); + assert_eq!(p2id_out.assets().num_assets(), 1, "P2ID must have 1 asset"); + assert_fungible_asset(p2id_out.assets().iter().next().unwrap(), eth_faucet.id(), 25); + + // Bob's vault: +50 USDC payout, -25 ETH fill. + let vault_delta = executed.account_delta().vault(); + assert_vault_added_removed(vault_delta, (usdc_faucet.id(), 50), (eth_faucet.id(), 25)); + + Ok(()) +} + #[rstest] #[case(5)] #[case(7)] @@ -802,7 +903,6 @@ async fn pswap_chained_partial_fills_test( let storage = PswapNoteStorage::builder() .requested_asset(requested_fungible) - .pswap_tag(pswap_tag) .swap_count(current_swap_count as u16) .creator_account_id(alice.id()) .build(); From be10bce6f567f4b20eacec7d94b1aaf6299d8739 Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Wed, 15 Apr 2026 19:13:01 +0530 Subject: [PATCH 42/66] fix(pswap): align attachment layout + drop dead swap_count slot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Attachment layout: MASM now emits `[fill, 0, 0, 0]` (Word[0] = fill) for both the P2ID and remainder attachments, matching the Rust-side `Word::from([fill, 0, 0, 0])` convention. Previously the two sides disagreed (Rust wrote Word[0], MASM wrote Word[3]), and the alice-reconstructs test was silently compensating by reading `aux_word[3]` against a Rust prediction it never compared to. - alice-reconstructs test: read from the executed transaction's output (was already), read `aux_word[0]`, and add a direct Rust <-> MASM attachment-word parity `assert_eq!` so a future drift actually fails. - Extend alice-reconstructs to also reconstruct the remainder PSWAP from her original pswap data + the remainder's on-chain attachment, verifying recipient + attachment parity against the executed output. - Drop `pswap_count` from note storage (9 -> 8 items). The field was never read or written by pswap.masm — a dead pass-through slot. The Rust side was silently bumping it on remainder creation, so the Rust-predicted remainder recipient diverged from MASM's (caught only by extending the alice test). Remove the field, getter, builder arg, storage offsets, and all callers; shift creator ID to slots [6-7]. --- .../asm/standards/notes/pswap.masm | 34 +++-- crates/miden-standards/src/note/pswap.rs | 41 +----- crates/miden-testing/tests/scripts/pswap.rs | 123 +++++++++++++++--- 3 files changed, 126 insertions(+), 72 deletions(-) diff --git a/crates/miden-standards/asm/standards/notes/pswap.masm b/crates/miden-standards/asm/standards/notes/pswap.masm index 1a71425faa..196349e5bb 100644 --- a/crates/miden-standards/asm/standards/notes/pswap.masm +++ b/crates/miden-standards/asm/standards/notes/pswap.masm @@ -12,7 +12,7 @@ use miden::standards::wallets::basic->wallet # CONSTANTS # ================================================================================================= -const NUM_STORAGE_ITEMS=9 +const NUM_STORAGE_ITEMS=8 # Fixed-point scaling factor (1e5) used by calculate_tokens_offered_for_requested # to approximate the offered/requested ratio with u64 integer math. @@ -27,16 +27,15 @@ const NUM_STORAGE_ITEMS=9 const FACTOR=100000 const MAX_U32=0x0000000100000000 -# Note storage layout (9 felts, loaded at STORAGE_PTR by get_storage): +# Note storage layout (8 felts, loaded at STORAGE_PTR by get_storage): # - requested_enable_callbacks [0] : 1 felt # - requested_faucet_suffix [1] : 1 felt # - requested_faucet_prefix [2] : 1 felt # - requested_amount [3] : 1 felt # - p2id_tag [4] : 1 felt # - payback_note_type [5] : 1 felt -# - pswap_count [6] : 1 felt -# - creator_id_prefix [7] : 1 felt -# - creator_id_suffix [8] : 1 felt +# - creator_id_prefix [6] : 1 felt +# - creator_id_suffix [7] : 1 felt # # Where: # - requested_enable_callbacks: Callback-enabled flag for the requested fungible asset @@ -47,7 +46,6 @@ const MAX_U32=0x0000000100000000 # - requested_amount: Amount of the requested fungible asset (Felt) # - p2id_tag: The NoteTag for P2ID payback notes, derived from the creator's account ID # - payback_note_type: The NoteType used for the P2ID payback note -# - pswap_count: Number of times this note has been partially filled and re-created # - creator_id_prefix: The prefix of the creator's AccountId (AccountIdPrefix as Felt) # - creator_id_suffix: The suffix of the creator's AccountId (Felt) # @@ -61,9 +59,8 @@ const REQUESTED_FAUCET_PREFIX_ITEM = STORAGE_PTR + 2 const REQUESTED_AMOUNT_ITEM = STORAGE_PTR + 3 const P2ID_TAG_ITEM = STORAGE_PTR + 4 const PAYBACK_NOTE_TYPE_ITEM = STORAGE_PTR + 5 -const PSWAP_COUNT_ITEM = STORAGE_PTR + 6 -const PSWAP_CREATOR_PREFIX_ITEM = STORAGE_PTR + 7 -const PSWAP_CREATOR_SUFFIX_ITEM = STORAGE_PTR + 8 +const PSWAP_CREATOR_PREFIX_ITEM = STORAGE_PTR + 6 +const PSWAP_CREATOR_SUFFIX_ITEM = STORAGE_PTR + 7 # Local memory offsets # ------------------------------------------------------------------------------------------------- @@ -102,7 +99,7 @@ const EXEC_AMT_REQUESTED_NOTE_FILL = 9 # ERRORS # ================================================================================================= -const ERR_PSWAP_WRONG_NUMBER_OF_STORAGE_ITEMS="PSWAP script expects exactly 9 note storage items" +const ERR_PSWAP_WRONG_NUMBER_OF_STORAGE_ITEMS="PSWAP script expects exactly 8 note storage items" const ERR_PSWAP_WRONG_NUMBER_OF_ASSETS="PSWAP script requires exactly one note asset" const ERR_PSWAP_FILL_EXCEEDS_REQUESTED="PSWAP fill amount exceeds requested amount" const ERR_PSWAP_FILL_SUM_OVERFLOW="PSWAP account_fill + note_fill overflows u64" @@ -255,18 +252,17 @@ proc create_p2id_note loc_store.P2ID_AMT_NOTE_FILL # => [] - # Set attachment: aux = amt_account_fill + amt_note_fill. + # Set attachment: aux = amt_account_fill + amt_note_fill at Word[0]. # attachment_scheme = 0 (NoteAttachmentScheme::none). # # The add cannot overflow: `execute_pswap` asserts # `amt_account_fill + amt_note_fill <= requested_amount` before calling # this procedure, and `requested_amount` itself fits in a felt. - loc_load.P2ID_AMT_ACCOUNT_FILL loc_load.P2ID_AMT_NOTE_FILL add - # => [total_fill] push.0.0.0 - # => [0, 0, 0, total_fill] + loc_load.P2ID_AMT_ACCOUNT_FILL loc_load.P2ID_AMT_NOTE_FILL add + # => [total_fill, 0, 0, 0] push.0 loc_load.P2ID_NOTE_IDX - # => [note_idx, attachment_scheme=0, 0, 0, 0, total_fill] + # => [note_idx, attachment_scheme=0, total_fill, 0, 0, 0] exec.output_note::set_word_attachment # => [] @@ -381,11 +377,11 @@ proc create_remainder_note loc_store.REMAINDER_NOTE_IDX # => [] - # Set attachment: aux = amt_payout - loc_load.REMAINDER_AMT_PAYOUT push.0.0.0 - # => [0, 0, 0, amt_payout] + # Set attachment: aux = amt_payout at Word[0] + push.0.0.0 loc_load.REMAINDER_AMT_PAYOUT + # => [amt_payout, 0, 0, 0] push.0 loc_load.REMAINDER_NOTE_IDX - # => [note_idx, attachment_scheme=0, ATTACHMENT] + # => [note_idx, attachment_scheme=0, amt_payout, 0, 0, 0] exec.output_note::set_word_attachment # => [] diff --git a/crates/miden-standards/src/note/pswap.rs b/crates/miden-standards/src/note/pswap.rs index 9bda7e747c..e919e544f5 100644 --- a/crates/miden-standards/src/note/pswap.rs +++ b/crates/miden-standards/src/note/pswap.rs @@ -41,7 +41,7 @@ static PSWAP_SCRIPT: LazyLock = LazyLock::new(|| { /// Canonical storage representation for a PSWAP note. /// -/// Maps to the 9-element [`NoteStorage`] layout consumed by the on-chain MASM script: +/// Maps to the 8-element [`NoteStorage`] layout consumed by the on-chain MASM script: /// /// | Slot | Field | /// |---------|-------| @@ -51,8 +51,7 @@ static PSWAP_SCRIPT: LazyLock = LazyLock::new(|| { /// | `[3]` | Requested asset amount | /// | `[4]` | Payback note routing tag (targets the creator) | /// | `[5]` | Payback note type (0 = private, 1 = public) | -/// | `[6]` | Swap count (incremented on each partial fill) | -/// | `[7-8]` | Creator account ID (prefix, suffix) | +/// | `[6-7]` | Creator account ID (prefix, suffix) | /// /// The PSWAP note's own tag is not stored: it lives in the note's metadata and /// is lifted from there by the on-chain script when a remainder note is created @@ -61,9 +60,6 @@ static PSWAP_SCRIPT: LazyLock = LazyLock::new(|| { pub struct PswapNoteStorage { requested_asset: FungibleAsset, - #[builder(default)] - swap_count: u16, - creator_account_id: AccountId, /// Note type of the payback note produced when the pswap is filled. Defaults to @@ -80,7 +76,7 @@ impl PswapNoteStorage { // -------------------------------------------------------------------------------------------- /// Expected number of storage items for the PSWAP note. - pub const NUM_STORAGE_ITEMS: usize = 9; + pub const NUM_STORAGE_ITEMS: usize = 8; /// Consumes the storage and returns a PSWAP [`NoteRecipient`] with the provided serial number. pub fn into_recipient(self, serial_num: Word) -> NoteRecipient { @@ -100,11 +96,6 @@ impl PswapNoteStorage { NoteTag::with_account_target(self.creator_account_id) } - /// Number of times this note has been partially filled and re-created. - pub fn swap_count(&self) -> u16 { - self.swap_count - } - /// Returns the account ID of the note creator. pub fn creator_account_id(&self) -> AccountId { self.creator_account_id @@ -126,7 +117,7 @@ impl PswapNoteStorage { } } -/// Serializes [`PswapNoteStorage`] into a 9-element [`NoteStorage`]. +/// Serializes [`PswapNoteStorage`] into an 8-element [`NoteStorage`]. impl From for NoteStorage { fn from(storage: PswapNoteStorage) -> Self { let storage_items = vec![ @@ -140,9 +131,7 @@ impl From for NoteStorage { Felt::from(storage.payback_note_tag()), // Payback note type [5] Felt::from(storage.payback_note_type.as_u8()), - // Swap count [6] - Felt::from(storage.swap_count), - // Creator ID [7-8] + // Creator ID [6-7] storage.creator_account_id.prefix().as_felt(), storage.creator_account_id.suffix(), ]; @@ -151,7 +140,7 @@ impl From for NoteStorage { } } -/// Deserializes [`PswapNoteStorage`] from a slice of exactly 9 [`Felt`]s. +/// Deserializes [`PswapNoteStorage`] from a slice of exactly 8 [`Felt`]s. impl TryFrom<&[Felt]> for PswapNoteStorage { type Error = NoteError; @@ -187,17 +176,11 @@ impl TryFrom<&[Felt]> for PswapNoteStorage { ) .map_err(|e| NoteError::other_with_source("failed to parse payback note type", e))?; - let swap_count: u16 = note_storage[6] - .as_canonical_u64() - .try_into() - .map_err(|_| NoteError::other("swap_count exceeds u16"))?; - - let creator_account_id = AccountId::try_from_elements(note_storage[8], note_storage[7]) + let creator_account_id = AccountId::try_from_elements(note_storage[7], note_storage[6]) .map_err(|e| NoteError::other_with_source("failed to parse creator account ID", e))?; Ok(Self { requested_asset, - swap_count, creator_account_id, payback_note_type, }) @@ -571,14 +554,8 @@ impl PswapNote { remaining_requested_asset: FungibleAsset, offered_amount_for_fill: u64, ) -> Result { - let next_swap_count = self - .storage - .swap_count - .checked_add(1) - .ok_or_else(|| NoteError::other("swap count overflow"))?; let new_storage = PswapNoteStorage::builder() .requested_asset(remaining_requested_asset) - .swap_count(next_swap_count) .creator_account_id(self.storage.creator_account_id) .payback_note_type(self.storage.payback_note_type) .build(); @@ -841,13 +818,11 @@ mod tests { Felt::try_from(requested_asset.amount()).unwrap(), Felt::from(0x80000001u32), // payback_note_tag Felt::from(NoteType::Private.as_u8()), // payback_note_type - Felt::from(3u16), // swap_count creator_id.prefix().as_felt(), creator_id.suffix(), ]; let parsed = PswapNoteStorage::try_from(storage_items.as_slice()).unwrap(); - assert_eq!(parsed.swap_count(), 3); assert_eq!(parsed.creator_account_id(), creator_id); assert_eq!(parsed.requested_asset_amount(), 500); } @@ -866,7 +841,6 @@ mod tests { 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_asset_amount(), 500); } @@ -907,7 +881,6 @@ mod tests { let remainder = remainder.expect("partial fill should produce remainder"); assert_eq!(remainder.storage().requested_asset_amount(), 20); assert_eq!(remainder.offered_asset().amount(), 40); - assert_eq!(remainder.storage().swap_count(), 1); assert_eq!(remainder.storage().creator_account_id(), creator_id); } diff --git a/crates/miden-testing/tests/scripts/pswap.rs b/crates/miden-testing/tests/scripts/pswap.rs index 366405890b..ea5011f1c6 100644 --- a/crates/miden-testing/tests/scripts/pswap.rs +++ b/crates/miden-testing/tests/scripts/pswap.rs @@ -4,7 +4,16 @@ use miden_protocol::account::auth::AuthScheme; use miden_protocol::account::{Account, AccountId, AccountStorageMode, AccountVaultDelta}; use miden_protocol::asset::{Asset, FungibleAsset}; use miden_protocol::crypto::rand::{FeltRng, RandomCoin}; -use miden_protocol::note::{Note, NoteAssets, NoteMetadata, NoteRecipient, NoteStorage, NoteType}; +use miden_protocol::note::{ + Note, + NoteAssets, + NoteAttachment, + NoteAttachmentScheme, + NoteMetadata, + NoteRecipient, + NoteStorage, + NoteType, +}; use miden_protocol::transaction::RawOutputNote; use miden_protocol::{Felt, ONE, Word, ZERO}; use miden_standards::account::wallets::BasicWallet; @@ -169,7 +178,7 @@ async fn pswap_note_alice_reconstructs_and_consumes_p2id() -> anyhow::Result<()> .extend_note_args(note_args_map) .extend_expected_output_notes(vec![ RawOutputNote::Full(p2id_note.clone()), - RawOutputNote::Full(remainder_note), + RawOutputNote::Full(remainder_note.clone()), ]) .build()?; @@ -179,17 +188,20 @@ async fn pswap_note_alice_reconstructs_and_consumes_p2id() -> anyhow::Result<()> // --- Step 2: Alice reconstructs the P2ID note from her PSWAP data + aux --- - //let aux_word = p2id_note.metadata().attachment().content().to_word(); - let aux_word = executed_transaction - .output_notes() - .get_note(0) - .metadata() - .attachment() - .content() - .to_word(); - let fill_amount_from_aux = aux_word[3].as_canonical_u64(); + // Read the attachment from the executed transaction's output (not from the + // Rust-predicted `p2id_note`) so this actually validates the MASM side. + let output_p2id = executed_transaction.output_notes().get_note(0); + let aux_word = output_p2id.metadata().attachment().content().to_word(); + let fill_amount_from_aux = aux_word[0].as_canonical_u64(); assert_eq!(fill_amount_from_aux, 20, "Fill amount from aux should be 20 ETH"); + // Parity check: Rust-predicted P2ID attachment must match the MASM output. + assert_eq!( + p2id_note.metadata().attachment().content().to_word(), + aux_word, + "Rust-predicted P2ID attachment does not match the MASM-produced one", + ); + // Alice reconstructs the recipient using her serial number and account ID let p2id_serial = Word::from([serial_number[0] + ONE, serial_number[1], serial_number[2], serial_number[3]]); @@ -198,10 +210,87 @@ async fn pswap_note_alice_reconstructs_and_consumes_p2id() -> anyhow::Result<()> // Verify the reconstructed recipient matches the actual output assert_eq!( reconstructed_recipient.digest(), - p2id_note.recipient().digest(), + output_p2id.recipient_digest(), "Alice's reconstructed P2ID recipient does not match the actual output" ); + // --- Step 2b: Alice reconstructs the remainder PSWAP note --- + // + // Alice only needs: her original PSWAP data + both on-chain attachments + // (fill_amount from the P2ID, amt_payout from the remainder). From those + // she derives the remaining offered/requested amounts and rebuilds the + // remainder PswapNote. + + let output_remainder = executed_transaction.output_notes().get_note(1); + let remainder_aux = output_remainder.metadata().attachment().content().to_word(); + let amt_payout_from_aux = remainder_aux[0].as_canonical_u64(); + + let expected_payout = pswap.calculate_offered_for_requested(fill_amount_from_aux); + assert_eq!( + amt_payout_from_aux, expected_payout, + "remainder aux should carry amt_payout matching the Rust-side calc", + ); + + let remaining_offered = offered_asset.amount() - amt_payout_from_aux; + let remaining_requested = requested_asset.amount() - fill_amount_from_aux; + + let remainder_storage = PswapNoteStorage::builder() + .requested_asset(FungibleAsset::new(eth_faucet.id(), remaining_requested)?) + .creator_account_id(alice.id()) + .payback_note_type(NoteType::Public) + .build(); + + // MASM increments serial_number[3], so the remainder serial is s[3] + 1. + let remainder_serial = Word::from([ + serial_number[0], + serial_number[1], + serial_number[2], + serial_number[3] + ONE, + ]); + + let remainder_attachment_word = Word::from([ + Felt::try_from(amt_payout_from_aux).expect("amt_payout fits in a felt"), + ZERO, + ZERO, + ZERO, + ]); + let remainder_attachment = + NoteAttachment::new_word(NoteAttachmentScheme::none(), remainder_attachment_word); + + let reconstructed_remainder: Note = PswapNote::builder() + .sender(bob.id()) + .storage(remainder_storage) + .serial_number(remainder_serial) + .note_type(NoteType::Public) + .offered_asset(FungibleAsset::new(usdc_faucet.id(), remaining_offered)?) + .attachment(remainder_attachment) + .build()? + .into(); + + // Sanity check: the Rust-predicted remainder (computed by pswap.execute + // above) must match the executed output. If this fires, the Rust/MASM + // parity itself is broken, independently of our reconstruction. + assert_eq!( + remainder_note.recipient().digest(), + output_remainder.recipient_digest(), + "Rust-predicted remainder recipient does not match executed output", + ); + + // Recipient digest covers the note's storage (creator, requested asset, + // payback tag/type) + serial + script root. + assert_eq!( + reconstructed_remainder.recipient().digest(), + output_remainder.recipient_digest(), + "reconstructed remainder recipient does not match executed output", + ); + + // Parity on the attachment word itself. + assert_eq!( + reconstructed_remainder.metadata().attachment().content().to_word(), + remainder_aux, + "reconstructed remainder attachment does not match executed output", + ); + // --- Step 3: Alice consumes the P2ID payback note --- let tx_context = mock_chain.build_tx_context(alice.id(), &[p2id_note.id()], &[])?.build()?; @@ -869,7 +958,7 @@ async fn pswap_chained_partial_fills_test( let mut rng = RandomCoin::new(Word::default()); let mut current_serial = rng.draw_word(); - for (current_swap_count, fill_amount) in fills.iter().enumerate() { + for (fill_index, fill_amount) in fills.iter().enumerate() { let remaining_requested = current_requested - fill_amount; let mut builder = MockChain::builder(); @@ -903,7 +992,6 @@ async fn pswap_chained_partial_fills_test( let storage = PswapNoteStorage::builder() .requested_asset(requested_fungible) - .swap_count(current_swap_count as u16) .creator_account_id(alice.id()) .build(); let note_assets = NoteAssets::new(vec![offered_asset])?; @@ -945,7 +1033,7 @@ async fn pswap_chained_partial_fills_test( let executed_tx = tx_context.execute().await.map_err(|e| { anyhow::anyhow!( "fill {} failed: {} (offered={}, requested={}, fill={})", - current_swap_count + 1, + fill_index + 1, e, current_offered, current_requested, @@ -955,7 +1043,7 @@ async fn pswap_chained_partial_fills_test( 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, "fill {}", current_swap_count + 1); + assert_eq!(output_notes.num_notes(), expected_count, "fill {}", fill_index + 1); let vault_delta = executed_tx.account_delta().vault(); assert_vault_single_added(vault_delta, usdc_faucet.id(), payout_amount); @@ -1027,7 +1115,6 @@ fn compare_pswap_create_output_notes_vs_test_helper() { 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.storage().requested_asset_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 @@ -1052,7 +1139,6 @@ fn compare_pswap_create_output_notes_vs_test_helper() { assert_fungible_asset(p2id_partial.assets().iter().next().unwrap(), eth_faucet.id(), 10); // Verify remainder properties - assert_eq!(remainder_pswap.storage().swap_count(), 1, "Remainder swap count should be 1"); assert_eq!( remainder_pswap.storage().creator_account_id(), alice.id(), @@ -1093,7 +1179,6 @@ fn pswap_parse_inputs_roundtrip() { 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"); // Verify requested amount from value word assert_eq!(parsed.requested_asset_amount(), 25, "Requested amount should be 25"); From 44216c4727db6ba7b6d5aad3a8c1317659de1da4 Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Wed, 15 Apr 2026 19:40:22 +0530 Subject: [PATCH 43/66] refactor(pswap): masm perf + style cleanups from PR review theme 4 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace three `push.0 gt` (~16 cycles) zero-checks with `neq.0` (~3 cycles) at the has_account_fill, has_note_fill, and has_account_fill_payout branches. - Use `dup.1 dup.1 neq` instead of `lt` for the `is_partial` check; the `lte requested` assertion above guarantees `total_in <= total_requested`, so `!=` is equivalent and cheaper. - Flip the faucet-ID limb order at the create_remainder_note boundary from prefix-then-suffix to suffix-then-prefix (standard miden convention), updating the caller in execute_pswap, the proc docstring, local-slot comment, and the store block. - Drop the redundant `movdn.2` / `movup.2` pair in create_remainder_note — `remaining_requested` is already on top, so `mem_store` can take it directly. - Drop the `(4)` suffix on NOTE_ATTACHMENT / METADATA_HEADER stack comments; uppercase identifiers already imply a full word. - Remove `exec.sys::truncate_stack` and its stale `use` import from execute_pswap. Tracing the proc end-to-end (including every syscall wrapper, call frame, and proc-local store) shows the stack is already balanced at exit, and all 59 pswap integration tests pass without it. --- .../asm/standards/notes/pswap.masm | 31 +++++++++---------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/crates/miden-standards/asm/standards/notes/pswap.masm b/crates/miden-standards/asm/standards/notes/pswap.masm index 196349e5bb..489c82380e 100644 --- a/crates/miden-standards/asm/standards/notes/pswap.masm +++ b/crates/miden-standards/asm/standards/notes/pswap.masm @@ -1,5 +1,4 @@ use miden::core::math::u64 -use miden::core::sys use miden::protocol::account_id use miden::protocol::active_account use miden::protocol::active_note @@ -268,7 +267,7 @@ proc create_p2id_note # => [] # Move account_fill_amount from consumer's vault to P2ID note (if > 0) - loc_load.P2ID_AMT_ACCOUNT_FILL push.0 gt + loc_load.P2ID_AMT_ACCOUNT_FILL neq.0 # => [has_account_fill] if.true # Build 16-element call frame: [ASSET_KEY, ASSET_VALUE, note_idx, pad(7)] @@ -293,7 +292,7 @@ proc create_p2id_note # => [] # Add note_fill_amount directly to P2ID note (no vault debit, if > 0) - loc_load.P2ID_AMT_NOTE_FILL push.0 gt + loc_load.P2ID_AMT_NOTE_FILL neq.0 # => [has_note_fill] if.true loc_load.P2ID_NOTE_IDX @@ -324,31 +323,29 @@ end #! most significant element of the active note's serial number), creates the output #! note, sets the attachment, and adds the remaining offered asset. #! -#! Inputs: [offered_faucet_prefix, offered_faucet_suffix, offered_enable_cb, +#! Inputs: [offered_faucet_suffix, offered_faucet_prefix, offered_enable_cb, #! amt_payout, amt_offered, #! remaining_requested, note_type, tag] #! Outputs: [] #! @locals(6) # REMAINDER_NOTE_IDX : note_idx -# REMAINDER_OFFERED_FAUCET_PREFIX : offered_faucet_prefix # REMAINDER_OFFERED_FAUCET_SUFFIX : offered_faucet_suffix +# REMAINDER_OFFERED_FAUCET_PREFIX : offered_faucet_prefix # REMAINDER_OFFERED_ENABLE_CB : offered_enable_cb # REMAINDER_AMT_PAYOUT : amt_payout # REMAINDER_AMT_OFFERED : amt_offered proc create_remainder_note # Store offered asset info to locals (top element stored first) - loc_store.REMAINDER_OFFERED_FAUCET_PREFIX loc_store.REMAINDER_OFFERED_FAUCET_SUFFIX + loc_store.REMAINDER_OFFERED_FAUCET_PREFIX loc_store.REMAINDER_OFFERED_ENABLE_CB loc_store.REMAINDER_AMT_PAYOUT loc_store.REMAINDER_AMT_OFFERED # => [remaining_requested, note_type, tag] # Update note storage with new requested amount (needed by build_recipient) - movdn.2 - # => [note_type, tag, remaining_requested] - movup.2 mem_store.REQUESTED_AMOUNT_ITEM + mem_store.REQUESTED_AMOUNT_ITEM # => [note_type, tag] # Build PSWAP remainder recipient using the same script as the active note. @@ -581,7 +578,7 @@ proc execute_pswap # => [] # Consumer receives only account_fill_payout into vault (not the note_fill portion, if > 0) - loc_load.EXEC_AMT_PAYOUT_ACCOUNT_FILL push.0 gt + loc_load.EXEC_AMT_PAYOUT_ACCOUNT_FILL neq.0 # => [has_account_fill_payout] if.true padw padw @@ -603,11 +600,13 @@ proc execute_pswap end # => [] - # Check if partial fill: total_in < total_requested + # Check if partial fill. `execute_pswap` has already asserted + # `total_in <= total_requested`, so `is_partial` is equivalent to + # `total_in != total_requested` (cheaper than `lt`). loc_load.EXEC_AMT_REQUESTED_ACCOUNT_FILL loc_load.EXEC_AMT_REQUESTED_NOTE_FILL add loc_load.EXEC_AMT_REQUESTED # => [total_requested, total_in] - dup.1 dup.1 lt + dup.1 dup.1 neq # => [is_partial, total_requested, total_in] if.true @@ -620,7 +619,7 @@ proc execute_pswap # so the tag derived in Rust at creation time still applies, and we # avoid spending a storage slot on `pswap_tag`. exec.active_note::get_metadata - # => [NOTE_ATTACHMENT(4), METADATA_HEADER(4), remaining_requested] + # => [NOTE_ATTACHMENT, METADATA_HEADER, remaining_requested] # where METADATA_HEADER = [sid_suf_ver, sid_pre, tag, att_ks] dropw # => [sid_suf_ver, sid_pre, tag, att_ks, remaining_requested] @@ -640,9 +639,9 @@ proc execute_pswap loc_load.EXEC_AMT_OFFERED loc_load.EXEC_AMT_PAYOUT_TOTAL loc_load.EXEC_OFFERED_ENABLE_CB - loc_load.EXEC_OFFERED_FAUCET_SUFFIX loc_load.EXEC_OFFERED_FAUCET_PREFIX - # => [offered_faucet_prefix, offered_faucet_suffix, offered_enable_cb, + loc_load.EXEC_OFFERED_FAUCET_SUFFIX + # => [offered_faucet_suffix, offered_faucet_prefix, offered_enable_cb, # amt_payout, amt_offered, # remaining_requested, note_type, tag] @@ -654,8 +653,6 @@ proc execute_pswap end # => [] - exec.sys::truncate_stack - # => [] end #! Partially-fillable swap note script: exchanges a portion of the offered asset for a From 1b034217a127410c888cb913c7141ce6c068ca7f Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Wed, 15 Apr 2026 19:57:03 +0530 Subject: [PATCH 44/66] refactor(pswap/tests): PR review theme 5 test quality cleanups MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `pswap_note_invalid_input_test` asserts the specific error kind via `assert_transaction_executor_error!(result, ERR_PSWAP_FILL_EXCEEDS_REQUESTED)` instead of a bare `.is_err()`. A future bug that fails the tx for the wrong reason will now surface. - Drop the `rng: &mut RandomCoin` parameter from `build_pswap_note` — the helper draws its serial number from `builder.rng_mut()` directly. Remove all the `let mut rng = RandomCoin::new(Word::default())` declarations at call sites that only existed to feed this param. - Rename local helper `note_args` -> `pswap_args` so it reads clearly next to the (unrelated) `TransactionContextBuilder::extend_note_args` method it gets passed into. - Switch `assert_vault_added_removed` / `assert_vault_single_added` / `assert_fungible_asset_eq` helpers to take `FungibleAsset` instead of `(AccountId, u64)` tuples, and update every call site. - `pswap_note_note_fill_cross_swap_test`: replace the flag-and-for-loop "which note contains which asset" check with `iter().any()` against two locally-built `Asset::Fungible(FungibleAsset::new(...))` values. - `pswap_note_alice_reconstructs_and_consumes_p2id`: propagate the `prove_next_block()` error via `?` instead of swallowing it with `let _ =`. Still outstanding from theme 5: - realistic large amounts (20 * 10^18) in the happy-path tests - an integration test where both account_fill and note_fill are non-zero on the same note (the Rust unit tests cover the predictor path, but the integration-level cross-flow needs a cleaner two-note setup than a simple adaption of the existing cross-swap test). - skipping `Note -> PswapNote::try_from` roundtrips by having `build_pswap_note` return the `PswapNote` directly — a broader signature churn that is orthogonal to the items above. --- crates/miden-testing/tests/scripts/pswap.rs | 165 ++++++++++---------- 1 file changed, 83 insertions(+), 82 deletions(-) diff --git a/crates/miden-testing/tests/scripts/pswap.rs b/crates/miden-testing/tests/scripts/pswap.rs index ea5011f1c6..2dbe44af4f 100644 --- a/crates/miden-testing/tests/scripts/pswap.rs +++ b/crates/miden-testing/tests/scripts/pswap.rs @@ -17,9 +17,10 @@ use miden_protocol::note::{ use miden_protocol::transaction::RawOutputNote; use miden_protocol::{Felt, ONE, Word, ZERO}; use miden_standards::account::wallets::BasicWallet; +use miden_standards::errors::standards::ERR_PSWAP_FILL_EXCEEDS_REQUESTED; use miden_standards::note::{PswapNote, PswapNoteStorage}; use miden_standards::testing::note::NoteBuilder; -use miden_testing::{Auth, MockChain, MockChainBuilder}; +use miden_testing::{Auth, MockChain, MockChainBuilder, assert_transaction_executor_error}; use rand::SeedableRng; use rand::rngs::SmallRng; use rstest::rstest; @@ -35,14 +36,15 @@ const BASIC_AUTH: Auth = Auth::BasicAuth { // ================================================================================================ /// Builds a PswapNote, registers it on the builder as an output note, returns the Note. +/// The serial number is drawn from the builder's internal rng. fn build_pswap_note( builder: &mut MockChainBuilder, sender: AccountId, offered_asset: FungibleAsset, requested_asset: FungibleAsset, note_type: NoteType, - rng: &mut RandomCoin, ) -> anyhow::Result { + let serial_number = builder.rng_mut().draw_word(); let storage = PswapNoteStorage::builder() .requested_asset(requested_asset) .creator_account_id(sender) @@ -50,7 +52,7 @@ fn build_pswap_note( let note: Note = PswapNote::builder() .sender(sender) .storage(storage) - .serial_number(rng.draw_word()) + .serial_number(serial_number) .note_type(note_type) .offered_asset(offered_asset) .build()? @@ -59,8 +61,8 @@ fn build_pswap_note( Ok(note) } -/// Note-args Word `[account_fill, note_fill, 0, 0]`. -fn note_args(account_fill: u64, note_fill: u64) -> Word { +/// PSWAP note args Word `[account_fill, note_fill, 0, 0]`. +fn pswap_args(account_fill: u64, note_fill: u64) -> Word { Word::from([ Felt::try_from(account_fill).expect("account_fill fits in a felt"), Felt::try_from(note_fill).expect("note_fill fits in a felt"), @@ -70,14 +72,15 @@ fn note_args(account_fill: u64, note_fill: u64) -> Word { } #[track_caller] -fn assert_fungible_asset(asset: &Asset, expected_faucet: AccountId, expected_amount: u64) { +fn assert_fungible_asset_eq(asset: &Asset, expected: FungibleAsset) { match asset { Asset::Fungible(f) => { - assert_eq!(f.faucet_id(), expected_faucet, "faucet id mismatch"); + assert_eq!(f.faucet_id(), expected.faucet_id(), "faucet id mismatch"); assert_eq!( f.amount(), - expected_amount, - "amount mismatch (expected {expected_amount}, got {})", + expected.amount(), + "amount mismatch (expected {}, got {})", + expected.amount(), f.amount() ); }, @@ -88,26 +91,22 @@ fn assert_fungible_asset(asset: &Asset, expected_faucet: AccountId, expected_amo #[track_caller] fn assert_vault_added_removed( vault_delta: &AccountVaultDelta, - expected_added: (AccountId, u64), - expected_removed: (AccountId, u64), + expected_added: FungibleAsset, + expected_removed: FungibleAsset, ) { let added: Vec = vault_delta.added_assets().collect(); let removed: Vec = vault_delta.removed_assets().collect(); assert_eq!(added.len(), 1, "expected exactly 1 added asset"); assert_eq!(removed.len(), 1, "expected exactly 1 removed asset"); - assert_fungible_asset(&added[0], expected_added.0, expected_added.1); - assert_fungible_asset(&removed[0], expected_removed.0, expected_removed.1); + assert_fungible_asset_eq(&added[0], expected_added); + assert_fungible_asset_eq(&removed[0], expected_removed); } #[track_caller] -fn assert_vault_single_added( - vault_delta: &AccountVaultDelta, - expected_faucet: AccountId, - expected_amount: u64, -) { +fn assert_vault_single_added(vault_delta: &AccountVaultDelta, expected: FungibleAsset) { let added: Vec = vault_delta.added_assets().collect(); assert_eq!(added.len(), 1, "expected exactly 1 added asset"); - assert_fungible_asset(&added[0], expected_faucet, expected_amount); + assert_fungible_asset_eq(&added[0], expected); } // TESTS @@ -165,7 +164,7 @@ async fn pswap_note_alice_reconstructs_and_consumes_p2id() -> anyhow::Result<()> let fill_amount = 20u64; let mut note_args_map = BTreeMap::new(); - note_args_map.insert(pswap_note.id(), note_args(fill_amount, 0)); + note_args_map.insert(pswap_note.id(), pswap_args(fill_amount, 0)); let pswap = PswapNote::try_from(&pswap_note)?; let (p2id_note, remainder_pswap) = @@ -184,7 +183,7 @@ async fn pswap_note_alice_reconstructs_and_consumes_p2id() -> anyhow::Result<()> let executed_transaction = tx_context.execute().await?; mock_chain.add_pending_executed_transaction(&executed_transaction)?; - let _ = mock_chain.prove_next_block(); + mock_chain.prove_next_block()?; // --- Step 2: Alice reconstructs the P2ID note from her PSWAP data + aux --- @@ -299,7 +298,7 @@ async fn pswap_note_alice_reconstructs_and_consumes_p2id() -> anyhow::Result<()> // Verify Alice received 20 ETH let vault_delta = executed_transaction.account_delta().vault(); - assert_vault_single_added(vault_delta, eth_faucet.id(), 20); + assert_vault_single_added(vault_delta, FungibleAsset::new(eth_faucet.id(), 20)?); Ok(()) } @@ -353,14 +352,12 @@ async fn pswap_fill_test( let offered_asset = FungibleAsset::new(usdc_faucet.id(), 50)?; let requested_asset = FungibleAsset::new(eth_faucet.id(), 25)?; - let mut rng = RandomCoin::new(Word::default()); let pswap_note = build_pswap_note( &mut builder, alice.id(), offered_asset, requested_asset, note_type, - &mut rng, )?; let mut mock_chain = builder.build()?; @@ -389,7 +386,7 @@ async fn pswap_fill_test( if !use_network_account { let mut note_args_map = BTreeMap::new(); - note_args_map.insert(pswap_note.id(), note_args(fill_amount, 0)); + note_args_map.insert(pswap_note.id(), pswap_args(fill_amount, 0)); tx_builder = tx_builder.extend_note_args(note_args_map); } @@ -413,15 +410,17 @@ async fn pswap_fill_test( // P2ID note carries fill_amount ETH let p2id_assets = output_notes.get_note(0).assets(); assert_eq!(p2id_assets.num_assets(), 1); - assert_fungible_asset(p2id_assets.iter().next().unwrap(), eth_faucet.id(), fill_amount); + assert_fungible_asset_eq( + p2id_assets.iter().next().unwrap(), + FungibleAsset::new(eth_faucet.id(), fill_amount)?, + ); // On partial fill, assert remainder note has offered - payout USDC if is_partial { let remainder_assets = output_notes.get_note(1).assets(); - assert_fungible_asset( + assert_fungible_asset_eq( remainder_assets.iter().next().unwrap(), - usdc_faucet.id(), - 50 - payout_amount, + FungibleAsset::new(usdc_faucet.id(), 50 - payout_amount)?, ); } @@ -429,12 +428,12 @@ async fn pswap_fill_test( let vault_delta = executed_transaction.account_delta().vault(); assert_vault_added_removed( vault_delta, - (usdc_faucet.id(), payout_amount), - (eth_faucet.id(), fill_amount), + FungibleAsset::new(usdc_faucet.id(), payout_amount)?, + FungibleAsset::new(eth_faucet.id(), fill_amount)?, ); mock_chain.add_pending_executed_transaction(&executed_transaction)?; - let _ = mock_chain.prove_next_block(); + mock_chain.prove_next_block()?; Ok(()) } @@ -456,8 +455,6 @@ async fn pswap_note_note_fill_cross_swap_test() -> anyhow::Result<()> { )?; let charlie = builder.add_existing_wallet_with_assets(BASIC_AUTH, [])?; - let mut rng = RandomCoin::new(Word::default()); - // Alice's note: offers 50 USDC, requests 25 ETH let alice_pswap_note = build_pswap_note( &mut builder, @@ -465,7 +462,6 @@ async fn pswap_note_note_fill_cross_swap_test() -> anyhow::Result<()> { FungibleAsset::new(usdc_faucet.id(), 50)?, FungibleAsset::new(eth_faucet.id(), 25)?, NoteType::Public, - &mut rng, )?; // Bob's note: offers 25 ETH, requests 50 USDC @@ -475,15 +471,14 @@ async fn pswap_note_note_fill_cross_swap_test() -> anyhow::Result<()> { FungibleAsset::new(eth_faucet.id(), 25)?, FungibleAsset::new(usdc_faucet.id(), 50)?, NoteType::Public, - &mut rng, )?; let mock_chain = builder.build()?; // Note args: pure note fill (account_fill = 0, note_fill = full amount) let mut note_args_map = BTreeMap::new(); - note_args_map.insert(alice_pswap_note.id(), note_args(0, 25)); - note_args_map.insert(bob_pswap_note.id(), note_args(0, 50)); + note_args_map.insert(alice_pswap_note.id(), pswap_args(0, 25)); + note_args_map.insert(bob_pswap_note.id(), pswap_args(0, 50)); // Expected P2ID notes let alice_pswap = PswapNote::try_from(&alice_pswap_note)?; @@ -509,20 +504,25 @@ async fn pswap_note_note_fill_cross_swap_test() -> anyhow::Result<()> { 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"); + let alice_payout = Asset::Fungible(FungibleAsset::new(eth_faucet.id(), 25)?); + let bob_payout = Asset::Fungible(FungibleAsset::new(usdc_faucet.id(), 50)?); + + let note_contains = |note_idx: usize, asset: &Asset| { + output_notes + .get_note(note_idx) + .assets() + .iter() + .any(|a| a == asset) + }; + + assert!( + (0..output_notes.num_notes()).any(|i| note_contains(i, &alice_payout)), + "Alice's P2ID note (25 ETH) not found", + ); + assert!( + (0..output_notes.num_notes()).any(|i| note_contains(i, &bob_payout)), + "Bob's P2ID note (50 USDC) not found", + ); // Charlie's vault should be unchanged let vault_delta = executed_transaction.account_delta().vault(); @@ -544,14 +544,12 @@ async fn pswap_note_creator_reclaim_test() -> anyhow::Result<()> { [FungibleAsset::new(usdc_faucet.id(), 50)?.into()], )?; - let mut rng = RandomCoin::new(Word::default()); let pswap_note = build_pswap_note( &mut builder, alice.id(), FungibleAsset::new(usdc_faucet.id(), 50)?, FungibleAsset::new(eth_faucet.id(), 25)?, NoteType::Public, - &mut rng, )?; let mock_chain = builder.build()?; @@ -565,7 +563,7 @@ async fn pswap_note_creator_reclaim_test() -> anyhow::Result<()> { assert_eq!(output_notes.num_notes(), 0, "Expected 0 output notes for reclaim"); let vault_delta = executed_transaction.account_delta().vault(); - assert_vault_single_added(vault_delta, usdc_faucet.id(), 50); + assert_vault_single_added(vault_delta, FungibleAsset::new(usdc_faucet.id(), 50)?); Ok(()) } @@ -586,20 +584,18 @@ async fn pswap_note_invalid_input_test() -> anyhow::Result<()> { [FungibleAsset::new(eth_faucet.id(), 30)?.into()], )?; - let mut rng = RandomCoin::new(Word::default()); let pswap_note = build_pswap_note( &mut builder, alice.id(), FungibleAsset::new(usdc_faucet.id(), 50)?, FungibleAsset::new(eth_faucet.id(), 25)?, NoteType::Public, - &mut rng, )?; 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(pswap_note.id(), note_args(30, 0)); + note_args_map.insert(pswap_note.id(), pswap_args(30, 0)); let tx_context = mock_chain .build_tx_context(bob.id(), &[pswap_note.id()], &[])? @@ -607,10 +603,7 @@ async fn pswap_note_invalid_input_test() -> anyhow::Result<()> { .build()?; let result = tx_context.execute().await; - assert!( - result.is_err(), - "Transaction should fail when fill_amount > requested_asset_total" - ); + assert_transaction_executor_error!(result, ERR_PSWAP_FILL_EXCEEDS_REQUESTED); Ok(()) } @@ -645,14 +638,12 @@ async fn pswap_note_idx_nonzero_regression_test() -> anyhow::Result<()> { [FungibleAsset::new(eth_faucet.id(), 25)?.into()], )?; - let mut rng = RandomCoin::new(Word::default()); let pswap_note = build_pswap_note( &mut builder, alice.id(), FungibleAsset::new(usdc_faucet.id(), 50)?, FungibleAsset::new(eth_faucet.id(), 25)?, NoteType::Public, - &mut rng, )?; // Dummy output note to be emitted by the SPAWN note. Sender must equal @@ -666,7 +657,7 @@ async fn pswap_note_idx_nonzero_regression_test() -> anyhow::Result<()> { // Full account-fill: 25 ETH out of bob's vault. Exercises the // `has_account_fill` branch where the `note_idx` bug lives. let mut note_args_map = BTreeMap::new(); - note_args_map.insert(pswap_note.id(), note_args(25, 0)); + note_args_map.insert(pswap_note.id(), pswap_args(25, 0)); let pswap = PswapNote::try_from(&pswap_note)?; let (expected_p2id, _) = @@ -701,11 +692,18 @@ async fn pswap_note_idx_nonzero_regression_test() -> anyhow::Result<()> { // P2ID at idx 1 must carry the full 25 ETH. let p2id_out = output_notes.get_note(1); assert_eq!(p2id_out.assets().num_assets(), 1, "P2ID must have 1 asset"); - assert_fungible_asset(p2id_out.assets().iter().next().unwrap(), eth_faucet.id(), 25); + assert_fungible_asset_eq( + p2id_out.assets().iter().next().unwrap(), + FungibleAsset::new(eth_faucet.id(), 25)?, + ); // Bob's vault: +50 USDC payout, -25 ETH fill. let vault_delta = executed.account_delta().vault(); - assert_vault_added_removed(vault_delta, (usdc_faucet.id(), 50), (eth_faucet.id(), 25)); + assert_vault_added_removed( + vault_delta, + FungibleAsset::new(usdc_faucet.id(), 50)?, + FungibleAsset::new(eth_faucet.id(), 25)?, + ); Ok(()) } @@ -736,20 +734,18 @@ async fn pswap_multiple_partial_fills_test(#[case] fill_amount: u64) -> anyhow:: [FungibleAsset::new(eth_faucet.id(), fill_amount)?.into()], )?; - let mut rng = RandomCoin::new(Word::default()); let pswap_note = build_pswap_note( &mut builder, alice.id(), FungibleAsset::new(usdc_faucet.id(), 50)?, FungibleAsset::new(eth_faucet.id(), 25)?, NoteType::Public, - &mut rng, )?; let mock_chain = builder.build()?; let mut note_args_map = BTreeMap::new(); - note_args_map.insert(pswap_note.id(), note_args(fill_amount, 0)); + note_args_map.insert(pswap_note.id(), pswap_args(fill_amount, 0)); let pswap = PswapNote::try_from(&pswap_note)?; let payout_amount = pswap.calculate_offered_for_requested(fill_amount); @@ -775,7 +771,7 @@ async fn pswap_multiple_partial_fills_test(#[case] fill_amount: u64) -> anyhow:: // Verify Bob's vault let vault_delta = executed_transaction.account_delta().vault(); - assert_vault_single_added(vault_delta, usdc_faucet.id(), payout_amount); + assert_vault_single_added(vault_delta, FungibleAsset::new(usdc_faucet.id(), payout_amount)?); Ok(()) } @@ -808,20 +804,18 @@ async fn run_partial_fill_ratio_case( [FungibleAsset::new(eth_faucet.id(), fill_eth)?.into()], )?; - let mut rng = RandomCoin::new(Word::default()); let pswap_note = build_pswap_note( &mut builder, alice.id(), FungibleAsset::new(usdc_faucet.id(), offered_usdc)?, FungibleAsset::new(eth_faucet.id(), requested_eth)?, NoteType::Public, - &mut rng, )?; let mock_chain = builder.build()?; let mut note_args_map = BTreeMap::new(); - note_args_map.insert(pswap_note.id(), note_args(fill_eth, 0)); + note_args_map.insert(pswap_note.id(), pswap_args(fill_eth, 0)); let pswap = PswapNote::try_from(&pswap_note)?; let payout_amount = pswap.calculate_offered_for_requested(fill_eth); @@ -854,8 +848,8 @@ async fn run_partial_fill_ratio_case( let vault_delta = executed_tx.account_delta().vault(); assert_vault_added_removed( vault_delta, - (usdc_faucet.id(), payout_amount), - (eth_faucet.id(), fill_eth), + FungibleAsset::new(usdc_faucet.id(), payout_amount)?, + FungibleAsset::new(eth_faucet.id(), fill_eth)?, ); assert_eq!(payout_amount + remaining_offered, offered_usdc, "conservation"); @@ -1006,7 +1000,7 @@ async fn pswap_chained_partial_fills_test( let mock_chain = builder.build()?; let mut note_args_map = BTreeMap::new(); - note_args_map.insert(pswap_note.id(), note_args(*fill_amount, 0)); + note_args_map.insert(pswap_note.id(), pswap_args(*fill_amount, 0)); let pswap = PswapNote::try_from(&pswap_note)?; let payout_amount = pswap.calculate_offered_for_requested(*fill_amount); @@ -1046,7 +1040,10 @@ async fn pswap_chained_partial_fills_test( assert_eq!(output_notes.num_notes(), expected_count, "fill {}", fill_index + 1); let vault_delta = executed_tx.account_delta().vault(); - assert_vault_single_added(vault_delta, usdc_faucet.id(), payout_amount); + assert_vault_single_added( + vault_delta, + FungibleAsset::new(usdc_faucet.id(), payout_amount)?, + ); // Update state for next fill total_usdc_to_bob += payout_amount; @@ -1127,7 +1124,10 @@ fn compare_pswap_create_output_notes_vs_test_helper() { 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"); - assert_fungible_asset(p2id_note.assets().iter().next().unwrap(), eth_faucet.id(), 25); + assert_fungible_asset_eq( + p2id_note.assets().iter().next().unwrap(), + FungibleAsset::new(eth_faucet.id(), 25).unwrap(), + ); // Partial fill: should produce P2ID note + remainder let (p2id_partial, remainder_partial) = pswap @@ -1136,7 +1136,10 @@ fn compare_pswap_create_output_notes_vs_test_helper() { let remainder_pswap = remainder_partial.expect("Partial fill should produce remainder"); assert_eq!(p2id_partial.assets().num_assets(), 1); - assert_fungible_asset(p2id_partial.assets().iter().next().unwrap(), eth_faucet.id(), 10); + assert_fungible_asset_eq( + p2id_partial.assets().iter().next().unwrap(), + FungibleAsset::new(eth_faucet.id(), 10).unwrap(), + ); // Verify remainder properties assert_eq!( @@ -1162,14 +1165,12 @@ fn pswap_parse_inputs_roundtrip() { ) .unwrap(); - let mut rng = RandomCoin::new(Word::default()); let pswap_note = build_pswap_note( &mut builder, alice.id(), FungibleAsset::new(usdc_faucet.id(), 50).unwrap(), FungibleAsset::new(eth_faucet.id(), 25).unwrap(), NoteType::Public, - &mut rng, ) .unwrap(); From 253005b06c43a38c12ddb3d5f4a4a198169f96ac Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Wed, 15 Apr 2026 20:22:07 +0530 Subject: [PATCH 45/66] =?UTF-8?q?test(pswap):=20finish=20theme=205=20?= =?UTF-8?q?=E2=80=94=20realistic=20amounts,=20combined=20fill,=20skip=20tr?= =?UTF-8?q?y=5Ffrom?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `pswap_fill_test`: scale every amount by `AMOUNT_SCALE = 10^12` so the happy path exercises 12-decimal token values instead of single digits. The scale cap is chosen so `requested * FACTOR` stays under `u64::MAX`, matching the current MASM assumption. - `build_pswap_note` now returns `(PswapNote, Note)` instead of just the `Note`, so call sites can execute the PswapNote directly and skip the `PswapNote::try_from(¬e)?` round-trip the reviewer flagged. Every caller updated. - New `pswap_note_combined_account_fill_and_note_fill_test`: exercises a PSWAP fill where the consumer supplies BOTH `account_fill` and `note_fill` on the same note in the same transaction. Alice offers 100 USDC for 50 ETH, Bob offers 30 ETH for 60 USDC, Charlie consumes both — Alice's pswap uses combined fill (20 ETH from Charlie's vault + 30 ETH sourced from inflight via Bob's pswap payout), Bob's pswap uses pure note_fill. Asserts output notes, recipient parity, and Charlie's vault delta (-20 ETH / +40 USDC; the note_fill legs flow through inflight and never touch his vault). --- crates/miden-testing/tests/scripts/pswap.rs | 186 ++++++++++++++++---- 1 file changed, 156 insertions(+), 30 deletions(-) diff --git a/crates/miden-testing/tests/scripts/pswap.rs b/crates/miden-testing/tests/scripts/pswap.rs index 2dbe44af4f..9d91bc6b05 100644 --- a/crates/miden-testing/tests/scripts/pswap.rs +++ b/crates/miden-testing/tests/scripts/pswap.rs @@ -35,30 +35,32 @@ const BASIC_AUTH: Auth = Auth::BasicAuth { // HELPERS // ================================================================================================ -/// Builds a PswapNote, registers it on the builder as an output note, returns the Note. -/// The serial number is drawn from the builder's internal rng. +/// Builds a PswapNote, registers it on the builder as an output note, and returns +/// both the `PswapNote` (for `.execute()`) and the protocol `Note` (for +/// `.id()` / `RawOutputNote::Full`), so callers don't need to round-trip via +/// `PswapNote::try_from(¬e)?`. Serial number is drawn from the builder's rng. fn build_pswap_note( builder: &mut MockChainBuilder, sender: AccountId, offered_asset: FungibleAsset, requested_asset: FungibleAsset, note_type: NoteType, -) -> anyhow::Result { +) -> anyhow::Result<(PswapNote, Note)> { let serial_number = builder.rng_mut().draw_word(); let storage = PswapNoteStorage::builder() .requested_asset(requested_asset) .creator_account_id(sender) .build(); - let note: Note = PswapNote::builder() + let pswap = PswapNote::builder() .sender(sender) .storage(storage) .serial_number(serial_number) .note_type(note_type) .offered_asset(offered_asset) - .build()? - .into(); + .build()?; + let note: Note = pswap.clone().into(); builder.add_output_note(RawOutputNote::Full(note.clone())); - Ok(note) + Ok((pswap, note)) } /// PSWAP note args Word `[account_fill, note_fill, 0, 0]`. @@ -309,6 +311,10 @@ async fn pswap_note_alice_reconstructs_and_consumes_p2id() -> anyhow::Result<()> /// - partial public fill (offered=50 USDC / requested=25 ETH / fill=20 ETH → payout=40 USDC, /// remainder=10 USDC) /// - full fill via a network account (no note_args → script defaults to full fill) +/// +/// Amounts are scaled by `AMOUNT_SCALE` (10^12) so the test exercises realistic +/// 12-decimal token values instead of single-digit toys. The cap is set so +/// `requested * FACTOR` (where FACTOR = 1e5) stays under `u64::MAX`. #[rstest] #[case::full_public(25, NoteType::Public, false)] #[case::full_private(25, NoteType::Private, false)] @@ -316,18 +322,27 @@ async fn pswap_note_alice_reconstructs_and_consumes_p2id() -> anyhow::Result<()> #[case::network_full_fill(25, NoteType::Public, true)] #[tokio::test] async fn pswap_fill_test( - #[case] fill_amount: u64, + #[case] fill_base: u64, #[case] note_type: NoteType, #[case] use_network_account: bool, ) -> anyhow::Result<()> { + const AMOUNT_SCALE: u64 = 1_000_000_000_000; // 10^12 + + let fill_amount = fill_base * AMOUNT_SCALE; + let offered_total = 50 * AMOUNT_SCALE; + let requested_total = 25 * AMOUNT_SCALE; + let max_supply = 1000 * AMOUNT_SCALE; + 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", max_supply, Some(150 * AMOUNT_SCALE))?; + let eth_faucet = + builder.add_existing_basic_faucet(BASIC_AUTH, "ETH", max_supply, Some(50 * AMOUNT_SCALE))?; let alice = builder.add_existing_wallet_with_assets( BASIC_AUTH, - [FungibleAsset::new(usdc_faucet.id(), 50)?.into()], + [FungibleAsset::new(usdc_faucet.id(), offered_total)?.into()], )?; let consumer_id = if use_network_account { @@ -349,10 +364,10 @@ async fn pswap_fill_test( bob.id() }; - let offered_asset = FungibleAsset::new(usdc_faucet.id(), 50)?; - let requested_asset = FungibleAsset::new(eth_faucet.id(), 25)?; + let offered_asset = FungibleAsset::new(usdc_faucet.id(), offered_total)?; + let requested_asset = FungibleAsset::new(eth_faucet.id(), requested_total)?; - let pswap_note = build_pswap_note( + let (pswap, pswap_note) = build_pswap_note( &mut builder, alice.id(), offered_asset, @@ -362,7 +377,6 @@ async fn pswap_fill_test( let mut mock_chain = builder.build()?; - let pswap = PswapNote::try_from(&pswap_note)?; let fill_asset = FungibleAsset::new(eth_faucet.id(), fill_amount)?; let (p2id_note, remainder_pswap) = if use_network_account { @@ -372,7 +386,7 @@ async fn pswap_fill_test( pswap.execute(consumer_id, Some(fill_asset), None)? }; - let is_partial = fill_amount < 25; + let is_partial = fill_amount < requested_total; let payout_amount = pswap.calculate_offered_for_requested(fill_amount); let mut expected_notes = vec![RawOutputNote::Full(p2id_note.clone())]; @@ -420,7 +434,7 @@ async fn pswap_fill_test( let remainder_assets = output_notes.get_note(1).assets(); assert_fungible_asset_eq( remainder_assets.iter().next().unwrap(), - FungibleAsset::new(usdc_faucet.id(), 50 - payout_amount)?, + FungibleAsset::new(usdc_faucet.id(), offered_total - payout_amount)?, ); } @@ -456,7 +470,7 @@ async fn pswap_note_note_fill_cross_swap_test() -> anyhow::Result<()> { let charlie = builder.add_existing_wallet_with_assets(BASIC_AUTH, [])?; // Alice's note: offers 50 USDC, requests 25 ETH - let alice_pswap_note = build_pswap_note( + let (alice_pswap, alice_pswap_note) = build_pswap_note( &mut builder, alice.id(), FungibleAsset::new(usdc_faucet.id(), 50)?, @@ -465,7 +479,7 @@ async fn pswap_note_note_fill_cross_swap_test() -> anyhow::Result<()> { )?; // Bob's note: offers 25 ETH, requests 50 USDC - let bob_pswap_note = build_pswap_note( + let (bob_pswap, bob_pswap_note) = build_pswap_note( &mut builder, bob.id(), FungibleAsset::new(eth_faucet.id(), 25)?, @@ -481,11 +495,9 @@ async fn pswap_note_note_fill_cross_swap_test() -> anyhow::Result<()> { note_args_map.insert(bob_pswap_note.id(), pswap_args(0, 50)); // Expected P2ID notes - let alice_pswap = PswapNote::try_from(&alice_pswap_note)?; let (alice_p2id_note, _) = alice_pswap.execute(charlie.id(), None, Some(FungibleAsset::new(eth_faucet.id(), 25)?))?; - let bob_pswap = PswapNote::try_from(&bob_pswap_note)?; let (bob_p2id_note, _) = bob_pswap.execute(charlie.id(), None, Some(FungibleAsset::new(usdc_faucet.id(), 50)?))?; @@ -532,6 +544,123 @@ async fn pswap_note_note_fill_cross_swap_test() -> anyhow::Result<()> { Ok(()) } +/// Integration test for a PSWAP fill that uses **both** `account_fill` and +/// `note_fill` on the same note in the same transaction. +/// +/// Setup: +/// - Alice's pswap: 100 USDC offered for 50 ETH requested (ratio 2:1). +/// - Bob's pswap: 30 ETH offered for 60 USDC requested (ratio 1:2). +/// - Charlie has 20 ETH in vault. +/// +/// Charlie consumes both notes in one tx: +/// - Alice's: `account_fill = 20 ETH` (debited from his vault) +/// + `note_fill = 30 ETH` (sourced from inflight, produced by Bob's pswap) +/// → 50 ETH total (full fill). Payout split: +/// - 40 USDC → Charlie's vault (account_fill path) +/// - 60 USDC → inflight (note_fill path, consumed by Bob's pswap) +/// - Bob's: `note_fill = 60 USDC` (sourced from inflight, produced by Alice's pswap) +/// → 60 USDC total (full fill). Payout: 30 ETH → inflight (matches +/// Alice's note_fill consumption above). +/// +/// Net effect: Charlie -20 ETH / +40 USDC; Alice's P2ID = 50 ETH; Bob's P2ID = 60 USDC. +#[tokio::test] +async fn pswap_note_combined_account_fill_and_note_fill_test() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + + let usdc_faucet = builder.add_existing_basic_faucet(BASIC_AUTH, "USDC", 1000, Some(200))?; + let eth_faucet = builder.add_existing_basic_faucet(BASIC_AUTH, "ETH", 1000, Some(60))?; + + let alice = builder.add_existing_wallet_with_assets( + BASIC_AUTH, + [FungibleAsset::new(usdc_faucet.id(), 100)?.into()], + )?; + let bob = builder.add_existing_wallet_with_assets( + BASIC_AUTH, + [FungibleAsset::new(eth_faucet.id(), 30)?.into()], + )?; + let charlie = builder.add_existing_wallet_with_assets( + BASIC_AUTH, + [FungibleAsset::new(eth_faucet.id(), 20)?.into()], + )?; + + // Alice's pswap: 100 USDC for 50 ETH + let (alice_pswap, alice_pswap_note) = build_pswap_note( + &mut builder, + alice.id(), + FungibleAsset::new(usdc_faucet.id(), 100)?, + FungibleAsset::new(eth_faucet.id(), 50)?, + NoteType::Public, + )?; + + // Bob's pswap: 30 ETH for 60 USDC + let (bob_pswap, bob_pswap_note) = build_pswap_note( + &mut builder, + bob.id(), + FungibleAsset::new(eth_faucet.id(), 30)?, + FungibleAsset::new(usdc_faucet.id(), 60)?, + NoteType::Public, + )?; + + let mock_chain = builder.build()?; + + // Alice's pswap uses a combined fill; Bob's pswap uses pure note_fill. + let mut note_args_map = BTreeMap::new(); + note_args_map.insert(alice_pswap_note.id(), pswap_args(20, 30)); + note_args_map.insert(bob_pswap_note.id(), pswap_args(0, 60)); + + let (alice_p2id_note, alice_remainder) = alice_pswap.execute( + charlie.id(), + Some(FungibleAsset::new(eth_faucet.id(), 20)?), + Some(FungibleAsset::new(eth_faucet.id(), 30)?), + )?; + assert!(alice_remainder.is_none(), "combined fill hits full fill — no remainder expected"); + + let (bob_p2id_note, bob_remainder) = + bob_pswap.execute(charlie.id(), None, Some(FungibleAsset::new(usdc_faucet.id(), 60)?))?; + assert!(bob_remainder.is_none(), "bob pswap is filled completely via note_fill"); + + let tx_context = mock_chain + .build_tx_context(charlie.id(), &[alice_pswap_note.id(), bob_pswap_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?; + + // Exactly 2 output notes: Alice's P2ID (50 ETH) + Bob's P2ID (60 USDC). + let output_notes = executed_transaction.output_notes(); + assert_eq!(output_notes.num_notes(), 2, "expected exactly 2 P2ID output notes"); + + let alice_payout = Asset::Fungible(FungibleAsset::new(eth_faucet.id(), 50)?); + let bob_payout = Asset::Fungible(FungibleAsset::new(usdc_faucet.id(), 60)?); + + let note_contains = |idx: usize, asset: &Asset| { + output_notes.get_note(idx).assets().iter().any(|a| a == asset) + }; + assert!( + (0..output_notes.num_notes()).any(|i| note_contains(i, &alice_payout)), + "Alice's P2ID (50 ETH) not found", + ); + assert!( + (0..output_notes.num_notes()).any(|i| note_contains(i, &bob_payout)), + "Bob's P2ID (60 USDC) not found", + ); + + // Charlie's vault: -20 ETH (account_fill) + 40 USDC (account_fill_payout). + // The note_fill legs flow entirely through inflight and never touch his vault. + let vault_delta = executed_transaction.account_delta().vault(); + assert_vault_added_removed( + vault_delta, + FungibleAsset::new(usdc_faucet.id(), 40)?, + FungibleAsset::new(eth_faucet.id(), 20)?, + ); + + Ok(()) +} + #[tokio::test] async fn pswap_note_creator_reclaim_test() -> anyhow::Result<()> { let mut builder = MockChain::builder(); @@ -544,7 +673,7 @@ async fn pswap_note_creator_reclaim_test() -> anyhow::Result<()> { [FungibleAsset::new(usdc_faucet.id(), 50)?.into()], )?; - let pswap_note = build_pswap_note( + let (_, pswap_note) = build_pswap_note( &mut builder, alice.id(), FungibleAsset::new(usdc_faucet.id(), 50)?, @@ -584,7 +713,7 @@ async fn pswap_note_invalid_input_test() -> anyhow::Result<()> { [FungibleAsset::new(eth_faucet.id(), 30)?.into()], )?; - let pswap_note = build_pswap_note( + let (_, pswap_note) = build_pswap_note( &mut builder, alice.id(), FungibleAsset::new(usdc_faucet.id(), 50)?, @@ -638,7 +767,7 @@ async fn pswap_note_idx_nonzero_regression_test() -> anyhow::Result<()> { [FungibleAsset::new(eth_faucet.id(), 25)?.into()], )?; - let pswap_note = build_pswap_note( + let (pswap, pswap_note) = build_pswap_note( &mut builder, alice.id(), FungibleAsset::new(usdc_faucet.id(), 50)?, @@ -659,7 +788,6 @@ async fn pswap_note_idx_nonzero_regression_test() -> anyhow::Result<()> { let mut note_args_map = BTreeMap::new(); note_args_map.insert(pswap_note.id(), pswap_args(25, 0)); - let pswap = PswapNote::try_from(&pswap_note)?; let (expected_p2id, _) = pswap.execute(bob.id(), Some(FungibleAsset::new(eth_faucet.id(), 25)?), None)?; @@ -734,7 +862,7 @@ async fn pswap_multiple_partial_fills_test(#[case] fill_amount: u64) -> anyhow:: [FungibleAsset::new(eth_faucet.id(), fill_amount)?.into()], )?; - let pswap_note = build_pswap_note( + let (pswap, pswap_note) = build_pswap_note( &mut builder, alice.id(), FungibleAsset::new(usdc_faucet.id(), 50)?, @@ -747,7 +875,6 @@ async fn pswap_multiple_partial_fills_test(#[case] fill_amount: u64) -> anyhow:: let mut note_args_map = BTreeMap::new(); note_args_map.insert(pswap_note.id(), pswap_args(fill_amount, 0)); - let pswap = PswapNote::try_from(&pswap_note)?; let payout_amount = pswap.calculate_offered_for_requested(fill_amount); let (p2id_note, remainder_pswap) = pswap.execute(bob.id(), Some(FungibleAsset::new(eth_faucet.id(), fill_amount)?), None)?; @@ -804,7 +931,7 @@ async fn run_partial_fill_ratio_case( [FungibleAsset::new(eth_faucet.id(), fill_eth)?.into()], )?; - let pswap_note = build_pswap_note( + let (pswap, pswap_note) = build_pswap_note( &mut builder, alice.id(), FungibleAsset::new(usdc_faucet.id(), offered_usdc)?, @@ -817,7 +944,6 @@ async fn run_partial_fill_ratio_case( let mut note_args_map = BTreeMap::new(); note_args_map.insert(pswap_note.id(), pswap_args(fill_eth, 0)); - let pswap = PswapNote::try_from(&pswap_note)?; let payout_amount = pswap.calculate_offered_for_requested(fill_eth); let remaining_offered = offered_usdc - payout_amount; @@ -1165,7 +1291,7 @@ fn pswap_parse_inputs_roundtrip() { ) .unwrap(); - let pswap_note = build_pswap_note( + let (_, pswap_note) = build_pswap_note( &mut builder, alice.id(), FungibleAsset::new(usdc_faucet.id(), 50).unwrap(), From 5ee897e4a60cd6b38b59748d69a5255dbf986060 Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Wed, 15 Apr 2026 22:10:35 +0530 Subject: [PATCH 46/66] refactor(pswap): rename execute_full_fill_network -> execute_full_fill MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The method has nothing network-specific about it — a "network transaction" is just one where the kernel defaults note_args to `[0, 0, 0, 0]` and the MASM script falls back to a full fill, which any caller can trigger by calling this method directly. Rename to reflect that, update the sole caller in the pswap_fill_test, and rewrite the doc comment to describe the behavior (full fill producing only the payback note, no remainder) rather than the network use case. Also scrub stale doc references left over from theme 3's `swap_count` removal: - drop the "swap count overflows u16::MAX" bullet from `execute`'s errors section - drop "swap count overflow" from `execute_full_fill`'s errors section (the whole section is gone — no error paths remain) - update the PswapNote struct doc: the remainder carries "an updated serial number", not "an incremented swap count" - update `create_remainder_pswap_note`'s doc for the same reason --- crates/miden-standards/src/note/pswap.rs | 28 ++++------- crates/miden-testing/tests/scripts/pswap.rs | 56 ++++++++++----------- 2 files changed, 37 insertions(+), 47 deletions(-) diff --git a/crates/miden-standards/src/note/pswap.rs b/crates/miden-standards/src/note/pswap.rs index e919e544f5..84213ddb25 100644 --- a/crates/miden-standards/src/note/pswap.rs +++ b/crates/miden-standards/src/note/pswap.rs @@ -194,7 +194,7 @@ impl TryFrom<&[Felt]> for PswapNoteStorage { /// /// 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 +/// is re-created as a remainder note with an updated serial number, while the creator /// receives the filled portion via a payback note. /// /// The note can be consumed both in local transactions (where the consumer provides @@ -297,26 +297,21 @@ impl PswapNote { // INSTANCE METHODS // -------------------------------------------------------------------------------------------- - /// Executes the swap as a full fill, intended for network transactions. + /// Executes the swap as a full fill, producing only the payback note (no remainder). /// - /// In network transactions, note_args are unavailable (the kernel defaults them to - /// `[0, 0, 0, 0]`), so the MASM script fills the entire requested amount. This method - /// mirrors that behavior. Returns only the payback note — no remainder is produced. - /// - /// # Errors - /// - /// Returns an error if the swap count overflows `u16::MAX`. - pub fn execute_full_fill_network( - &self, - network_account_id: AccountId, - ) -> Result { + /// Equivalent to calling [`Self::execute`] with `account_fill_asset` set to the full + /// requested amount and `note_fill_asset = None`. It also matches the on-chain + /// behavior when a note is consumed without explicit `note_args` (e.g. in a network + /// transaction, where the kernel defaults `note_args` to `[0, 0, 0, 0]` and the MASM + /// script falls back to a full fill). + pub fn execute_full_fill(&self, consumer_account_id: AccountId) -> Result { let requested_faucet_id = self.storage.requested_faucet_id(); let total_requested_amount = self.storage.requested_asset_amount(); let fill_asset = FungibleAsset::new(requested_faucet_id, total_requested_amount) .map_err(|e| NoteError::other_with_source("failed to create full fill asset", e))?; - self.create_payback_note(network_account_id, fill_asset, total_requested_amount) + self.create_payback_note(consumer_account_id, fill_asset, total_requested_amount) } /// Executes the swap, producing the output notes for a given fill. @@ -334,7 +329,6 @@ impl PswapNote { /// - Both assets are `None`. /// - The fill amount is zero. /// - The fill amount exceeds the total requested amount. - /// - The swap count overflows `u16::MAX`. pub fn execute( &self, consumer_account_id: AccountId, @@ -542,8 +536,8 @@ impl PswapNote { /// Builds a remainder PSWAP note carrying the unfilled portion of the swap. /// - /// The remainder inherits the original creator, tags, and note type, but has an - /// incremented swap count and an updated serial number (`serial[3] + 1`). + /// The remainder inherits the original creator, tags, and note type, with an updated + /// serial number (`serial[3] + 1`) matching the MASM-side derivation. /// /// The attachment carries the total offered amount for the fill as auxiliary data /// with `NoteAttachmentScheme::none()`, matching the on-chain MASM behavior. diff --git a/crates/miden-testing/tests/scripts/pswap.rs b/crates/miden-testing/tests/scripts/pswap.rs index 9d91bc6b05..3370e7dfc3 100644 --- a/crates/miden-testing/tests/scripts/pswap.rs +++ b/crates/miden-testing/tests/scripts/pswap.rs @@ -242,12 +242,8 @@ async fn pswap_note_alice_reconstructs_and_consumes_p2id() -> anyhow::Result<()> .build(); // MASM increments serial_number[3], so the remainder serial is s[3] + 1. - let remainder_serial = Word::from([ - serial_number[0], - serial_number[1], - serial_number[2], - serial_number[3] + ONE, - ]); + let remainder_serial = + Word::from([serial_number[0], serial_number[1], serial_number[2], serial_number[3] + ONE]); let remainder_attachment_word = Word::from([ Felt::try_from(amt_payout_from_aux).expect("amt_payout fits in a felt"), @@ -335,10 +331,18 @@ async fn pswap_fill_test( let mut builder = MockChain::builder(); - let usdc_faucet = - builder.add_existing_basic_faucet(BASIC_AUTH, "USDC", max_supply, Some(150 * AMOUNT_SCALE))?; - let eth_faucet = - builder.add_existing_basic_faucet(BASIC_AUTH, "ETH", max_supply, Some(50 * AMOUNT_SCALE))?; + let usdc_faucet = builder.add_existing_basic_faucet( + BASIC_AUTH, + "USDC", + max_supply, + Some(150 * AMOUNT_SCALE), + )?; + let eth_faucet = builder.add_existing_basic_faucet( + BASIC_AUTH, + "ETH", + max_supply, + Some(50 * AMOUNT_SCALE), + )?; let alice = builder.add_existing_wallet_with_assets( BASIC_AUTH, @@ -367,20 +371,15 @@ async fn pswap_fill_test( let offered_asset = FungibleAsset::new(usdc_faucet.id(), offered_total)?; let requested_asset = FungibleAsset::new(eth_faucet.id(), requested_total)?; - let (pswap, pswap_note) = build_pswap_note( - &mut builder, - alice.id(), - offered_asset, - requested_asset, - note_type, - )?; + let (pswap, pswap_note) = + build_pswap_note(&mut builder, alice.id(), offered_asset, requested_asset, note_type)?; let mut mock_chain = builder.build()?; let fill_asset = FungibleAsset::new(eth_faucet.id(), fill_amount)?; let (p2id_note, remainder_pswap) = if use_network_account { - let p2id = pswap.execute_full_fill_network(consumer_id)?; + let p2id = pswap.execute_full_fill(consumer_id)?; (p2id, None) } else { pswap.execute(consumer_id, Some(fill_asset), None)? @@ -520,11 +519,7 @@ async fn pswap_note_note_fill_cross_swap_test() -> anyhow::Result<()> { let bob_payout = Asset::Fungible(FungibleAsset::new(usdc_faucet.id(), 50)?); let note_contains = |note_idx: usize, asset: &Asset| { - output_notes - .get_note(note_idx) - .assets() - .iter() - .any(|a| a == asset) + output_notes.get_note(note_idx).assets().iter().any(|a| a == asset) }; assert!( @@ -558,9 +553,8 @@ async fn pswap_note_note_fill_cross_swap_test() -> anyhow::Result<()> { /// → 50 ETH total (full fill). Payout split: /// - 40 USDC → Charlie's vault (account_fill path) /// - 60 USDC → inflight (note_fill path, consumed by Bob's pswap) -/// - Bob's: `note_fill = 60 USDC` (sourced from inflight, produced by Alice's pswap) -/// → 60 USDC total (full fill). Payout: 30 ETH → inflight (matches -/// Alice's note_fill consumption above). +/// - Bob's: `note_fill = 60 USDC` (sourced from inflight, produced by Alice's pswap) → 60 USDC +/// total (full fill). Payout: 30 ETH → inflight (matches Alice's note_fill consumption above). /// /// Net effect: Charlie -20 ETH / +40 USDC; Alice's P2ID = 50 ETH; Bob's P2ID = 60 USDC. #[tokio::test] @@ -613,7 +607,10 @@ async fn pswap_note_combined_account_fill_and_note_fill_test() -> anyhow::Result Some(FungibleAsset::new(eth_faucet.id(), 20)?), Some(FungibleAsset::new(eth_faucet.id(), 30)?), )?; - assert!(alice_remainder.is_none(), "combined fill hits full fill — no remainder expected"); + assert!( + alice_remainder.is_none(), + "combined fill hits full fill — no remainder expected" + ); let (bob_p2id_note, bob_remainder) = bob_pswap.execute(charlie.id(), None, Some(FungibleAsset::new(usdc_faucet.id(), 60)?))?; @@ -637,9 +634,8 @@ async fn pswap_note_combined_account_fill_and_note_fill_test() -> anyhow::Result let alice_payout = Asset::Fungible(FungibleAsset::new(eth_faucet.id(), 50)?); let bob_payout = Asset::Fungible(FungibleAsset::new(usdc_faucet.id(), 60)?); - let note_contains = |idx: usize, asset: &Asset| { - output_notes.get_note(idx).assets().iter().any(|a| a == asset) - }; + let note_contains = + |idx: usize, asset: &Asset| output_notes.get_note(idx).assets().iter().any(|a| a == asset); assert!( (0..output_notes.num_notes()).any(|i| note_contains(i, &alice_payout)), "Alice's P2ID (50 ETH) not found", From 16dc925a39888919a5d57e8e9f6b64e6f8402eaa Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Wed, 15 Apr 2026 23:37:13 +0530 Subject: [PATCH 47/66] refactor(pswap): drop FACTOR, use u128 math for payout calculation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `calculate_tokens_offered_for_requested` previously used a fixed-point 1e5 precision factor with two separate branches (offered >= requested vs requested > offered) plus a full-fill early return, to approximate `offered * fill / requested` without overflowing u64 intermediates. That design had three problems: - Four `u64::wrapping_mul` calls silently produced wrong results for inputs where `x * FACTOR >= 2^64`, capping each operand at about 1.84e14 — roughly 0.000184 whole tokens for any 18-decimal asset. - The two-branch structure was fragile and harder to reason about than a single linear formula. - The FACTOR-scaled intermediate introduced rounding error in `ratio` that then got amplified by `fill`. Replace the whole proc with one linear path: product = u64::widening_mul(offered, fill_amount) # -> u128 quot = u128::div(product, requested_u128) # -> u128 assert q.hi_limbs == 0 # payout fits in u64 return q.lo_limbs combined back into a felt miden-core-lib 0.22 already exposes `u128::div` (advice-provider assisted, same host-compute-and-verify pattern as `u64::div`), so no custom division implementation is needed. Properties of the new version: - Exact integer precision (one floor division at the end, no FACTOR rounding). - Each operand can go up to `FungibleAsset::MAX ≈ 2^63`, so 18-decimal tokens now work at realistic volumes (~9.2 billion whole tokens per swap) — a ~50,000x improvement in dynamic range. - No branching on the offered/requested relationship. No full-fill early return. One generic formula that also handles the `fill_amount == requested` case trivially. - All wrapping_mul references gone, closing the last deferred theme-1 item from the PR review. Also mirror the same change on the Rust side: `calculate_output_amount` now just does `(offered as u128) * (fill as u128) / (requested as u128)` and `try_from`s back to u64, matching MASM exactly. Scrub the old PRECISION_FACTOR constant, the two-branch Rust logic, and the FACTOR doc block. The 8 in-crate unit tests and all 60 pswap script tests (including the 27-case hand-picked ratio regression suite and two seeded 30-iteration fuzzers) still pass. --- .../asm/standards/notes/pswap.masm | 129 +++++------------- crates/miden-standards/src/note/pswap.rs | 28 +--- 2 files changed, 38 insertions(+), 119 deletions(-) diff --git a/crates/miden-standards/asm/standards/notes/pswap.masm b/crates/miden-standards/asm/standards/notes/pswap.masm index 489c82380e..8b0574742d 100644 --- a/crates/miden-standards/asm/standards/notes/pswap.masm +++ b/crates/miden-standards/asm/standards/notes/pswap.masm @@ -1,4 +1,5 @@ use miden::core::math::u64 +use miden::core::math::u128 use miden::protocol::account_id use miden::protocol::active_account use miden::protocol::active_note @@ -12,18 +13,6 @@ use miden::standards::wallets::basic->wallet # ================================================================================================= const NUM_STORAGE_ITEMS=8 - -# Fixed-point scaling factor (1e5) used by calculate_tokens_offered_for_requested -# to approximate the offered/requested ratio with u64 integer math. -# -# Precision: ~5 decimal digits on the ratio; rounding is always toward zero -# (integer division), so a filler never receives more than proportional. -# -# Overflow headroom: the hottest intermediate is `offered * FACTOR` (and -# symmetrically `requested * FACTOR`, `fill_amount * FACTOR`), which must fit -# in u64. With FACTOR = 100000 that caps each side at ~1.8e14 (2^64 / 1e5), -# well above any realistic fungible-asset amount. -const FACTOR=100000 const MAX_U32=0x0000000100000000 # Note storage layout (8 felts, loaded at STORAGE_PTR by get_storage): @@ -66,6 +55,7 @@ const PSWAP_CREATOR_SUFFIX_ITEM = STORAGE_PTR + 7 # calculate_tokens_offered_for_requested locals const CALC_FILL_AMOUNT = 0 +const CALC_REQUESTED = 1 # create_p2id_note locals const P2ID_NOTE_IDX = 0 @@ -102,114 +92,61 @@ const ERR_PSWAP_WRONG_NUMBER_OF_STORAGE_ITEMS="PSWAP script expects exactly 8 no const ERR_PSWAP_WRONG_NUMBER_OF_ASSETS="PSWAP script requires exactly one note asset" const ERR_PSWAP_FILL_EXCEEDS_REQUESTED="PSWAP fill amount exceeds requested amount" const ERR_PSWAP_FILL_SUM_OVERFLOW="PSWAP account_fill + note_fill overflows u64" +const ERR_PSWAP_PAYOUT_OVERFLOW="PSWAP payout quotient does not fit in u64" # PRICE CALCULATION # ================================================================================================= #! Computes the proportional amount of offered tokens for a given fill amount. #! -#! Uses u64 integer arithmetic with a precision factor of 1e5 to handle -#! non-integer ratios without floating point. +#! Formula: `payout = floor((offered * fill_amount) / requested)`. #! -#! Formula: -#! if fill_amount == requested: result = offered (full fill, avoids precision loss) -#! if offered >= requested: result = (offered * FACTOR / requested) * fill_amount / FACTOR -#! if requested > offered: result = (fill_amount * FACTOR) / (requested * FACTOR / offered) +#! The intermediate product `offered * fill_amount` is computed as a full +#! u128 via `u64::widening_mul`, then divided by `requested` (extended to +#! u128) via `u128::div`. This gives exact integer precision with one floor +#! division at the end and no FACTOR/scaling tradeoffs: each input is safe +#! up to `FungibleAsset::MAX ≈ 2^63`, and the resulting quotient +#! `payout ≤ offered` always fits back in u64 (asserted via the upper-half +#! limb check below). #! #! Inputs: [offered, requested, fill_amount] (offered on top) #! Outputs: [payout_amount] #! -@locals(1) +@locals(2) # CALC_FILL_AMOUNT: fill amount +# CALC_REQUESTED: requested amount proc calculate_tokens_offered_for_requested movup.2 loc_store.CALC_FILL_AMOUNT # => [offered, requested] - # Early return: if fill_amount == requested (full fill), return offered directly. - # This avoids precision loss from integer division with the FACTOR. - dup.1 loc_load.CALC_FILL_AMOUNT eq - # => [is_full_fill, 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 - # => [requested_exceeds_offered, offered, requested] - - if.true - # Case: requested > offered - # ratio = (requested * FACTOR) / offered - # result = (fill_amount * 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] + swap loc_store.CALC_REQUESTED + # => [offered] - movup.2 u32split - # => [off_lo, off_hi, (req*F)_lo, (req*F)_hi] + u32split + # => [off_lo, off_hi] - exec.u64::div - # => [ratio_lo, ratio_hi] + loc_load.CALC_FILL_AMOUNT u32split + # => [fill_lo, fill_hi, off_lo, off_hi] - loc_load.CALC_FILL_AMOUNT u32split push.FACTOR u32split - # => [F_lo, F_hi, in_lo, in_hi, ratio_lo, ratio_hi] + exec.u64::widening_mul + # => [p0, p1, p2, p3] - exec.u64::wrapping_mul - # => [(in*F)_lo, (in*F)_hi, ratio_lo, ratio_hi] + loc_load.CALC_REQUESTED u32split + # => [req_lo, req_hi, p0, p1, p2, p3] - movup.3 movup.3 - # => [ratio_lo, ratio_hi, (in*F)_lo, (in*F)_hi] + push.0 movdn.2 push.0 movdn.3 + # => [req_lo, req_hi, 0, 0, p0, p1, p2, p3] - exec.u64::div - # => [result_lo, result_hi] + exec.u128::div + # => [q0, q1, q2, q3] (quotient, little-endian limbs) - swap mul.MAX_U32 add - # => [result] + # assert whether the output fits in u64 + movup.3 eq.0 assert.err=ERR_PSWAP_PAYOUT_OVERFLOW + # => [q0, q1, q2] + movup.2 eq.0 assert.err=ERR_PSWAP_PAYOUT_OVERFLOW + # => [q0, q1] - else - # Case: offered >= requested - # result = ((offered * FACTOR) / requested) * fill_amount / 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] - - loc_load.CALC_FILL_AMOUNT 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 mul.MAX_U32 add - # => [result] - end - # => [result] - end + swap mul.MAX_U32 add # => [payout_amount] end diff --git a/crates/miden-standards/src/note/pswap.rs b/crates/miden-standards/src/note/pswap.rs index 84213ddb25..8eb78cb667 100644 --- a/crates/miden-standards/src/note/pswap.rs +++ b/crates/miden-standards/src/note/pswap.rs @@ -465,30 +465,12 @@ impl PswapNote { NoteTag::new(tag) } - /// Computes `offered_total * fill_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 `fill_amount == requested_total`. - /// - /// The formula is implemented in two branches to maximize precision: - /// - When `offered > requested`: the ratio `offered/requested` is >= 1, so we compute `(offered - /// * FACTOR / requested) * fill_amount / FACTOR` to avoid losing the fractional part. - /// - When `requested >= offered`: the ratio `offered/requested` is < 1, so computing it - /// directly would truncate to zero. Instead we compute the inverse ratio `(requested * FACTOR - /// / offered)` and divide: `(fill_amount * FACTOR) / inverse_ratio`. - fn calculate_output_amount(offered_total: u64, requested_total: u64, fill_amount: u64) -> u64 { - const PRECISION_FACTOR: u64 = 100_000; - - if requested_total == fill_amount { - return offered_total; - } + /// Computes `(offered_total * fill_amount) / requested_total)`. - if offered_total > requested_total { - let ratio = (offered_total * PRECISION_FACTOR) / requested_total; - (fill_amount * ratio) / PRECISION_FACTOR - } else { - let ratio = (requested_total * PRECISION_FACTOR) / offered_total; - (fill_amount * PRECISION_FACTOR) / ratio - } + fn calculate_output_amount(offered_total: u64, requested_total: u64, fill_amount: u64) -> u64 { + let product = (offered_total as u128) * (fill_amount as u128); + let quotient = product / (requested_total as u128); + u64::try_from(quotient).expect("payout quotient does not fit in u64") } /// Builds a payback note (P2ID) that delivers the filled assets to the swap creator. From a20090aecea4f661db1f47bb5e31e3011788d54d Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Thu, 16 Apr 2026 00:20:05 +0530 Subject: [PATCH 48/66] test(pswap): second pass on PR review follow-ups MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `calculate_output_amount` doc: fix a mismatched paren and the empty blank line between doc and fn signature that triggered clippy's `empty_line_after_doc_comments` (caught by CI after the last push). - `pswap_note_alice_reconstructs_and_consumes_p2id`: build the `PswapNote` via the builder, call `pswap.clone().into()` to get the protocol `Note`, and keep `pswap` in scope for `.execute(...)`. Drops the `PswapNote::try_from(&pswap_note)?` roundtrip the reviewer flagged on this exact test as a nit. - `pswap_chained_partial_fills_test`: same pattern — replace the manual `Note::new(NoteAssets / NoteMetadata / NoteRecipient)` construction with `PswapNote::builder().serial_number(current_serial)...build()?` + `pswap.clone().into()`. The raw-Note construction was only there to inject a specific serial per chain position, which the builder already supports. Also drops the dependent `PswapNote::try_from` roundtrip. - `pswap_fill_test`: bump `AMOUNT_SCALE` from 10^12 to 10^18 so the happy-path test exercises the MASM u128 calculation at realistic 18-decimal token magnitudes. Base values adjusted (offered=8, requested=4, fills=3/4) so every amount stays under AssetAmount::MAX ≈ 9.22 × 10^18. Previously the 10^12 scale was chosen to stay under the old FACTOR=1e5 cap; now that the u128 rewrite lifted that cap, the test can stress the calculation at the actual wei-equivalent scale the reviewer asked for. - New `pswap_attachment_layout_matches_masm_test`: dedicated regression test for the shared P2ID and remainder attachment-word layout. Does a partial fill, then explicitly asserts the executed transaction's P2ID attachment equals `[fill_amount, 0, 0, 0]` and the remainder attachment equals `[amt_payout, 0, 0, 0]`, and cross-checks both against the Rust-predicted attachments. Fires if either MASM or Rust drifts the load-bearing felt off `Word[0]`. - Apply the "blank line after every `# => [...]` stack comment" convention across pswap.masm where it was missing — one reviewer tends to leave this as a nit so sweep the whole file for consistency rather than wait for the comments. --- .../asm/standards/notes/pswap.masm | 21 +- crates/miden-standards/src/note/pswap.rs | 5 +- crates/miden-testing/tests/scripts/pswap.rs | 307 +++++++++++------- 3 files changed, 213 insertions(+), 120 deletions(-) diff --git a/crates/miden-standards/asm/standards/notes/pswap.masm b/crates/miden-standards/asm/standards/notes/pswap.masm index 8b0574742d..79574d3939 100644 --- a/crates/miden-standards/asm/standards/notes/pswap.masm +++ b/crates/miden-standards/asm/standards/notes/pswap.masm @@ -143,9 +143,11 @@ proc calculate_tokens_offered_for_requested # assert whether the output fits in u64 movup.3 eq.0 assert.err=ERR_PSWAP_PAYOUT_OVERFLOW # => [q0, q1, q2] + movup.2 eq.0 assert.err=ERR_PSWAP_PAYOUT_OVERFLOW # => [q0, q1] + # Reconstruct the u64 quotient as a single felt: `q0 + q1 * 2^32`. swap mul.MAX_U32 add # => [payout_amount] end @@ -197,6 +199,7 @@ proc create_p2id_note push.0.0.0 loc_load.P2ID_AMT_ACCOUNT_FILL loc_load.P2ID_AMT_NOTE_FILL add # => [total_fill, 0, 0, 0] + push.0 loc_load.P2ID_NOTE_IDX # => [note_idx, attachment_scheme=0, total_fill, 0, 0, 0] @@ -292,6 +295,7 @@ proc create_remainder_note # Derive remainder serial: increment most significant element exec.active_note::get_serial_number # => [s0, s1, s2, s3, SCRIPT_ROOT, note_type, tag] + movup.3 add.1 movdn.3 # => [s0, s1, s2, s3+1, SCRIPT_ROOT, note_type, tag] @@ -314,6 +318,7 @@ proc create_remainder_note # Set attachment: aux = amt_payout at Word[0] push.0.0.0 loc_load.REMAINDER_AMT_PAYOUT # => [amt_payout, 0, 0, 0] + push.0 loc_load.REMAINDER_NOTE_IDX # => [note_idx, attachment_scheme=0, amt_payout, 0, 0, 0] @@ -438,10 +443,13 @@ proc execute_pswap # Extract and store offered faucet info from ASSET_KEY exec.asset::key_to_callbacks_enabled loc_store.EXEC_OFFERED_ENABLE_CB # => [ASSET_KEY, ASSET_VALUE] + exec.asset::key_into_faucet_id # => [faucet_id_suffix, faucet_id_prefix, ASSET_VALUE] + loc_store.EXEC_OFFERED_FAUCET_SUFFIX loc_store.EXEC_OFFERED_FAUCET_PREFIX # => [ASSET_VALUE] + dropw # => [] @@ -455,14 +463,19 @@ proc execute_pswap loc_load.EXEC_AMT_REQUESTED_ACCOUNT_FILL u32split loc_load.EXEC_AMT_REQUESTED_NOTE_FILL u32split # => [nf_lo, nf_hi, af_lo, af_hi] + exec.u64::overflowing_add # => [overflow, sum_lo, sum_hi] + eq.0 assert.err=ERR_PSWAP_FILL_SUM_OVERFLOW # => [sum_lo, sum_hi] + swap mul.MAX_U32 add # => [fill_amount] + loc_load.EXEC_AMT_REQUESTED # => [requested, fill_amount] + lte assert.err=ERR_PSWAP_FILL_EXCEEDS_REQUESTED # => [] @@ -471,6 +484,7 @@ proc execute_pswap loc_load.EXEC_AMT_REQUESTED loc_load.EXEC_AMT_OFFERED # => [offered, requested, account_fill_amount] + exec.calculate_tokens_offered_for_requested # => [account_fill_payout] @@ -485,6 +499,7 @@ proc execute_pswap exec.calculate_tokens_offered_for_requested # => [note_fill_payout] + loc_store.EXEC_AMT_PAYOUT_NOTE_FILL # => [] @@ -532,17 +547,17 @@ proc execute_pswap call.wallet::receive_asset # => [pad(16)] + dropw dropw dropw dropw # => [] end # => [] - # Check if partial fill. `execute_pswap` has already asserted - # `total_in <= total_requested`, so `is_partial` is equivalent to - # `total_in != total_requested` (cheaper than `lt`). + # Check if partial fill. loc_load.EXEC_AMT_REQUESTED_ACCOUNT_FILL loc_load.EXEC_AMT_REQUESTED_NOTE_FILL add loc_load.EXEC_AMT_REQUESTED # => [total_requested, total_in] + dup.1 dup.1 neq # => [is_partial, total_requested, total_in] diff --git a/crates/miden-standards/src/note/pswap.rs b/crates/miden-standards/src/note/pswap.rs index 8eb78cb667..6251dc9fdd 100644 --- a/crates/miden-standards/src/note/pswap.rs +++ b/crates/miden-standards/src/note/pswap.rs @@ -465,8 +465,9 @@ impl PswapNote { NoteTag::new(tag) } - /// Computes `(offered_total * fill_amount) / requested_total)`. - + /// Computes `floor((offered_total * fill_amount) / requested_total)` via a + /// u128 intermediate, mirroring `u64::widening_mul` + `u128::div` on the + /// MASM side. fn calculate_output_amount(offered_total: u64, requested_total: u64, fill_amount: u64) -> u64 { let product = (offered_total as u128) * (fill_amount as u128); let quotient = product / (requested_total as u128); diff --git a/crates/miden-testing/tests/scripts/pswap.rs b/crates/miden-testing/tests/scripts/pswap.rs index 3370e7dfc3..481e6b0cfd 100644 --- a/crates/miden-testing/tests/scripts/pswap.rs +++ b/crates/miden-testing/tests/scripts/pswap.rs @@ -150,14 +150,14 @@ async fn pswap_note_alice_reconstructs_and_consumes_p2id() -> anyhow::Result<()> .creator_account_id(alice.id()) .payback_note_type(NoteType::Public) .build(); - let pswap_note: Note = PswapNote::builder() + let pswap = PswapNote::builder() .sender(alice.id()) .storage(storage) .serial_number(serial_number) .note_type(NoteType::Public) .offered_asset(offered_asset) - .build()? - .into(); + .build()?; + let pswap_note: Note = pswap.clone().into(); builder.add_output_note(RawOutputNote::Full(pswap_note.clone())); let mut mock_chain = builder.build()?; @@ -168,7 +168,6 @@ async fn pswap_note_alice_reconstructs_and_consumes_p2id() -> anyhow::Result<()> let mut note_args_map = BTreeMap::new(); note_args_map.insert(pswap_note.id(), pswap_args(fill_amount, 0)); - let pswap = PswapNote::try_from(&pswap_note)?; let (p2id_note, remainder_pswap) = pswap.execute(bob.id(), Some(FungibleAsset::new(eth_faucet.id(), 20)?), None)?; let remainder_note = @@ -301,33 +300,138 @@ async fn pswap_note_alice_reconstructs_and_consumes_p2id() -> anyhow::Result<()> Ok(()) } +/// Dedicated regression test for the attachment word layout shared between +/// `create_p2id_note` / `create_remainder_note` in pswap.masm and +/// `create_payback_note` / `create_remainder_pswap_note` in pswap.rs. +/// +/// Both sides agree on: +/// - P2ID payback attachment: `[fill_amount, 0, 0, 0]` +/// - Remainder PSWAP attachment: `[amt_payout, 0, 0, 0]` +/// +/// i.e. the load-bearing felt sits at `Word[0]` and the remaining three felts +/// are zero padding. If either side drifts (e.g. MASM switches to +/// `[0, 0, 0, x]` or Rust does), this test fires. +/// +/// Uses a simple partial fill — offered 50 USDC, requested 25 ETH, fill 20 ETH +/// — so both output notes exist and the expected amounts are +/// `fill_amount = 20` and `amt_payout = floor(50 * 20 / 25) = 40`. +#[tokio::test] +async fn pswap_attachment_layout_matches_masm_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 usdc_50 = FungibleAsset::new(usdc_faucet.id(), 50)?; + let eth_20 = FungibleAsset::new(eth_faucet.id(), 20)?; + let eth_25 = FungibleAsset::new(eth_faucet.id(), 25)?; + + let alice = builder.add_existing_wallet_with_assets(BASIC_AUTH, [usdc_50.into()])?; + let bob = builder.add_existing_wallet_with_assets(BASIC_AUTH, [eth_20.into()])?; + + let (pswap, pswap_note) = + build_pswap_note(&mut builder, alice.id(), usdc_50, eth_25, NoteType::Public)?; + + let mock_chain = builder.build()?; + + let fill_amount = 20u64; + let expected_payout = 40u64; // floor(50 * 20 / 25) + + let mut note_args_map = BTreeMap::new(); + note_args_map.insert(pswap_note.id(), pswap_args(fill_amount, 0)); + + let (p2id_note, remainder_pswap) = pswap.execute(bob.id(), Some(eth_20), None)?; + let remainder_note = + Note::from(remainder_pswap.expect("partial fill should produce remainder")); + + let tx_context = mock_chain + .build_tx_context(bob.id(), &[pswap_note.id()], &[])? + .extend_note_args(note_args_map) + .extend_expected_output_notes(vec![ + RawOutputNote::Full(p2id_note.clone()), + RawOutputNote::Full(remainder_note.clone()), + ]) + .build()?; + + let executed_transaction = tx_context.execute().await?; + let output_notes = executed_transaction.output_notes(); + assert_eq!(output_notes.num_notes(), 2, "expected P2ID + remainder"); + + let p2id_attachment = output_notes.get_note(0).metadata().attachment().content().to_word(); + let remainder_attachment = + output_notes.get_note(1).metadata().attachment().content().to_word(); + + // P2ID payback attachment: `[fill_amount, 0, 0, 0]` — fill_amount at Word[0]. + let expected_p2id_attachment = Word::from([ + Felt::try_from(fill_amount).expect("fill_amount fits in a felt"), + ZERO, + ZERO, + ZERO, + ]); + assert_eq!( + p2id_attachment, expected_p2id_attachment, + "P2ID attachment layout mismatch: expected [fill_amount, 0, 0, 0] at Word[0..3]", + ); + + // Remainder PSWAP attachment: `[amt_payout, 0, 0, 0]` — amt_payout at Word[0]. + let expected_remainder_attachment = Word::from([ + Felt::try_from(expected_payout).expect("amt_payout fits in a felt"), + ZERO, + ZERO, + ZERO, + ]); + assert_eq!( + remainder_attachment, expected_remainder_attachment, + "remainder attachment layout mismatch: expected [amt_payout, 0, 0, 0] at Word[0..3]", + ); + + // Cross-check: the Rust-predicted notes must produce the same attachment + // words as the on-chain executed ones. A future drift between either side + // would fail here even if the Word[0] position stays correct. + assert_eq!( + p2id_note.metadata().attachment().content().to_word(), + p2id_attachment, + "Rust-predicted P2ID attachment does not match MASM output", + ); + assert_eq!( + remainder_note.metadata().attachment().content().to_word(), + remainder_attachment, + "Rust-predicted remainder attachment does not match MASM output", + ); + + Ok(()) +} + /// Parameterized fill test covering: /// - full public fill /// - full private fill -/// - partial public fill (offered=50 USDC / requested=25 ETH / fill=20 ETH → payout=40 USDC, -/// remainder=10 USDC) +/// - partial public fill (offered=8 USDC / requested=4 ETH / fill=3 ETH → payout=6 USDC, +/// remainder=2 USDC, all scaled by 10^18) /// - full fill via a network account (no note_args → script defaults to full fill) /// -/// Amounts are scaled by `AMOUNT_SCALE` (10^12) so the test exercises realistic -/// 12-decimal token values instead of single-digit toys. The cap is set so -/// `requested * FACTOR` (where FACTOR = 1e5) stays under `u64::MAX`. +/// Amounts are scaled by `AMOUNT_SCALE = 10^18` so the test exercises realistic +/// 18-decimal token base units (the wei-equivalent of ETH / most ERC-20 tokens). +/// This stresses the MASM payout calculation at operand sizes in the ~10^18 +/// range, verifying `u64::widening_mul` + `u128::div` handle them without +/// overflow. Base values stay below `AssetAmount::MAX ≈ 9.22 × 10^18`. #[rstest] -#[case::full_public(25, NoteType::Public, false)] -#[case::full_private(25, NoteType::Private, false)] -#[case::partial_public(20, NoteType::Public, false)] -#[case::network_full_fill(25, NoteType::Public, true)] +#[case::full_public(4, NoteType::Public, false)] +#[case::full_private(4, NoteType::Private, false)] +#[case::partial_public(3, NoteType::Public, false)] +#[case::network_full_fill(4, NoteType::Public, true)] #[tokio::test] async fn pswap_fill_test( #[case] fill_base: u64, #[case] note_type: NoteType, #[case] use_network_account: bool, ) -> anyhow::Result<()> { - const AMOUNT_SCALE: u64 = 1_000_000_000_000; // 10^12 + // 10^18: one whole 18-decimal token (e.g. 1 ETH in wei). + const AMOUNT_SCALE: u64 = 1_000_000_000_000_000_000; let fill_amount = fill_base * AMOUNT_SCALE; - let offered_total = 50 * AMOUNT_SCALE; - let requested_total = 25 * AMOUNT_SCALE; - let max_supply = 1000 * AMOUNT_SCALE; + let offered_total = 8 * AMOUNT_SCALE; // 8 × 10^18 USDC offered + let requested_total = 4 * AMOUNT_SCALE; // 4 × 10^18 ETH requested + let max_supply = 9 * AMOUNT_SCALE; // just under AssetAmount::MAX let mut builder = MockChain::builder(); @@ -335,13 +439,13 @@ async fn pswap_fill_test( BASIC_AUTH, "USDC", max_supply, - Some(150 * AMOUNT_SCALE), + Some(offered_total), )?; let eth_faucet = builder.add_existing_basic_faucet( BASIC_AUTH, "ETH", max_supply, - Some(50 * AMOUNT_SCALE), + Some(requested_total), )?; let alice = builder.add_existing_wallet_with_assets( @@ -458,33 +562,23 @@ async fn pswap_note_note_fill_cross_swap_test() -> anyhow::Result<()> { 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()], - )?; + // Alice offers 50 USDC for 25 ETH. Bob offers 25 ETH for 50 USDC. They + // cross-swap through Charlie, so each side's offered asset is the other + // side's requested asset. + let usdc_50 = FungibleAsset::new(usdc_faucet.id(), 50)?; + let eth_25 = FungibleAsset::new(eth_faucet.id(), 25)?; + + let alice = builder.add_existing_wallet_with_assets(BASIC_AUTH, [usdc_50.into()])?; + let bob = builder.add_existing_wallet_with_assets(BASIC_AUTH, [eth_25.into()])?; let charlie = builder.add_existing_wallet_with_assets(BASIC_AUTH, [])?; // Alice's note: offers 50 USDC, requests 25 ETH - let (alice_pswap, alice_pswap_note) = build_pswap_note( - &mut builder, - alice.id(), - FungibleAsset::new(usdc_faucet.id(), 50)?, - FungibleAsset::new(eth_faucet.id(), 25)?, - NoteType::Public, - )?; + let (alice_pswap, alice_pswap_note) = + build_pswap_note(&mut builder, alice.id(), usdc_50, eth_25, NoteType::Public)?; // Bob's note: offers 25 ETH, requests 50 USDC - let (bob_pswap, bob_pswap_note) = build_pswap_note( - &mut builder, - bob.id(), - FungibleAsset::new(eth_faucet.id(), 25)?, - FungibleAsset::new(usdc_faucet.id(), 50)?, - NoteType::Public, - )?; + let (bob_pswap, bob_pswap_note) = + build_pswap_note(&mut builder, bob.id(), eth_25, usdc_50, NoteType::Public)?; let mock_chain = builder.build()?; @@ -494,11 +588,8 @@ async fn pswap_note_note_fill_cross_swap_test() -> anyhow::Result<()> { note_args_map.insert(bob_pswap_note.id(), pswap_args(0, 50)); // Expected P2ID notes - let (alice_p2id_note, _) = - alice_pswap.execute(charlie.id(), None, Some(FungibleAsset::new(eth_faucet.id(), 25)?))?; - - let (bob_p2id_note, _) = - bob_pswap.execute(charlie.id(), None, Some(FungibleAsset::new(usdc_faucet.id(), 50)?))?; + let (alice_p2id_note, _) = alice_pswap.execute(charlie.id(), None, Some(eth_25))?; + let (bob_p2id_note, _) = bob_pswap.execute(charlie.id(), None, Some(usdc_50))?; let tx_context = mock_chain .build_tx_context(charlie.id(), &[alice_pswap_note.id(), bob_pswap_note.id()], &[])? @@ -511,24 +602,22 @@ async fn pswap_note_note_fill_cross_swap_test() -> anyhow::Result<()> { let executed_transaction = tx_context.execute().await?; - // Verify: 2 P2ID notes + // Verify: 2 P2ID notes, one carrying Alice's requested (25 ETH), one + // carrying Bob's requested (50 USDC). let output_notes = executed_transaction.output_notes(); assert_eq!(output_notes.num_notes(), 2); - let alice_payout = Asset::Fungible(FungibleAsset::new(eth_faucet.id(), 25)?); - let bob_payout = Asset::Fungible(FungibleAsset::new(usdc_faucet.id(), 50)?); - - let note_contains = |note_idx: usize, asset: &Asset| { - output_notes.get_note(note_idx).assets().iter().any(|a| a == asset) - }; - assert!( - (0..output_notes.num_notes()).any(|i| note_contains(i, &alice_payout)), - "Alice's P2ID note (25 ETH) not found", + output_notes + .iter() + .any(|note| note.assets().iter_fungible().any(|a| a == eth_25)), + "Alice's P2ID note ({eth_25:?}) not found", ); assert!( - (0..output_notes.num_notes()).any(|i| note_contains(i, &bob_payout)), - "Bob's P2ID note (50 USDC) not found", + output_notes + .iter() + .any(|note| note.assets().iter_fungible().any(|a| a == usdc_50)), + "Bob's P2ID note ({usdc_50:?}) not found", ); // Charlie's vault should be unchanged @@ -564,36 +653,34 @@ async fn pswap_note_combined_account_fill_and_note_fill_test() -> anyhow::Result let usdc_faucet = builder.add_existing_basic_faucet(BASIC_AUTH, "USDC", 1000, Some(200))?; let eth_faucet = builder.add_existing_basic_faucet(BASIC_AUTH, "ETH", 1000, Some(60))?; - let alice = builder.add_existing_wallet_with_assets( - BASIC_AUTH, - [FungibleAsset::new(usdc_faucet.id(), 100)?.into()], - )?; - let bob = builder.add_existing_wallet_with_assets( - BASIC_AUTH, - [FungibleAsset::new(eth_faucet.id(), 30)?.into()], - )?; - let charlie = builder.add_existing_wallet_with_assets( - BASIC_AUTH, - [FungibleAsset::new(eth_faucet.id(), 20)?.into()], - )?; + // Alice's pswap: 100 USDC offered for 50 ETH requested. + // Bob's pswap: 30 ETH offered for 60 USDC requested. + // Charlie consumes both; his vault supplies 20 ETH (account_fill) and + // the other 30 ETH is sourced from Bob's offered leg via note_fill. + let alice_offered = FungibleAsset::new(usdc_faucet.id(), 100)?; + let alice_requested = FungibleAsset::new(eth_faucet.id(), 50)?; + let bob_offered = FungibleAsset::new(eth_faucet.id(), 30)?; + let bob_requested = FungibleAsset::new(usdc_faucet.id(), 60)?; + + let charlie_vault_eth = FungibleAsset::new(eth_faucet.id(), 20)?; + let account_fill_eth = charlie_vault_eth; + let note_fill_eth = bob_offered; + let charlie_payout_usdc = FungibleAsset::new(usdc_faucet.id(), 40)?; + + let alice = builder.add_existing_wallet_with_assets(BASIC_AUTH, [alice_offered.into()])?; + let bob = builder.add_existing_wallet_with_assets(BASIC_AUTH, [bob_offered.into()])?; + let charlie = + builder.add_existing_wallet_with_assets(BASIC_AUTH, [charlie_vault_eth.into()])?; - // Alice's pswap: 100 USDC for 50 ETH let (alice_pswap, alice_pswap_note) = build_pswap_note( &mut builder, alice.id(), - FungibleAsset::new(usdc_faucet.id(), 100)?, - FungibleAsset::new(eth_faucet.id(), 50)?, - NoteType::Public, - )?; - - // Bob's pswap: 30 ETH for 60 USDC - let (bob_pswap, bob_pswap_note) = build_pswap_note( - &mut builder, - bob.id(), - FungibleAsset::new(eth_faucet.id(), 30)?, - FungibleAsset::new(usdc_faucet.id(), 60)?, + alice_offered, + alice_requested, NoteType::Public, )?; + let (bob_pswap, bob_pswap_note) = + build_pswap_note(&mut builder, bob.id(), bob_offered, bob_requested, NoteType::Public)?; let mock_chain = builder.build()?; @@ -602,18 +689,15 @@ async fn pswap_note_combined_account_fill_and_note_fill_test() -> anyhow::Result note_args_map.insert(alice_pswap_note.id(), pswap_args(20, 30)); note_args_map.insert(bob_pswap_note.id(), pswap_args(0, 60)); - let (alice_p2id_note, alice_remainder) = alice_pswap.execute( - charlie.id(), - Some(FungibleAsset::new(eth_faucet.id(), 20)?), - Some(FungibleAsset::new(eth_faucet.id(), 30)?), - )?; + let (alice_p2id_note, alice_remainder) = + alice_pswap.execute(charlie.id(), Some(account_fill_eth), Some(note_fill_eth))?; assert!( alice_remainder.is_none(), "combined fill hits full fill — no remainder expected" ); let (bob_p2id_note, bob_remainder) = - bob_pswap.execute(charlie.id(), None, Some(FungibleAsset::new(usdc_faucet.id(), 60)?))?; + bob_pswap.execute(charlie.id(), None, Some(bob_requested))?; assert!(bob_remainder.is_none(), "bob pswap is filled completely via note_fill"); let tx_context = mock_chain @@ -631,28 +715,23 @@ async fn pswap_note_combined_account_fill_and_note_fill_test() -> anyhow::Result let output_notes = executed_transaction.output_notes(); assert_eq!(output_notes.num_notes(), 2, "expected exactly 2 P2ID output notes"); - let alice_payout = Asset::Fungible(FungibleAsset::new(eth_faucet.id(), 50)?); - let bob_payout = Asset::Fungible(FungibleAsset::new(usdc_faucet.id(), 60)?); - - let note_contains = - |idx: usize, asset: &Asset| output_notes.get_note(idx).assets().iter().any(|a| a == asset); assert!( - (0..output_notes.num_notes()).any(|i| note_contains(i, &alice_payout)), - "Alice's P2ID (50 ETH) not found", + output_notes + .iter() + .any(|note| note.assets().iter_fungible().any(|a| a == alice_requested)), + "Alice's P2ID ({alice_requested:?}) not found", ); assert!( - (0..output_notes.num_notes()).any(|i| note_contains(i, &bob_payout)), - "Bob's P2ID (60 USDC) not found", + output_notes + .iter() + .any(|note| note.assets().iter_fungible().any(|a| a == bob_requested)), + "Bob's P2ID ({bob_requested:?}) not found", ); // Charlie's vault: -20 ETH (account_fill) + 40 USDC (account_fill_payout). // The note_fill legs flow entirely through inflight and never touch his vault. let vault_delta = executed_transaction.account_delta().vault(); - assert_vault_added_removed( - vault_delta, - FungibleAsset::new(usdc_faucet.id(), 40)?, - FungibleAsset::new(eth_faucet.id(), 20)?, - ); + assert_vault_added_removed(vault_delta, charlie_payout_usdc, charlie_vault_eth); Ok(()) } @@ -1098,25 +1177,24 @@ async fn pswap_chained_partial_fills_test( [FungibleAsset::new(eth_faucet.id(), *fill_amount)?.into()], )?; - // Build storage and note manually to use the correct serial for chain position + // Use the PswapNote builder directly so we can inject `current_serial` + // for this chain position (each remainder in the chain bumps + // `serial[3] + 1`, and the test walks through that sequence manually). let offered_fungible = FungibleAsset::new(usdc_faucet.id(), current_offered)?; let requested_fungible = FungibleAsset::new(eth_faucet.id(), current_requested)?; - let pswap_tag = - PswapNote::create_tag(NoteType::Public, &offered_fungible, &requested_fungible); - let offered_asset = Asset::Fungible(offered_fungible); - let storage = PswapNoteStorage::builder() .requested_asset(requested_fungible) .creator_account_id(alice.id()) .build(); - let note_assets = NoteAssets::new(vec![offered_asset])?; - - // Create note with the correct serial for this chain position - 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 pswap_note = Note::new(note_assets, metadata, recipient); + let pswap = PswapNote::builder() + .sender(alice.id()) + .storage(storage) + .serial_number(current_serial) + .note_type(NoteType::Public) + .offered_asset(offered_fungible) + .build()?; + let pswap_note: Note = pswap.clone().into(); builder.add_output_note(RawOutputNote::Full(pswap_note.clone())); let mock_chain = builder.build()?; @@ -1124,7 +1202,6 @@ async fn pswap_chained_partial_fills_test( let mut note_args_map = BTreeMap::new(); note_args_map.insert(pswap_note.id(), pswap_args(*fill_amount, 0)); - let pswap = PswapNote::try_from(&pswap_note)?; let payout_amount = pswap.calculate_offered_for_requested(*fill_amount); let remaining_offered = current_offered - payout_amount; let (p2id_note, remainder_pswap) = pswap.execute( From 0fd99fcbee8e30fc26040a46e222feebb6a86089 Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Thu, 16 Apr 2026 00:28:23 +0530 Subject: [PATCH 49/66] style(pswap/tests): apply rustfmt --- crates/miden-testing/tests/scripts/pswap.rs | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/crates/miden-testing/tests/scripts/pswap.rs b/crates/miden-testing/tests/scripts/pswap.rs index 481e6b0cfd..ba7b8e7211 100644 --- a/crates/miden-testing/tests/scripts/pswap.rs +++ b/crates/miden-testing/tests/scripts/pswap.rs @@ -358,8 +358,7 @@ async fn pswap_attachment_layout_matches_masm_test() -> anyhow::Result<()> { assert_eq!(output_notes.num_notes(), 2, "expected P2ID + remainder"); let p2id_attachment = output_notes.get_note(0).metadata().attachment().content().to_word(); - let remainder_attachment = - output_notes.get_note(1).metadata().attachment().content().to_word(); + let remainder_attachment = output_notes.get_note(1).metadata().attachment().content().to_word(); // P2ID payback attachment: `[fill_amount, 0, 0, 0]` — fill_amount at Word[0]. let expected_p2id_attachment = Word::from([ @@ -435,18 +434,10 @@ async fn pswap_fill_test( let mut builder = MockChain::builder(); - let usdc_faucet = builder.add_existing_basic_faucet( - BASIC_AUTH, - "USDC", - max_supply, - Some(offered_total), - )?; - let eth_faucet = builder.add_existing_basic_faucet( - BASIC_AUTH, - "ETH", - max_supply, - Some(requested_total), - )?; + let usdc_faucet = + builder.add_existing_basic_faucet(BASIC_AUTH, "USDC", max_supply, Some(offered_total))?; + let eth_faucet = + builder.add_existing_basic_faucet(BASIC_AUTH, "ETH", max_supply, Some(requested_total))?; let alice = builder.add_existing_wallet_with_assets( BASIC_AUTH, From b5de6e06fdc3411dcfd88aad73fc88ddb722ff7f Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Thu, 16 Apr 2026 01:17:10 +0530 Subject: [PATCH 50/66] refactor(pswap): expose `PswapNote::create_args` as public helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add `PswapNote::create_args(account_fill: u64, note_fill: u64) -> Result` so downstream consumers building PSWAP `NOTE_ARGS` words don't need to know the `[account_fill, note_fill, 0, 0]` layout by hand. Returns a `Result` instead of panicking so the underlying `Felt::try_from` conversion errors propagate cleanly through the standard `NoteError` path — although for any amount that fits in `FungibleAsset::MAX_AMOUNT` this cannot actually fail, the conversion is surfaced explicitly rather than hidden behind an `.expect(...)` in a public API. Drop the local `pswap_args` test helper — every call site now uses `PswapNote::create_args(...)?` directly, matching the reviewer's suggestion that the layout helper live on `PswapNote` rather than being reimplemented in each consumer. All 61 pswap script tests pass with the new signature (twelve call sites updated to propagate the Result via `?`). --- crates/miden-standards/src/note/pswap.rs | 30 ++++++++++++++++++ crates/miden-testing/tests/scripts/pswap.rs | 34 ++++++++------------- 2 files changed, 42 insertions(+), 22 deletions(-) diff --git a/crates/miden-standards/src/note/pswap.rs b/crates/miden-standards/src/note/pswap.rs index 6251dc9fdd..dd5135bf80 100644 --- a/crates/miden-standards/src/note/pswap.rs +++ b/crates/miden-standards/src/note/pswap.rs @@ -260,6 +260,36 @@ impl PswapNote { PSWAP_SCRIPT.root() } + /// Builds the `NOTE_ARGS` word that the PSWAP script expects when a + /// consumer wants to fill part of the swap: + /// + /// `[account_fill, note_fill, 0, 0]` + /// + /// - `account_fill` is the portion of the requested asset the consumer pays out of their own + /// vault. + /// - `note_fill` is the portion sourced from another note in the same transaction (cross-swap / + /// net-zero flow). + /// + /// Both values are in the requested asset's base units. In a network + /// transaction the kernel defaults `NOTE_ARGS` to `[0, 0, 0, 0]` and the + /// script falls back to a full fill, so this helper is only needed for + /// local transactions where the consumer is choosing the fill split. + /// + /// # Errors + /// + /// Returns an error if either value exceeds the Goldilocks field size + /// (i.e. cannot be represented as a [`Felt`]). In practice this cannot + /// happen for any amount that fits in a [`FungibleAsset`] — + /// `FungibleAsset::MAX_AMOUNT` is comfortably below `2^63` — but the + /// conversion is surfaced explicitly rather than hidden behind a panic. + pub fn create_args(account_fill: u64, note_fill: u64) -> Result { + let account_fill = Felt::try_from(account_fill) + .map_err(|e| NoteError::other_with_source("account_fill is not a valid felt", e))?; + let note_fill = Felt::try_from(note_fill) + .map_err(|e| NoteError::other_with_source("note_fill is not a valid felt", e))?; + Ok(Word::from([account_fill, note_fill, ZERO, ZERO])) + } + /// Returns the account ID of the note sender. pub fn sender(&self) -> AccountId { self.sender diff --git a/crates/miden-testing/tests/scripts/pswap.rs b/crates/miden-testing/tests/scripts/pswap.rs index ba7b8e7211..ffa9dc45dc 100644 --- a/crates/miden-testing/tests/scripts/pswap.rs +++ b/crates/miden-testing/tests/scripts/pswap.rs @@ -63,16 +63,6 @@ fn build_pswap_note( Ok((pswap, note)) } -/// PSWAP note args Word `[account_fill, note_fill, 0, 0]`. -fn pswap_args(account_fill: u64, note_fill: u64) -> Word { - Word::from([ - Felt::try_from(account_fill).expect("account_fill fits in a felt"), - Felt::try_from(note_fill).expect("note_fill fits in a felt"), - ZERO, - ZERO, - ]) -} - #[track_caller] fn assert_fungible_asset_eq(asset: &Asset, expected: FungibleAsset) { match asset { @@ -166,7 +156,7 @@ async fn pswap_note_alice_reconstructs_and_consumes_p2id() -> anyhow::Result<()> let fill_amount = 20u64; let mut note_args_map = BTreeMap::new(); - note_args_map.insert(pswap_note.id(), pswap_args(fill_amount, 0)); + note_args_map.insert(pswap_note.id(), PswapNote::create_args(fill_amount, 0)?); let (p2id_note, remainder_pswap) = pswap.execute(bob.id(), Some(FungibleAsset::new(eth_faucet.id(), 20)?), None)?; @@ -338,7 +328,7 @@ async fn pswap_attachment_layout_matches_masm_test() -> anyhow::Result<()> { let expected_payout = 40u64; // floor(50 * 20 / 25) let mut note_args_map = BTreeMap::new(); - note_args_map.insert(pswap_note.id(), pswap_args(fill_amount, 0)); + note_args_map.insert(pswap_note.id(), PswapNote::create_args(fill_amount, 0)?); let (p2id_note, remainder_pswap) = pswap.execute(bob.id(), Some(eth_20), None)?; let remainder_note = @@ -494,7 +484,7 @@ async fn pswap_fill_test( if !use_network_account { let mut note_args_map = BTreeMap::new(); - note_args_map.insert(pswap_note.id(), pswap_args(fill_amount, 0)); + note_args_map.insert(pswap_note.id(), PswapNote::create_args(fill_amount, 0)?); tx_builder = tx_builder.extend_note_args(note_args_map); } @@ -575,8 +565,8 @@ async fn pswap_note_note_fill_cross_swap_test() -> anyhow::Result<()> { // Note args: pure note fill (account_fill = 0, note_fill = full amount) let mut note_args_map = BTreeMap::new(); - note_args_map.insert(alice_pswap_note.id(), pswap_args(0, 25)); - note_args_map.insert(bob_pswap_note.id(), pswap_args(0, 50)); + note_args_map.insert(alice_pswap_note.id(), PswapNote::create_args(0, 25)?); + note_args_map.insert(bob_pswap_note.id(), PswapNote::create_args(0, 50)?); // Expected P2ID notes let (alice_p2id_note, _) = alice_pswap.execute(charlie.id(), None, Some(eth_25))?; @@ -677,8 +667,8 @@ async fn pswap_note_combined_account_fill_and_note_fill_test() -> anyhow::Result // Alice's pswap uses a combined fill; Bob's pswap uses pure note_fill. let mut note_args_map = BTreeMap::new(); - note_args_map.insert(alice_pswap_note.id(), pswap_args(20, 30)); - note_args_map.insert(bob_pswap_note.id(), pswap_args(0, 60)); + note_args_map.insert(alice_pswap_note.id(), PswapNote::create_args(20, 30)?); + note_args_map.insert(bob_pswap_note.id(), PswapNote::create_args(0, 60)?); let (alice_p2id_note, alice_remainder) = alice_pswap.execute(charlie.id(), Some(account_fill_eth), Some(note_fill_eth))?; @@ -790,7 +780,7 @@ async fn pswap_note_invalid_input_test() -> anyhow::Result<()> { // Try to fill with 30 ETH when only 25 is requested - should fail let mut note_args_map = BTreeMap::new(); - note_args_map.insert(pswap_note.id(), pswap_args(30, 0)); + note_args_map.insert(pswap_note.id(), PswapNote::create_args(30, 0)?); let tx_context = mock_chain .build_tx_context(bob.id(), &[pswap_note.id()], &[])? @@ -852,7 +842,7 @@ async fn pswap_note_idx_nonzero_regression_test() -> anyhow::Result<()> { // Full account-fill: 25 ETH out of bob's vault. Exercises the // `has_account_fill` branch where the `note_idx` bug lives. let mut note_args_map = BTreeMap::new(); - note_args_map.insert(pswap_note.id(), pswap_args(25, 0)); + note_args_map.insert(pswap_note.id(), PswapNote::create_args(25, 0)?); let (expected_p2id, _) = pswap.execute(bob.id(), Some(FungibleAsset::new(eth_faucet.id(), 25)?), None)?; @@ -939,7 +929,7 @@ async fn pswap_multiple_partial_fills_test(#[case] fill_amount: u64) -> anyhow:: let mock_chain = builder.build()?; let mut note_args_map = BTreeMap::new(); - note_args_map.insert(pswap_note.id(), pswap_args(fill_amount, 0)); + note_args_map.insert(pswap_note.id(), PswapNote::create_args(fill_amount, 0)?); let payout_amount = pswap.calculate_offered_for_requested(fill_amount); let (p2id_note, remainder_pswap) = @@ -1008,7 +998,7 @@ async fn run_partial_fill_ratio_case( let mock_chain = builder.build()?; let mut note_args_map = BTreeMap::new(); - note_args_map.insert(pswap_note.id(), pswap_args(fill_eth, 0)); + note_args_map.insert(pswap_note.id(), PswapNote::create_args(fill_eth, 0)?); let payout_amount = pswap.calculate_offered_for_requested(fill_eth); let remaining_offered = offered_usdc - payout_amount; @@ -1191,7 +1181,7 @@ async fn pswap_chained_partial_fills_test( let mock_chain = builder.build()?; let mut note_args_map = BTreeMap::new(); - note_args_map.insert(pswap_note.id(), pswap_args(*fill_amount, 0)); + note_args_map.insert(pswap_note.id(), PswapNote::create_args(*fill_amount, 0)?); let payout_amount = pswap.calculate_offered_for_requested(*fill_amount); let remaining_offered = current_offered - payout_amount; From c1940ded0d33176b6db69adeaef4871b3b7d30fd Mon Sep 17 00:00:00 2001 From: VaibhavJindal Date: Thu, 16 Apr 2026 01:19:28 +0530 Subject: [PATCH 51/66] Update crates/miden-standards/asm/standards/notes/pswap.masm Co-authored-by: Alexander John Lee <77119221+partylikeits1983@users.noreply.github.com> --- crates/miden-standards/asm/standards/notes/pswap.masm | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/miden-standards/asm/standards/notes/pswap.masm b/crates/miden-standards/asm/standards/notes/pswap.masm index 79574d3939..7b715921b0 100644 --- a/crates/miden-standards/asm/standards/notes/pswap.masm +++ b/crates/miden-standards/asm/standards/notes/pswap.masm @@ -532,6 +532,7 @@ proc execute_pswap # Consumer receives only account_fill_payout into vault (not the note_fill portion, if > 0) loc_load.EXEC_AMT_PAYOUT_ACCOUNT_FILL neq.0 # => [has_account_fill_payout] + if.true padw padw # => [pad(8)] From f69d73e805bfad0cac7e4340d29ee28088a1fe4a Mon Sep 17 00:00:00 2001 From: VaibhavJindal Date: Thu, 16 Apr 2026 01:19:48 +0530 Subject: [PATCH 52/66] Update crates/miden-standards/asm/standards/notes/pswap.masm Co-authored-by: Alexander John Lee <77119221+partylikeits1983@users.noreply.github.com> --- crates/miden-standards/asm/standards/notes/pswap.masm | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/miden-standards/asm/standards/notes/pswap.masm b/crates/miden-standards/asm/standards/notes/pswap.masm index 7b715921b0..980d2dc67d 100644 --- a/crates/miden-standards/asm/standards/notes/pswap.masm +++ b/crates/miden-standards/asm/standards/notes/pswap.masm @@ -234,6 +234,7 @@ proc create_p2id_note # Add note_fill_amount directly to P2ID note (no vault debit, if > 0) loc_load.P2ID_AMT_NOTE_FILL neq.0 # => [has_note_fill] + if.true loc_load.P2ID_NOTE_IDX # => [note_idx] From 2fd7ee604321cefe7658fcfd7245b6cd95da671f Mon Sep 17 00:00:00 2001 From: VaibhavJindal Date: Thu, 16 Apr 2026 01:20:20 +0530 Subject: [PATCH 53/66] Update crates/miden-standards/asm/standards/notes/pswap.masm Co-authored-by: Alexander John Lee <77119221+partylikeits1983@users.noreply.github.com> --- crates/miden-standards/asm/standards/notes/pswap.masm | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/miden-standards/asm/standards/notes/pswap.masm b/crates/miden-standards/asm/standards/notes/pswap.masm index 980d2dc67d..e63ce3e68d 100644 --- a/crates/miden-standards/asm/standards/notes/pswap.masm +++ b/crates/miden-standards/asm/standards/notes/pswap.masm @@ -296,7 +296,8 @@ proc create_remainder_note # Derive remainder serial: increment most significant element exec.active_note::get_serial_number # => [s0, s1, s2, s3, SCRIPT_ROOT, note_type, tag] - + # => [s0, s1, s2, s3, SCRIPT_ROOT, note_type, tag] + movup.3 add.1 movdn.3 # => [s0, s1, s2, s3+1, SCRIPT_ROOT, note_type, tag] From 2c8e305bfbcce62303ba156629631ce66319bf04 Mon Sep 17 00:00:00 2001 From: VaibhavJindal Date: Thu, 16 Apr 2026 01:21:50 +0530 Subject: [PATCH 54/66] Update crates/miden-standards/asm/standards/notes/pswap.masm Co-authored-by: Alexander John Lee <77119221+partylikeits1983@users.noreply.github.com> --- crates/miden-standards/asm/standards/notes/pswap.masm | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/miden-standards/asm/standards/notes/pswap.masm b/crates/miden-standards/asm/standards/notes/pswap.masm index e63ce3e68d..a02be25c82 100644 --- a/crates/miden-standards/asm/standards/notes/pswap.masm +++ b/crates/miden-standards/asm/standards/notes/pswap.masm @@ -486,7 +486,8 @@ proc execute_pswap loc_load.EXEC_AMT_REQUESTED loc_load.EXEC_AMT_OFFERED # => [offered, requested, account_fill_amount] - + # => [offered, requested, account_fill_amount] + exec.calculate_tokens_offered_for_requested # => [account_fill_payout] From 4e28adaeb01d1aef359d5ca5f2b883e3134d086b Mon Sep 17 00:00:00 2001 From: VaibhavJindal Date: Thu, 16 Apr 2026 01:22:10 +0530 Subject: [PATCH 55/66] Update crates/miden-standards/asm/standards/notes/pswap.masm Co-authored-by: Alexander John Lee <77119221+partylikeits1983@users.noreply.github.com> --- crates/miden-standards/asm/standards/notes/pswap.masm | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/miden-standards/asm/standards/notes/pswap.masm b/crates/miden-standards/asm/standards/notes/pswap.masm index a02be25c82..2b6776f7f7 100644 --- a/crates/miden-standards/asm/standards/notes/pswap.masm +++ b/crates/miden-standards/asm/standards/notes/pswap.masm @@ -209,6 +209,7 @@ proc create_p2id_note # Move account_fill_amount from consumer's vault to P2ID note (if > 0) loc_load.P2ID_AMT_ACCOUNT_FILL neq.0 # => [has_account_fill] + if.true # Build 16-element call frame: [ASSET_KEY, ASSET_VALUE, note_idx, pad(7)] padw push.0.0.0 loc_load.P2ID_NOTE_IDX From 28615c336252252a197302c2bc6cb9ab392b79ac Mon Sep 17 00:00:00 2001 From: VaibhavJindal Date: Thu, 16 Apr 2026 01:23:22 +0530 Subject: [PATCH 56/66] Update crates/miden-standards/asm/standards/notes/pswap.masm Co-authored-by: Alexander John Lee <77119221+partylikeits1983@users.noreply.github.com> --- crates/miden-standards/asm/standards/notes/pswap.masm | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/miden-standards/asm/standards/notes/pswap.masm b/crates/miden-standards/asm/standards/notes/pswap.masm index 2b6776f7f7..6778540055 100644 --- a/crates/miden-standards/asm/standards/notes/pswap.masm +++ b/crates/miden-standards/asm/standards/notes/pswap.masm @@ -478,7 +478,8 @@ proc execute_pswap loc_load.EXEC_AMT_REQUESTED # => [requested, fill_amount] - + # => [requested, fill_amount] + lte assert.err=ERR_PSWAP_FILL_EXCEEDS_REQUESTED # => [] From 32b3b683434b8f86ab760f9297793e04a2840880 Mon Sep 17 00:00:00 2001 From: VaibhavJindal Date: Thu, 16 Apr 2026 01:23:38 +0530 Subject: [PATCH 57/66] Update crates/miden-standards/asm/standards/notes/pswap.masm Co-authored-by: Alexander John Lee <77119221+partylikeits1983@users.noreply.github.com> --- crates/miden-standards/asm/standards/notes/pswap.masm | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/miden-standards/asm/standards/notes/pswap.masm b/crates/miden-standards/asm/standards/notes/pswap.masm index 6778540055..bf95a383d8 100644 --- a/crates/miden-standards/asm/standards/notes/pswap.masm +++ b/crates/miden-standards/asm/standards/notes/pswap.masm @@ -502,9 +502,10 @@ proc execute_pswap loc_load.EXEC_AMT_OFFERED # => [offered, requested, note_fill_amount] + exec.calculate_tokens_offered_for_requested exec.calculate_tokens_offered_for_requested # => [note_fill_payout] - + loc_store.EXEC_AMT_PAYOUT_NOTE_FILL # => [] From 0ca6aa656ee46c138f1d959a3f26fe1d3b559dd8 Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Thu, 16 Apr 2026 01:54:04 +0530 Subject: [PATCH 58/66] =?UTF-8?q?fix(pswap):=20CI=20green=20=E2=80=94=20un?= =?UTF-8?q?used=20imports=20+=20duplicated=20exec?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two fixes triggered by `make check-features` / `clippy -D warnings` and the full pswap test suite: - `tests/scripts/pswap.rs`: drop `NoteAssets`, `NoteMetadata`, `NoteRecipient`, `NoteStorage` from the `miden_protocol::note` import list. Leftovers from the theme-5 chained-fills refactor that switched from low-level `Note::new(NoteAssets, NoteMetadata, NoteRecipient)` construction to `PswapNote::builder()`. CI compiles with `-D warnings` so the unused-imports warning was fatal. - `asm/standards/notes/pswap.masm`: remove a duplicate `exec.calculate_tokens_offered_for_requested` line that had crept into the note-fill payout path in one of the upstream edits, plus a duplicate stack comment above the account-fill payout block. With the duplicate `exec`, the second call read garbage off a stack that only held the first call's payout — specifically a zero divisor, which surfaced as `u128::div` "division by zero" and failed 57/61 pswap tests. All 61 pswap script tests pass after the fix, and `cargo check --all-targets --all-features` on miden-testing is clean. --- crates/miden-standards/asm/standards/notes/pswap.masm | 6 ++---- crates/miden-testing/tests/scripts/pswap.rs | 11 +---------- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/crates/miden-standards/asm/standards/notes/pswap.masm b/crates/miden-standards/asm/standards/notes/pswap.masm index bf95a383d8..68134b20e0 100644 --- a/crates/miden-standards/asm/standards/notes/pswap.masm +++ b/crates/miden-standards/asm/standards/notes/pswap.masm @@ -488,8 +488,7 @@ proc execute_pswap loc_load.EXEC_AMT_REQUESTED loc_load.EXEC_AMT_OFFERED # => [offered, requested, account_fill_amount] - # => [offered, requested, account_fill_amount] - + exec.calculate_tokens_offered_for_requested # => [account_fill_payout] @@ -502,10 +501,9 @@ proc execute_pswap loc_load.EXEC_AMT_OFFERED # => [offered, requested, note_fill_amount] - exec.calculate_tokens_offered_for_requested exec.calculate_tokens_offered_for_requested # => [note_fill_payout] - + loc_store.EXEC_AMT_PAYOUT_NOTE_FILL # => [] diff --git a/crates/miden-testing/tests/scripts/pswap.rs b/crates/miden-testing/tests/scripts/pswap.rs index ffa9dc45dc..b2d3f9fa15 100644 --- a/crates/miden-testing/tests/scripts/pswap.rs +++ b/crates/miden-testing/tests/scripts/pswap.rs @@ -4,16 +4,7 @@ use miden_protocol::account::auth::AuthScheme; use miden_protocol::account::{Account, AccountId, AccountStorageMode, AccountVaultDelta}; use miden_protocol::asset::{Asset, FungibleAsset}; use miden_protocol::crypto::rand::{FeltRng, RandomCoin}; -use miden_protocol::note::{ - Note, - NoteAssets, - NoteAttachment, - NoteAttachmentScheme, - NoteMetadata, - NoteRecipient, - NoteStorage, - NoteType, -}; +use miden_protocol::note::{Note, NoteAttachment, NoteAttachmentScheme, NoteType}; use miden_protocol::transaction::RawOutputNote; use miden_protocol::{Felt, ONE, Word, ZERO}; use miden_standards::account::wallets::BasicWallet; From 5bfcf108fe7350967cc81ab612bfaff0d1d23489 Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Thu, 16 Apr 2026 02:04:30 +0530 Subject: [PATCH 59/66] refactoring changes --- .../miden-standards/asm/standards/notes/pswap.masm | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/crates/miden-standards/asm/standards/notes/pswap.masm b/crates/miden-standards/asm/standards/notes/pswap.masm index 68134b20e0..d5fff86cac 100644 --- a/crates/miden-standards/asm/standards/notes/pswap.masm +++ b/crates/miden-standards/asm/standards/notes/pswap.masm @@ -104,7 +104,7 @@ const ERR_PSWAP_PAYOUT_OVERFLOW="PSWAP payout quotient does not fit in u64" #! The intermediate product `offered * fill_amount` is computed as a full #! u128 via `u64::widening_mul`, then divided by `requested` (extended to #! u128) via `u128::div`. This gives exact integer precision with one floor -#! division at the end and no FACTOR/scaling tradeoffs: each input is safe +#! division at the end, each input is safe #! up to `FungibleAsset::MAX ≈ 2^63`, and the resulting quotient #! `payout ≤ offered` always fits back in u64 (asserted via the upper-half #! limb check below). @@ -514,9 +514,8 @@ proc execute_pswap loc_store.EXEC_AMT_PAYOUT_TOTAL # => [] - # Create P2ID note for creator. Content is keyed off the fill amounts - # (`account_fill + note_fill` of the requested asset), not `total_payout`, - # and fills are guaranteed > 0 by the `both_zero` fallback in `main`. + # Create P2ID note for creator. + # fills are guaranteed > 0 by the `both_zero` fallback in `main`. loc_load.EXEC_AMT_REQUESTED_NOTE_FILL loc_load.EXEC_AMT_REQUESTED_ACCOUNT_FILL mem_load.REQUESTED_FAUCET_PREFIX_ITEM @@ -571,19 +570,12 @@ proc execute_pswap swap sub # => [remaining_requested] - # Fetch active note metadata: the remainder note inherits both the - # note type and the pswap tag from it. The asset pair is unchanged, - # so the tag derived in Rust at creation time still applies, and we - # avoid spending a storage slot on `pswap_tag`. exec.active_note::get_metadata # => [NOTE_ATTACHMENT, METADATA_HEADER, remaining_requested] # where METADATA_HEADER = [sid_suf_ver, sid_pre, tag, att_ks] dropw # => [sid_suf_ver, sid_pre, tag, att_ks, remaining_requested] - # Dup the tag out of METADATA_HEADER (at depth 2) and park it just - # below the header so `metadata_into_note_type` can consume the - # header intact. dup.2 movdn.4 # => [sid_suf_ver, sid_pre, tag, att_ks, tag, remaining_requested] From 83daa775840bc31186feb8a7ff124ec7e5b8cb00 Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Thu, 16 Apr 2026 18:04:59 +0530 Subject: [PATCH 60/66] =?UTF-8?q?fix(pswap):=20address=20PR=20review=20?= =?UTF-8?q?=E2=80=94=20storage,=20attachments,=20asset=20validation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Derive payback note tag from creator ID in MASM via note_tag::create_account_target instead of storing in note storage, reducing storage from 8 to 7 items. - Add dedicated attachment constructors (payback_attachment, remainder_attachment) on PswapNote for clarity and reuse. - Validate computed amounts as valid fungible asset amounts: - calculate_output_amount now returns Result and checks via AssetAmount. - MASM assert_valid_asset_amount proc checks u64 values against FUNGIBLE_ASSET_MAX_AMOUNT before felt conversion to prevent silent wrapping, applied to both fill sum and payout quotient. - Fix P2ID comment nit per reviewer suggestion. - Remove unused NoteTag import from p2id test. --- .../asm/standards/notes/pswap.masm | 94 ++++++++++--- crates/miden-standards/src/note/pswap.rs | 130 +++++++++++------- crates/miden-testing/tests/scripts/p2id.rs | 6 +- crates/miden-testing/tests/scripts/pswap.rs | 10 +- 4 files changed, 161 insertions(+), 79 deletions(-) diff --git a/crates/miden-standards/asm/standards/notes/pswap.masm b/crates/miden-standards/asm/standards/notes/pswap.masm index d5fff86cac..819c6cb9f7 100644 --- a/crates/miden-standards/asm/standards/notes/pswap.masm +++ b/crates/miden-standards/asm/standards/notes/pswap.masm @@ -6,24 +6,25 @@ use miden::protocol::active_note use miden::protocol::asset use miden::protocol::note use miden::protocol::output_note +use miden::protocol::asset::FUNGIBLE_ASSET_MAX_AMOUNT +use miden::standards::note_tag use miden::standards::notes::p2id use miden::standards::wallets::basic->wallet # CONSTANTS # ================================================================================================= -const NUM_STORAGE_ITEMS=8 +const NUM_STORAGE_ITEMS=7 const MAX_U32=0x0000000100000000 -# Note storage layout (8 felts, loaded at STORAGE_PTR by get_storage): +# Note storage layout (7 felts, loaded at STORAGE_PTR by get_storage): # - requested_enable_callbacks [0] : 1 felt # - requested_faucet_suffix [1] : 1 felt # - requested_faucet_prefix [2] : 1 felt # - requested_amount [3] : 1 felt -# - p2id_tag [4] : 1 felt -# - payback_note_type [5] : 1 felt -# - creator_id_prefix [6] : 1 felt -# - creator_id_suffix [7] : 1 felt +# - payback_note_type [4] : 1 felt +# - creator_id_prefix [5] : 1 felt +# - creator_id_suffix [6] : 1 felt # # Where: # - requested_enable_callbacks: Callback-enabled flag for the requested fungible asset @@ -32,11 +33,13 @@ const MAX_U32=0x0000000100000000 # - requested_faucet_prefix: Prefix of the requested asset's faucet AccountId # (AccountIdPrefix as Felt) # - requested_amount: Amount of the requested fungible asset (Felt) -# - p2id_tag: The NoteTag for P2ID payback notes, derived from the creator's account ID # - payback_note_type: The NoteType used for the P2ID payback note # - creator_id_prefix: The prefix of the creator's AccountId (AccountIdPrefix as Felt) # - creator_id_suffix: The suffix of the creator's AccountId (Felt) # +# The payback note tag is derived at runtime from the creator's account ID +# (via note_tag::create_account_target) rather than stored. +# # The remainder PSWAP note's own tag is not stored — it is lifted from the # active note's metadata at remainder-creation time (same asset pair => same # tag), saving one storage slot. @@ -45,10 +48,9 @@ const REQUESTED_ENABLE_CALLBACKS_ITEM = STORAGE_PTR const REQUESTED_FAUCET_SUFFIX_ITEM = STORAGE_PTR + 1 const REQUESTED_FAUCET_PREFIX_ITEM = STORAGE_PTR + 2 const REQUESTED_AMOUNT_ITEM = STORAGE_PTR + 3 -const P2ID_TAG_ITEM = STORAGE_PTR + 4 -const PAYBACK_NOTE_TYPE_ITEM = STORAGE_PTR + 5 -const PSWAP_CREATOR_PREFIX_ITEM = STORAGE_PTR + 6 -const PSWAP_CREATOR_SUFFIX_ITEM = STORAGE_PTR + 7 +const PAYBACK_NOTE_TYPE_ITEM = STORAGE_PTR + 4 +const PSWAP_CREATOR_PREFIX_ITEM = STORAGE_PTR + 5 +const PSWAP_CREATOR_SUFFIX_ITEM = STORAGE_PTR + 6 # Local memory offsets # ------------------------------------------------------------------------------------------------- @@ -88,12 +90,51 @@ const EXEC_AMT_REQUESTED_NOTE_FILL = 9 # ERRORS # ================================================================================================= -const ERR_PSWAP_WRONG_NUMBER_OF_STORAGE_ITEMS="PSWAP script expects exactly 8 note storage items" +const ERR_PSWAP_WRONG_NUMBER_OF_STORAGE_ITEMS="PSWAP script expects exactly 7 note storage items" const ERR_PSWAP_WRONG_NUMBER_OF_ASSETS="PSWAP script requires exactly one note asset" const ERR_PSWAP_FILL_EXCEEDS_REQUESTED="PSWAP fill amount exceeds requested amount" const ERR_PSWAP_FILL_SUM_OVERFLOW="PSWAP account_fill + note_fill overflows u64" +const ERR_PSWAP_NOT_VALID_ASSET_AMOUNT="PSWAP computed amount exceeds max fungible asset amount" const ERR_PSWAP_PAYOUT_OVERFLOW="PSWAP payout quotient does not fit in u64" +# U64 VALIDATION +# ================================================================================================= + +#! Asserts that the u64 value represented by [lo, hi] on the stack is a valid fungible asset +#! amount, i.e. <= FUNGIBLE_ASSET_MAX_AMOUNT (2^63 - 2^31). +#! +#! Inputs: [lo, hi] +#! Outputs: [lo, hi] +#! +#! Panics if the value exceeds FUNGIBLE_ASSET_MAX_AMOUNT. +proc assert_valid_asset_amount + dup.1 dup.1 + # => [lo, hi, lo, hi] + + push.FUNGIBLE_ASSET_MAX_AMOUNT u32split + # => [max_hi, max_lo, lo, hi, lo, hi] + + movup.3 + # => [hi, max_hi, max_lo, lo, lo, hi] + + # If hi > max_hi => fail. + dup dup.2 u32gt assertz.err=ERR_PSWAP_NOT_VALID_ASSET_AMOUNT + # => [hi, max_hi, max_lo, lo, lo, hi] + + # Check if hi == max_hi; if so, also need lo <= max_lo. + eq + # => [is_equal, max_lo, lo, lo, hi] + + if.true + u32lte assert.err=ERR_PSWAP_NOT_VALID_ASSET_AMOUNT + # => [lo, hi] + else + drop drop + # => [lo, hi] + end + # => [lo, hi] +end + # PRICE CALCULATION # ================================================================================================= @@ -147,6 +188,9 @@ proc calculate_tokens_offered_for_requested movup.2 eq.0 assert.err=ERR_PSWAP_PAYOUT_OVERFLOW # => [q0, q1] + exec.assert_valid_asset_amount + # => [q0, q1] + # Reconstruct the u64 quotient as a single felt: `q0 + q1 * 2^32`. swap mul.MAX_U32 add # => [payout_amount] @@ -162,7 +206,7 @@ end #! attachment, and adds the requested assets (from the consumer's account vault #! and/or from another note in the same transaction). #! -#! Inputs: [creator_suffix, creator_prefix, tag, note_type, SERIAL_NUM, +#! Inputs: [creator_suffix, creator_prefix, note_type, SERIAL_NUM, #! enable_callbacks, faucet_suffix, faucet_prefix, amt_account_fill, amt_note_fill] #! Outputs: [] #! @@ -175,10 +219,18 @@ end # P2ID_AMT_NOTE_FILL : amt_note_fill proc create_p2id_note # Derive P2ID serial: increment least significant element - movup.4 add.1 movdn.4 - # => [creator_suffix, creator_prefix, tag, note_type, P2ID_SERIAL_NUM, + movup.3 add.1 movdn.3 + # => [creator_suffix, creator_prefix, note_type, P2ID_SERIAL_NUM, # enable_callbacks, faucet_suffix, faucet_prefix, amt_account_fill, amt_note_fill] + # Derive the payback tag from the creator's account ID prefix + swap dup + # => [creator_prefix, creator_prefix, creator_suffix, note_type, P2ID_SERIAL_NUM, ...] + exec.note_tag::create_account_target + # => [tag, creator_prefix, creator_suffix, note_type, P2ID_SERIAL_NUM, ...] + swap movup.2 + # => [creator_suffix, creator_prefix, tag, note_type, P2ID_SERIAL_NUM, ...] + exec.p2id::new # => [note_idx, enable_callbacks, faucet_suffix, faucet_prefix, amt_account_fill, amt_note_fill] @@ -473,6 +525,9 @@ proc execute_pswap eq.0 assert.err=ERR_PSWAP_FILL_SUM_OVERFLOW # => [sum_lo, sum_hi] + exec.assert_valid_asset_amount + # => [sum_lo, sum_hi] + swap mul.MAX_U32 add # => [fill_amount] @@ -514,8 +569,10 @@ proc execute_pswap loc_store.EXEC_AMT_PAYOUT_TOTAL # => [] - # Create P2ID note for creator. - # fills are guaranteed > 0 by the `both_zero` fallback in `main`. + # Create P2ID note carrying the requested asset to the creator. The note's + # assets are derived from the requested fill amounts (`account_fill + + # note_fill`), not from `total_payout` (which is the offered asset owed to + # the consumer). Fills are guaranteed > 0 by the `both_zero` guard in `main`. loc_load.EXEC_AMT_REQUESTED_NOTE_FILL loc_load.EXEC_AMT_REQUESTED_ACCOUNT_FILL mem_load.REQUESTED_FAUCET_PREFIX_ITEM @@ -523,10 +580,9 @@ proc execute_pswap mem_load.REQUESTED_ENABLE_CALLBACKS_ITEM exec.active_note::get_serial_number mem_load.PAYBACK_NOTE_TYPE_ITEM - mem_load.P2ID_TAG_ITEM mem_load.PSWAP_CREATOR_PREFIX_ITEM mem_load.PSWAP_CREATOR_SUFFIX_ITEM - # => [creator_suffix, creator_prefix, tag, note_type, SERIAL_NUM, + # => [creator_suffix, creator_prefix, note_type, SERIAL_NUM, # enable_callbacks, faucet_suffix, faucet_prefix, amt_account_fill, amt_note_fill] exec.create_p2id_note diff --git a/crates/miden-standards/src/note/pswap.rs b/crates/miden-standards/src/note/pswap.rs index dd5135bf80..c3ff92eb79 100644 --- a/crates/miden-standards/src/note/pswap.rs +++ b/crates/miden-standards/src/note/pswap.rs @@ -2,19 +2,11 @@ use alloc::vec; use miden_protocol::account::AccountId; use miden_protocol::assembly::Path; -use miden_protocol::asset::{Asset, AssetCallbackFlag, FungibleAsset}; +use miden_protocol::asset::{Asset, AssetAmount, AssetCallbackFlag, FungibleAsset}; use miden_protocol::errors::NoteError; use miden_protocol::note::{ - Note, - NoteAssets, - NoteAttachment, - NoteAttachmentScheme, - NoteMetadata, - NoteRecipient, - NoteScript, - NoteStorage, - NoteTag, - NoteType, + Note, NoteAssets, NoteAttachment, NoteAttachmentScheme, NoteMetadata, NoteRecipient, + NoteScript, NoteStorage, NoteTag, NoteType, }; use miden_protocol::utils::sync::LazyLock; use miden_protocol::{Felt, ONE, Word, ZERO}; @@ -41,7 +33,7 @@ static PSWAP_SCRIPT: LazyLock = LazyLock::new(|| { /// Canonical storage representation for a PSWAP note. /// -/// Maps to the 8-element [`NoteStorage`] layout consumed by the on-chain MASM script: +/// Maps to the 7-element [`NoteStorage`] layout consumed by the on-chain MASM script: /// /// | Slot | Field | /// |---------|-------| @@ -49,9 +41,11 @@ static PSWAP_SCRIPT: LazyLock = LazyLock::new(|| { /// | `[1]` | Requested asset faucet ID suffix | /// | `[2]` | Requested asset faucet ID prefix | /// | `[3]` | Requested asset amount | -/// | `[4]` | Payback note routing tag (targets the creator) | -/// | `[5]` | Payback note type (0 = private, 1 = public) | -/// | `[6-7]` | Creator account ID (prefix, suffix) | +/// | `[4]` | Payback note type (0 = private, 1 = public) | +/// | `[5-6]` | Creator account ID (prefix, suffix) | +/// +/// The payback note tag is derived at runtime from the creator account ID +/// (via `note_tag::create_account_target` in MASM) rather than stored. /// /// The PSWAP note's own tag is not stored: it lives in the note's metadata and /// is lifted from there by the on-chain script when a remainder note is created @@ -76,7 +70,7 @@ impl PswapNoteStorage { // -------------------------------------------------------------------------------------------- /// Expected number of storage items for the PSWAP note. - pub const NUM_STORAGE_ITEMS: usize = 8; + pub const NUM_STORAGE_ITEMS: usize = 7; /// Consumes the storage and returns a PSWAP [`NoteRecipient`] with the provided serial number. pub fn into_recipient(self, serial_num: Word) -> NoteRecipient { @@ -117,7 +111,7 @@ impl PswapNoteStorage { } } -/// Serializes [`PswapNoteStorage`] into an 8-element [`NoteStorage`]. +/// Serializes [`PswapNoteStorage`] into a 7-element [`NoteStorage`]. impl From for NoteStorage { fn from(storage: PswapNoteStorage) -> Self { let storage_items = vec![ @@ -127,11 +121,9 @@ impl From for NoteStorage { storage.requested_asset.faucet_id().prefix().as_felt(), Felt::try_from(storage.requested_asset.amount()) .expect("asset amount should fit in a felt"), - // Payback tag [4] - Felt::from(storage.payback_note_tag()), - // Payback note type [5] + // Payback note type [4] Felt::from(storage.payback_note_type.as_u8()), - // Creator ID [6-7] + // Creator ID [5-6] storage.creator_account_id.prefix().as_felt(), storage.creator_account_id.suffix(), ]; @@ -140,7 +132,7 @@ impl From for NoteStorage { } } -/// Deserializes [`PswapNoteStorage`] from a slice of exactly 8 [`Felt`]s. +/// Deserializes [`PswapNoteStorage`] from a slice of exactly 7 [`Felt`]s. impl TryFrom<&[Felt]> for PswapNoteStorage { type Error = NoteError; @@ -168,15 +160,15 @@ impl TryFrom<&[Felt]> for PswapNoteStorage { .map_err(|e| NoteError::other_with_source("failed to create requested asset", e))? .with_callbacks(callbacks); - // [4] is the payback_note_tag, which is derived from the creator ID at - // serialization time and not re-parsed here. + // [4] = payback_note_type let payback_note_type = NoteType::try_from( - u8::try_from(note_storage[5].as_canonical_u64()) + u8::try_from(note_storage[4].as_canonical_u64()) .map_err(|_| NoteError::other("payback_note_type exceeds u8"))?, ) .map_err(|e| NoteError::other_with_source("failed to parse payback note type", e))?; - let creator_account_id = AccountId::try_from_elements(note_storage[7], note_storage[6]) + // [5-6] = creator account ID (prefix, suffix) + let creator_account_id = AccountId::try_from_elements(note_storage[6], note_storage[5]) .map_err(|e| NoteError::other_with_source("failed to parse creator account ID", e))?; Ok(Self { @@ -408,12 +400,12 @@ impl PswapNote { total_offered_amount, total_requested_amount, account_fill_amount, - ); + )?; let payout_for_note_fill = Self::calculate_output_amount( total_offered_amount, total_requested_amount, note_fill_amount, - ); + )?; let offered_amount_for_fill = payout_for_account_fill + payout_for_note_fill; let payback_note = @@ -449,7 +441,11 @@ impl PswapNote { /// Returns how many offered tokens a consumer receives for `fill_amount` of the /// requested asset, based on this note's current offered/requested ratio. - pub fn calculate_offered_for_requested(&self, fill_amount: u64) -> u64 { + /// + /// # Errors + /// + /// Returns an error if the calculated payout is not a valid asset amount. + pub fn calculate_offered_for_requested(&self, fill_amount: u64) -> Result { let total_requested = self.storage.requested_asset_amount(); let total_offered = self.offered_asset.amount(); @@ -498,10 +494,56 @@ impl PswapNote { /// Computes `floor((offered_total * fill_amount) / requested_total)` via a /// u128 intermediate, mirroring `u64::widening_mul` + `u128::div` on the /// MASM side. - fn calculate_output_amount(offered_total: u64, requested_total: u64, fill_amount: u64) -> u64 { + /// + /// # Errors + /// + /// Returns an error if the result does not fit in a valid [`AssetAmount`]. + fn calculate_output_amount( + offered_total: u64, + requested_total: u64, + fill_amount: u64, + ) -> Result { let product = (offered_total as u128) * (fill_amount as u128); let quotient = product / (requested_total as u128); - u64::try_from(quotient).expect("payout quotient does not fit in u64") + let amount = u64::try_from(quotient) + .map_err(|_| NoteError::other("payout quotient does not fit in u64"))?; + // Validate the result is a valid fungible asset amount. + AssetAmount::new(amount).map_err(|e| { + NoteError::other_with_source("payout amount exceeds max fungible asset amount", e) + })?; + Ok(amount) + } + + /// Creates a [`NoteAttachment`] for a payback P2ID note. + /// + /// The attachment carries the fill amount as auxiliary data with + /// `NoteAttachmentScheme::none()`, matching the on-chain MASM behavior. + fn payback_attachment(fill_amount: u64) -> Result { + let word = Word::from([ + Felt::try_from(fill_amount).map_err(|e| { + NoteError::other_with_source("fill amount does not fit in a felt", e) + })?, + ZERO, + ZERO, + ZERO, + ]); + Ok(NoteAttachment::new_word(NoteAttachmentScheme::none(), word)) + } + + /// Creates a [`NoteAttachment`] for a remainder PSWAP note. + /// + /// The attachment carries the total offered amount for the fill as auxiliary data + /// with `NoteAttachmentScheme::none()`, matching the on-chain MASM behavior. + fn remainder_attachment(offered_amount_for_fill: u64) -> Result { + let word = Word::from([ + Felt::try_from(offered_amount_for_fill).map_err(|e| { + NoteError::other_with_source("offered amount for fill does not fit in a felt", e) + })?, + ZERO, + ZERO, + ZERO, + ]); + Ok(NoteAttachment::new_word(NoteAttachmentScheme::none(), word)) } /// Builds a payback note (P2ID) that delivers the filled assets to the swap creator. @@ -531,13 +573,7 @@ impl PswapNote { let recipient = P2idNoteStorage::new(self.storage.creator_account_id).into_recipient(p2id_serial_num); - let attachment_word = Word::from([ - Felt::try_from(fill_amount).expect("fill amount should fit in a felt"), - ZERO, - ZERO, - ZERO, - ]); - let attachment = NoteAttachment::new_word(NoteAttachmentScheme::none(), attachment_word); + let attachment = Self::payback_attachment(fill_amount)?; let p2id_assets = NoteAssets::new(vec![Asset::Fungible(payback_asset)])?; let p2id_metadata = NoteMetadata::new(consumer_account_id, self.storage.payback_note_type) @@ -576,14 +612,7 @@ impl PswapNote { self.serial_number[3] + ONE, ]); - let attachment_word = Word::from([ - Felt::try_from(offered_amount_for_fill) - .expect("offered amount for fill should fit in a felt"), - ZERO, - ZERO, - ZERO, - ]); - let attachment = NoteAttachment::new_word(NoteAttachmentScheme::none(), attachment_word); + let attachment = Self::remainder_attachment(offered_amount_for_fill)?; Ok(PswapNote { sender: consumer_account_id, @@ -804,12 +833,12 @@ mod tests { #[test] fn calculate_output_amount() { - assert_eq!(PswapNote::calculate_output_amount(100, 100, 50), 50); // Equal ratio - assert_eq!(PswapNote::calculate_output_amount(200, 100, 50), 100); // 2:1 ratio - assert_eq!(PswapNote::calculate_output_amount(100, 200, 50), 25); // 1:2 ratio + assert_eq!(PswapNote::calculate_output_amount(100, 100, 50).unwrap(), 50); // Equal ratio + assert_eq!(PswapNote::calculate_output_amount(200, 100, 50).unwrap(), 100); // 2:1 ratio + assert_eq!(PswapNote::calculate_output_amount(100, 200, 50).unwrap(), 25); // 1:2 ratio // Non-integer ratio (100/73) - let result = PswapNote::calculate_output_amount(100, 73, 7); + let result = PswapNote::calculate_output_amount(100, 73, 7).unwrap(); assert!(result > 0, "Should produce non-zero output"); } @@ -823,7 +852,6 @@ mod tests { requested_asset.faucet_id().suffix(), requested_asset.faucet_id().prefix().as_felt(), Felt::try_from(requested_asset.amount()).unwrap(), - Felt::from(0x80000001u32), // payback_note_tag Felt::from(NoteType::Private.as_u8()), // payback_note_type creator_id.prefix().as_felt(), creator_id.suffix(), diff --git a/crates/miden-testing/tests/scripts/p2id.rs b/crates/miden-testing/tests/scripts/p2id.rs index a7989a81ef..80b923bf63 100644 --- a/crates/miden-testing/tests/scripts/p2id.rs +++ b/crates/miden-testing/tests/scripts/p2id.rs @@ -4,11 +4,9 @@ use miden_protocol::asset::{Asset, AssetVault, FungibleAsset}; use miden_protocol::crypto::rand::RandomCoin; use miden_protocol::note::{NoteAttachment, NoteTag, NoteType}; use miden_protocol::testing::account_id::{ - ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET, - ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2, + ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET, ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2, ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE, - ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE_2, - ACCOUNT_ID_SENDER, + ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE_2, ACCOUNT_ID_SENDER, }; use miden_protocol::transaction::RawOutputNote; use miden_protocol::{Felt, Word}; diff --git a/crates/miden-testing/tests/scripts/pswap.rs b/crates/miden-testing/tests/scripts/pswap.rs index b2d3f9fa15..39edcf54b8 100644 --- a/crates/miden-testing/tests/scripts/pswap.rs +++ b/crates/miden-testing/tests/scripts/pswap.rs @@ -206,7 +206,7 @@ async fn pswap_note_alice_reconstructs_and_consumes_p2id() -> anyhow::Result<()> let remainder_aux = output_remainder.metadata().attachment().content().to_word(); let amt_payout_from_aux = remainder_aux[0].as_canonical_u64(); - let expected_payout = pswap.calculate_offered_for_requested(fill_amount_from_aux); + let expected_payout = pswap.calculate_offered_for_requested(fill_amount_from_aux)?; assert_eq!( amt_payout_from_aux, expected_payout, "remainder aux should carry amt_payout matching the Rust-side calc", @@ -462,7 +462,7 @@ async fn pswap_fill_test( }; let is_partial = fill_amount < requested_total; - let payout_amount = pswap.calculate_offered_for_requested(fill_amount); + let payout_amount = pswap.calculate_offered_for_requested(fill_amount)?; let mut expected_notes = vec![RawOutputNote::Full(p2id_note.clone())]; if let Some(remainder) = remainder_pswap { @@ -922,7 +922,7 @@ async fn pswap_multiple_partial_fills_test(#[case] fill_amount: u64) -> anyhow:: let mut note_args_map = BTreeMap::new(); note_args_map.insert(pswap_note.id(), PswapNote::create_args(fill_amount, 0)?); - let payout_amount = pswap.calculate_offered_for_requested(fill_amount); + let payout_amount = pswap.calculate_offered_for_requested(fill_amount)?; let (p2id_note, remainder_pswap) = pswap.execute(bob.id(), Some(FungibleAsset::new(eth_faucet.id(), fill_amount)?), None)?; @@ -991,7 +991,7 @@ async fn run_partial_fill_ratio_case( let mut note_args_map = BTreeMap::new(); note_args_map.insert(pswap_note.id(), PswapNote::create_args(fill_eth, 0)?); - let payout_amount = pswap.calculate_offered_for_requested(fill_eth); + let payout_amount = pswap.calculate_offered_for_requested(fill_eth)?; let remaining_offered = offered_usdc - payout_amount; assert!(payout_amount > 0, "payout_amount must be > 0"); @@ -1174,7 +1174,7 @@ async fn pswap_chained_partial_fills_test( let mut note_args_map = BTreeMap::new(); note_args_map.insert(pswap_note.id(), PswapNote::create_args(*fill_amount, 0)?); - let payout_amount = pswap.calculate_offered_for_requested(*fill_amount); + let payout_amount = pswap.calculate_offered_for_requested(*fill_amount)?; let remaining_offered = current_offered - payout_amount; let (p2id_note, remainder_pswap) = pswap.execute( bob.id(), From 550d5035ec144e5e857c7da47c4e9231aaf104bb Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Thu, 16 Apr 2026 18:45:13 +0530 Subject: [PATCH 61/66] style: apply nightly rustfmt --- crates/miden-standards/src/note/pswap.rs | 12 ++++++++++-- crates/miden-testing/tests/scripts/p2id.rs | 6 ++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/crates/miden-standards/src/note/pswap.rs b/crates/miden-standards/src/note/pswap.rs index c3ff92eb79..d235b0feb5 100644 --- a/crates/miden-standards/src/note/pswap.rs +++ b/crates/miden-standards/src/note/pswap.rs @@ -5,8 +5,16 @@ use miden_protocol::assembly::Path; use miden_protocol::asset::{Asset, AssetAmount, AssetCallbackFlag, FungibleAsset}; use miden_protocol::errors::NoteError; use miden_protocol::note::{ - Note, NoteAssets, NoteAttachment, NoteAttachmentScheme, NoteMetadata, NoteRecipient, - NoteScript, NoteStorage, NoteTag, NoteType, + Note, + NoteAssets, + NoteAttachment, + NoteAttachmentScheme, + NoteMetadata, + NoteRecipient, + NoteScript, + NoteStorage, + NoteTag, + NoteType, }; use miden_protocol::utils::sync::LazyLock; use miden_protocol::{Felt, ONE, Word, ZERO}; diff --git a/crates/miden-testing/tests/scripts/p2id.rs b/crates/miden-testing/tests/scripts/p2id.rs index 80b923bf63..a7989a81ef 100644 --- a/crates/miden-testing/tests/scripts/p2id.rs +++ b/crates/miden-testing/tests/scripts/p2id.rs @@ -4,9 +4,11 @@ use miden_protocol::asset::{Asset, AssetVault, FungibleAsset}; use miden_protocol::crypto::rand::RandomCoin; use miden_protocol::note::{NoteAttachment, NoteTag, NoteType}; use miden_protocol::testing::account_id::{ - ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET, ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2, + ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET, + ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2, ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE, - ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE_2, ACCOUNT_ID_SENDER, + ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE_2, + ACCOUNT_ID_SENDER, }; use miden_protocol::transaction::RawOutputNote; use miden_protocol::{Felt, Word}; From 277d60f48fd0fe34b12a6c736ba711da0a2f71e1 Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Fri, 17 Apr 2026 16:39:54 +0530 Subject: [PATCH 62/66] fix(pswap): simplify assert_valid_asset_amount and rename procedure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace manual hi/lo branch logic with exec.u64::lte - Rename calculate_tokens_offered_for_requested to calculate_output_amount (matches Rust) - Simplify tag derivation in create_p2id_note: swap+dup+swap+movup.2 → dup.1+movdn.2 --- .../asm/standards/notes/pswap.masm | 40 ++++++------------- 1 file changed, 13 insertions(+), 27 deletions(-) diff --git a/crates/miden-standards/asm/standards/notes/pswap.masm b/crates/miden-standards/asm/standards/notes/pswap.masm index 819c6cb9f7..fff6a5f4ba 100644 --- a/crates/miden-standards/asm/standards/notes/pswap.masm +++ b/crates/miden-standards/asm/standards/notes/pswap.masm @@ -55,7 +55,7 @@ const PSWAP_CREATOR_SUFFIX_ITEM = STORAGE_PTR + 6 # Local memory offsets # ------------------------------------------------------------------------------------------------- -# calculate_tokens_offered_for_requested locals +# calculate_output_amount locals const CALC_FILL_AMOUNT = 0 const CALC_REQUESTED = 1 @@ -112,26 +112,12 @@ proc assert_valid_asset_amount # => [lo, hi, lo, hi] push.FUNGIBLE_ASSET_MAX_AMOUNT u32split - # => [max_hi, max_lo, lo, hi, lo, hi] + # => [max_lo, max_hi, lo, hi, lo, hi] - movup.3 - # => [hi, max_hi, max_lo, lo, lo, hi] + exec.u64::lte + # => [is_lte, lo, hi] - # If hi > max_hi => fail. - dup dup.2 u32gt assertz.err=ERR_PSWAP_NOT_VALID_ASSET_AMOUNT - # => [hi, max_hi, max_lo, lo, lo, hi] - - # Check if hi == max_hi; if so, also need lo <= max_lo. - eq - # => [is_equal, max_lo, lo, lo, hi] - - if.true - u32lte assert.err=ERR_PSWAP_NOT_VALID_ASSET_AMOUNT - # => [lo, hi] - else - drop drop - # => [lo, hi] - end + assert.err=ERR_PSWAP_NOT_VALID_ASSET_AMOUNT # => [lo, hi] end @@ -156,7 +142,7 @@ end @locals(2) # CALC_FILL_AMOUNT: fill amount # CALC_REQUESTED: requested amount -proc calculate_tokens_offered_for_requested +proc calculate_output_amount movup.2 loc_store.CALC_FILL_AMOUNT # => [offered, requested] @@ -224,11 +210,11 @@ proc create_p2id_note # enable_callbacks, faucet_suffix, faucet_prefix, amt_account_fill, amt_note_fill] # Derive the payback tag from the creator's account ID prefix - swap dup - # => [creator_prefix, creator_prefix, creator_suffix, note_type, P2ID_SERIAL_NUM, ...] + dup.1 + # => [creator_prefix, creator_suffix, creator_prefix, note_type, P2ID_SERIAL_NUM, ...] exec.note_tag::create_account_target - # => [tag, creator_prefix, creator_suffix, note_type, P2ID_SERIAL_NUM, ...] - swap movup.2 + # => [tag, creator_suffix, creator_prefix, note_type, P2ID_SERIAL_NUM, ...] + movdn.2 # => [creator_suffix, creator_prefix, tag, note_type, P2ID_SERIAL_NUM, ...] exec.p2id::new @@ -382,7 +368,7 @@ proc create_remainder_note # Add remaining offered asset: remainder_amount = amt_offered - amt_payout. # Sub cannot underflow: amt_payout <= amt_offered by construction in - # `calculate_tokens_offered_for_requested`. + # `calculate_output_amount`. loc_load.REMAINDER_NOTE_IDX # => [note_idx] @@ -544,7 +530,7 @@ proc execute_pswap loc_load.EXEC_AMT_OFFERED # => [offered, requested, account_fill_amount] - exec.calculate_tokens_offered_for_requested + exec.calculate_output_amount # => [account_fill_payout] loc_store.EXEC_AMT_PAYOUT_ACCOUNT_FILL @@ -556,7 +542,7 @@ proc execute_pswap loc_load.EXEC_AMT_OFFERED # => [offered, requested, note_fill_amount] - exec.calculate_tokens_offered_for_requested + exec.calculate_output_amount # => [note_fill_payout] loc_store.EXEC_AMT_PAYOUT_NOTE_FILL From a253b485b2137038eebb30625aece4012f672f77 Mon Sep 17 00:00:00 2001 From: VaibhavJindal Date: Sat, 18 Apr 2026 01:56:46 +0530 Subject: [PATCH 63/66] Update crates/miden-standards/asm/standards/notes/pswap.masm Co-authored-by: Alexander John Lee <77119221+partylikeits1983@users.noreply.github.com> --- crates/miden-standards/asm/standards/notes/pswap.masm | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/miden-standards/asm/standards/notes/pswap.masm b/crates/miden-standards/asm/standards/notes/pswap.masm index fff6a5f4ba..d5d507ac3f 100644 --- a/crates/miden-standards/asm/standards/notes/pswap.masm +++ b/crates/miden-standards/asm/standards/notes/pswap.masm @@ -335,7 +335,6 @@ proc create_remainder_note # Derive remainder serial: increment most significant element exec.active_note::get_serial_number # => [s0, s1, s2, s3, SCRIPT_ROOT, note_type, tag] - # => [s0, s1, s2, s3, SCRIPT_ROOT, note_type, tag] movup.3 add.1 movdn.3 # => [s0, s1, s2, s3+1, SCRIPT_ROOT, note_type, tag] From 2c9105169af2a3e051e44f331a36c5aa0cec016a Mon Sep 17 00:00:00 2001 From: VaibhavJindal Date: Sat, 18 Apr 2026 01:57:03 +0530 Subject: [PATCH 64/66] Update crates/miden-standards/asm/standards/notes/pswap.masm Co-authored-by: Alexander John Lee <77119221+partylikeits1983@users.noreply.github.com> --- crates/miden-standards/asm/standards/notes/pswap.masm | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/miden-standards/asm/standards/notes/pswap.masm b/crates/miden-standards/asm/standards/notes/pswap.masm index d5d507ac3f..3c0e498137 100644 --- a/crates/miden-standards/asm/standards/notes/pswap.masm +++ b/crates/miden-standards/asm/standards/notes/pswap.masm @@ -518,7 +518,6 @@ proc execute_pswap loc_load.EXEC_AMT_REQUESTED # => [requested, fill_amount] - # => [requested, fill_amount] lte assert.err=ERR_PSWAP_FILL_EXCEEDS_REQUESTED # => [] From ea51805c4b8dc0cdec5c1770ea3b5177f21c52d5 Mon Sep 17 00:00:00 2001 From: VaibhavJindal Date: Sat, 18 Apr 2026 01:57:21 +0530 Subject: [PATCH 65/66] Update crates/miden-standards/asm/standards/notes/pswap.masm Co-authored-by: Alexander John Lee <77119221+partylikeits1983@users.noreply.github.com> --- crates/miden-standards/asm/standards/notes/pswap.masm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/miden-standards/asm/standards/notes/pswap.masm b/crates/miden-standards/asm/standards/notes/pswap.masm index 3c0e498137..377fbf2a96 100644 --- a/crates/miden-standards/asm/standards/notes/pswap.masm +++ b/crates/miden-standards/asm/standards/notes/pswap.masm @@ -53,7 +53,7 @@ const PSWAP_CREATOR_PREFIX_ITEM = STORAGE_PTR + 5 const PSWAP_CREATOR_SUFFIX_ITEM = STORAGE_PTR + 6 # Local memory offsets -# ------------------------------------------------------------------------------------------------- +# ================================================================================================= # calculate_output_amount locals const CALC_FILL_AMOUNT = 0 From 312cc695b35e93d8e63c1e3c8d9174b8f7937357 Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Sun, 19 Apr 2026 18:08:28 +0530 Subject: [PATCH 66/66] test(pswap): add rstest cases for fill-sum overflow and max-asset-amount validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Converts pswap_note_invalid_input_test to rstest with three named cases: - fill_exceeds_requested (existing) - fill_sum_u64_overflow: both fills at 2^63, sum overflows u64 → ERR_PSWAP_FILL_SUM_OVERFLOW - fill_sum_exceeds_max_asset_amount: both fills at MAX_AMOUNT, sum > MAX_AMOUNT → ERR_PSWAP_NOT_VALID_ASSET_AMOUNT --- crates/miden-testing/tests/scripts/pswap.rs | 33 +++++++++++++++++---- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/crates/miden-testing/tests/scripts/pswap.rs b/crates/miden-testing/tests/scripts/pswap.rs index 39edcf54b8..118f1ce7d9 100644 --- a/crates/miden-testing/tests/scripts/pswap.rs +++ b/crates/miden-testing/tests/scripts/pswap.rs @@ -4,11 +4,16 @@ use miden_protocol::account::auth::AuthScheme; use miden_protocol::account::{Account, AccountId, AccountStorageMode, AccountVaultDelta}; use miden_protocol::asset::{Asset, FungibleAsset}; use miden_protocol::crypto::rand::{FeltRng, RandomCoin}; +use miden_protocol::errors::MasmError; use miden_protocol::note::{Note, NoteAttachment, NoteAttachmentScheme, NoteType}; use miden_protocol::transaction::RawOutputNote; use miden_protocol::{Felt, ONE, Word, ZERO}; use miden_standards::account::wallets::BasicWallet; -use miden_standards::errors::standards::ERR_PSWAP_FILL_EXCEEDS_REQUESTED; +use miden_standards::errors::standards::{ + ERR_PSWAP_FILL_EXCEEDS_REQUESTED, + ERR_PSWAP_FILL_SUM_OVERFLOW, + ERR_PSWAP_NOT_VALID_ASSET_AMOUNT, +}; use miden_standards::note::{PswapNote, PswapNoteStorage}; use miden_standards::testing::note::NoteBuilder; use miden_testing::{Auth, MockChain, MockChainBuilder, assert_transaction_executor_error}; @@ -744,8 +749,27 @@ async fn pswap_note_creator_reclaim_test() -> anyhow::Result<()> { Ok(()) } +/// The fill sum overflow case uses `1u64 << 63` for each fill: both are valid +/// Felt values (< field modulus), but their sum `2^64` exceeds `u64::MAX`, so +/// the `overflowing_add` check fires before `assert_valid_asset_amount`. +/// +/// The max-asset-amount case uses `FungibleAsset::MAX_AMOUNT` for each fill: +/// the sum `2 * MAX_AMOUNT` fits in u64 but exceeds `MAX_AMOUNT`, so +/// `assert_valid_asset_amount` fires instead. +#[rstest] +#[case::fill_exceeds_requested(30, 0, ERR_PSWAP_FILL_EXCEEDS_REQUESTED)] +#[case::fill_sum_u64_overflow(1u64 << 63, 1u64 << 63, ERR_PSWAP_FILL_SUM_OVERFLOW)] +#[case::fill_sum_exceeds_max_asset_amount( + FungibleAsset::MAX_AMOUNT, + FungibleAsset::MAX_AMOUNT, + ERR_PSWAP_NOT_VALID_ASSET_AMOUNT +)] #[tokio::test] -async fn pswap_note_invalid_input_test() -> anyhow::Result<()> { +async fn pswap_note_invalid_input_test( + #[case] account_fill: u64, + #[case] note_fill: u64, + #[case] expected_err: MasmError, +) -> anyhow::Result<()> { let mut builder = MockChain::builder(); let usdc_faucet = builder.add_existing_basic_faucet(BASIC_AUTH, "USDC", 1000, Some(50))?; @@ -769,9 +793,8 @@ async fn pswap_note_invalid_input_test() -> anyhow::Result<()> { )?; 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(pswap_note.id(), PswapNote::create_args(30, 0)?); + note_args_map.insert(pswap_note.id(), PswapNote::create_args(account_fill, note_fill)?); let tx_context = mock_chain .build_tx_context(bob.id(), &[pswap_note.id()], &[])? @@ -779,7 +802,7 @@ async fn pswap_note_invalid_input_test() -> anyhow::Result<()> { .build()?; let result = tx_context.execute().await; - assert_transaction_executor_error!(result, ERR_PSWAP_FILL_EXCEEDS_REQUESTED); + assert_transaction_executor_error!(result, expected_err); Ok(()) }