From 7b39725a16e081fa05e835ceb2d5ae0e0250de59 Mon Sep 17 00:00:00 2001 From: JereSalo Date: Thu, 14 May 2026 19:20:53 -0300 Subject: [PATCH 1/6] expose public payback on the swap CLI command Adds --payback-note-type to the swap subcommand so the payback note can be created public or private (defaults to private, matching pswap's CLI). Fixes the printed payback tag to use the creator account target instead of the SWAP note's own tag. Adds swap_public_payback_test in the mock-RPC test suite, exercising build_swap end-to-end with NoteType::Public payback against the companion protocol branch. Cargo.toml temporarily redirects miden-protocol, miden-standards, miden-testing, miden-tx, miden-tx-batch-prover to a local protocol checkout via [patch], paired with the SWAP standard rewrite in protocol branch jere/swap-v2. The patch block must be removed and the protocol revision pinned normally before this is opened as a PR. --- CHANGELOG.md | 1 + Cargo.lock | 17 ++--- Cargo.toml | 8 +++ .../src/commands/new_transactions.rs | 18 +++--- .../testing/miden-client-tests/src/tests.rs | 63 ++++++++++++++++++- 5 files changed, 88 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17f91d123a..9e025b9e97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ ### Enhancements +* [FEATURE][cli] `swap` now accepts `--payback-note-type` so the payback note can be created as public or private. Public payback works without any off-band advice now that SWAP derives the payback recipient deterministically. * [FEATURE][rust] Added `Client::sync_chain()` (on-chain sync only) and `Client::sync_note_transport()` (Note Transport Layer fetch only) for callers needing finer-grained control over sync. ([#2091](https://github.com/0xMiden/miden-client/pull/2091)) * [FEATURE][rust] Added `GrpcClient::with_bearer_auth(token)` to attach an `authorization: Bearer ` header to every outbound gRPC call, for use behind authenticating gateways. Tokens are validated at connection time and preserved across `set_genesis_commitment` updates ([#2101](https://github.com/0xMiden/miden-client/pull/2101)). * Made new-account construction use merged storage schema commitment (`build_with_schema_commitment`), re-exported `AccountBuilderSchemaCommitmentExt`, added WASM `buildWithoutSchemaCommitment()`, and fixed contract `accounts.create()` to require explicit `components` ([#1996](https://github.com/0xMiden/miden-client/pull/1996)). diff --git a/Cargo.lock b/Cargo.lock index abbf62d949..9276d92a64 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2960,6 +2960,14 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "miden-block-prover" +version = "0.15.0" +dependencies = [ + "miden-protocol", + "thiserror 2.0.18", +] + [[package]] name = "miden-block-prover" version = "0.15.0" @@ -3529,7 +3537,7 @@ dependencies = [ "indexmap", "libsqlite3-sys", "miden-agglayer", - "miden-block-prover", + "miden-block-prover 0.15.0 (git+https://github.com/0xMiden/protocol.git?branch=next)", "miden-crypto", "miden-large-smt-backend-rocksdb", "miden-node-db", @@ -3680,7 +3688,6 @@ dependencies = [ [[package]] name = "miden-protocol" version = "0.15.0" -source = "git+https://github.com/0xMiden/protocol.git?branch=next#c6f1ecf9fa49bcebb77deada74e443f07b45c0ba" dependencies = [ "bech32", "fs-err", @@ -3758,7 +3765,6 @@ dependencies = [ [[package]] name = "miden-standards" version = "0.15.0" -source = "git+https://github.com/0xMiden/protocol.git?branch=next#c6f1ecf9fa49bcebb77deada74e443f07b45c0ba" dependencies = [ "bon", "fs-err", @@ -3775,11 +3781,10 @@ dependencies = [ [[package]] name = "miden-testing" version = "0.15.0" -source = "git+https://github.com/0xMiden/protocol.git?branch=next#c6f1ecf9fa49bcebb77deada74e443f07b45c0ba" dependencies = [ "anyhow", "itertools 0.14.0", - "miden-block-prover", + "miden-block-prover 0.15.0", "miden-core-lib", "miden-crypto", "miden-processor", @@ -3795,7 +3800,6 @@ dependencies = [ [[package]] name = "miden-tx" version = "0.15.0" -source = "git+https://github.com/0xMiden/protocol.git?branch=next#c6f1ecf9fa49bcebb77deada74e443f07b45c0ba" dependencies = [ "miden-processor", "miden-protocol", @@ -3808,7 +3812,6 @@ dependencies = [ [[package]] name = "miden-tx-batch-prover" version = "0.15.0" -source = "git+https://github.com/0xMiden/protocol.git?branch=next#c6f1ecf9fa49bcebb77deada74e443f07b45c0ba" dependencies = [ "miden-protocol", "miden-tx", diff --git a/Cargo.toml b/Cargo.toml index 9e06e927e6..5a65d3c902 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -96,3 +96,11 @@ module_name_repetitions = "allow" # Many triggers, and is a stylistic choice must_use_candidate = "allow" # This marks many fn's which isn't helpful. should_panic_without_expect = "allow" # We don't care about the specific panic message. # End of pedantic lints. + +# Local patch for Phase 2 development (issue #1739). Remove before merging. +[patch."https://github.com/0xMiden/protocol.git"] +miden-protocol = { path = "../protocol/crates/miden-protocol" } +miden-standards = { path = "../protocol/crates/miden-standards" } +miden-testing = { path = "../protocol/crates/miden-testing" } +miden-tx = { path = "../protocol/crates/miden-tx" } +miden-tx-batch-prover = { path = "../protocol/crates/miden-tx-batch-prover" } diff --git a/bin/miden-cli/src/commands/new_transactions.rs b/bin/miden-cli/src/commands/new_transactions.rs index 4a33401c6c..ba9d10b8f5 100644 --- a/bin/miden-cli/src/commands/new_transactions.rs +++ b/bin/miden-cli/src/commands/new_transactions.rs @@ -8,8 +8,8 @@ use miden_client::note::{ BlockNumber, Note, NoteAttachment, + NoteTag, NoteType as MidenNoteType, - SwapNote, get_input_note_with_id_prefix, }; use miden_client::store::NoteRecordError; @@ -211,6 +211,11 @@ pub struct SwapCmd { #[arg(short, long, value_enum)] note_type: NoteType, + /// Visibility of the payback note produced when the swap is consumed. Defaults to private + /// (cheaper, and the swap already records the trade in the consuming transaction). + #[arg(long, value_enum, default_value_t = NoteType::Private)] + payback_note_type: NoteType, + /// Flag to submit the executed transaction without asking for confirmation. #[arg(long, default_value_t = false)] force: bool, @@ -248,7 +253,7 @@ impl SwapCmd { .build_swap( &swap_transaction, (&self.note_type).into(), - MidenNoteType::Private, + (&self.payback_note_type).into(), client.rng(), ) .map_err(|err| { @@ -264,14 +269,9 @@ impl SwapCmd { ) .await?; - let payback_note_tag: u32 = SwapNote::build_tag( - (&self.note_type).into(), - &swap_transaction.offered_asset(), - &swap_transaction.requested_asset(), - ) - .into(); + let payback_note_tag: u32 = NoteTag::with_account_target(sender_account_id).into(); println!( - "To receive updates about the payback Swap Note run `miden-client tags --add {payback_note_tag}`", + "To receive updates about the payback note run `miden-client tags --add {payback_note_tag}`", ); Ok(()) diff --git a/crates/testing/miden-client-tests/src/tests.rs b/crates/testing/miden-client-tests/src/tests.rs index 8f763cbc62..07f0794b47 100644 --- a/crates/testing/miden-client-tests/src/tests.rs +++ b/crates/testing/miden-client-tests/src/tests.rs @@ -2589,6 +2589,62 @@ async fn swap_chain_test() { assert_eq!(last_wallet_balance, 1); } +/// Exercises SWAP with `NoteType::Public` payback. With pre-Phase-2 SWAP this fails because the +/// consumer cannot produce the public payback recipient without off-band advice; with the new +/// SWAP standard the payback recipient is derived deterministically and the trade completes. +#[tokio::test] +async fn swap_public_payback_test() { + let (mut client, mock_rpc_api, keystore) = create_test_client().await; + + let (wallet_a, faucet_a) = + setup_wallet_and_faucet(&mut client, AccountStorageMode::Private, &keystore, RPO_FALCON_SCHEME_ID) + .await + .unwrap(); + mint_and_consume(&mut client, wallet_a.id(), faucet_a.id(), NoteType::Private).await; + mock_rpc_api.prove_block(); + client.sync_state().await.unwrap(); + + let (wallet_b, faucet_b) = + setup_wallet_and_faucet(&mut client, AccountStorageMode::Private, &keystore, RPO_FALCON_SCHEME_ID) + .await + .unwrap(); + mint_and_consume(&mut client, wallet_b.id(), faucet_b.id(), NoteType::Private).await; + mock_rpc_api.prove_block(); + client.sync_state().await.unwrap(); + + // wallet_a offers asset_a and requests asset_b, with PUBLIC payback. + let tx_request = TransactionRequestBuilder::new() + .build_swap( + &SwapTransactionData::new( + wallet_a.id(), + Asset::Fungible(FungibleAsset::new(faucet_a.id(), 1).unwrap()), + Asset::Fungible(FungibleAsset::new(faucet_b.id(), 1).unwrap()), + ), + NoteType::Private, + NoteType::Public, + client.rng(), + ) + .unwrap(); + + let swap_note = tx_request.expected_output_own_notes()[0].clone(); + Box::pin(client.submit_new_transaction(wallet_a.id(), tx_request)).await.unwrap(); + mock_rpc_api.prove_block(); + client.sync_state().await.unwrap(); + + // wallet_b consumes the swap, producing a public P2ID payback to wallet_a with no off-band data. + let tx_request = TransactionRequestBuilder::new() + .build_consume_notes(vec![swap_note]) + .unwrap(); + Box::pin(client.submit_new_transaction(wallet_b.id(), tx_request)).await.unwrap(); + mock_rpc_api.prove_block(); + client.sync_state().await.unwrap(); + + // wallet_b ended up with asset_a, wallet_a will receive asset_b via the public payback note. + let wallet_b_balance = + client.account_reader(wallet_b.id()).get_balance(faucet_a.id()).await.unwrap(); + assert_eq!(wallet_b_balance, 1); +} + /// Tests that partial output notes (created when a SWAP note is consumed) are correctly included in /// `NoteFilter::Unspent` and receive inclusion proofs during sync, transitioning from /// `ExpectedPartial` to `CommittedPartial` state. @@ -2650,9 +2706,10 @@ async fn partial_output_note_receives_inclusion_proof_after_sync() { mock_rpc_api.prove_block(); client.sync_state().await.unwrap(); - // Wallet B consumes the SWAP note. The SWAP script creates a payback note for wallet A using - // only the recipient digest committed inside the SWAP note. From the VM's perspective this - // payback note is an OutputNote::Partial, which gets stored as ExpectedPartial. + // Wallet B consumes the SWAP note. The SWAP script derives the payback recipient at consume + // time (P2ID to the creator with serial = SWAP_serial with element 0 + 1) and emits the + // payback note. From the VM's perspective this payback note is an OutputNote::Partial, which + // gets stored as ExpectedPartial. let consume_tx_request = TransactionRequestBuilder::new().build_consume_notes(vec![swap_note]).unwrap(); From 040730c7684b9c115f5d2226400e694bd9d9cb61 Mon Sep 17 00:00:00 2001 From: JereSalo Date: Mon, 18 May 2026 14:48:10 -0300 Subject: [PATCH 2/6] add integration test for public swap payback --- .../src/tests/swap_transaction.rs | 127 ++++++++++++++++++ .../testing/miden-client-tests/src/tests.rs | 37 +++-- 2 files changed, 151 insertions(+), 13 deletions(-) diff --git a/bin/integration-tests/src/tests/swap_transaction.rs b/bin/integration-tests/src/tests/swap_transaction.rs index 4dea6815b8..ef59b289c3 100644 --- a/bin/integration-tests/src/tests/swap_transaction.rs +++ b/bin/integration-tests/src/tests/swap_transaction.rs @@ -162,6 +162,133 @@ pub async fn test_swap_fully_onchain(client_config: ClientConfig) -> Result<()> Ok(()) } +/// Same shape as `test_swap_fully_onchain` but with a public payback note. Exercises the +/// deterministic payback-recipient derivation: the consumer materializes the public payback +/// without any off-band advice, the node indexes it, and the original sender retrieves and +/// consumes it after a regular sync. +pub async fn test_swap_public_payback(client_config: ClientConfig) -> Result<()> { + const OFFERED_ASSET_AMOUNT: u64 = 1; + const REQUESTED_ASSET_AMOUNT: u64 = 25; + let (mut client1, authenticator_1) = client_config.clone().into_client().await?; + wait_for_node(&mut client1).await; + let (mut client2, authenticator_2) = ClientConfig::default() + .with_rpc_endpoint(client_config.rpc_endpoint()) + .into_client() + .await?; + + client1.sync_state().await?; + client2.sync_state().await?; + + let (account_a, ..) = insert_new_wallet( + &mut client1, + AccountStorageMode::Private, + &authenticator_1, + RPO_FALCON_SCHEME_ID, + ) + .await?; + let (account_b, ..) = insert_new_wallet( + &mut client2, + AccountStorageMode::Private, + &authenticator_2, + RPO_FALCON_SCHEME_ID, + ) + .await?; + + let (btc_faucet_account, _) = insert_new_fungible_faucet( + &mut client1, + AccountStorageMode::Private, + &authenticator_1, + RPO_FALCON_SCHEME_ID, + ) + .await?; + let (eth_faucet_account, _) = insert_new_fungible_faucet( + &mut client2, + AccountStorageMode::Private, + &authenticator_2, + RPO_FALCON_SCHEME_ID, + ) + .await?; + + info!(account_id = %account_a.id(), faucet_id = %btc_faucet_account.id(), "Minting 1000 BTC for account A"); + let tx_id = + mint_and_consume(&mut client1, account_a.id(), btc_faucet_account.id(), NoteType::Public) + .await; + wait_for_tx(&mut client1, tx_id).await?; + + info!(account_id = %account_b.id(), faucet_id = %eth_faucet_account.id(), "Minting 1000 ETH for account B"); + let tx_id = + mint_and_consume(&mut client2, account_b.id(), eth_faucet_account.id(), NoteType::Public) + .await; + wait_for_tx(&mut client2, tx_id).await?; + + let offered_asset = FungibleAsset::new(btc_faucet_account.id(), OFFERED_ASSET_AMOUNT)?; + let requested_asset = FungibleAsset::new(eth_faucet_account.id(), REQUESTED_ASSET_AMOUNT)?; + info!(account_id = %account_a.id(), "Creating swap note with public payback"); + + let tx_request = TransactionRequestBuilder::new().build_swap( + &SwapTransactionData::new( + account_a.id(), + Asset::Fungible(offered_asset), + Asset::Fungible(requested_asset), + ), + NoteType::Public, + NoteType::Public, + client1.rng(), + )?; + + let expected_output_notes: Vec = tx_request.expected_output_own_notes(); + let expected_payback_note_details: Vec = + tx_request.expected_future_notes().cloned().map(|(n, _)| n).collect(); + assert_eq!(expected_output_notes.len(), 1); + assert_eq!(expected_payback_note_details.len(), 1); + + execute_tx_and_sync(&mut client1, account_a.id(), tx_request).await?; + + let swap_note_tag = SwapNote::build_tag( + NoteType::Public, + &Asset::Fungible(offered_asset), + &Asset::Fungible(requested_asset), + ); + info!(tag = %swap_note_tag, "Adding swap note tag to client 2"); + client2.add_note_tag(swap_note_tag).await?; + + client2.sync_state().await?; + info!(note_id = %expected_output_notes[0].id(), account_id = %account_b.id(), "Consuming swap note on client 2"); + + let note = client2 + .get_input_note(expected_output_notes[0].id()) + .await? + .unwrap() + .try_into()?; + let tx_request = TransactionRequestBuilder::new().build_consume_notes(vec![note])?; + execute_tx_and_sync(&mut client2, account_b.id(), tx_request).await?; + + client1.sync_state().await?; + info!(note_id = %expected_payback_note_details[0].id(), account_id = %account_a.id(), "Consuming public payback note on client 1"); + + let note = client1 + .get_input_note(expected_payback_note_details[0].id()) + .await? + .unwrap() + .try_into()?; + let tx_request = TransactionRequestBuilder::new().build_consume_notes(vec![note])?; + execute_tx_and_sync(&mut client1, account_a.id(), tx_request).await?; + + let account_a_reader = client1.account_reader(account_a.id()); + let account_a_btc = account_a_reader.get_balance(btc_faucet_account.id()).await?; + let account_a_eth = account_a_reader.get_balance(eth_faucet_account.id()).await?; + assert_eq!(account_a_btc, 999); + assert_eq!(account_a_eth, 25); + + let account_b_reader = client2.account_reader(account_b.id()); + let account_b_btc = account_b_reader.get_balance(btc_faucet_account.id()).await?; + let account_b_eth = account_b_reader.get_balance(eth_faucet_account.id()).await?; + assert_eq!(account_b_btc, 1); + assert_eq!(account_b_eth, 975); + + Ok(()) +} + pub async fn test_swap_private(client_config: ClientConfig) -> Result<()> { const OFFERED_ASSET_AMOUNT: u64 = 1; const REQUESTED_ASSET_AMOUNT: u64 = 25; diff --git a/crates/testing/miden-client-tests/src/tests.rs b/crates/testing/miden-client-tests/src/tests.rs index 07f0794b47..11b78e7273 100644 --- a/crates/testing/miden-client-tests/src/tests.rs +++ b/crates/testing/miden-client-tests/src/tests.rs @@ -2596,18 +2596,26 @@ async fn swap_chain_test() { async fn swap_public_payback_test() { let (mut client, mock_rpc_api, keystore) = create_test_client().await; - let (wallet_a, faucet_a) = - setup_wallet_and_faucet(&mut client, AccountStorageMode::Private, &keystore, RPO_FALCON_SCHEME_ID) - .await - .unwrap(); + let (wallet_a, faucet_a) = setup_wallet_and_faucet( + &mut client, + AccountStorageMode::Private, + &keystore, + RPO_FALCON_SCHEME_ID, + ) + .await + .unwrap(); mint_and_consume(&mut client, wallet_a.id(), faucet_a.id(), NoteType::Private).await; mock_rpc_api.prove_block(); client.sync_state().await.unwrap(); - let (wallet_b, faucet_b) = - setup_wallet_and_faucet(&mut client, AccountStorageMode::Private, &keystore, RPO_FALCON_SCHEME_ID) - .await - .unwrap(); + let (wallet_b, faucet_b) = setup_wallet_and_faucet( + &mut client, + AccountStorageMode::Private, + &keystore, + RPO_FALCON_SCHEME_ID, + ) + .await + .unwrap(); mint_and_consume(&mut client, wallet_b.id(), faucet_b.id(), NoteType::Private).await; mock_rpc_api.prove_block(); client.sync_state().await.unwrap(); @@ -2627,15 +2635,18 @@ async fn swap_public_payback_test() { .unwrap(); let swap_note = tx_request.expected_output_own_notes()[0].clone(); - Box::pin(client.submit_new_transaction(wallet_a.id(), tx_request)).await.unwrap(); + Box::pin(client.submit_new_transaction(wallet_a.id(), tx_request)) + .await + .unwrap(); mock_rpc_api.prove_block(); client.sync_state().await.unwrap(); - // wallet_b consumes the swap, producing a public P2ID payback to wallet_a with no off-band data. - let tx_request = TransactionRequestBuilder::new() - .build_consume_notes(vec![swap_note]) + // wallet_b consumes the swap, producing a public P2ID payback to wallet_a with no off-band + // data. + let tx_request = TransactionRequestBuilder::new().build_consume_notes(vec![swap_note]).unwrap(); + Box::pin(client.submit_new_transaction(wallet_b.id(), tx_request)) + .await .unwrap(); - Box::pin(client.submit_new_transaction(wallet_b.id(), tx_request)).await.unwrap(); mock_rpc_api.prove_block(); client.sync_state().await.unwrap(); From 825d6e9b53275350be7f63b152e1741d625f2a33 Mon Sep 17 00:00:00 2001 From: JereSalo Date: Mon, 18 May 2026 15:00:01 -0300 Subject: [PATCH 3/6] remove local protocol patch --- Cargo.lock | 17 +++++++---------- Cargo.toml | 8 -------- 2 files changed, 7 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9276d92a64..abbf62d949 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2960,14 +2960,6 @@ dependencies = [ "thiserror 2.0.18", ] -[[package]] -name = "miden-block-prover" -version = "0.15.0" -dependencies = [ - "miden-protocol", - "thiserror 2.0.18", -] - [[package]] name = "miden-block-prover" version = "0.15.0" @@ -3537,7 +3529,7 @@ dependencies = [ "indexmap", "libsqlite3-sys", "miden-agglayer", - "miden-block-prover 0.15.0 (git+https://github.com/0xMiden/protocol.git?branch=next)", + "miden-block-prover", "miden-crypto", "miden-large-smt-backend-rocksdb", "miden-node-db", @@ -3688,6 +3680,7 @@ dependencies = [ [[package]] name = "miden-protocol" version = "0.15.0" +source = "git+https://github.com/0xMiden/protocol.git?branch=next#c6f1ecf9fa49bcebb77deada74e443f07b45c0ba" dependencies = [ "bech32", "fs-err", @@ -3765,6 +3758,7 @@ dependencies = [ [[package]] name = "miden-standards" version = "0.15.0" +source = "git+https://github.com/0xMiden/protocol.git?branch=next#c6f1ecf9fa49bcebb77deada74e443f07b45c0ba" dependencies = [ "bon", "fs-err", @@ -3781,10 +3775,11 @@ dependencies = [ [[package]] name = "miden-testing" version = "0.15.0" +source = "git+https://github.com/0xMiden/protocol.git?branch=next#c6f1ecf9fa49bcebb77deada74e443f07b45c0ba" dependencies = [ "anyhow", "itertools 0.14.0", - "miden-block-prover 0.15.0", + "miden-block-prover", "miden-core-lib", "miden-crypto", "miden-processor", @@ -3800,6 +3795,7 @@ dependencies = [ [[package]] name = "miden-tx" version = "0.15.0" +source = "git+https://github.com/0xMiden/protocol.git?branch=next#c6f1ecf9fa49bcebb77deada74e443f07b45c0ba" dependencies = [ "miden-processor", "miden-protocol", @@ -3812,6 +3808,7 @@ dependencies = [ [[package]] name = "miden-tx-batch-prover" version = "0.15.0" +source = "git+https://github.com/0xMiden/protocol.git?branch=next#c6f1ecf9fa49bcebb77deada74e443f07b45c0ba" dependencies = [ "miden-protocol", "miden-tx", diff --git a/Cargo.toml b/Cargo.toml index 5a65d3c902..9e06e927e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -96,11 +96,3 @@ module_name_repetitions = "allow" # Many triggers, and is a stylistic choice must_use_candidate = "allow" # This marks many fn's which isn't helpful. should_panic_without_expect = "allow" # We don't care about the specific panic message. # End of pedantic lints. - -# Local patch for Phase 2 development (issue #1739). Remove before merging. -[patch."https://github.com/0xMiden/protocol.git"] -miden-protocol = { path = "../protocol/crates/miden-protocol" } -miden-standards = { path = "../protocol/crates/miden-standards" } -miden-testing = { path = "../protocol/crates/miden-testing" } -miden-tx = { path = "../protocol/crates/miden-tx" } -miden-tx-batch-prover = { path = "../protocol/crates/miden-tx-batch-prover" } From 55fe8e8cd1514112958245a119439f98b21d420e Mon Sep 17 00:00:00 2001 From: JereSalo Date: Mon, 18 May 2026 18:40:32 -0300 Subject: [PATCH 4/6] remove stale comment on swap_public_payback_test --- crates/testing/miden-client-tests/src/tests.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/crates/testing/miden-client-tests/src/tests.rs b/crates/testing/miden-client-tests/src/tests.rs index 11b78e7273..e305af11d1 100644 --- a/crates/testing/miden-client-tests/src/tests.rs +++ b/crates/testing/miden-client-tests/src/tests.rs @@ -2589,9 +2589,6 @@ async fn swap_chain_test() { assert_eq!(last_wallet_balance, 1); } -/// Exercises SWAP with `NoteType::Public` payback. With pre-Phase-2 SWAP this fails because the -/// consumer cannot produce the public payback recipient without off-band advice; with the new -/// SWAP standard the payback recipient is derived deterministically and the trade completes. #[tokio::test] async fn swap_public_payback_test() { let (mut client, mock_rpc_api, keystore) = create_test_client().await; From eef98f631ff8ec3940e225b12cdc52bee6af8c3f Mon Sep 17 00:00:00 2001 From: JereSalo Date: Fri, 19 Jun 2026 17:31:16 -0300 Subject: [PATCH 5/6] adapt swap payback tests to merged-next account and note APIs --- .../src/tests/swap_transaction.rs | 18 +++++++------- .../testing/miden-client-tests/src/tests.rs | 24 +++++++------------ 2 files changed, 18 insertions(+), 24 deletions(-) diff --git a/bin/integration-tests/src/tests/swap_transaction.rs b/bin/integration-tests/src/tests/swap_transaction.rs index f8ba79648b..cbbf286fae 100644 --- a/bin/integration-tests/src/tests/swap_transaction.rs +++ b/bin/integration-tests/src/tests/swap_transaction.rs @@ -184,14 +184,14 @@ pub async fn test_swap_public_payback(client_config: ClientConfig) -> Result<()> let (account_a, ..) = insert_new_wallet( &mut client1, - AccountStorageMode::Private, + AccountType::Private, &authenticator_1, RPO_FALCON_SCHEME_ID, ) .await?; let (account_b, ..) = insert_new_wallet( &mut client2, - AccountStorageMode::Private, + AccountType::Private, &authenticator_2, RPO_FALCON_SCHEME_ID, ) @@ -199,14 +199,14 @@ pub async fn test_swap_public_payback(client_config: ClientConfig) -> Result<()> let (btc_faucet_account, _) = insert_new_fungible_faucet( &mut client1, - AccountStorageMode::Private, + AccountType::Private, &authenticator_1, RPO_FALCON_SCHEME_ID, ) .await?; let (eth_faucet_account, _) = insert_new_fungible_faucet( &mut client2, - AccountStorageMode::Private, + AccountType::Private, &authenticator_2, RPO_FALCON_SCHEME_ID, ) @@ -267,12 +267,14 @@ pub async fn test_swap_public_payback(client_config: ClientConfig) -> Result<()> execute_tx_and_sync(&mut client2, account_b.id(), tx_request).await?; client1.sync_state().await?; - info!(note_id = %expected_payback_note_details[0].id(), account_id = %account_a.id(), "Consuming public payback note on client 1"); + let payback_commitment = expected_payback_note_details[0].commitment(); + info!(note = %payback_commitment.to_hex(), account_id = %account_a.id(), "Consuming public payback note on client 1"); - let note = client1 - .get_input_note(expected_payback_note_details[0].id()) + let note: Note = client1 + .get_input_notes(NoteFilter::DetailsCommitments(vec![payback_commitment])) .await? - .unwrap() + .pop() + .with_context(|| format!("Payback note {} not found", payback_commitment.to_hex()))? .try_into()?; let tx_request = TransactionRequestBuilder::new().build_consume_notes(vec![note])?; execute_tx_and_sync(&mut client1, account_a.id(), tx_request).await?; diff --git a/crates/testing/miden-client-tests/src/tests.rs b/crates/testing/miden-client-tests/src/tests.rs index a4c4e2ad9e..e0dda9e476 100644 --- a/crates/testing/miden-client-tests/src/tests.rs +++ b/crates/testing/miden-client-tests/src/tests.rs @@ -2617,26 +2617,18 @@ async fn swap_chain_test() { async fn swap_public_payback_test() { let (mut client, mock_rpc_api, keystore) = create_test_client().await; - let (wallet_a, faucet_a) = setup_wallet_and_faucet( - &mut client, - AccountStorageMode::Private, - &keystore, - RPO_FALCON_SCHEME_ID, - ) - .await - .unwrap(); + let (wallet_a, faucet_a) = + setup_wallet_and_faucet(&mut client, AccountType::Private, &keystore, RPO_FALCON_SCHEME_ID) + .await + .unwrap(); mint_and_consume(&mut client, wallet_a.id(), faucet_a.id(), NoteType::Private).await; mock_rpc_api.prove_block(); client.sync_state().await.unwrap(); - let (wallet_b, faucet_b) = setup_wallet_and_faucet( - &mut client, - AccountStorageMode::Private, - &keystore, - RPO_FALCON_SCHEME_ID, - ) - .await - .unwrap(); + let (wallet_b, faucet_b) = + setup_wallet_and_faucet(&mut client, AccountType::Private, &keystore, RPO_FALCON_SCHEME_ID) + .await + .unwrap(); mint_and_consume(&mut client, wallet_b.id(), faucet_b.id(), NoteType::Private).await; mock_rpc_api.prove_block(); client.sync_state().await.unwrap(); From 4ccc9aac5a40b4cdfa51a21908c51c76c9b39be4 Mon Sep 17 00:00:00 2001 From: JereSalo Date: Mon, 20 Jul 2026 12:15:09 -0300 Subject: [PATCH 6/6] move changelog entry to unreleased, document --payback-note-type, assert payback note visibility in tests --- CHANGELOG.md | 7 ++++++- .../src/tests/swap_transaction.rs | 16 +++++++++++++--- crates/testing/miden-client-tests/src/tests.rs | 10 ++++++++++ docs/external/src/rust-client/cli/index.md | 4 +++- 4 files changed, 32 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e2900cebe..4ddad0e579 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Enhancements + +* [FEATURE][cli] Added a `--payback-note-type` option to `swap` so the payback note can be created as public or private (defaults to private). Public payback works without any off-band advice now that SWAP derives the payback recipient deterministically ([#2190](https://github.com/0xMiden/rust-sdk/pull/2190)). + ## 0.16.0-alpha.1 (2026-07-17) ### Breaking Changes @@ -118,7 +124,6 @@ ### Enhancements -* [FEATURE][cli] `swap` now accepts `--payback-note-type` so the payback note can be created as public or private. Public payback works without any off-band advice now that SWAP derives the payback recipient deterministically. * [FEATURE][rust] Added `Client::sync_chain()` (on-chain sync only) and `Client::sync_note_transport()` (Note Transport Layer fetch only) for callers needing finer-grained control over sync. ([#2091](https://github.com/0xMiden/rust-sdk/pull/2091)) * [FEATURE][rust] Added `GrpcClient::with_bearer_auth(token)` to attach an `authorization: Bearer ` header to every outbound gRPC call, for use behind authenticating gateways. Tokens are validated at connection time and preserved across `set_genesis_commitment` updates ([#2101](https://github.com/0xMiden/rust-sdk/pull/2101)). * Made new-account construction use merged storage schema commitment (`build_with_schema_commitment`), re-exported `AccountBuilderSchemaCommitmentExt`, added WASM `buildWithoutSchemaCommitment()`, and fixed contract `accounts.create()` to require explicit `components` ([#1996](https://github.com/0xMiden/rust-sdk/pull/1996)). diff --git a/bin/integration-tests/src/tests/swap_transaction.rs b/bin/integration-tests/src/tests/swap_transaction.rs index 1c0d469b5f..664650a1e6 100644 --- a/bin/integration-tests/src/tests/swap_transaction.rs +++ b/bin/integration-tests/src/tests/swap_transaction.rs @@ -271,12 +271,22 @@ pub async fn test_swap_public_payback(client_config: ClientConfig) -> Result<()> let payback_commitment = expected_payback_note_details[0].commitment(); info!(note = %payback_commitment.to_hex(), account_id = %account_a.id(), "Consuming public payback note on client 1"); - let note: Note = client1 + let payback_record = client1 .get_input_notes(NoteFilter::DetailsCommitments(vec![payback_commitment])) .await? .pop() - .with_context(|| format!("Payback note {} not found", payback_commitment.to_hex()))? - .try_into()?; + .with_context(|| format!("Payback note {} not found", payback_commitment.to_hex()))?; + + // The node must have committed the payback as a public note. + assert_eq!( + payback_record + .metadata() + .context("payback note should have metadata")? + .note_type(), + NoteType::Public + ); + + let note: Note = payback_record.try_into()?; let tx_request = TransactionRequestBuilder::new().build_consume_notes(vec![note])?; execute_tx_and_sync(&mut client1, account_a.id(), tx_request).await?; diff --git a/crates/testing/miden-client-tests/src/tests.rs b/crates/testing/miden-client-tests/src/tests.rs index 59f2b2db84..9abaf37284 100644 --- a/crates/testing/miden-client-tests/src/tests.rs +++ b/crates/testing/miden-client-tests/src/tests.rs @@ -2733,6 +2733,7 @@ async fn swap_public_payback_test() { .unwrap(); let swap_note = tx_request.expected_output_own_notes()[0].clone(); + let payback_commitment = tx_request.expected_future_notes().next().unwrap().0.commitment(); Box::pin(client.submit_new_transaction(wallet_a.id(), tx_request)) .await .unwrap(); @@ -2748,6 +2749,15 @@ async fn swap_public_payback_test() { mock_rpc_api.prove_block(); client.sync_state().await.unwrap(); + // The payback note's committed metadata must show it was emitted as a public note. + let payback_record = client + .get_input_notes(NoteFilter::DetailsCommitments(vec![payback_commitment])) + .await + .unwrap() + .pop() + .unwrap(); + assert_eq!(payback_record.metadata().unwrap().note_type(), NoteType::Public); + // wallet_b ended up with asset_a, wallet_a will receive asset_b via the public payback note. let wallet_b_balance = client.account_reader(wallet_b.id()).get_balance(faucet_a.id()).await.unwrap(); diff --git a/docs/external/src/rust-client/cli/index.md b/docs/external/src/rust-client/cli/index.md index 277be2dad7..29da31343c 100644 --- a/docs/external/src/rust-client/cli/index.md +++ b/docs/external/src/rust-client/cli/index.md @@ -302,7 +302,9 @@ Usage: `miden-client transfer --sender --target --offered-asset :: --requested-asset :: --note-type ` +Usage: `miden-client swap --source --offered-asset :: --requested-asset :: --note-type [--payback-note-type ]` + +The `--payback-note-type` option controls the visibility of the payback note created when the swap is consumed. It defaults to `private`. ### `address`