diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a35c0930..d8d820bd6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ * [BREAKING] Introduced named storage slots, changed `FilesystemKeystore` to not be generic over RNG ([#1626](https://github.com/0xMiden/miden-client/pull/1626)). * Added `submit_new_transaction_with_prover` to the Rust client and `submitNewTransactionWithProver` to the WebClient([#1622](https://github.com/0xMiden/miden-client/pull/1622)). * [BREAKING] Modified JS binding for `AccountComponent::compile` which now takes an `AccountComponentCode` built with the newly added binding `CodeBuilder::compile_account_component_code` ([#1627](https://github.com/0xMiden/miden-client/pull/1627)). +* [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/bin/integration-tests/src/tests/client.rs b/bin/integration-tests/src/tests/client.rs index adc29818b..7b6308264 100644 --- a/bin/integration-tests/src/tests/client.rs +++ b/bin/integration-tests/src/tests/client.rs @@ -498,7 +498,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 @@ -509,7 +509,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 @@ -729,11 +729,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(); @@ -830,7 +830,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 @@ -895,7 +897,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(); @@ -1026,7 +1030,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 = @@ -1403,10 +1409,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(); @@ -1500,7 +1506,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 10ac33ef7..85d842278 100644 --- a/bin/integration-tests/src/tests/custom_transaction.rs +++ b/bin/integration-tests/src/tests/custom_transaction.rs @@ -102,7 +102,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()) @@ -118,7 +118,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) @@ -232,7 +232,7 @@ pub async fn test_merkle_store(client_config: ClientConfig) -> Result<()> { let tx_script = client.code_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 b6337632f..fda52dfcc 100644 --- a/bin/integration-tests/src/tests/network_transaction.rs +++ b/bin/integration-tests/src/tests/network_transaction.rs @@ -224,7 +224,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/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 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..b886cd1db 100644 --- a/bin/miden-cli/src/commands/new_transactions.rs +++ b/bin/miden-cli/src/commands/new_transactions.rs @@ -310,18 +310,29 @@ impl ConsumeNotesCmd { ) -> Result<(), CliError> { 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) .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()); - } else { - unauthenticated_notes.push(( + 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 = + get_input_acc_id_by_prefix_or_default(&client, self.account_id.clone()).await?; + + 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 { + input_notes.push(( note_record.try_into().map_err(|err: NoteRecordError| { CliError::Transaction( err.into(), @@ -333,17 +344,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() { - 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())); - } - - if authenticated_notes.is_empty() && unauthenticated_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(), @@ -351,8 +352,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/errors.rs b/crates/rust-client/src/errors.rs index 2ab22f48e..7421d92bb 100644 --- a/crates/rust-client/src/errors.rs +++ b/crates/rust-client/src/errors.rs @@ -183,14 +183,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/note/note_screener.rs b/crates/rust-client/src/note/note_screener.rs index 79ab9adf3..9c89e4353 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(account))?; diff --git a/crates/rust-client/src/test_utils/common.rs b/crates/rust-client/src/test_utils/common.rs index a3f6eba10..8129ef5e7 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_protocol::account::auth::AuthSecretKey; use miden_protocol::account::{Account, AccountId, AccountStorageMode}; use miden_protocol::asset::{Asset, FungibleAsset, TokenSymbol}; -use miden_protocol::note::{NoteId, NoteType}; +use miden_protocol::note::NoteType; use miden_protocol::testing::account_id::ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE; use miden_protocol::transaction::{OutputNote, TransactionId}; use miden_protocol::{Felt, FieldElement}; @@ -420,7 +420,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() } @@ -450,14 +450,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 { @@ -465,7 +465,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()), } } @@ -512,10 +512,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 177f870f8..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; @@ -235,27 +234,42 @@ 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. + let mut stored_note_records = self .store - .get_input_notes(NoteFilter::List(authenticated_input_note_ids)) + .get_input_notes(NoteFilter::List(transaction_request.input_note_ids().collect())) .await?; - // If tx request contains unauthenticated_input_notes we should insert them + // Verify that none of the authenticated input notes are already consumed. + for note in &stored_note_records { + if note.is_consumed() { + return Err(ClientError::TransactionRequestError( + TransactionRequestError::InputNoteAlreadyConsumed(note.id()), + )); + } + } + + // Only keep authenticated input notes from the store. + stored_note_records.retain(InputNoteRecord::is_authenticated); + + let authenticated_note_ids = + stored_note_records.iter().map(InputNoteRecord::id).collect::>(); + + // 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 - .unauthenticated_input_notes() + .input_notes() .iter() + .filter(|n| !authenticated_note_ids.contains(&n.id())) .cloned() .map(Into::into) .collect::>(); 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::>(); @@ -318,7 +332,6 @@ where .await?; validate_executed_transaction(&executed_transaction, &output_recipients)?; - TransactionResult::new(executed_transaction, future_notes) } @@ -567,135 +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> - { - // Get incoming asset notes excluding unauthenticated ones - let incoming_notes_ids: Vec<_> = transaction_request - .input_notes() - .iter() - .filter_map(|(note_id, _)| { - if transaction_request - .unauthenticated_input_notes() - .iter() - .any(|note| note.id() == *note_id) - { - None - } else { - Some(*note_id) - } - }) - .collect(); - - let store_input_notes = self - .get_input_notes(NoteFilter::List(incoming_notes_ids)) - .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()), - ); - - 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 @@ -724,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) } } @@ -863,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/builder.rs b/crates/rust-client/src/transaction/request/builder.rs index 18c8feabd..64907ae5f 100644 --- a/crates/rust-client/src/transaction/request/builder.rs +++ b/crates/rust-client/src/transaction/request/builder.rs @@ -40,11 +40,13 @@ use crate::ClientRng; /// scripts, and setting other transaction parameters. #[derive(Clone, Debug)] pub struct TransactionRequestBuilder { - /// 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. + /// 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. - 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 +90,8 @@ impl TransactionRequestBuilder { /// Creates a new, empty [`TransactionRequestBuilder`]. pub fn new() -> Self { Self { - unauthenticated_input_notes: vec![], input_notes: vec![], + input_notes_args: vec![], own_output_notes: Vec::new(), expected_output_recipients: BTreeMap::new(), expected_future_notes: BTreeMap::new(), @@ -104,27 +106,15 @@ impl TransactionRequestBuilder { } } - /// Adds the specified notes as unauthenticated input notes to the transaction request. + /// Adds the specified notes as input notes to the transaction request. #[must_use] - pub fn unauthenticated_input_notes( + 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 - } - - /// 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.input_notes_args.push((note.id(), argument)); + self.input_notes.push(note); } self } @@ -270,13 +260,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, ) -> 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 @@ -387,7 +377,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)); } @@ -425,8 +415,8 @@ impl TransactionRequestBuilder { }; Ok(TransactionRequest { - unauthenticated_input_notes: self.unauthenticated_input_notes, input_notes: self.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 7f33d1a8a..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}; @@ -62,11 +63,14 @@ 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)>, + /// 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. + 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. @@ -103,44 +107,25 @@ impl TransactionRequest { // PUBLIC ACCESSORS // -------------------------------------------------------------------------------------------- - /// 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)] { + /// 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(|(id, _)| *id).collect() + pub fn input_note_ids(&self) -> impl Iterator { + 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 { - self.input_notes + self.input_notes_args .iter() .filter_map(|(note, args)| args.map(|a| (*note, a))) .collect() @@ -220,10 +205,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, @@ -244,38 +231,27 @@ impl TransactionRequest { )); } + let authenticated_note_id = authenticated_note_record.id(); input_notes.insert( - authenticated_note_record.id(), + authenticated_note_id, authenticated_note_record .try_into() .expect("Authenticated note record should be convertible to InputNote"), ); } - // 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 { - input_notes.insert( - unauthenticated_input_notes.id(), - InputNote::Unauthenticated { - note: unauthenticated_input_notes.clone(), - }, - ); + 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() }); } 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(), @@ -337,8 +313,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)) => { @@ -364,8 +340,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, @@ -396,8 +372,8 @@ impl Deserializable for TransactionRequest { let auth_arg = Option::::read_from(source)?; Ok(TransactionRequest { - unauthenticated_input_notes, input_notes, + input_notes_args, script_template, expected_output_recipients, expected_future_notes, @@ -412,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() @@ -448,8 +449,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), #[error("a transaction without output notes must have at least one input note")] NoInputNotesNorAccountChange, #[error("note not found: {0}")] @@ -561,8 +560,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 bad62b0fe..e51597171 100644 --- a/crates/testing/miden-client-tests/src/tests.rs +++ b/crates/testing/miden-client-tests/src/tests.rs @@ -691,10 +691,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(); @@ -871,8 +868,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 @@ -994,7 +992,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 @@ -1033,7 +1031,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] @@ -1172,8 +1175,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(); @@ -1205,7 +1210,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] @@ -1265,7 +1270,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; @@ -1286,7 +1291,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 @@ -1313,7 +1318,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] @@ -1364,7 +1374,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, @@ -1385,7 +1397,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(); @@ -1904,7 +1918,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, @@ -1912,7 +1926,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 = @@ -1939,7 +1953,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 @@ -1949,23 +1963,6 @@ async fn input_note_checks() { error, ClientError::TransactionRequestError(TransactionRequestError::InputNoteAlreadyConsumed(_)) )); - - // 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] @@ -2026,7 +2023,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(); @@ -2040,7 +2037,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 @@ -2425,7 +2422,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/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/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 3a2040582..bc513d7ae 100644 --- a/crates/web-client/src/models/note.rs +++ b/crates/web-client/src/models/note.rs @@ -32,7 +32,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 852cc08f0..a11e086e0 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, @@ -11,6 +11,7 @@ use wasm_bindgen::prelude::*; use crate::models::NoteType; use crate::models::account_id::AccountId; +use crate::models::note::Note; use crate::models::proven_transaction::ProvenTransaction; use crate::models::provers::TransactionProver; use crate::models::transaction_id::TransactionId; @@ -240,21 +241,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 d11517be3..0362ee08f 100644 --- a/crates/web-client/test/new_transactions.test.ts +++ b/crates/web-client/test/new_transactions.test.ts @@ -93,8 +93,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( @@ -485,7 +492,7 @@ export const customTransaction = async ( adviceMap.insert(noteArgsCommitment2, feltArray); let transactionRequest2 = new window.TransactionRequestBuilder() - .withUnauthenticatedInputNotes(noteAndArgsArray) + .withInputNotes(noteAndArgsArray) .withCustomScript(transactionScript) .extendAdviceMap(adviceMap) .build(); @@ -871,8 +878,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(), @@ -907,20 +925,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 = 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 3cff3a7ad..b3699d1eb 100644 --- a/docs/external/src/web-client/new-transactions.md +++ b/docs/external/src/web-client/new-transactions.md @@ -260,7 +260,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 6afb6e897..390ca9b8c 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