Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,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 <token>` 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)).
Expand Down
129 changes: 129 additions & 0 deletions bin/integration-tests/src/tests/swap_transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,135 @@ 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,
AccountType::Private,
&authenticator_1,
RPO_FALCON_SCHEME_ID,
)
.await?;
let (account_b, ..) = insert_new_wallet(
&mut client2,
AccountType::Private,
&authenticator_2,
RPO_FALCON_SCHEME_ID,
)
.await?;

let (btc_faucet_account, _) = insert_new_fungible_faucet(
&mut client1,
AccountType::Private,
&authenticator_1,
RPO_FALCON_SCHEME_ID,
)
.await?;
let (eth_faucet_account, _) = insert_new_fungible_faucet(
&mut client2,
AccountType::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<Note> = tx_request.expected_output_own_notes();
let expected_payback_note_details: Vec<NoteDetails> =
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?;
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
.get_input_notes(NoteFilter::DetailsCommitments(vec![payback_commitment]))
.await?
.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?;

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;
Expand Down
18 changes: 9 additions & 9 deletions bin/miden-cli/src/commands/new_transactions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ use miden_client::keystore::Keystore;
use miden_client::note::{
BlockNumber,
Note,
NoteTag,
NoteType as MidenNoteType,
SwapNote,
get_input_note_with_id_prefix,
};
use miden_client::store::NoteRecordError;
Expand Down Expand Up @@ -210,6 +210,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,
Expand Down Expand Up @@ -247,7 +252,7 @@ impl SwapCmd {
.build_swap(
&swap_transaction,
(&self.note_type).into(),
MidenNoteType::Private,
(&self.payback_note_type).into(),
client.rng(),
)
.map_err(|err| {
Expand All @@ -263,14 +268,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(())
Expand Down
63 changes: 60 additions & 3 deletions crates/testing/miden-client-tests/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2613,6 +2613,62 @@ async fn swap_chain_test() {
assert_eq!(last_wallet_balance, 1);
}

#[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, 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, 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();

// 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.
Expand Down Expand Up @@ -2666,9 +2722,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();

Expand Down
Loading