From 8ad3365fbc6b07b1ab9356803c750df77a617075 Mon Sep 17 00:00:00 2001 From: Juan Munoz Date: Thu, 18 Dec 2025 10:02:09 -0300 Subject: [PATCH 01/11] refactor: transaction builder only takes one type of note --- bin/integration-tests/src/tests/client.rs | 31 ++++--- .../src/tests/custom_transaction.rs | 6 +- .../src/tests/network_transaction.rs | 2 +- bin/integration-tests/src/tests/onchain.rs | 5 +- .../src/tests/swap_transaction.rs | 32 +++++-- .../src/commands/new_transactions.rs | 37 ++++---- crates/rust-client/src/note/note_screener.rs | 2 +- crates/rust-client/src/test_utils/common.rs | 15 ++-- crates/rust-client/src/transaction/mod.rs | 32 ++++--- .../src/transaction/request/builder.rs | 29 ++---- .../src/transaction/request/mod.rs | 59 +++--------- .../testing/miden-client-tests/src/tests.rs | 89 +++++++++++-------- .../src/models/input_note_record.rs | 11 +++ crates/web-client/src/models/note.rs | 2 +- .../transaction_request_builder.rs | 19 +--- crates/web-client/src/new_transactions.rs | 15 ++-- crates/web-client/test/mockchain.test.ts | 8 +- .../web-client/test/new_transactions.test.ts | 54 ++++++++--- crates/web-client/test/notes.test.ts | 16 +++- crates/web-client/test/webClientTestUtils.ts | 34 +++++-- 20 files changed, 288 insertions(+), 210 deletions(-) diff --git a/bin/integration-tests/src/tests/client.rs b/bin/integration-tests/src/tests/client.rs index 9ecd2d049..a31e53041 100644 --- a/bin/integration-tests/src/tests/client.rs +++ b/bin/integration-tests/src/tests/client.rs @@ -484,7 +484,7 @@ pub async fn test_sync_detail_values(client_config: ClientConfig) -> Result<()> NoteType::Public, client1.rng(), )?; - let note_id = tx_request.expected_output_own_notes().pop().unwrap().id(); + let note = tx_request.expected_output_own_notes().pop().unwrap(); execute_tx_and_sync(&mut client1, from_account_id, tx_request).await?; // Second client sync should have new note @@ -495,7 +495,7 @@ pub async fn test_sync_detail_values(client_config: ClientConfig) -> Result<()> assert_eq!(new_details.updated_accounts.len(), 0); // Consume the note with the second account - let tx_request = TransactionRequestBuilder::new().build_consume_notes(vec![note_id]).unwrap(); + let tx_request = TransactionRequestBuilder::new().build_consume_notes(vec![note]).unwrap(); execute_tx_and_sync(&mut client2, to_account_id, tx_request).await?; // First client sync should have a new nullifier as the note was consumed @@ -715,11 +715,11 @@ pub async fn test_consume_multiple_expected_notes(client_config: ClientConfig) - // Create and execute transactions let tx_request_1 = TransactionRequestBuilder::new() - .authenticated_input_notes(client_owned_notes.iter().map(|note| (note.id(), None))) + .input_notes(client_owned_notes.iter().map(|note| (note.clone(), None))) .build()?; let tx_request_2 = TransactionRequestBuilder::new() - .unauthenticated_input_notes(unauth_owned_notes.iter().map(|note| ((*note).clone(), None))) + .input_notes(unauth_owned_notes.iter().map(|note| ((*note).clone(), None))) .build()?; let tx_id_1 = client.submit_new_transaction(to_account_ids[0], tx_request_1).await.unwrap(); @@ -816,7 +816,9 @@ pub async fn test_import_consumed_note_with_proof(client_config: ClientConfig) - // Consume the note with the sender account println!("Consuming Note..."); - let tx_request = TransactionRequestBuilder::new().build_consume_notes(vec![note.id()]).unwrap(); + let tx_request = TransactionRequestBuilder::new() + .build_consume_notes(vec![note.clone().try_into().unwrap()]) + .unwrap(); execute_tx_and_sync(&mut client_1, from_account_id, tx_request).await?; // Import the consumed note @@ -881,7 +883,9 @@ pub async fn test_import_consumed_note_with_id(client_config: ClientConfig) -> R // Consume the note with the sender account println!("Consuming Note..."); - let tx_request = TransactionRequestBuilder::new().build_consume_notes(vec![note.id()]).unwrap(); + let tx_request = TransactionRequestBuilder::new() + .build_consume_notes(vec![note.clone().try_into().unwrap()]) + .unwrap(); execute_tx_and_sync(&mut client_1, from_account_id, tx_request).await?; client_2.sync_state().await.unwrap(); @@ -1012,7 +1016,9 @@ pub async fn test_discarded_transaction(client_config: ClientConfig) -> Result<( .clone(); println!("Consuming Note..."); - let tx_request = TransactionRequestBuilder::new().build_consume_notes(vec![note.id()]).unwrap(); + let tx_request = TransactionRequestBuilder::new() + .build_consume_notes(vec![note.clone().try_into().unwrap()]) + .unwrap(); // Consume the note in client 1 but dont submit it to the node let transaction_result = @@ -1390,10 +1396,10 @@ pub async fn test_unused_rpc_api(client_config: ClientConfig) -> Result<()> { NoteType::Public, client.rng(), )?; - let note_id = tx_request.expected_output_own_notes().pop().unwrap().id(); + let note = tx_request.expected_output_own_notes().pop().unwrap(); execute_tx_and_sync(&mut client, fungible_asset.faucet_id(), tx_request.clone()).await?; - let tx_request = TransactionRequestBuilder::new().build_consume_notes(vec![note_id])?; + let tx_request = TransactionRequestBuilder::new().build_consume_notes(vec![note.clone()])?; execute_tx_and_sync(&mut client, first_basic_account.id(), tx_request).await?; let nullifier = note.nullifier(); @@ -1483,7 +1489,12 @@ pub async fn test_ignore_invalid_notes(client_config: ClientConfig) -> Result<() // Create a transaction to consume all 4 notes but ignore the invalid ones let tx_request = TransactionRequestBuilder::new() .ignore_invalid_input_notes() - .build_consume_notes(vec![note_1.id(), note_3.id(), note_2.id(), note_4.id()])?; + .build_consume_notes(vec![ + note_1.clone(), + note_3.clone(), + note_2.clone(), + note_4.clone(), + ])?; execute_tx_and_sync(&mut client, account_id, tx_request).await?; diff --git a/bin/integration-tests/src/tests/custom_transaction.rs b/bin/integration-tests/src/tests/custom_transaction.rs index 7630b03d8..b3b7b1423 100644 --- a/bin/integration-tests/src/tests/custom_transaction.rs +++ b/bin/integration-tests/src/tests/custom_transaction.rs @@ -104,7 +104,7 @@ pub async fn test_transaction_request(client_config: ClientConfig) -> Result<()> // FAILURE ATTEMPT let transaction_request = TransactionRequestBuilder::new() - .unauthenticated_input_notes(note_args_map.clone()) + .input_notes(note_args_map.clone()) .custom_script(tx_script.clone()) .script_arg(Word::empty()) .extend_advice_map(advice_map.clone()) @@ -120,7 +120,7 @@ pub async fn test_transaction_request(client_config: ClientConfig) -> Result<()> // SUCCESS EXECUTION let transaction_request = TransactionRequestBuilder::new() - .unauthenticated_input_notes(note_args_map) + .input_notes(note_args_map) .custom_script(tx_script) .script_arg([Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)].into()) .extend_advice_map(advice_map) @@ -239,7 +239,7 @@ pub async fn test_merkle_store(client_config: ClientConfig) -> Result<()> { let tx_script = client.script_builder().compile_tx_script(&code)?; let transaction_request = TransactionRequestBuilder::new() - .unauthenticated_input_notes(note_args_map) + .input_notes(note_args_map) .custom_script(tx_script) .extend_advice_map(advice_map) .extend_merkle_store(merkle_store.inner_nodes()) diff --git a/bin/integration-tests/src/tests/network_transaction.rs b/bin/integration-tests/src/tests/network_transaction.rs index 80c44b8cb..de3c8ecc6 100644 --- a/bin/integration-tests/src/tests/network_transaction.rs +++ b/bin/integration-tests/src/tests/network_transaction.rs @@ -201,7 +201,7 @@ pub async fn test_recall_note_before_ntx_consumes_it(client_config: ClientConfig client.apply_transaction(&bump_result, current_height).await?; let tx_request = TransactionRequestBuilder::new() - .unauthenticated_input_notes(vec![(network_note, None)]) + .input_notes(vec![(network_note, None)]) .build()?; let consume_result = client.execute_transaction(native_account.id(), tx_request).await?; diff --git a/bin/integration-tests/src/tests/onchain.rs b/bin/integration-tests/src/tests/onchain.rs index 63b2422ab..7652c74cd 100644 --- a/bin/integration-tests/src/tests/onchain.rs +++ b/bin/integration-tests/src/tests/onchain.rs @@ -129,7 +129,7 @@ pub async fn test_onchain_notes_flow(client_config: ClientConfig) -> Result<()> .clone(); execute_tx_and_sync(&mut client_2, basic_wallet_1.id(), tx_request).await?; - let tx_request = TransactionRequestBuilder::new().build_consume_notes(vec![note.id()])?; + let tx_request = TransactionRequestBuilder::new().build_consume_notes(vec![note.clone()])?; execute_tx_and_sync(&mut client_2, basic_wallet_1.id(), tx_request).await?; // sync client 3 (basic account 2) @@ -301,7 +301,8 @@ pub async fn test_onchain_accounts(client_config: ClientConfig) -> Result<()> { // Consume the note println!("Consuming note on second client..."); - let tx_request = TransactionRequestBuilder::new().build_consume_notes(vec![notes[0].id()])?; + let tx_request = TransactionRequestBuilder::new() + .build_consume_notes(vec![notes[0].clone().try_into().unwrap()])?; execute_tx_and_sync(&mut client_2, to_account_id, tx_request).await?; // sync on first client diff --git a/bin/integration-tests/src/tests/swap_transaction.rs b/bin/integration-tests/src/tests/swap_transaction.rs index 0ebd0db8a..5c67dae0c 100644 --- a/bin/integration-tests/src/tests/swap_transaction.rs +++ b/bin/integration-tests/src/tests/swap_transaction.rs @@ -118,8 +118,12 @@ pub async fn test_swap_fully_onchain(client_config: ClientConfig) -> Result<()> client2.sync_state().await?; println!("Consuming swap note on second client..."); - let tx_request = TransactionRequestBuilder::new() - .build_consume_notes(vec![expected_output_notes[0].id()])?; + 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?; // sync on client 1, we should get the missing payback note details. @@ -127,8 +131,12 @@ pub async fn test_swap_fully_onchain(client_config: ClientConfig) -> Result<()> client1.sync_state().await?; println!("Consuming swap payback note on first client..."); - let tx_request = TransactionRequestBuilder::new() - .build_consume_notes(vec![expected_payback_note_details[0].id()])?; + 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?; // At the end we should end up with @@ -322,8 +330,12 @@ pub async fn test_swap_private(client_config: ClientConfig) -> Result<()> { // consume swap note with accountB, and check that the vault changed appropriately println!("Consuming swap note on second client..."); - let tx_request = TransactionRequestBuilder::new() - .build_consume_notes(vec![expected_output_notes[0].id()])?; + 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?; // sync on client 1, we should get the missing payback note details. @@ -331,8 +343,12 @@ pub async fn test_swap_private(client_config: ClientConfig) -> Result<()> { client1.sync_state().await?; println!("Consuming swap payback note on first client..."); - let tx_request = TransactionRequestBuilder::new() - .build_consume_notes(vec![expected_payback_note_details[0].id()])?; + 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?; // At the end we should end up with diff --git a/bin/miden-cli/src/commands/new_transactions.rs b/bin/miden-cli/src/commands/new_transactions.rs index ea5bb3f4c..1d70158cd 100644 --- a/bin/miden-cli/src/commands/new_transactions.rs +++ b/bin/miden-cli/src/commands/new_transactions.rs @@ -311,7 +311,7 @@ impl ConsumeNotesCmd { let force = self.force; let mut authenticated_notes = Vec::new(); - let mut unauthenticated_notes = Vec::new(); + let mut input_notes = Vec::new(); for note_id in &self.list_of_notes { let note_record = get_input_note_with_id_prefix(&client, note_id) @@ -320,17 +320,14 @@ impl ConsumeNotesCmd { if note_record.is_authenticated() { authenticated_notes.push(note_record.id()); - } else { - unauthenticated_notes.push(( - note_record.try_into().map_err(|err: NoteRecordError| { - CliError::Transaction( - err.into(), - "Failed to convert note record".to_string(), - ) - })?, - None, - )); } + + input_notes.push(( + note_record.try_into().map_err(|err: NoteRecordError| { + CliError::Transaction(err.into(), "Failed to convert note record".to_string()) + })?, + None, + )); } let account_id = @@ -339,11 +336,20 @@ impl ConsumeNotesCmd { if authenticated_notes.is_empty() { info!("No input note IDs provided, getting all notes consumable by {}", account_id); let consumable_notes = client.get_consumable_notes(Some(account_id)).await?; - - authenticated_notes.extend(consumable_notes.iter().map(|(note, _)| note.id())); + for (note_record, _) in consumable_notes { + input_notes.push(( + note_record.try_into().map_err(|err: NoteRecordError| { + CliError::Transaction( + err.into(), + "Failed to convert note record".to_string(), + ) + })?, + None, + )); + } } - if authenticated_notes.is_empty() && unauthenticated_notes.is_empty() { + if authenticated_notes.is_empty() && input_notes.is_empty() { return Err(CliError::Transaction( "No input notes were provided and the store does not contain any notes consumable by {account_id}".into(), "Input notes check failed".to_string(), @@ -351,8 +357,7 @@ impl ConsumeNotesCmd { } let transaction_request = TransactionRequestBuilder::new() - .authenticated_input_notes(authenticated_notes.into_iter().map(|id| (id, None))) - .unauthenticated_input_notes(unauthenticated_notes) + .input_notes(input_notes) .build() .map_err(|err| { CliError::Transaction( diff --git a/crates/rust-client/src/note/note_screener.rs b/crates/rust-client/src/note/note_screener.rs index 24e0d06b4..a2d433a91 100644 --- a/crates/rust-client/src/note/note_screener.rs +++ b/crates/rust-client/src/note/note_screener.rs @@ -117,7 +117,7 @@ where note: &Note, ) -> Result, NoteScreenerError> { let transaction_request = - TransactionRequestBuilder::new().build_consume_notes(vec![note.id()])?; + TransactionRequestBuilder::new().build_consume_notes(vec![note.clone()])?; let tx_script = transaction_request.build_transaction_script( &AccountInterface::from(account), diff --git a/crates/rust-client/src/test_utils/common.rs b/crates/rust-client/src/test_utils/common.rs index 5859efb1e..ffd4206e3 100644 --- a/crates/rust-client/src/test_utils/common.rs +++ b/crates/rust-client/src/test_utils/common.rs @@ -12,7 +12,7 @@ use anyhow::{Context, Result}; use miden_objects::account::auth::AuthSecretKey; use miden_objects::account::{Account, AccountId, AccountStorageMode}; use miden_objects::asset::{Asset, FungibleAsset, TokenSymbol}; -use miden_objects::note::{NoteId, NoteType}; +use miden_objects::note::NoteType; use miden_objects::transaction::{OutputNote, TransactionId}; use miden_objects::{Felt, FieldElement}; use rand::RngCore; @@ -427,7 +427,7 @@ pub async fn consume_notes( ) -> TransactionId { println!("Consuming Note..."); let tx_request = TransactionRequestBuilder::new() - .build_consume_notes(input_notes.iter().map(Note::id).collect()) + .build_consume_notes(input_notes.to_vec()) .unwrap(); Box::pin(client.submit_new_transaction(account_id, tx_request)).await.unwrap() } @@ -457,14 +457,14 @@ pub async fn assert_account_has_single_asset( pub async fn assert_note_cannot_be_consumed_twice( client: &mut TestClient, consuming_account_id: AccountId, - note_to_consume_id: NoteId, + note_to_consume: Note, ) { // Check that we can't consume the P2ID note again println!("Consuming Note..."); // Double-spend error expected to be received since we are consuming the same note let tx_request = TransactionRequestBuilder::new() - .build_consume_notes(vec![note_to_consume_id]) + .build_consume_notes(vec![note_to_consume.clone()]) .unwrap(); match Box::pin(client.submit_new_transaction(consuming_account_id, tx_request)).await { @@ -472,7 +472,7 @@ pub async fn assert_note_cannot_be_consumed_twice( TransactionRequestError::InputNoteAlreadyConsumed(_), )) => {}, Ok(_) => panic!("Double-spend error: Note should not be consumable!"), - err => panic!("Unexpected error {:?} for note ID: {}", err, note_to_consume_id.to_hex()), + err => panic!("Unexpected error {:?} for note ID: {}", err, note_to_consume.id().to_hex()), } } @@ -519,10 +519,7 @@ pub async fn execute_tx_and_consume_output_notes( Box::pin(client.submit_new_transaction(executor, tx_request)).await.unwrap(); - let tx_request = TransactionRequestBuilder::new() - .unauthenticated_input_notes(output_notes) - .build() - .unwrap(); + let tx_request = TransactionRequestBuilder::new().input_notes(output_notes).build().unwrap(); Box::pin(client.submit_new_transaction(consumer, tx_request)).await.unwrap() } diff --git a/crates/rust-client/src/transaction/mod.rs b/crates/rust-client/src/transaction/mod.rs index c4228503d..8c4223e73 100644 --- a/crates/rust-client/src/transaction/mod.rs +++ b/crates/rust-client/src/transaction/mod.rs @@ -211,20 +211,29 @@ where // Validates the transaction request before executing self.validate_request(account_id, &transaction_request).await?; - // Ensure authenticated notes have their inclusion proofs (a.k.a they're in a committed - // state) - let authenticated_input_note_ids: Vec = - transaction_request.authenticated_input_note_ids().collect::>(); - - let authenticated_note_records = self + // Retrieve all input notes from the store. + // But only mark as authenticated if they are committed or consumed. + let mut authenticated_note_records = self .store - .get_input_notes(NoteFilter::List(authenticated_input_note_ids)) + .get_input_notes(NoteFilter::List(transaction_request.get_input_note_ids())) .await?; + authenticated_note_records.retain(|note| { + matches!( + note.state(), + InputNoteState::Committed(_) + | InputNoteState::ConsumedAuthenticatedLocal(_) + | InputNoteState::ConsumedExternal(_) + ) + }); + + let authenticated_note_ids = + authenticated_note_records.iter().map(InputNoteRecord::id).collect::>(); // If tx request contains unauthenticated_input_notes we should insert them let unauthenticated_input_notes = transaction_request .unauthenticated_input_notes() .iter() + .filter(|n| !authenticated_note_ids.contains(&n.id())) .cloned() .map(Into::into) .collect::>(); @@ -296,7 +305,6 @@ where .await?; validate_executed_transaction(&executed_transaction, &output_recipients)?; - TransactionResult::new(executed_transaction, future_notes) } @@ -592,17 +600,17 @@ where { // Get incoming asset notes excluding unauthenticated ones let incoming_notes_ids: Vec<_> = transaction_request - .input_notes() + .unauthenticated_input_notes() .iter() - .filter_map(|(note_id, _)| { + .filter_map(|note| { if transaction_request .unauthenticated_input_notes() .iter() - .any(|note| note.id() == *note_id) + .any(|n| n.id() == note.id()) { None } else { - Some(*note_id) + Some(note.id()) } }) .collect(); diff --git a/crates/rust-client/src/transaction/request/builder.rs b/crates/rust-client/src/transaction/request/builder.rs index 275060f69..2a82a7d94 100644 --- a/crates/rust-client/src/transaction/request/builder.rs +++ b/crates/rust-client/src/transaction/request/builder.rs @@ -39,6 +39,7 @@ use crate::ClientRng; /// scripts, and setting other transaction parameters. #[derive(Clone, Debug)] pub struct TransactionRequestBuilder { + // TODO: merge `unauthenticated_input_notes` & `unauthenticated_input_notes` into one /// Notes to be consumed by the transaction that aren't authenticated. unauthenticated_input_notes: Vec, /// Notes to be consumed by the transaction together with their (optional) arguments. This @@ -103,9 +104,9 @@ impl TransactionRequestBuilder { } } - /// Adds the specified notes as unauthenticated input notes to the transaction request. + /// TODO: docs #[must_use] - pub fn unauthenticated_input_notes( + pub fn input_notes( mut self, notes: impl IntoIterator)>, ) -> Self { @@ -116,18 +117,6 @@ impl TransactionRequestBuilder { self } - /// Adds the specified notes as authenticated input notes to the transaction request. - #[must_use] - pub fn authenticated_input_notes( - mut self, - notes: impl IntoIterator)>, - ) -> Self { - for (note_id, argument) in notes { - self.input_notes.push((note_id, argument)); - } - self - } - /// Specifies the output notes that should be created in the transaction script and will /// be used as a transaction script template. These notes will also be added to the expected /// output recipients of the transaction. @@ -269,13 +258,13 @@ impl TransactionRequestBuilder { /// Consumes the builder and returns a [`TransactionRequest`] for a transaction to consume the /// specified notes. /// - /// - `note_ids` is a list of note IDs to be consumed. + /// - `notes` is a list of notes to be consumed. pub fn build_consume_notes( self, - note_ids: Vec, + notes: Vec, // TODO: SHOULD BE NOTE ) -> Result { - let input_notes = note_ids.into_iter().map(|id| (id, None)); - self.authenticated_input_notes(input_notes).build() + let input_notes = notes.into_iter().map(|id| (id, None)); + self.input_notes(input_notes).build() } /// Consumes the builder and returns a [`TransactionRequest`] for a transaction to mint fungible @@ -424,8 +413,8 @@ impl TransactionRequestBuilder { }; Ok(TransactionRequest { - unauthenticated_input_notes: self.unauthenticated_input_notes, - input_notes: self.input_notes, + input_notes: self.unauthenticated_input_notes, + input_notes_args: self.input_notes, script_template, expected_output_recipients: self.expected_output_recipients, expected_future_notes: self.expected_future_notes, diff --git a/crates/rust-client/src/transaction/request/mod.rs b/crates/rust-client/src/transaction/request/mod.rs index ffb026f82..075dfb607 100644 --- a/crates/rust-client/src/transaction/request/mod.rs +++ b/crates/rust-client/src/transaction/request/mod.rs @@ -61,11 +61,12 @@ pub enum TransactionScriptTemplate { /// to be generated by the transaction or by consuming notes generated by the transaction. #[derive(Clone, Debug, PartialEq, Eq)] pub struct TransactionRequest { - /// Notes to be consumed by the transaction that aren't authenticated. - unauthenticated_input_notes: Vec, - /// Notes to be consumed by the transaction together with their (optional) arguments. This + /// Notes to be consumed by the transaction. /// includes both authenticated and unauthenticated notes. - input_notes: Vec<(NoteId, Option)>, + input_notes: Vec, + /// Optional arguments of the input notes to be consumed by the transaction. This + /// includes both authenticated and unauthenticated notes. + input_notes_args: Vec<(NoteId, Option)>, /// Template for the creation of the transaction script. script_template: Option, /// A map of recipients of the output notes expected to be generated by the transaction. @@ -102,44 +103,21 @@ impl TransactionRequest { // PUBLIC ACCESSORS // -------------------------------------------------------------------------------------------- + // TODO: RENAME /// Returns a reference to the transaction request's unauthenticated note list. pub fn unauthenticated_input_notes(&self) -> &[Note] { - &self.unauthenticated_input_notes - } - - /// Returns an iterator over unauthenticated note IDs for the transaction request. - pub fn unauthenticated_input_note_ids(&self) -> impl Iterator + '_ { - self.unauthenticated_input_notes.iter().map(Note::id) - } - - /// Returns an iterator over authenticated input note IDs for the transaction request. - pub fn authenticated_input_note_ids(&self) -> impl Iterator + '_ { - let unauthenticated_note_ids = - self.unauthenticated_input_note_ids().collect::>(); - - self.input_notes().iter().filter_map(move |(note_id, _)| { - if unauthenticated_note_ids.contains(note_id) { - None - } else { - Some(*note_id) - } - }) - } - - /// Returns the input note IDs and their optional [`NoteArgs`]. - pub fn input_notes(&self) -> &[(NoteId, Option)] { &self.input_notes } /// Returns a list of all input note IDs. pub fn get_input_note_ids(&self) -> Vec { - self.input_notes.iter().map(|(id, _)| *id).collect() + self.input_notes.iter().map(Note::id).collect() } /// Returns a map of note IDs to their respective [`NoteArgs`]. The result will include /// exclusively note IDs for notes for which [`NoteArgs`] have been defined. pub fn get_note_args(&self) -> BTreeMap { - self.input_notes + self.input_notes_args .iter() .filter_map(|(note, args)| args.map(|a| (*note, a))) .collect() @@ -251,16 +229,8 @@ impl TransactionRequest { ); } - // Ensure that all authenticated input notes are present in the input notes map before - // continuing. - for id in self.authenticated_input_note_ids() { - if !input_notes.contains_key(&id) { - return Err(TransactionRequestError::MissingAuthenticatedInputNote(id)); - } - } - // Add unauthenticated input notes to the input notes map. - for unauthenticated_input_notes in &self.unauthenticated_input_notes { + for unauthenticated_input_notes in &self.input_notes { input_notes.insert( unauthenticated_input_notes.id(), InputNote::Unauthenticated { @@ -336,8 +306,8 @@ impl TransactionRequest { impl Serializable for TransactionRequest { fn write_into(&self, target: &mut W) { - self.unauthenticated_input_notes.write_into(target); self.input_notes.write_into(target); + self.input_notes_args.write_into(target); match &self.script_template { None => target.write_u8(0), Some(TransactionScriptTemplate::CustomScript(script)) => { @@ -395,8 +365,8 @@ impl Deserializable for TransactionRequest { let auth_arg = Option::::read_from(source)?; Ok(TransactionRequest { - unauthenticated_input_notes, - input_notes, + input_notes: unauthenticated_input_notes, + input_notes_args: input_notes, script_template, expected_output_recipients, expected_future_notes, @@ -448,7 +418,7 @@ pub enum TransactionRequestError { #[error("merkle error")] MerkleError(#[from] MerkleError), #[error("specified authenticated input note with id {0} is missing")] - MissingAuthenticatedInputNote(NoteId), + MissingAuthenticatedInputNote(NoteId), // TODO: REMOVE UNUSED ERROR #[error("a transaction without output notes must have at least one input note")] NoInputNotesNorAccountChange, #[error("note not found: {0}")] @@ -554,8 +524,7 @@ mod tests { // This transaction request wouldn't be valid in a real scenario, it's intended for testing let tx_request = TransactionRequestBuilder::new() - .authenticated_input_notes(vec![(notes.pop().unwrap().id(), None)]) - .unauthenticated_input_notes(vec![(notes.pop().unwrap(), None)]) + .input_notes(vec![(notes.pop().unwrap(), None)]) .expected_output_recipients(vec![notes.pop().unwrap().recipient().clone()]) .expected_future_notes(vec![( notes.pop().unwrap().into(), diff --git a/crates/testing/miden-client-tests/src/tests.rs b/crates/testing/miden-client-tests/src/tests.rs index 9640c7904..fec9467c7 100644 --- a/crates/testing/miden-client-tests/src/tests.rs +++ b/crates/testing/miden-client-tests/src/tests.rs @@ -685,10 +685,7 @@ async fn import_processing_note_returns_error() { let note = client.get_input_note(note_id).await.unwrap().unwrap(); let input = [(note.try_into().unwrap(), None)]; - let consume_note_request = TransactionRequestBuilder::new() - .unauthenticated_input_notes(input) - .build() - .unwrap(); + let consume_note_request = TransactionRequestBuilder::new().input_notes(input).build().unwrap(); Box::pin(client.submit_new_transaction(account.id(), consume_note_request)) .await .unwrap(); @@ -865,8 +862,9 @@ async fn real_note_roundtrip() { assert!(matches!(note.state(), &InputNoteState::Committed(_))); // Consume note - let transaction_request = - TransactionRequestBuilder::new().build_consume_notes(vec![note_id]).unwrap(); + let transaction_request = TransactionRequestBuilder::new() + .build_consume_notes(vec![note.clone().try_into().unwrap()]) + .unwrap(); Box::pin(client.submit_new_transaction(wallet.id(), transaction_request)) .await @@ -988,7 +986,7 @@ async fn p2id_transfer() { // Consume P2ID note println!("Consuming Note..."); let tx_request = TransactionRequestBuilder::new() - .build_consume_notes(vec![notes[0].id()]) + .build_consume_notes(vec![notes[0].clone().try_into().unwrap()]) .unwrap(); Box::pin(client.submit_new_transaction(to_account_id, tx_request)) .await @@ -1027,7 +1025,12 @@ async fn p2id_transfer() { panic!("Error: Account should have a fungible asset"); } - assert_note_cannot_be_consumed_twice(&mut client, to_account_id, notes[0].id()).await; + assert_note_cannot_be_consumed_twice( + &mut client, + to_account_id, + notes[0].clone().try_into().unwrap(), + ) + .await; } #[tokio::test] @@ -1166,8 +1169,10 @@ async fn p2ide_transfer_consumed_by_target() { assert!(!notes.is_empty()); // Make the `to_account_id` consume P2IDE note - let note_id = tx_request.expected_output_own_notes().pop().unwrap().id(); - let tx_request = TransactionRequestBuilder::new().build_consume_notes(vec![note_id]).unwrap(); + let note = tx_request.expected_output_own_notes().pop().unwrap(); + let tx_request = TransactionRequestBuilder::new() + .build_consume_notes(vec![note.clone()]) + .unwrap(); Box::pin(client.submit_new_transaction(to_account_id, tx_request)) .await .unwrap(); @@ -1199,7 +1204,7 @@ async fn p2ide_transfer_consumed_by_target() { panic!("Error: Account should have a fungible asset"); } - assert_note_cannot_be_consumed_twice(&mut client, to_account_id, note_id).await; + assert_note_cannot_be_consumed_twice(&mut client, to_account_id, note).await; } #[tokio::test] @@ -1259,7 +1264,7 @@ async fn p2ide_transfer_consumed_by_sender() { // Check that it's still too early to consume println!("Consuming Note (too early)..."); let tx_request = TransactionRequestBuilder::new() - .build_consume_notes(vec![notes[0].id()]) + .build_consume_notes(vec![notes[0].clone().try_into().unwrap()]) .unwrap(); let transaction_execution_result = Box::pin(client.execute_transaction(from_account_id, tx_request)).await; @@ -1280,7 +1285,7 @@ async fn p2ide_transfer_consumed_by_sender() { // Consume the note with the sender account println!("Consuming Note..."); let tx_request = TransactionRequestBuilder::new() - .build_consume_notes(vec![notes[0].id()]) + .build_consume_notes(vec![notes[0].clone().try_into().unwrap()]) .unwrap(); Box::pin(client.submit_new_transaction(from_account_id, tx_request)) .await @@ -1307,7 +1312,12 @@ async fn p2ide_transfer_consumed_by_sender() { assert_eq!(regular_account.vault().assets().count(), 0); // Check that the target can't consume the note anymore - assert_note_cannot_be_consumed_twice(&mut client, to_account_id, notes[0].id()).await; + assert_note_cannot_be_consumed_twice( + &mut client, + to_account_id, + notes[0].clone().try_into().unwrap(), + ) + .await; } #[tokio::test] @@ -1358,7 +1368,9 @@ async fn p2ide_timelocked() { client.sync_state().await.unwrap(); // Check that it's still too early to consume by both accounts - let tx_request = TransactionRequestBuilder::new().build_consume_notes(vec![note.id()]).unwrap(); + let tx_request = TransactionRequestBuilder::new() + .build_consume_notes(vec![note.clone()]) + .unwrap(); let results = [ Box::pin(client.execute_transaction(from_account_id, tx_request.clone())).await, Box::pin(client.execute_transaction(to_account_id, tx_request)).await, @@ -1379,7 +1391,9 @@ async fn p2ide_timelocked() { client.sync_state().await.unwrap(); // Consume the note with the target account - let tx_request = TransactionRequestBuilder::new().build_consume_notes(vec![note.id()]).unwrap(); + let tx_request = TransactionRequestBuilder::new() + .build_consume_notes(vec![note.clone()]) + .unwrap(); Box::pin(client.submit_new_transaction(to_account_id, tx_request)) .await .unwrap(); @@ -1898,7 +1912,7 @@ async fn input_note_checks() { } let duplicate_note_tx_request = TransactionRequestBuilder::new() - .build_consume_notes(vec![mint_notes[0].id(), mint_notes[0].id()]); + .build_consume_notes(vec![mint_notes[0].clone(), mint_notes[0].clone()]); assert!(matches!( duplicate_note_tx_request, @@ -1906,7 +1920,7 @@ async fn input_note_checks() { )); let tx_request = TransactionRequestBuilder::new() - .build_consume_notes(mint_notes.iter().map(Note::id).collect()) + .build_consume_notes(mint_notes.clone()) .unwrap(); let transaction_result = @@ -1933,7 +1947,7 @@ async fn input_note_checks() { // Check that using consumed notes will return an error let consumed_note_tx_request = TransactionRequestBuilder::new() - .build_consume_notes(vec![mint_notes[0].id()]) + .build_consume_notes(vec![mint_notes[0].clone()]) .unwrap(); let error = Box::pin(client.submit_new_transaction(wallet.id(), consumed_note_tx_request)) .await @@ -1944,22 +1958,25 @@ async fn input_note_checks() { ClientError::TransactionRequestError(TransactionRequestError::InputNoteAlreadyConsumed(_)) )); + // TODO: remove deprecated check + // No longer errors if the Note passed is not authenticated, + // It will just not get marked as such. // Check that adding an authenticated note that is not tracked by the client will return an // error - let missing_authenticated_note_tx_request = TransactionRequestBuilder::new() - .build_consume_notes(vec![NoteId::from_raw(EMPTY_WORD)]) - .unwrap(); - let error = - Box::pin(client.submit_new_transaction(wallet.id(), missing_authenticated_note_tx_request)) - .await - .unwrap_err(); - - assert!(matches!( - error, - ClientError::TransactionRequestError( - TransactionRequestError::MissingAuthenticatedInputNote(_) - ) - )); + // let missing_authenticated_note_tx_request = TransactionRequestBuilder::new() + // .build_consume_notes(vec![NoteId::from_raw(EMPTY_WORD)]) + // .unwrap(); + // let error = + // Box::pin(client.submit_new_transaction(wallet.id(), + // missing_authenticated_note_tx_request)) .await + // .unwrap_err(); + + // assert!(matches!( + // error, + // ClientError::TransactionRequestError( + // TransactionRequestError::MissingAuthenticatedInputNote(_) + // ) + // )); } #[tokio::test] @@ -2020,7 +2037,7 @@ async fn swap_chain_test() { // The notes are inserted in reverse order because the first note to be consumed will be the // last one generated. - swap_notes.insert(0, tx_request.expected_output_own_notes()[0].id()); + swap_notes.insert(0, tx_request.expected_output_own_notes()[0].clone()); Box::pin(client.submit_new_transaction(pairs[0].0.id(), tx_request)) .await .unwrap(); @@ -2034,7 +2051,7 @@ async fn swap_chain_test() { // Trying to consume the notes in another order will fail. let tx_request = TransactionRequestBuilder::new() - .build_consume_notes(swap_notes.iter().rev().copied().collect()) + .build_consume_notes(swap_notes.iter().rev().cloned().collect()) .unwrap(); let error = Box::pin(client.submit_new_transaction(last_wallet, tx_request)) .await @@ -2392,7 +2409,7 @@ async fn consume_note_with_custom_script() { // Consume note let transaction_request = TransactionRequestBuilder::new() - .build_consume_notes(vec![custom_note.id()]) + .build_consume_notes(vec![custom_note.clone()]) .unwrap(); // The transaction should be submitted successfully diff --git a/crates/web-client/src/models/input_note_record.rs b/crates/web-client/src/models/input_note_record.rs index 5613ecbce..6770910b1 100644 --- a/crates/web-client/src/models/input_note_record.rs +++ b/crates/web-client/src/models/input_note_record.rs @@ -1,3 +1,4 @@ +use miden_client::note::Note as NativeNote; use miden_client::store::InputNoteRecord as NativeInputNoteRecord; use miden_client::transaction::InputNote as NativeInputNote; use wasm_bindgen::prelude::*; @@ -10,6 +11,7 @@ use super::note_metadata::NoteMetadata; use super::word::Word; use crate::js_error_with_context; use crate::models::input_note::InputNote; +use crate::models::note::Note; /// Represents a Note of which the Store can keep track and retrieve. /// @@ -96,6 +98,15 @@ impl InputNoteRecord { })?; Ok(InputNote(input_note)) } + + /// Converts the record into a `Note` (including proof when available). + #[wasm_bindgen(js_name = "toNote")] + pub fn to_note(&self) -> Result { + let note: NativeNote = self.0.clone().try_into().map_err(|err| { + js_error_with_context(err, "could not create InputNote from InputNoteRecord") + })?; + Ok(Note(note)) + } } // CONVERSIONS diff --git a/crates/web-client/src/models/note.rs b/crates/web-client/src/models/note.rs index 0cba8f676..1d5c0da16 100644 --- a/crates/web-client/src/models/note.rs +++ b/crates/web-client/src/models/note.rs @@ -31,7 +31,7 @@ use crate::utils::{deserialize_from_uint8array, serialize_to_uint8array}; /// happen. See `NoteRecipient` for the shape of the recipient data. #[wasm_bindgen] #[derive(Clone)] -pub struct Note(NativeNote); +pub struct Note(pub(crate) NativeNote); #[wasm_bindgen] impl Note { diff --git a/crates/web-client/src/models/transaction_request/transaction_request_builder.rs b/crates/web-client/src/models/transaction_request/transaction_request_builder.rs index c39475ade..4388635be 100644 --- a/crates/web-client/src/models/transaction_request/transaction_request_builder.rs +++ b/crates/web-client/src/models/transaction_request/transaction_request_builder.rs @@ -2,7 +2,6 @@ use miden_client::Word as NativeWord; use miden_client::note::{ Note as NativeNote, NoteDetails as NativeNoteDetails, - NoteId as NativeNoteId, NoteRecipient as NativeNoteRecipient, NoteTag as NativeNoteTag, }; @@ -21,7 +20,6 @@ use crate::models::miden_arrays::{ ForeignAccountArray, NoteAndArgsArray, NoteDetailsAndTagArray, - NoteIdAndArgsArray, NoteRecipientArray, OutputNoteArray, }; @@ -46,20 +44,11 @@ impl TransactionRequestBuilder { TransactionRequestBuilder(native_transaction_request) } - /// Adds unauthenticated input notes with optional arguments. - #[wasm_bindgen(js_name = "withUnauthenticatedInputNotes")] - pub fn with_unauthenticated_input_notes(mut self, notes: &NoteAndArgsArray) -> Self { + /// Adds input notes with optional arguments. + #[wasm_bindgen(js_name = "withInputNotes")] + pub fn with_input_notes(mut self, notes: &NoteAndArgsArray) -> Self { let native_note_and_note_args: Vec<(NativeNote, Option)> = notes.into(); - self.0 = self.0.unauthenticated_input_notes(native_note_and_note_args); - self - } - - /// Adds authenticated input notes (identified by ID) with optional arguments. - #[wasm_bindgen(js_name = "withAuthenticatedInputNotes")] - pub fn with_authenticated_input_notes(mut self, notes: &NoteIdAndArgsArray) -> Self { - let native_note_id_and_note_args: Vec<(NativeNoteId, Option)> = - notes.into(); - self.0 = self.0.authenticated_input_notes(native_note_id_and_note_args); + self.0 = self.0.input_notes(native_note_and_note_args); self } diff --git a/crates/web-client/src/new_transactions.rs b/crates/web-client/src/new_transactions.rs index 516ed3216..a139a5f12 100644 --- a/crates/web-client/src/new_transactions.rs +++ b/crates/web-client/src/new_transactions.rs @@ -1,5 +1,5 @@ use miden_client::asset::FungibleAsset; -use miden_client::note::{BlockNumber, NoteId as NativeNoteId}; +use miden_client::note::{BlockNumber, Note as NativeNote}; use miden_client::transaction::{ PaymentNoteDescription, ProvenTransaction as NativeProvenTransaction, @@ -10,6 +10,7 @@ use miden_client::transaction::{ use wasm_bindgen::prelude::*; use crate::models::account_id::AccountId; +use crate::models::note::Note; use crate::models::note_type::NoteType; use crate::models::proven_transaction::ProvenTransaction; use crate::models::provers::TransactionProver; @@ -211,21 +212,19 @@ impl WebClient { #[wasm_bindgen(js_name = "newConsumeTransactionRequest")] pub fn new_consume_transaction_request( &mut self, - list_of_note_ids: Vec, + list_of_notes: Vec, ) -> Result { let consume_transaction_request = { - let native_note_ids = list_of_note_ids + let native_notes = list_of_notes .into_iter() - .map(|note_id| NativeNoteId::try_from_hex(note_id.as_str())) + .map(NativeNote::try_from) .collect::, _>>() .map_err(|err| { - JsValue::from_str(&format!( - "Failed to convert note id to native note id: {err}" - )) + JsValue::from_str(&format!("Failed to convert note to native note: {err}")) })?; NativeTransactionRequestBuilder::new() - .build_consume_notes(native_note_ids) + .build_consume_notes(native_notes) .map_err(|err| { JsValue::from_str(&format!( "Failed to create Consume Transaction Request: {err}" diff --git a/crates/web-client/test/mockchain.test.ts b/crates/web-client/test/mockchain.test.ts index 8c651d6e3..53a3c9e77 100644 --- a/crates/web-client/test/mockchain.test.ts +++ b/crates/web-client/test/mockchain.test.ts @@ -60,8 +60,14 @@ const mockChainTest = async (testingPage: Page) => { .id() .toString(); + const mintedNoteRecord = await client.getInputNote(mintedNoteId); + if (!mintedNoteRecord) { + throw new Error(`Note with ID ${mintedNoteId} not found`); + } + + const mintedNote = mintedNoteRecord.toNote(); const consumeTransactionRequest = client.newConsumeTransactionRequest([ - mintedNoteId, + mintedNote, ]); await client.submitNewTransaction(account.id(), consumeTransactionRequest); await client.proveBlock(); diff --git a/crates/web-client/test/new_transactions.test.ts b/crates/web-client/test/new_transactions.test.ts index 4c6f8e28d..03345da1d 100644 --- a/crates/web-client/test/new_transactions.test.ts +++ b/crates/web-client/test/new_transactions.test.ts @@ -94,8 +94,15 @@ const multipleMintsTest = async ( // Consume the minted notes for (let i = 0; i < result.createdNoteIds.length; i++) { + let noteId = result.createdNoteIds[i]; + const inputNoteRecord = await client.getInputNote(noteId); + if (!inputNoteRecord) { + throw new Error(`Note with ID ${noteId} not found`); + } + + const note = inputNoteRecord.toNote(); const consumeTransactionRequest = client.newConsumeTransactionRequest([ - result.createdNoteIds[i], + note, ]); const consumeTransactionUpdate = await window.helpers.executeAndApplyTransaction( @@ -547,7 +554,7 @@ export const customTransaction = async ( adviceMap.insert(noteArgsCommitment2, feltArray); let transactionRequest2 = new window.TransactionRequestBuilder() - .withUnauthenticatedInputNotes(noteAndArgsArray) + .withInputNotes(noteAndArgsArray) .withCustomScript(transactionScript) .extendAdviceMap(adviceMap) .build(); @@ -931,8 +938,19 @@ export const discardedTransaction = async ( mintTransactionUpdate.executedTransaction().id().toHex() ); + let notes: Note[] = []; + for (const _noteId of createdNoteIds) { + const inputNoteRecord = await client.getInputNote(_noteId); + + if (!inputNoteRecord) { + throw new Error(`Note with ID ${_noteId} not found`); + } + + const note = inputNoteRecord.toNote(); + notes.push(note); + } const senderConsumeTransactionRequest = - client.newConsumeTransactionRequest(createdNoteIds); + client.newConsumeTransactionRequest(notes); let senderConsumeTransactionUpdate = await window.helpers.executeAndApplyTransaction( senderAccount.id(), @@ -967,20 +985,34 @@ export const discardedTransaction = async ( sendTransactionUpdate.executedTransaction().id().toHex() ); - let noteIdAndArgs = new window.NoteIdAndArgs( - sendCreatedNotes[0].id(), - null - ); - let noteIdAndArgsArray = new window.NoteIdAndArgsArray([noteIdAndArgs]); + const inputNoteRecord = await client.getInputNote(sendCreatedNoteIds[0]); + if (!inputNoteRecord) { + throw new Error(`Note with ID ${sendCreatedNoteIds[0]} not found`); + } + + const note = inputNoteRecord.toNote(); + let noteAndArgs = new window.NoteAndArgs(note, null); + let noteAndArgsArray = new window.NoteAndArgsArray([noteAndArgs]); const consumeTransactionRequest = new window.TransactionRequestBuilder() - .withAuthenticatedInputNotes(noteIdAndArgsArray) + .withInputNotes(noteAndArgsArray) .build(); let preConsumeStore = await client.exportStore(); // Sender retrieves the note - let senderTxRequest = - await client.newConsumeTransactionRequest(sendCreatedNoteIds); + + notes = []; + for (const _noteId of sendCreatedNoteIds) { + const inputNoteRecord = await client.getInputNote(_noteId); + + if (!inputNoteRecord) { + throw new Error(`Note with ID ${_noteId} not found`); + } + + const note = inputNoteRecord.toNote(); + notes.push(note); + } + let senderTxRequest = client.newConsumeTransactionRequest(notes); let senderTxResult = await window.helpers.executeAndApplyTransaction( senderAccount.id(), senderTxRequest diff --git a/crates/web-client/test/notes.test.ts b/crates/web-client/test/notes.test.ts index 1c815490c..d93aef5c2 100644 --- a/crates/web-client/test/notes.test.ts +++ b/crates/web-client/test/notes.test.ts @@ -273,8 +273,14 @@ test.describe("createP2IDNote and createP2IDENote", () => { .id() .toString(); + const inputNoteRecord = await client.getInputNote(createdNoteId); + if (!inputNoteRecord) { + throw new Error(`Note with ID ${createdNoteId} not found`); + } + + const note = inputNoteRecord.toNote(); let consumeTransactionRequest = client.newConsumeTransactionRequest([ - createdNoteId, + note, ]); let consumeTransactionUpdate = @@ -376,8 +382,14 @@ test.describe("createP2IDNote and createP2IDENote", () => { .id() .toString(); + const inputNoteRecord = await client.getInputNote(createdNoteId); + if (!inputNoteRecord) { + throw new Error(`Note with ID ${createdNoteId} not found`); + } + + const note = inputNoteRecord.toNote(); let consumeTransactionRequest = client.newConsumeTransactionRequest([ - createdNoteId, + note, ]); let consumeTransactionUpdate = diff --git a/crates/web-client/test/webClientTestUtils.ts b/crates/web-client/test/webClientTestUtils.ts index 333954bf0..9c7115435 100644 --- a/crates/web-client/test/webClientTestUtils.ts +++ b/crates/web-client/test/webClientTestUtils.ts @@ -238,7 +238,7 @@ export const sendTransaction = async ( let noteAndArgsArray = new window.NoteAndArgsArray([noteAndArgs]); let txRequest = new window.TransactionRequestBuilder() - .withUnauthenticatedInputNotes(noteAndArgsArray) + .withInputNotes(noteAndArgsArray) .build(); let consumeTransactionUpdate = @@ -379,9 +379,14 @@ export const swapTransaction = async ( // Consuming swap note for account B - let txRequest1 = client.newConsumeTransactionRequest([ - expectedOutputNotes[0].id().toString(), - ]); + let noteId = expectedOutputNotes[0].id().toString(); + let inputNoteRecord = await client.getInputNote(noteId); + if (!inputNoteRecord) { + throw new Error(`Note with ID ${noteId} not found`); + } + + let note = inputNoteRecord.toNote(); + let txRequest1 = client.newConsumeTransactionRequest([note]); let consumeTransaction1Result = await window.helpers.executeAndApplyTransaction( @@ -396,9 +401,14 @@ export const swapTransaction = async ( // Consuming payback note for account A - let txRequest2 = client.newConsumeTransactionRequest([ - expectedPaybackNoteDetails[0].id().toString(), - ]); + noteId = expectedPaybackNoteDetails[0].id().toString(); + inputNoteRecord = await client.getInputNote(noteId); + if (!inputNoteRecord) { + throw new Error(`Note with ID ${noteId} not found`); + } + + note = inputNoteRecord.toNote(); + let txRequest2 = client.newConsumeTransactionRequest([note]); let consumeTransaction2Result = await window.helpers.executeAndApplyTransaction( @@ -679,8 +689,14 @@ export const consumeTransaction = async ( const targetAccountId = window.AccountId.fromHex(_targetAccountId); const faucetId = window.AccountId.fromHex(_faucetId); + const inputNoteRecord = await client.getInputNote(_noteId); + if (!inputNoteRecord) { + throw new Error(`Note with ID ${_noteId} not found`); + } + + const note = inputNoteRecord.toNote(); const consumeTransactionRequest = client.newConsumeTransactionRequest([ - _noteId, + note, ]); const prover = _withRemoteProver && window.remoteProverUrl != null @@ -786,7 +802,7 @@ export const mintAndConsumeTransaction = async ( let noteAndArgsArray = new window.NoteAndArgsArray([noteAndArgs]); let txRequest = new window.TransactionRequestBuilder() - .withUnauthenticatedInputNotes(noteAndArgsArray) + .withInputNotes(noteAndArgsArray) .build(); let consumeTransactionUpdate = From 1925d419944854a3e1466f10c4a26b4877d93c70 Mon Sep 17 00:00:00 2001 From: Juan Munoz Date: Fri, 19 Dec 2025 09:35:11 -0300 Subject: [PATCH 02/11] docs: run typedoc / update markdown files --- CHANGELOG.md | 3 +- crates/web-client/README.md | 4 +- docs/external/src/web-client/mock.md | 14 ++++-- .../src/web-client/new-transactions.md | 2 +- .../web-client/classes/InputNoteRecord.md | 12 +++++ .../classes/TransactionRequestBuilder.md | 46 ++++++------------- docs/typedoc/web-client/classes/WebClient.md | 6 +-- 7 files changed, 43 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d58de47e..d2c41c0e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,8 @@ * [BREAKING] Renamed `NodeRpcClient::get_account_proofs` to `NodeRpcClient::get_account_proof` & added `account_state` parameter (block at which we want to retrieve the proof) ([#1616](https://github.com/0xMiden/miden-client/pull/1616)). * [BREAKING] Refactored `NetworkId` to allow custom networks ([#1612](https://github.com/0xMiden/miden-client/pull/1612)). * [BREAKING] Removed `toBech32Custom` and implemented custom id conversion for wasm derived class `NetworkId` ([#1612](https://github.com/0xMiden/miden-client/pull/1612)). -* [BREAKING] Remove `SecretKey` model and consolidated functionality into `AuthSecretKey` ([#1592](https://github.com/0xMiden/miden-client/issues/1380)) +* [BREAKING] Remove `SecretKey` model and consolidated functionality into `AuthSecretKey` ([#1592](https://github.com/0xMiden/miden-client/pull/1592)). +* [BREAKING] Replace `TransactionRequestBuilder::unauthenticated_input_notes` & `TransactionRequestBuilder::authenticated_input_notes` for `TransactionRequestBuilder::input_notes`, now the user passes a list of notes which the `Client` itself determines the authentication status of ([#1624](https://github.com/0xMiden/miden-client/issues/1624)). ## 0.12.5 (2025-12-01) diff --git a/crates/web-client/README.md b/crates/web-client/README.md index 86feb97be..70852140e 100644 --- a/crates/web-client/README.md +++ b/crates/web-client/README.md @@ -164,11 +164,11 @@ await webClient.syncState(); // Query the client for consumable notes, and retrieve the id of the new note to be consumed let consumableNotes = await webClient.getConsumableNotes(account); -const noteIdToConsume = consumableNotes[0].inputNoteRecord().id(); +const noteToConsume = consumableNotes[0].inputNoteRecord().toNote(); // Create a consume transaction request object const consumeTransactionRequest = webClient.newConsumeTransactionRequest([ - noteIdToConsume, + noteToConsume, ]); // Execute, prove, submit, and apply the transaction in one step diff --git a/docs/external/src/web-client/mock.md b/docs/external/src/web-client/mock.md index be040749a..ef23bcd21 100644 --- a/docs/external/src/web-client/mock.md +++ b/docs/external/src/web-client/mock.md @@ -37,9 +37,13 @@ try { .inputNoteRecord() .id() .toString(); - + const inputNoteRecord = await client.getInputNote(noteIdToConsume); + if (!inputNoteRecord) { + throw new Error(`Note with ID ${noteIdToConsume} not found`); + } + const noteToConsume = inputNoteRecord.toNote(); const consumeRequest = mockWebClient.newConsumeTransactionRequest([ - noteIdToConsume, + noteToConsume, ]); const consumeTransactionId = await mockWebClient.submitNewTransaction( @@ -71,16 +75,16 @@ try { // Send a private note (example with placeholders) const note = /* create your note */; const recipientAddress = /* create recipient address */; - + await mockWebClient.sendPrivateNote(note, recipientAddress); // Fetch private notes await mockWebClient.fetchPrivateNotes(); - + // Retrieve the fetched notes const filter = new NoteFilter(NoteFilterTypes.All); const notes = await mockWebClient.getInputNotes(filter); - + console.log(`Fetched ${notes.length} private notes`); } catch (error) { console.error("Error:", error.message); diff --git a/docs/external/src/web-client/new-transactions.md b/docs/external/src/web-client/new-transactions.md index 31549c183..5c3529616 100644 --- a/docs/external/src/web-client/new-transactions.md +++ b/docs/external/src/web-client/new-transactions.md @@ -206,7 +206,7 @@ try { const webClient = await WebClient.createClient(); const transactionRequest = webClient.newConsumeTransactionRequest( - [noteId1, noteId2] // Array of note IDs to consume + [note1, note2] // Array of notes to consume, can be retrieved from the client by their noteID ); const result = await webClient.executeTransaction( diff --git a/docs/typedoc/web-client/classes/InputNoteRecord.md b/docs/typedoc/web-client/classes/InputNoteRecord.md index 20cbb81f0..2e3f70d4f 100644 --- a/docs/typedoc/web-client/classes/InputNoteRecord.md +++ b/docs/typedoc/web-client/classes/InputNoteRecord.md @@ -181,3 +181,15 @@ Converts the record into an `InputNote` (including proof when available). #### Returns [`InputNote`](InputNote.md) + +*** + +### toNote() + +> **toNote**(): [`Note`](Note.md) + +Converts the record into a `Note` (including proof when available). + +#### Returns + +[`Note`](Note.md) diff --git a/docs/typedoc/web-client/classes/TransactionRequestBuilder.md b/docs/typedoc/web-client/classes/TransactionRequestBuilder.md index d32a39c92..bb93506fd 100644 --- a/docs/typedoc/web-client/classes/TransactionRequestBuilder.md +++ b/docs/typedoc/web-client/classes/TransactionRequestBuilder.md @@ -93,24 +93,6 @@ Adds an authentication argument. *** -### withAuthenticatedInputNotes() - -> **withAuthenticatedInputNotes**(`notes`): `TransactionRequestBuilder` - -Adds authenticated input notes (identified by ID) with optional arguments. - -#### Parameters - -##### notes - -[`NoteIdAndArgsArray`](NoteIdAndArgsArray.md) - -#### Returns - -`TransactionRequestBuilder` - -*** - ### withCustomScript() > **withCustomScript**(`script`): `TransactionRequestBuilder` @@ -183,17 +165,17 @@ Registers foreign accounts referenced by the transaction. *** -### withOwnOutputNotes() +### withInputNotes() -> **withOwnOutputNotes**(`notes`): `TransactionRequestBuilder` +> **withInputNotes**(`notes`): `TransactionRequestBuilder` -Adds notes created by the sender that should be emitted by the transaction. +Adds input notes with optional arguments. #### Parameters ##### notes -[`OutputNoteArray`](OutputNoteArray.md) +[`NoteAndArgsArray`](NoteAndArgsArray.md) #### Returns @@ -201,17 +183,17 @@ Adds notes created by the sender that should be emitted by the transaction. *** -### withScriptArg() +### withOwnOutputNotes() -> **withScriptArg**(`script_arg`): `TransactionRequestBuilder` +> **withOwnOutputNotes**(`notes`): `TransactionRequestBuilder` -Adds a transaction script argument. +Adds notes created by the sender that should be emitted by the transaction. #### Parameters -##### script\_arg +##### notes -[`Word`](Word.md) +[`OutputNoteArray`](OutputNoteArray.md) #### Returns @@ -219,17 +201,17 @@ Adds a transaction script argument. *** -### withUnauthenticatedInputNotes() +### withScriptArg() -> **withUnauthenticatedInputNotes**(`notes`): `TransactionRequestBuilder` +> **withScriptArg**(`script_arg`): `TransactionRequestBuilder` -Adds unauthenticated input notes with optional arguments. +Adds a transaction script argument. #### Parameters -##### notes +##### script\_arg -[`NoteAndArgsArray`](NoteAndArgsArray.md) +[`Word`](Word.md) #### Returns diff --git a/docs/typedoc/web-client/classes/WebClient.md b/docs/typedoc/web-client/classes/WebClient.md index c4a08d02f..feccf0283 100644 --- a/docs/typedoc/web-client/classes/WebClient.md +++ b/docs/typedoc/web-client/classes/WebClient.md @@ -619,13 +619,13 @@ Returns all the existing setting keys from the store. ### newConsumeTransactionRequest() -> **newConsumeTransactionRequest**(`list_of_note_ids`): [`TransactionRequest`](TransactionRequest.md) +> **newConsumeTransactionRequest**(`list_of_notes`): [`TransactionRequest`](TransactionRequest.md) #### Parameters -##### list\_of\_note\_ids +##### list\_of\_notes -`string`[] +[`Note`](Note.md)[] #### Returns From ae4110da020042487eb055b2165069587c1e94f3 Mon Sep 17 00:00:00 2001 From: Juan Munoz Date: Fri, 19 Dec 2025 11:35:07 -0300 Subject: [PATCH 03/11] chore: address pending TODOs --- crates/rust-client/src/errors.rs | 8 ------ crates/rust-client/src/transaction/mod.rs | 21 +++++---------- .../src/transaction/request/builder.rs | 27 +++++++++---------- .../src/transaction/request/mod.rs | 5 +--- .../testing/miden-client-tests/src/tests.rs | 20 -------------- 5 files changed, 21 insertions(+), 60 deletions(-) diff --git a/crates/rust-client/src/errors.rs b/crates/rust-client/src/errors.rs index 476883d02..d7ac57de5 100644 --- a/crates/rust-client/src/errors.rs +++ b/crates/rust-client/src/errors.rs @@ -190,14 +190,6 @@ impl ClientError { impl From<&TransactionRequestError> for Option { fn from(err: &TransactionRequestError) -> Self { match err { - TransactionRequestError::MissingAuthenticatedInputNote(note_id) => { - Some(ErrorHint { - message: format!( - "Note {note_id} was listed via `TransactionRequestBuilder::authenticated_input_notes(...)`, but the store lacks an authenticated `InputNoteRecord`. Import or sync the note so its record and authentication data are available before executing the request." - ), - docs_url: Some(TROUBLESHOOTING_DOC), - }) - }, TransactionRequestError::NoInputNotesNorAccountChange => Some(ErrorHint { message: "Transactions must consume input notes or mutate tracked account state. Add at least one authenticated/unauthenticated input note or include an explicit account state update in the request.".to_string(), docs_url: Some(TROUBLESHOOTING_DOC), diff --git a/crates/rust-client/src/transaction/mod.rs b/crates/rust-client/src/transaction/mod.rs index 8c4223e73..df4409db5 100644 --- a/crates/rust-client/src/transaction/mod.rs +++ b/crates/rust-client/src/transaction/mod.rs @@ -231,7 +231,7 @@ where // If tx request contains unauthenticated_input_notes we should insert them let unauthenticated_input_notes = transaction_request - .unauthenticated_input_notes() + .input_notes() .iter() .filter(|n| !authenticated_note_ids.contains(&n.id())) .cloned() @@ -600,14 +600,10 @@ where { // Get incoming asset notes excluding unauthenticated ones let incoming_notes_ids: Vec<_> = transaction_request - .unauthenticated_input_notes() + .input_notes() .iter() .filter_map(|note| { - if transaction_request - .unauthenticated_input_notes() - .iter() - .any(|n| n.id() == note.id()) - { + if transaction_request.input_notes().iter().any(|n| n.id() == note.id()) { None } else { Some(note.id()) @@ -620,13 +616,10 @@ where .await .map_err(|err| TransactionRequestError::NoteNotFound(err.to_string()))?; - let all_incoming_assets = - store_input_notes.iter().flat_map(|note| note.assets().iter()).chain( - transaction_request - .unauthenticated_input_notes() - .iter() - .flat_map(|note| note.assets().iter()), - ); + let all_incoming_assets = store_input_notes + .iter() + .flat_map(|note| note.assets().iter()) + .chain(transaction_request.input_notes().iter().flat_map(|note| note.assets().iter())); Ok(collect_assets(all_incoming_assets)) } diff --git a/crates/rust-client/src/transaction/request/builder.rs b/crates/rust-client/src/transaction/request/builder.rs index 2a82a7d94..b21422cd2 100644 --- a/crates/rust-client/src/transaction/request/builder.rs +++ b/crates/rust-client/src/transaction/request/builder.rs @@ -39,12 +39,11 @@ use crate::ClientRng; /// scripts, and setting other transaction parameters. #[derive(Clone, Debug)] pub struct TransactionRequestBuilder { - // TODO: merge `unauthenticated_input_notes` & `unauthenticated_input_notes` into one - /// Notes to be consumed by the transaction that aren't authenticated. - unauthenticated_input_notes: Vec, - /// Notes to be consumed by the transaction together with their (optional) arguments. This + /// Notes to be consumed by the transaction. + authenticated_input_notes: Vec, + /// Optional arguments of the Notes to be consumed by the transaction. This /// includes both authenticated and unauthenticated notes. - input_notes: Vec<(NoteId, Option)>, + input_notes_args: Vec<(NoteId, Option)>, /// Notes to be created by the transaction. This includes both full and partial output notes. /// The transaction script will be generated based on these notes. own_output_notes: Vec, @@ -88,8 +87,8 @@ impl TransactionRequestBuilder { /// Creates a new, empty [`TransactionRequestBuilder`]. pub fn new() -> Self { Self { - unauthenticated_input_notes: vec![], - input_notes: vec![], + authenticated_input_notes: vec![], + input_notes_args: vec![], own_output_notes: Vec::new(), expected_output_recipients: BTreeMap::new(), expected_future_notes: BTreeMap::new(), @@ -104,15 +103,15 @@ impl TransactionRequestBuilder { } } - /// TODO: docs + // Adds the specified notes as input notes to the transaction request. #[must_use] pub fn input_notes( mut self, notes: impl IntoIterator)>, ) -> Self { for (note, argument) in notes { - self.input_notes.push((note.id(), argument)); - self.unauthenticated_input_notes.push(note); + self.input_notes_args.push((note.id(), argument)); + self.authenticated_input_notes.push(note); } self } @@ -261,7 +260,7 @@ impl TransactionRequestBuilder { /// - `notes` is a list of notes to be consumed. pub fn build_consume_notes( self, - notes: Vec, // TODO: SHOULD BE NOTE + notes: Vec, ) -> Result { let input_notes = notes.into_iter().map(|id| (id, None)); self.input_notes(input_notes).build() @@ -375,7 +374,7 @@ impl TransactionRequestBuilder { /// - If an invalid note variant is encountered in the own output notes. pub fn build(self) -> Result { let mut seen_input_notes = BTreeSet::new(); - for (note_id, _) in &self.input_notes { + for (note_id, _) in &self.input_notes_args { if !seen_input_notes.insert(note_id) { return Err(TransactionRequestError::DuplicateInputNote(*note_id)); } @@ -413,8 +412,8 @@ impl TransactionRequestBuilder { }; Ok(TransactionRequest { - input_notes: self.unauthenticated_input_notes, - input_notes_args: self.input_notes, + input_notes: self.authenticated_input_notes, + input_notes_args: self.input_notes_args, script_template, expected_output_recipients: self.expected_output_recipients, expected_future_notes: self.expected_future_notes, diff --git a/crates/rust-client/src/transaction/request/mod.rs b/crates/rust-client/src/transaction/request/mod.rs index 075dfb607..f9ad77610 100644 --- a/crates/rust-client/src/transaction/request/mod.rs +++ b/crates/rust-client/src/transaction/request/mod.rs @@ -103,9 +103,8 @@ impl TransactionRequest { // PUBLIC ACCESSORS // -------------------------------------------------------------------------------------------- - // TODO: RENAME /// Returns a reference to the transaction request's unauthenticated note list. - pub fn unauthenticated_input_notes(&self) -> &[Note] { + pub fn input_notes(&self) -> &[Note] { &self.input_notes } @@ -417,8 +416,6 @@ pub enum TransactionRequestError { InvalidTransactionScript(#[from] TransactionScriptError), #[error("merkle error")] MerkleError(#[from] MerkleError), - #[error("specified authenticated input note with id {0} is missing")] - MissingAuthenticatedInputNote(NoteId), // TODO: REMOVE UNUSED ERROR #[error("a transaction without output notes must have at least one input note")] NoInputNotesNorAccountChange, #[error("note not found: {0}")] diff --git a/crates/testing/miden-client-tests/src/tests.rs b/crates/testing/miden-client-tests/src/tests.rs index fec9467c7..14fe0d395 100644 --- a/crates/testing/miden-client-tests/src/tests.rs +++ b/crates/testing/miden-client-tests/src/tests.rs @@ -1957,26 +1957,6 @@ async fn input_note_checks() { error, ClientError::TransactionRequestError(TransactionRequestError::InputNoteAlreadyConsumed(_)) )); - - // TODO: remove deprecated check - // No longer errors if the Note passed is not authenticated, - // It will just not get marked as such. - // Check that adding an authenticated note that is not tracked by the client will return an - // error - // let missing_authenticated_note_tx_request = TransactionRequestBuilder::new() - // .build_consume_notes(vec![NoteId::from_raw(EMPTY_WORD)]) - // .unwrap(); - // let error = - // Box::pin(client.submit_new_transaction(wallet.id(), - // missing_authenticated_note_tx_request)) .await - // .unwrap_err(); - - // assert!(matches!( - // error, - // ClientError::TransactionRequestError( - // TransactionRequestError::MissingAuthenticatedInputNote(_) - // ) - // )); } #[tokio::test] From a9e5113add0b79ffe7132e7633e08ee679fe4bf5 Mon Sep 17 00:00:00 2001 From: Juan Munoz Date: Fri, 19 Dec 2025 18:56:21 -0300 Subject: [PATCH 04/11] chore: address PR comment --- crates/rust-client/src/transaction/mod.rs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/crates/rust-client/src/transaction/mod.rs b/crates/rust-client/src/transaction/mod.rs index df4409db5..9d8f33937 100644 --- a/crates/rust-client/src/transaction/mod.rs +++ b/crates/rust-client/src/transaction/mod.rs @@ -217,14 +217,7 @@ where .store .get_input_notes(NoteFilter::List(transaction_request.get_input_note_ids())) .await?; - authenticated_note_records.retain(|note| { - matches!( - note.state(), - InputNoteState::Committed(_) - | InputNoteState::ConsumedAuthenticatedLocal(_) - | InputNoteState::ConsumedExternal(_) - ) - }); + authenticated_note_records.retain(InputNoteRecord::is_authenticated); let authenticated_note_ids = authenticated_note_records.iter().map(InputNoteRecord::id).collect::>(); @@ -598,7 +591,6 @@ where transaction_request: &TransactionRequest, ) -> Result<(BTreeMap, BTreeSet), TransactionRequestError> { - // Get incoming asset notes excluding unauthenticated ones let incoming_notes_ids: Vec<_> = transaction_request .input_notes() .iter() @@ -611,11 +603,14 @@ where }) .collect(); - let store_input_notes = self + let mut store_input_notes = self .get_input_notes(NoteFilter::List(incoming_notes_ids)) .await .map_err(|err| TransactionRequestError::NoteNotFound(err.to_string()))?; + // Get incoming asset notes excluding unauthenticated ones + store_input_notes.retain(InputNoteRecord::is_authenticated); + let all_incoming_assets = store_input_notes .iter() .flat_map(|note| note.assets().iter()) From 2b9bc780d1210171be0445a3c8d0b22256cdf4b0 Mon Sep 17 00:00:00 2001 From: Juan Munoz Date: Sat, 27 Dec 2025 09:36:36 -0300 Subject: [PATCH 05/11] chore: rename variable --- crates/rust-client/src/transaction/request/builder.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/rust-client/src/transaction/request/builder.rs b/crates/rust-client/src/transaction/request/builder.rs index b21422cd2..abfa4f402 100644 --- a/crates/rust-client/src/transaction/request/builder.rs +++ b/crates/rust-client/src/transaction/request/builder.rs @@ -40,7 +40,7 @@ use crate::ClientRng; #[derive(Clone, Debug)] pub struct TransactionRequestBuilder { /// Notes to be consumed by the transaction. - authenticated_input_notes: Vec, + input_notes: Vec, /// Optional arguments of the Notes to be consumed by the transaction. This /// includes both authenticated and unauthenticated notes. input_notes_args: Vec<(NoteId, Option)>, @@ -87,7 +87,7 @@ impl TransactionRequestBuilder { /// Creates a new, empty [`TransactionRequestBuilder`]. pub fn new() -> Self { Self { - authenticated_input_notes: vec![], + input_notes: vec![], input_notes_args: vec![], own_output_notes: Vec::new(), expected_output_recipients: BTreeMap::new(), @@ -111,7 +111,7 @@ impl TransactionRequestBuilder { ) -> Self { for (note, argument) in notes { self.input_notes_args.push((note.id(), argument)); - self.authenticated_input_notes.push(note); + self.input_notes.push(note); } self } @@ -412,7 +412,7 @@ impl TransactionRequestBuilder { }; Ok(TransactionRequest { - input_notes: self.authenticated_input_notes, + input_notes: self.input_notes, input_notes_args: self.input_notes_args, script_template, expected_output_recipients: self.expected_output_recipients, From 8db0e045c8e7f95d20f85f3498de18cf42a10b05 Mon Sep 17 00:00:00 2001 From: Juan Munoz Date: Sat, 27 Dec 2025 10:42:25 -0300 Subject: [PATCH 06/11] chore: address PR comments --- CHANGELOG.md | 2 +- crates/rust-client/src/transaction/mod.rs | 16 ++++++-- .../src/transaction/request/builder.rs | 4 +- .../src/transaction/request/mod.rs | 37 ++++++++++--------- 4 files changed, 36 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae9472335..23cc45ddb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,7 @@ * [BREAKING] Remove `SecretKey` model and consolidated functionality into `AuthSecretKey` ([#1592](https://github.com/0xMiden/miden-client/issues/1380)) * Incremented the limits for various RPC calls to accommodate larger data sets ([#1621](https://github.com/0xMiden/miden-client/pull/1621)). * [BREAKING] Introduced named storage slots, changed `FilesystemKeystore` to not be generic over RNG ([#1626](https://github.com/0xMiden/miden-client/pull/1626)). -* [BREAKING] Replace `TransactionRequestBuilder::unauthenticated_input_notes` & `TransactionRequestBuilder::authenticated_input_notes` for `TransactionRequestBuilder::input_notes`, now the user passes a list of notes which the `Client` itself determines the authentication status of ([#1624](https://github.com/0xMiden/miden-client/issues/1624)). +* [BREAKING] Replaced `TransactionRequestBuilder::unauthenticated_input_notes` & `TransactionRequestBuilder::authenticated_input_notes` for `TransactionRequestBuilder::input_notes`, now the user passes a list of notes which the `Client` itself determines the authentication status of ([#1624](https://github.com/0xMiden/miden-client/issues/1624)). ## 0.12.5 (2025-12-01) diff --git a/crates/rust-client/src/transaction/mod.rs b/crates/rust-client/src/transaction/mod.rs index 9d8f33937..531a42fab 100644 --- a/crates/rust-client/src/transaction/mod.rs +++ b/crates/rust-client/src/transaction/mod.rs @@ -212,11 +212,21 @@ where self.validate_request(account_id, &transaction_request).await?; // Retrieve all input notes from the store. - // But only mark as authenticated if they are committed or consumed. let mut authenticated_note_records = self .store - .get_input_notes(NoteFilter::List(transaction_request.get_input_note_ids())) + .get_input_notes(NoteFilter::List(transaction_request.input_note_ids().collect())) .await?; + + // Verify that none of the authenticated input notes are already consumed. + for note in &authenticated_note_records { + if note.is_consumed() { + return Err(ClientError::TransactionRequestError( + TransactionRequestError::InputNoteAlreadyConsumed(note.id()), + )); + } + } + + // Only keep authenticated input notes from the store. authenticated_note_records.retain(InputNoteRecord::is_authenticated); let authenticated_note_ids = @@ -233,7 +243,7 @@ where self.store.upsert_input_notes(&unauthenticated_input_notes).await?; - let mut notes = transaction_request.build_input_notes(authenticated_note_records)?; + let mut notes = transaction_request.build_input_notes(&authenticated_note_records)?; let output_recipients = transaction_request.expected_output_recipients().cloned().collect::>(); diff --git a/crates/rust-client/src/transaction/request/builder.rs b/crates/rust-client/src/transaction/request/builder.rs index abfa4f402..ee6febeea 100644 --- a/crates/rust-client/src/transaction/request/builder.rs +++ b/crates/rust-client/src/transaction/request/builder.rs @@ -40,6 +40,8 @@ use crate::ClientRng; #[derive(Clone, Debug)] pub struct TransactionRequestBuilder { /// Notes to be consumed by the transaction. + /// Notes which ID is present in the store are considered authenticated, + /// the ones which ID is does not exist are considered unauthenticated. input_notes: Vec, /// Optional arguments of the Notes to be consumed by the transaction. This /// includes both authenticated and unauthenticated notes. @@ -103,7 +105,7 @@ impl TransactionRequestBuilder { } } - // Adds the specified notes as input notes to the transaction request. + /// Adds the specified notes as input notes to the transaction request. #[must_use] pub fn input_notes( mut self, diff --git a/crates/rust-client/src/transaction/request/mod.rs b/crates/rust-client/src/transaction/request/mod.rs index 5a4f3901c..10f2e1a61 100644 --- a/crates/rust-client/src/transaction/request/mod.rs +++ b/crates/rust-client/src/transaction/request/mod.rs @@ -63,6 +63,8 @@ pub enum TransactionScriptTemplate { pub struct TransactionRequest { /// Notes to be consumed by the transaction. /// includes both authenticated and unauthenticated notes. + /// Notes which ID is present in the store are considered authenticated, + /// the ones which ID is does not exist are considered unauthenticated. input_notes: Vec, /// Optional arguments of the input notes to be consumed by the transaction. This /// includes both authenticated and unauthenticated notes. @@ -103,14 +105,14 @@ impl TransactionRequest { // PUBLIC ACCESSORS // -------------------------------------------------------------------------------------------- - /// Returns a reference to the transaction request's unauthenticated note list. + /// Returns a reference to the transaction request's input note list. pub fn input_notes(&self) -> &[Note] { &self.input_notes } /// Returns a list of all input note IDs. - pub fn get_input_note_ids(&self) -> Vec { - self.input_notes.iter().map(Note::id).collect() + pub fn input_note_ids(&self) -> impl Iterator { + self.input_notes.iter().map(Note::id) } /// Returns a map of note IDs to their respective [`NoteArgs`]. The result will include @@ -202,7 +204,7 @@ impl TransactionRequest { /// order they were provided in the transaction request. pub(crate) fn build_input_notes( &self, - authenticated_note_records: Vec, + authenticated_note_records: &[InputNoteRecord], ) -> Result, TransactionRequestError> { let mut input_notes: BTreeMap = BTreeMap::new(); @@ -223,27 +225,26 @@ impl TransactionRequest { input_notes.insert( authenticated_note_record.id(), authenticated_note_record + .clone() .try_into() .expect("Authenticated note record should be convertible to InputNote"), ); } // Add unauthenticated input notes to the input notes map. - for unauthenticated_input_notes in &self.input_notes { - input_notes.insert( - unauthenticated_input_notes.id(), - InputNote::Unauthenticated { - note: unauthenticated_input_notes.clone(), - }, - ); + for note in self + .input_notes() + .iter() + .filter(|n| !authenticated_note_records.iter().any(|a| a.id() == n.id())) + { + input_notes.insert(note.id(), InputNote::Unauthenticated { note: note.clone() }); } Ok(InputNotes::new( - self.get_input_note_ids() - .iter() + self.input_note_ids() .map(|note_id| { input_notes - .remove(note_id) + .remove(¬e_id) .expect("The input note map was checked to contain all input notes") }) .collect(), @@ -332,8 +333,8 @@ impl Serializable for TransactionRequest { impl Deserializable for TransactionRequest { fn read_from(source: &mut R) -> Result { - let unauthenticated_input_notes = Vec::::read_from(source)?; - let input_notes = Vec::<(NoteId, Option)>::read_from(source)?; + let input_notes = Vec::::read_from(source)?; + let input_notes_args = Vec::<(NoteId, Option)>::read_from(source)?; let script_template = match source.read_u8()? { 0 => None, @@ -364,8 +365,8 @@ impl Deserializable for TransactionRequest { let auth_arg = Option::::read_from(source)?; Ok(TransactionRequest { - input_notes: unauthenticated_input_notes, - input_notes_args: input_notes, + input_notes, + input_notes_args, script_template, expected_output_recipients, expected_future_notes, From c07e6343cd69ca96e2e2f81790240d8e573bc438 Mon Sep 17 00:00:00 2001 From: SantiagoPittella Date: Fri, 9 Jan 2026 15:58:13 -0300 Subject: [PATCH 07/11] fix: pase notes to build_consume_notes --- bin/integration-tests/src/tests/pass_through.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/integration-tests/src/tests/pass_through.rs b/bin/integration-tests/src/tests/pass_through.rs index 12fa97e1a..131e773c9 100644 --- a/bin/integration-tests/src/tests/pass_through.rs +++ b/bin/integration-tests/src/tests/pass_through.rs @@ -119,7 +119,7 @@ pub async fn test_pass_through(client_config: ClientConfig) -> Result<()> { let tx_request = TransactionRequestBuilder::new() .expected_output_recipients(vec![pass_through_note_details_1.recipient().clone()]) - .build_consume_notes(vec![pass_through_note_1.id()]) + .build_consume_notes(vec![pass_through_note_1]) .unwrap(); let tx_id = client @@ -150,7 +150,7 @@ pub async fn test_pass_through(client_config: ClientConfig) -> Result<()> { // now try another transaction against the pass-through account let tx_request = TransactionRequestBuilder::new() .expected_output_recipients(vec![pass_through_note_details_2.recipient().clone()]) - .build_consume_notes(vec![pass_through_note_2.id()]) + .build_consume_notes(vec![pass_through_note_2]) .unwrap(); let tx_id = client From 9901b04ff50a2842a998d8c72ae20607a8fa057e Mon Sep 17 00:00:00 2001 From: SantiagoPittella Date: Mon, 12 Jan 2026 12:40:58 -0300 Subject: [PATCH 08/11] review: avoid clone --- crates/rust-client/src/transaction/mod.rs | 2 +- crates/rust-client/src/transaction/request/mod.rs | 12 +++++------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/crates/rust-client/src/transaction/mod.rs b/crates/rust-client/src/transaction/mod.rs index 78938b663..5e3c82252 100644 --- a/crates/rust-client/src/transaction/mod.rs +++ b/crates/rust-client/src/transaction/mod.rs @@ -267,7 +267,7 @@ where self.store.upsert_input_notes(&unauthenticated_input_notes).await?; - let mut notes = transaction_request.build_input_notes(&authenticated_note_records)?; + let mut notes = transaction_request.build_input_notes(authenticated_note_records)?; let output_recipients = transaction_request.expected_output_recipients().cloned().collect::>(); diff --git a/crates/rust-client/src/transaction/request/mod.rs b/crates/rust-client/src/transaction/request/mod.rs index a8a75ac79..0ffa8ba03 100644 --- a/crates/rust-client/src/transaction/request/mod.rs +++ b/crates/rust-client/src/transaction/request/mod.rs @@ -205,7 +205,7 @@ impl TransactionRequest { /// order they were provided in the transaction request. pub(crate) fn build_input_notes( &self, - authenticated_note_records: &[InputNoteRecord], + authenticated_note_records: Vec, ) -> Result, TransactionRequestError> { let mut input_notes: BTreeMap = BTreeMap::new(); @@ -223,20 +223,18 @@ impl TransactionRequest { )); } + let authenticated_note_id = authenticated_note_record.id(); input_notes.insert( - authenticated_note_record.id(), + authenticated_note_id, authenticated_note_record - .clone() .try_into() .expect("Authenticated note record should be convertible to InputNote"), ); } // Add unauthenticated input notes to the input notes map. - for note in self - .input_notes() - .iter() - .filter(|n| !authenticated_note_records.iter().any(|a| a.id() == n.id())) + let authenticated_note_ids: BTreeSet = input_notes.keys().copied().collect(); + for note in self.input_notes().iter().filter(|n| !authenticated_note_ids.contains(&n.id())) { input_notes.insert(note.id(), InputNote::Unauthenticated { note: note.clone() }); } From 7cb0b97fd966696bb3aea744e4d75061244a3edc Mon Sep 17 00:00:00 2001 From: Ignacio Amigo Date: Tue, 13 Jan 2026 16:03:58 -0300 Subject: [PATCH 09/11] reviews: simplify function, expand comments, rename vars --- .../src/commands/new_transactions.rs | 9 +--- crates/rust-client/src/transaction/mod.rs | 41 +++++-------------- .../src/transaction/request/builder.rs | 4 +- 3 files changed, 15 insertions(+), 39 deletions(-) diff --git a/bin/miden-cli/src/commands/new_transactions.rs b/bin/miden-cli/src/commands/new_transactions.rs index 1d70158cd..b886cd1db 100644 --- a/bin/miden-cli/src/commands/new_transactions.rs +++ b/bin/miden-cli/src/commands/new_transactions.rs @@ -310,7 +310,6 @@ impl ConsumeNotesCmd { ) -> Result<(), CliError> { let force = self.force; - let mut authenticated_notes = Vec::new(); let mut input_notes = Vec::new(); for note_id in &self.list_of_notes { @@ -318,10 +317,6 @@ impl ConsumeNotesCmd { .await .map_err(|_| CliError::Input(format!("Input note ID {note_id} is neither a valid Note ID nor a prefix of a known Note ID")))?; - if note_record.is_authenticated() { - authenticated_notes.push(note_record.id()); - } - input_notes.push(( note_record.try_into().map_err(|err: NoteRecordError| { CliError::Transaction(err.into(), "Failed to convert note record".to_string()) @@ -333,7 +328,7 @@ impl ConsumeNotesCmd { let account_id = get_input_acc_id_by_prefix_or_default(&client, self.account_id.clone()).await?; - if authenticated_notes.is_empty() { + if input_notes.is_empty() { info!("No input note IDs provided, getting all notes consumable by {}", account_id); let consumable_notes = client.get_consumable_notes(Some(account_id)).await?; for (note_record, _) in consumable_notes { @@ -349,7 +344,7 @@ impl ConsumeNotesCmd { } } - if authenticated_notes.is_empty() && input_notes.is_empty() { + if input_notes.is_empty() { return Err(CliError::Transaction( "No input notes were provided and the store does not contain any notes consumable by {account_id}".into(), "Input notes check failed".to_string(), diff --git a/crates/rust-client/src/transaction/mod.rs b/crates/rust-client/src/transaction/mod.rs index 5e3c82252..7747820e3 100644 --- a/crates/rust-client/src/transaction/mod.rs +++ b/crates/rust-client/src/transaction/mod.rs @@ -236,13 +236,13 @@ where self.validate_request(account_id, &transaction_request).await?; // Retrieve all input notes from the store. - let mut authenticated_note_records = self + let mut stored_note_records = self .store .get_input_notes(NoteFilter::List(transaction_request.input_note_ids().collect())) .await?; // Verify that none of the authenticated input notes are already consumed. - for note in &authenticated_note_records { + for note in &stored_note_records { if note.is_consumed() { return Err(ClientError::TransactionRequestError( TransactionRequestError::InputNoteAlreadyConsumed(note.id()), @@ -251,12 +251,15 @@ where } // Only keep authenticated input notes from the store. - authenticated_note_records.retain(InputNoteRecord::is_authenticated); + stored_note_records.retain(InputNoteRecord::is_authenticated); let authenticated_note_ids = - authenticated_note_records.iter().map(InputNoteRecord::id).collect::>(); + stored_note_records.iter().map(InputNoteRecord::id).collect::>(); - // If tx request contains unauthenticated_input_notes we should insert them + // Upsert request notes missing from the store so they can be tracked and updated + // NOTE: Unauthenticated notes may be stored locally in an unverified/invalid state at this + // point. The upsert will replace the state to an InputNoteState::Expected (with + // metadata included). let unauthenticated_input_notes = transaction_request .input_notes() .iter() @@ -267,7 +270,7 @@ where self.store.upsert_input_notes(&unauthenticated_input_notes).await?; - let mut notes = transaction_request.build_input_notes(authenticated_note_records)?; + let mut notes = transaction_request.build_input_notes(stored_note_records)?; let output_recipients = transaction_request.expected_output_recipients().cloned().collect::>(); @@ -616,30 +619,8 @@ where transaction_request: &TransactionRequest, ) -> Result<(BTreeMap, BTreeSet), TransactionRequestError> { - let incoming_notes_ids: Vec<_> = transaction_request - .input_notes() - .iter() - .filter_map(|note| { - if transaction_request.input_notes().iter().any(|n| n.id() == note.id()) { - None - } else { - Some(note.id()) - } - }) - .collect(); - - let mut store_input_notes = self - .get_input_notes(NoteFilter::List(incoming_notes_ids)) - .await - .map_err(|err| TransactionRequestError::NoteNotFound(err.to_string()))?; - - // Get incoming asset notes excluding unauthenticated ones - store_input_notes.retain(InputNoteRecord::is_authenticated); - - let all_incoming_assets = store_input_notes - .iter() - .flat_map(|note| note.assets().iter()) - .chain(transaction_request.input_notes().iter().flat_map(|note| note.assets().iter())); + let all_incoming_assets = + transaction_request.input_notes().iter().flat_map(|note| note.assets().iter()); Ok(collect_assets(all_incoming_assets)) } diff --git a/crates/rust-client/src/transaction/request/builder.rs b/crates/rust-client/src/transaction/request/builder.rs index ed08e1e56..64907ae5f 100644 --- a/crates/rust-client/src/transaction/request/builder.rs +++ b/crates/rust-client/src/transaction/request/builder.rs @@ -41,8 +41,8 @@ use crate::ClientRng; #[derive(Clone, Debug)] pub struct TransactionRequestBuilder { /// Notes to be consumed by the transaction. - /// Notes which ID is present in the store are considered authenticated, - /// the ones which ID is does not exist are considered unauthenticated. + /// Notes whose inclusion proof is present in the store are will be consumed as authenticated; + /// the ones that do not have proofs will be consumed as unauthenticated. input_notes: Vec, /// Optional arguments of the Notes to be consumed by the transaction. This /// includes both authenticated and unauthenticated notes. From 00cc4b5eea00aa8b85c4a659008d13884ad74a14 Mon Sep 17 00:00:00 2001 From: Ignacio Amigo Date: Tue, 13 Jan 2026 16:09:40 -0300 Subject: [PATCH 10/11] reviews: docs --- crates/rust-client/src/transaction/request/mod.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/rust-client/src/transaction/request/mod.rs b/crates/rust-client/src/transaction/request/mod.rs index 0ffa8ba03..d2790ff24 100644 --- a/crates/rust-client/src/transaction/request/mod.rs +++ b/crates/rust-client/src/transaction/request/mod.rs @@ -199,10 +199,12 @@ impl TransactionRequest { &self.auth_arg } - /// Builds the [`InputNotes`] needed for the transaction execution. Full valid notes for the - /// specified authenticated notes need to be provided, otherwise an error will be returned. - /// The transaction input notes will include both authenticated and unauthenticated notes in the - /// order they were provided in the transaction request. + /// Builds the [`InputNotes`] needed for the transaction execution. + /// + /// Authenticated input notes are provided by the caller (typically fetched from the store). + /// Any requested notes not present in that authenticated set are treated as unauthenticated. + /// The transaction input notes will include both authenticated and unauthenticated notes in + /// the order they were provided in the transaction request. pub(crate) fn build_input_notes( &self, authenticated_note_records: Vec, From dfb85251bfe1f1134485f79c1dea50a7086e1513 Mon Sep 17 00:00:00 2001 From: Ignacio Amigo Date: Tue, 13 Jan 2026 16:39:19 -0300 Subject: [PATCH 11/11] chore: more simplifications --- crates/rust-client/src/transaction/mod.rs | 206 +++++++----------- .../src/transaction/request/mod.rs | 31 +++ 2 files changed, 113 insertions(+), 124 deletions(-) diff --git a/crates/rust-client/src/transaction/mod.rs b/crates/rust-client/src/transaction/mod.rs index 7747820e3..3dc479ba0 100644 --- a/crates/rust-client/src/transaction/mod.rs +++ b/crates/rust-client/src/transaction/mod.rs @@ -63,12 +63,11 @@ //! documentation. use alloc::collections::{BTreeMap, BTreeSet}; -use alloc::string::ToString; use alloc::sync::Arc; use alloc::vec::Vec; use miden_protocol::account::{Account, AccountId}; -use miden_protocol::asset::{Asset, NonFungibleAsset}; +use miden_protocol::asset::NonFungibleAsset; use miden_protocol::block::BlockNumber; use miden_protocol::note::{Note, NoteDetails, NoteId, NoteRecipient, NoteScript, NoteTag}; use miden_protocol::transaction::AccountInputs; @@ -581,108 +580,6 @@ where )) } - /// Helper to get the account outgoing assets. - /// - /// Any outgoing assets resulting from executing note scripts but not present in expected output - /// notes wouldn't be included. - fn get_outgoing_assets( - transaction_request: &TransactionRequest, - ) -> (BTreeMap, BTreeSet) { - // Get own notes assets - let mut own_notes_assets = match transaction_request.script_template() { - Some(TransactionScriptTemplate::SendNotes(notes)) => notes - .iter() - .map(|note| (note.id(), note.assets().clone())) - .collect::>(), - _ => BTreeMap::default(), - }; - // Get transaction output notes assets - let mut output_notes_assets = transaction_request - .expected_output_own_notes() - .into_iter() - .map(|note| (note.id(), note.assets().clone())) - .collect::>(); - - // Merge with own notes assets and delete duplicates - output_notes_assets.append(&mut own_notes_assets); - - // Create a map of the fungible and non-fungible assets in the output notes - let outgoing_assets = - output_notes_assets.values().flat_map(|note_assets| note_assets.iter()); - - collect_assets(outgoing_assets) - } - - /// Helper to get the account incoming assets. - async fn get_incoming_assets( - &self, - transaction_request: &TransactionRequest, - ) -> Result<(BTreeMap, BTreeSet), TransactionRequestError> - { - let all_incoming_assets = - transaction_request.input_notes().iter().flat_map(|note| note.assets().iter()); - - Ok(collect_assets(all_incoming_assets)) - } - - /// Ensures a transaction request is compatible with the current account state, - /// primarily by checking asset balances against the requested transfers. - async fn validate_basic_account_request( - &self, - transaction_request: &TransactionRequest, - account: &Account, - ) -> Result<(), ClientError> { - // Get outgoing assets - let (fungible_balance_map, non_fungible_set) = - Client::::get_outgoing_assets(transaction_request); - - // Get incoming assets - let (incoming_fungible_balance_map, incoming_non_fungible_balance_set) = - self.get_incoming_assets(transaction_request).await?; - - // Check if the account balance plus incoming assets is greater than or equal to the - // outgoing fungible assets - for (faucet_id, amount) in fungible_balance_map { - let account_asset_amount = account.vault().get_balance(faucet_id).unwrap_or(0); - let incoming_balance = incoming_fungible_balance_map.get(&faucet_id).unwrap_or(&0); - if account_asset_amount + incoming_balance < amount { - return Err(ClientError::AssetError( - AssetError::FungibleAssetAmountNotSufficient { - minuend: account_asset_amount, - subtrahend: amount, - }, - )); - } - } - - // Check if the account balance plus incoming assets is greater than or equal to the - // outgoing non fungible assets - for non_fungible in non_fungible_set { - match account.vault().has_non_fungible_asset(non_fungible) { - Ok(true) => (), - Ok(false) => { - // Check if the non fungible asset is in the incoming assets - if !incoming_non_fungible_balance_set.contains(&non_fungible) { - return Err(ClientError::AssetError( - AssetError::NonFungibleFaucetIdTypeMismatch( - non_fungible.faucet_id_prefix(), - ), - )); - } - }, - _ => { - return Err(ClientError::AssetError( - AssetError::NonFungibleFaucetIdTypeMismatch( - non_fungible.faucet_id_prefix(), - ), - )); - }, - } - } - - Ok(()) - } - /// Validates that the specified transaction request can be executed by the specified account. /// /// This does't guarantee that the transaction will succeed, but it's useful to avoid submitting @@ -711,7 +608,7 @@ where // TODO(SantiagoPittella): Add faucet validations. Ok(()) } else { - self.validate_basic_account_request(transaction_request, &account).await + validate_basic_account_request(transaction_request, &account) } } @@ -850,26 +747,87 @@ where // HELPERS // ================================================================================================ -/// Accumulates fungible totals and collectable non-fungible assets from an iterator of assets. -fn collect_assets<'a>( - assets: impl Iterator, +/// Helper to get the account outgoing assets. +/// +/// Any outgoing assets resulting from executing note scripts but not present in expected output +/// notes wouldn't be included. +fn get_outgoing_assets( + transaction_request: &TransactionRequest, ) -> (BTreeMap, BTreeSet) { - let mut fungible_balance_map = BTreeMap::new(); - let mut non_fungible_set = BTreeSet::new(); - - assets.for_each(|asset| match asset { - Asset::Fungible(fungible) => { - fungible_balance_map - .entry(fungible.faucet_id()) - .and_modify(|balance| *balance += fungible.amount()) - .or_insert(fungible.amount()); - }, - Asset::NonFungible(non_fungible) => { - non_fungible_set.insert(*non_fungible); - }, - }); - - (fungible_balance_map, non_fungible_set) + // Get own notes assets + let mut own_notes_assets = match transaction_request.script_template() { + Some(TransactionScriptTemplate::SendNotes(notes)) => notes + .iter() + .map(|note| (note.id(), note.assets().clone())) + .collect::>(), + _ => BTreeMap::default(), + }; + // Get transaction output notes assets + let mut output_notes_assets = transaction_request + .expected_output_own_notes() + .into_iter() + .map(|note| (note.id(), note.assets().clone())) + .collect::>(); + + // Merge with own notes assets and delete duplicates + output_notes_assets.append(&mut own_notes_assets); + + // Create a map of the fungible and non-fungible assets in the output notes + let outgoing_assets = output_notes_assets.values().flat_map(|note_assets| note_assets.iter()); + + request::collect_assets(outgoing_assets) +} + +/// Ensures a transaction request is compatible with the current account state, +/// primarily by checking asset balances against the requested transfers. +fn validate_basic_account_request( + transaction_request: &TransactionRequest, + account: &Account, +) -> Result<(), ClientError> { + // Get outgoing assets + let (fungible_balance_map, non_fungible_set) = get_outgoing_assets(transaction_request); + + // Get incoming assets + let (incoming_fungible_balance_map, incoming_non_fungible_balance_set) = + transaction_request.incoming_assets(); + + // Check if the account balance plus incoming assets is greater than or equal to the + // outgoing fungible assets + for (faucet_id, amount) in fungible_balance_map { + let account_asset_amount = account.vault().get_balance(faucet_id).unwrap_or(0); + let incoming_balance = incoming_fungible_balance_map.get(&faucet_id).unwrap_or(&0); + if account_asset_amount + incoming_balance < amount { + return Err(ClientError::AssetError(AssetError::FungibleAssetAmountNotSufficient { + minuend: account_asset_amount, + subtrahend: amount, + })); + } + } + + // Check if the account balance plus incoming assets is greater than or equal to the + // outgoing non fungible assets + for non_fungible in non_fungible_set { + match account.vault().has_non_fungible_asset(non_fungible) { + Ok(true) => (), + Ok(false) => { + // Check if the non fungible asset is in the incoming assets + if !incoming_non_fungible_balance_set.contains(&non_fungible) { + return Err(ClientError::AssetError( + AssetError::NonFungibleFaucetIdTypeMismatch( + non_fungible.faucet_id_prefix(), + ), + )); + } + }, + _ => { + return Err(ClientError::AssetError(AssetError::NonFungibleFaucetIdTypeMismatch( + non_fungible.faucet_id_prefix(), + ))); + }, + } + } + + Ok(()) } /// Extracts notes from [`OutputNotes`]. diff --git a/crates/rust-client/src/transaction/request/mod.rs b/crates/rust-client/src/transaction/request/mod.rs index d2790ff24..08c48f613 100644 --- a/crates/rust-client/src/transaction/request/mod.rs +++ b/crates/rust-client/src/transaction/request/mod.rs @@ -6,6 +6,7 @@ use alloc::string::{String, ToString}; use alloc::vec::Vec; use miden_protocol::account::AccountId; +use miden_protocol::asset::{Asset, NonFungibleAsset}; use miden_protocol::crypto::merkle::MerkleError; use miden_protocol::crypto::merkle::store::MerkleStore; use miden_protocol::note::{Note, NoteDetails, NoteId, NoteRecipient, NoteTag, PartialNote}; @@ -116,6 +117,11 @@ impl TransactionRequest { self.input_notes.iter().map(Note::id) } + /// Returns the assets held by the transaction's input notes. + pub fn incoming_assets(&self) -> (BTreeMap, BTreeSet) { + collect_assets(self.input_notes.iter().flat_map(|note| note.assets().iter())) + } + /// Returns a map of note IDs to their respective [`NoteArgs`]. The result will include /// exclusively note IDs for notes for which [`NoteArgs`] have been defined. pub fn get_note_args(&self) -> BTreeMap { @@ -382,6 +388,31 @@ impl Deserializable for TransactionRequest { } } +// HELPERS +// ================================================================================================ + +/// Accumulates fungible totals and collectable non-fungible assets from an iterator of assets. +pub(crate) fn collect_assets<'a>( + assets: impl Iterator, +) -> (BTreeMap, BTreeSet) { + let mut fungible_balance_map = BTreeMap::new(); + let mut non_fungible_set = BTreeSet::new(); + + assets.for_each(|asset| match asset { + Asset::Fungible(fungible) => { + fungible_balance_map + .entry(fungible.faucet_id()) + .and_modify(|balance| *balance += fungible.amount()) + .or_insert(fungible.amount()); + }, + Asset::NonFungible(non_fungible) => { + non_fungible_set.insert(*non_fungible); + }, + }); + + (fungible_balance_map, non_fungible_set) +} + impl Default for TransactionRequestBuilder { fn default() -> Self { Self::new()