From 2e71e7a89ea0c65dfaf6100d8969a5846808eab5 Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Thu, 25 Jun 2026 13:45:06 +0530 Subject: [PATCH 1/2] feat(standards): replace SwapNote::create with a bon builder Convert the `SwapNote` marker type into a struct built via a `bon` typestate builder (`SwapNote::builder()`), mirroring `P2idNote`. `SwapNote::into_notes` returns the outgoing SWAP note together with its payback `NoteDetails`. The "requested asset must differ from offered" check now surfaces via `NoteError::other`. Migrate the mock-chain `add_swap_note` call site. Part of #2283. --- CHANGELOG.md | 1 + .../src/account/interface/test.rs | 19 +- crates/miden-standards/src/note/swap.rs | 221 ++++++++++++++---- .../src/mock_chain/chain_builder.rs | 18 +- 4 files changed, 200 insertions(+), 59 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eac8622c1b..d2fb3a2af4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Changes - [BREAKING] Replaced the `P2idNote` marker type and its `P2idNote::create` factory with a `P2idNote` struct built via a `bon` typestate builder (`P2idNote::builder()`). P2ID notes must now carry at least one asset; a `P2idNote` converts into a `Note` via `Note::from`, and the builder offers `.asset()`/`.assets()`, `.attachment()`/`.attachments()`, and `.generate_serial_number()` ([#2283](https://github.com/0xMiden/protocol/issues/2283)). +- [BREAKING] Replaced the `SwapNote` marker type and its `SwapNote::create` factory with a struct built via a `bon` typestate builder (`SwapNote::builder()`). `SwapNote::into_notes` returns the outgoing SWAP note together with its payback `NoteDetails`, and the builder offers `.attachment()`/`.attachments()` and `.generate_serial_numbers()` ([#2283](https://github.com/0xMiden/protocol/issues/2283)). - Added a skeleton batch kernel ([#1122](https://github.com/0xMiden/protocol/issues/1122)) wired through `LocalBatchProver::prove` and attached to `ProvenBatch` as an `ExecutionProof`. It does not yet perform any verification. - [BREAKING] Renamed `AccountStorageDelta` to `AccountStoragePatch` ([#3002](https://github.com/0xMiden/protocol/pull/3002)). - [BREAKING] Replaced the per-tree account and nullifier backend traits with shared `SmtBackend` and `SmtBackendReader` traits, split into read-only and read-write capabilities, enabling read-only `LargeSmt`-backed tree views via `reader()` ([#2755](https://github.com/0xMiden/protocol/pull/2755), [#3009](https://github.com/0xMiden/protocol/pull/3009)). diff --git a/crates/miden-standards/src/account/interface/test.rs b/crates/miden-standards/src/account/interface/test.rs index 97442d38da..313d908a43 100644 --- a/crates/miden-standards/src/account/interface/test.rs +++ b/crates/miden-standards/src/account/interface/test.rs @@ -5,7 +5,7 @@ use miden_protocol::account::auth::{self, PublicKeyCommitment}; use miden_protocol::asset::NonFungibleAsset; use miden_protocol::crypto::rand::RandomCoin; use miden_protocol::errors::NoteError; -use miden_protocol::note::{NoteAttachments, NoteType}; +use miden_protocol::note::NoteType; use miden_protocol::testing::account_id::ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE; use crate::account::auth::{AuthMultisig, AuthMultisigConfig, AuthSingleSig, NoAuth}; @@ -21,15 +21,14 @@ fn test_required_asset_same_as_offered() { let offered_asset = NonFungibleAsset::mock(&[1, 2, 3, 4]); let requested_asset = NonFungibleAsset::mock(&[1, 2, 3, 4]); - let result = SwapNote::create( - ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE.try_into().unwrap(), - offered_asset, - requested_asset, - NoteType::Public, - NoteAttachments::default(), - NoteType::Public, - &mut RandomCoin::new(Word::from([1, 2, 3, 4u32])), - ); + let result = SwapNote::builder() + .sender(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE.try_into().unwrap()) + .offered_asset(offered_asset) + .requested_asset(requested_asset) + .swap_note_type(NoteType::Public) + .payback_note_type(NoteType::Public) + .generate_serial_numbers(&mut RandomCoin::new(Word::from([1, 2, 3, 4u32]))) + .build(); assert_matches!(result, Err(NoteError::Other { error_msg, .. }) if error_msg == "requested asset same as offered asset".into()); } diff --git a/crates/miden-standards/src/note/swap.rs b/crates/miden-standards/src/note/swap.rs index 17a7347af6..c3ef178984 100644 --- a/crates/miden-standards/src/note/swap.rs +++ b/crates/miden-standards/src/note/swap.rs @@ -9,6 +9,7 @@ use miden_protocol::errors::NoteError; use miden_protocol::note::{ Note, NoteAssets, + NoteAttachment, NoteAttachments, NoteDetails, NoteRecipient, @@ -41,8 +42,62 @@ static SWAP_SCRIPT: LazyLock = LazyLock::new(|| { // SWAP NOTE // ================================================================================================ -/// TODO: add docs -pub struct SwapNote; +/// A SWAP note: offers `offered_asset` in exchange for `requested_asset`. +/// +/// Any account may consume the note: the consumer receives the `offered_asset` and, in turn, +/// creates a P2ID payback note that returns the `requested_asset` to the swap's `sender`. +/// +/// Construct one with the [builder](SwapNote::builder), then call [`SwapNote::into_notes`] to +/// obtain the outgoing SWAP [`Note`] together with the [`NoteDetails`] of the payback note. +#[derive(Debug, Clone)] +pub struct SwapNote { + sender: AccountId, + offered_asset: Asset, + requested_asset: Asset, + swap_note_type: NoteType, + payback_note_type: NoteType, + serial_number: Word, + payback_serial_number: Word, + attachments: NoteAttachments, +} + +#[bon::bon] +impl SwapNote { + /// Builds a new [`SwapNote`] offering `offered_asset` in exchange for `requested_asset`. + /// + /// # Errors + /// + /// Returns an error if the requested asset is the same as the offered asset, or if the + /// attachments exceed their protocol limit (see [`NoteAttachments::new`]). + #[builder] + pub fn new( + #[builder(field)] attachments: Vec, + sender: AccountId, + #[builder(into)] offered_asset: Asset, + #[builder(into)] requested_asset: Asset, + #[builder(default)] swap_note_type: NoteType, + #[builder(default)] payback_note_type: NoteType, + serial_number: Word, + payback_serial_number: Word, + ) -> Result { + if requested_asset == offered_asset { + return Err(NoteError::other("requested asset same as offered asset")); + } + + let attachments = NoteAttachments::new(attachments)?; + + Ok(Self { + sender, + offered_asset, + requested_asset, + swap_note_type, + payback_note_type, + serial_number, + payback_serial_number, + attachments, + }) + } +} impl SwapNote { // CONSTANTS @@ -64,53 +119,73 @@ impl SwapNote { SWAP_SCRIPT.root() } - // BUILDERS - // -------------------------------------------------------------------------------------------- + /// Returns the account ID of the note's sender. + pub fn sender(&self) -> AccountId { + self.sender + } - /// Generates a SWAP note - swap of assets between two accounts - and returns the note as well - /// as [`NoteDetails`] for the payback note. - /// - /// This script enables a swap of 2 assets between the `sender` account and any other account - /// that is willing to consume the note. The consumer will receive the `offered_asset` and - /// will create a new P2ID note with `sender` as target, containing the `requested_asset`. - /// - /// # Errors - /// Returns an error if deserialization or compilation of the `SWAP` script fails. - pub fn create( - sender: AccountId, - offered_asset: Asset, - requested_asset: Asset, - swap_note_type: NoteType, - swap_note_attachments: NoteAttachments, - payback_note_type: NoteType, - rng: &mut R, - ) -> Result<(Note, NoteDetails), NoteError> { - if requested_asset == offered_asset { - return Err(NoteError::other("requested asset same as offered asset")); - } + /// Returns the asset offered by the note. + pub fn offered_asset(&self) -> Asset { + self.offered_asset + } + + /// Returns the asset requested in exchange for the offered asset. + pub fn requested_asset(&self) -> Asset { + self.requested_asset + } + + /// Returns the type of the outgoing SWAP note. + pub fn swap_note_type(&self) -> NoteType { + self.swap_note_type + } - let payback_serial_num = rng.draw_word(); + /// Returns the type of the payback note created on consumption. + pub fn payback_note_type(&self) -> NoteType { + self.payback_note_type + } - let swap_storage = - SwapNoteStorage::new(sender, requested_asset, payback_note_type, payback_serial_num); + /// Returns the serial number of the outgoing SWAP note. + pub fn serial_number(&self) -> Word { + self.serial_number + } - let serial_num = rng.draw_word(); - let recipient = swap_storage.into_recipient(serial_num); + /// Returns the serial number of the payback note. + pub fn payback_serial_number(&self) -> Word { + self.payback_serial_number + } - // build the tag for the SWAP use case - let tag = Self::build_tag(swap_note_type, &offered_asset, &requested_asset); + /// Returns the attachments carried by the SWAP note. + pub fn attachments(&self) -> &NoteAttachments { + &self.attachments + } - // build the outgoing note - let metadata = PartialNoteMetadata::new(sender, swap_note_type).with_tag(tag); - let assets = NoteAssets::new(vec![offered_asset])?; - let note = Note::with_attachments(assets, metadata, recipient, swap_note_attachments); + // INSTANCE METHODS + // -------------------------------------------------------------------------------------------- - // build the payback note details - let payback_recipient = P2idNoteStorage::new(sender).into_recipient(payback_serial_num); - let payback_assets = NoteAssets::new(vec![requested_asset])?; + /// Consumes the note and returns the outgoing SWAP [`Note`] together with the [`NoteDetails`] + /// of the payback P2ID note that the consumer will create for the sender. + pub fn into_notes(self) -> (Note, NoteDetails) { + let swap_storage = SwapNoteStorage::new( + self.sender, + self.requested_asset, + self.payback_note_type, + self.payback_serial_number, + ); + let recipient = swap_storage.into_recipient(self.serial_number); + + let tag = Self::build_tag(self.swap_note_type, &self.offered_asset, &self.requested_asset); + let metadata = PartialNoteMetadata::new(self.sender, self.swap_note_type).with_tag(tag); + let assets = NoteAssets::new(vec![self.offered_asset]) + .expect("a single offered asset never exceeds the note asset limit"); + let note = Note::with_attachments(assets, metadata, recipient, self.attachments); + + let payback_recipient = + P2idNoteStorage::new(self.sender).into_recipient(self.payback_serial_number); + let payback_assets = NoteAssets::new(vec![self.requested_asset]) + .expect("a single requested asset never exceeds the note asset limit"); let payback_note = NoteDetails::new(payback_assets, payback_recipient); - Ok((note, payback_note)) + (note, payback_note) } /// Returns a note tag for a swap note with the specified parameters. @@ -153,6 +228,44 @@ impl SwapNote { } } +// BUILDER EXTENSIONS +// ================================================================================================ + +impl SwapNoteBuilder { + /// Adds a single attachment to the SWAP note. + pub fn attachment(mut self, attachment: impl Into) -> Self { + self.attachments.push(attachment.into()); + self + } + + /// Adds multiple attachments to the SWAP note. + pub fn attachments( + mut self, + attachments: impl IntoIterator>, + ) -> Self { + self.attachments.extend(attachments.into_iter().map(Into::into)); + self + } +} + +impl SwapNoteBuilder +where + S::SerialNumber: swap_note_builder::IsUnset, + S::PaybackSerialNumber: swap_note_builder::IsUnset, +{ + /// Draws the SWAP and payback serial numbers from `rng` (payback first) and sets them. + pub fn generate_serial_numbers( + self, + rng: &mut impl FeltRng, + ) -> SwapNoteBuilder< + swap_note_builder::SetSerialNumber>, + > { + let payback_serial_number = rng.draw_word(); + let serial_number = rng.draw_word(); + self.payback_serial_number(payback_serial_number).serial_number(serial_number) + } +} + // SWAP NOTE STORAGE // ================================================================================================ @@ -262,6 +375,7 @@ mod tests { use miden_protocol::account::{AccountIdVersion, AccountType}; use miden_protocol::asset::{FungibleAsset, NonFungibleAsset, NonFungibleAssetDetails}; + use miden_protocol::crypto::rand::RandomCoin; use miden_protocol::note::{NoteStorage, NoteTag, NoteType}; use miden_protocol::testing::account_id::{ ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET, @@ -389,4 +503,31 @@ mod tests { "swap script root byte 1 should match with the highest bit set to zero" ); } + + #[test] + fn builder_produces_swap_and_payback_notes() { + let sender = AccountId::dummy([7u8; 15], AccountIdVersion::Version1, AccountType::Private); + let offered_asset = fungible_asset(); + let requested_asset = non_fungible_asset(); + let mut rng = RandomCoin::new(Word::empty()); + + let swap = SwapNote::builder() + .sender(sender) + .offered_asset(offered_asset) + .requested_asset(requested_asset) + .swap_note_type(NoteType::Public) + .payback_note_type(NoteType::Private) + .generate_serial_numbers(&mut rng) + .build() + .unwrap(); + + assert_eq!(swap.sender(), sender); + assert_eq!(swap.offered_asset(), offered_asset); + assert_eq!(swap.requested_asset(), requested_asset); + + let (note, payback_note) = swap.into_notes(); + assert_eq!(note.metadata().note_type(), NoteType::Public); + assert_eq!(note.assets().num_assets(), 1); + assert_eq!(payback_note.assets().num_assets(), 1); + } } diff --git a/crates/miden-testing/src/mock_chain/chain_builder.rs b/crates/miden-testing/src/mock_chain/chain_builder.rs index 3736f9a8ab..f34b82550e 100644 --- a/crates/miden-testing/src/mock_chain/chain_builder.rs +++ b/crates/miden-testing/src/mock_chain/chain_builder.rs @@ -720,15 +720,15 @@ impl MockChainBuilder { requested_asset: Asset, payback_note_type: NoteType, ) -> anyhow::Result<(Note, NoteDetails)> { - let (swap_note, payback_note) = SwapNote::create( - sender, - offered_asset, - requested_asset, - NoteType::Public, - NoteAttachments::default(), - payback_note_type, - &mut self.rng, - )?; + let (swap_note, payback_note) = SwapNote::builder() + .sender(sender) + .offered_asset(offered_asset) + .requested_asset(requested_asset) + .swap_note_type(NoteType::Public) + .payback_note_type(payback_note_type) + .generate_serial_numbers(&mut self.rng) + .build()? + .into_notes(); self.add_output_note(RawOutputNote::Full(swap_note.clone())); From ca1a53c945c414ab632c375c4213d7cc2e0e3135 Mon Sep 17 00:00:00 2001 From: Vaibhav Jindal Date: Fri, 26 Jun 2026 10:54:54 +0530 Subject: [PATCH 2/2] refactor(standards): return Result from SwapNote::into_notes Propagate NoteAssets::new errors with ? instead of panicking via .expect(...), matching the error handling the original create used and keeping the (Note, NoteDetails) return. Also comment the builder test. --- crates/miden-standards/src/note/swap.rs | 18 +++++++++++------- .../src/mock_chain/chain_builder.rs | 2 +- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/crates/miden-standards/src/note/swap.rs b/crates/miden-standards/src/note/swap.rs index c3ef178984..7c820284eb 100644 --- a/crates/miden-standards/src/note/swap.rs +++ b/crates/miden-standards/src/note/swap.rs @@ -164,7 +164,11 @@ impl SwapNote { /// Consumes the note and returns the outgoing SWAP [`Note`] together with the [`NoteDetails`] /// of the payback P2ID note that the consumer will create for the sender. - pub fn into_notes(self) -> (Note, NoteDetails) { + /// + /// # Errors + /// + /// Returns an error if constructing the note's assets fails (see [`NoteAssets::new`]). + pub fn into_notes(self) -> Result<(Note, NoteDetails), NoteError> { let swap_storage = SwapNoteStorage::new( self.sender, self.requested_asset, @@ -175,17 +179,15 @@ impl SwapNote { let tag = Self::build_tag(self.swap_note_type, &self.offered_asset, &self.requested_asset); let metadata = PartialNoteMetadata::new(self.sender, self.swap_note_type).with_tag(tag); - let assets = NoteAssets::new(vec![self.offered_asset]) - .expect("a single offered asset never exceeds the note asset limit"); + let assets = NoteAssets::new(vec![self.offered_asset])?; let note = Note::with_attachments(assets, metadata, recipient, self.attachments); let payback_recipient = P2idNoteStorage::new(self.sender).into_recipient(self.payback_serial_number); - let payback_assets = NoteAssets::new(vec![self.requested_asset]) - .expect("a single requested asset never exceeds the note asset limit"); + let payback_assets = NoteAssets::new(vec![self.requested_asset])?; let payback_note = NoteDetails::new(payback_assets, payback_recipient); - (note, payback_note) + Ok((note, payback_note)) } /// Returns a note tag for a swap note with the specified parameters. @@ -506,6 +508,8 @@ mod tests { #[test] fn builder_produces_swap_and_payback_notes() { + // The builder produces a SWAP note whose accessors echo the inputs, and `into_notes` + // splits it into the public SWAP note and its payback note, each carrying one asset. let sender = AccountId::dummy([7u8; 15], AccountIdVersion::Version1, AccountType::Private); let offered_asset = fungible_asset(); let requested_asset = non_fungible_asset(); @@ -525,7 +529,7 @@ mod tests { assert_eq!(swap.offered_asset(), offered_asset); assert_eq!(swap.requested_asset(), requested_asset); - let (note, payback_note) = swap.into_notes(); + let (note, payback_note) = swap.into_notes().unwrap(); assert_eq!(note.metadata().note_type(), NoteType::Public); assert_eq!(note.assets().num_assets(), 1); assert_eq!(payback_note.assets().num_assets(), 1); diff --git a/crates/miden-testing/src/mock_chain/chain_builder.rs b/crates/miden-testing/src/mock_chain/chain_builder.rs index f34b82550e..f7164a4603 100644 --- a/crates/miden-testing/src/mock_chain/chain_builder.rs +++ b/crates/miden-testing/src/mock_chain/chain_builder.rs @@ -728,7 +728,7 @@ impl MockChainBuilder { .payback_note_type(payback_note_type) .generate_serial_numbers(&mut self.rng) .build()? - .into_notes(); + .into_notes()?; self.add_output_note(RawOutputNote::Full(swap_note.clone()));