miden_client/transaction/
mod.rs

1//! Provides APIs for creating, executing, proving, and submitting transactions to the Miden
2//! network.
3//!
4//! ## Overview
5//!
6//! This module enables clients to:
7//!
8//! - Build transaction requests using the [`TransactionRequestBuilder`].
9//!   - [`TransactionRequestBuilder`] contains simple builders for standard transaction types, such
10//!     as `p2id` (pay-to-id)
11//! - Execute transactions via the local transaction executor and generate a [`TransactionResult`]
12//!   that includes execution details and relevant notes for state tracking.
13//! - Prove transactions (locally or remotely) using a [`TransactionProver`] and submit the proven
14//!   transactions to the network.
15//! - Track and update the state of transactions, including their status (e.g., `Pending`,
16//!   `Committed`, or `Discarded`).
17//!
18//! ## Example
19//!
20//! The following example demonstrates how to create and submit a transaction:
21//!
22//! ```rust
23//! use miden_client::{
24//!     Client,
25//!     crypto::FeltRng,
26//!     transaction::{PaymentTransactionData, TransactionRequestBuilder, TransactionResult},
27//! };
28//! use miden_objects::{account::AccountId, asset::FungibleAsset, note::NoteType};
29//! # use std::error::Error;
30//!
31//! /// Executes, proves and submits a P2ID transaction.
32//! ///
33//! /// This transaction is executed by `sender_id`, and creates an output note
34//! /// containing 100 tokens of `faucet_id`'s fungible asset.
35//! async fn create_and_submit_transaction<R: rand::Rng>(
36//!     client: &mut Client,
37//!     sender_id: AccountId,
38//!     target_id: AccountId,
39//!     faucet_id: AccountId,
40//! ) -> Result<(), Box<dyn Error>> {
41//!     // Create an asset representing the amount to be transferred.
42//!     let asset = FungibleAsset::new(faucet_id, 100)?;
43//!
44//!     // Build a transaction request for a pay-to-id transaction.
45//!     let tx_request = TransactionRequestBuilder::pay_to_id(
46//!         PaymentTransactionData::new(vec![asset.into()], sender_id, target_id),
47//!         None, // No recall height
48//!         NoteType::Private,
49//!         client.rng(),
50//!     )?
51//!     .build()?;
52//!
53//!     // Execute the transaction. This returns a TransactionResult.
54//!     let tx_result: TransactionResult = client.new_transaction(sender_id, tx_request).await?;
55//!
56//!     // Prove and submit the transaction, persisting its details to the local store.
57//!     client.submit_transaction(tx_result).await?;
58//!
59//!     Ok(())
60//! }
61//! ```
62//!
63//! For more detailed information about each function and error type, refer to the specific API
64//! documentation.
65
66use alloc::{
67    collections::{BTreeMap, BTreeSet},
68    string::ToString,
69    sync::Arc,
70    vec::Vec,
71};
72use core::fmt::{self};
73
74pub use miden_lib::{
75    account::interface::{AccountComponentInterface, AccountInterface},
76    transaction::TransactionKernel,
77};
78use miden_objects::{
79    AssetError, Digest, Felt, Word, ZERO,
80    account::{Account, AccountCode, AccountDelta, AccountId},
81    asset::{Asset, NonFungibleAsset},
82    block::BlockNumber,
83    crypto::merkle::MerklePath,
84    note::{Note, NoteDetails, NoteId, NoteTag},
85    transaction::{InputNotes, TransactionArgs},
86    vm::AdviceInputs,
87};
88pub use miden_tx::{
89    LocalTransactionProver, ProvingOptions, TransactionProver, TransactionProverError,
90    auth::TransactionAuthenticator,
91};
92use miden_tx::{
93    TransactionExecutor,
94    utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable},
95};
96use tracing::info;
97
98use super::Client;
99use crate::{
100    ClientError,
101    note::{NoteScreener, NoteUpdates},
102    rpc::domain::{account::AccountProof, transaction::TransactionUpdate},
103    store::{
104        InputNoteRecord, InputNoteState, NoteFilter, OutputNoteRecord, StoreError,
105        TransactionFilter, input_note_states::ExpectedNoteState,
106    },
107    sync::{MAX_BLOCK_NUMBER_DELTA, NoteTagRecord},
108};
109
110mod request;
111pub use miden_objects::transaction::{
112    ExecutedTransaction, InputNote, OutputNote, OutputNotes, ProvenTransaction, TransactionId,
113    TransactionScript,
114};
115pub use miden_tx::{DataStoreError, TransactionExecutorError};
116pub use request::{
117    ForeignAccount, ForeignAccountInputs, NoteArgs, PaymentTransactionData, SwapTransactionData,
118    TransactionRequest, TransactionRequestBuilder, TransactionRequestError,
119    TransactionScriptTemplate,
120};
121
122// TRANSACTION RESULT
123// ================================================================================================
124
125/// Represents the result of executing a transaction by the client.
126///
127/// It contains an [`ExecutedTransaction`], and a list of `relevant_notes` that contains the
128/// `output_notes` that the client has to store as input notes, based on the `NoteScreener`
129/// output from filtering the transaction's output notes or some partial note we expect to receive
130/// in the future (you can check at swap notes for an example of this).
131#[derive(Clone, Debug, PartialEq)]
132pub struct TransactionResult {
133    transaction: ExecutedTransaction,
134    relevant_notes: Vec<InputNoteRecord>,
135}
136
137impl TransactionResult {
138    /// Screens the output notes to store and track the relevant ones, and instantiates a
139    /// [`TransactionResult`].
140    pub async fn new(
141        transaction: ExecutedTransaction,
142        note_screener: NoteScreener,
143        partial_notes: Vec<(NoteDetails, NoteTag)>,
144        current_block_num: BlockNumber,
145        current_timestamp: Option<u64>,
146    ) -> Result<Self, ClientError> {
147        let mut relevant_notes = vec![];
148
149        for note in notes_from_output(transaction.output_notes()) {
150            let account_relevance = note_screener.check_relevance(note).await?;
151            if !account_relevance.is_empty() {
152                let metadata = *note.metadata();
153                relevant_notes.push(InputNoteRecord::new(
154                    note.into(),
155                    current_timestamp,
156                    ExpectedNoteState {
157                        metadata: Some(metadata),
158                        after_block_num: current_block_num,
159                        tag: Some(metadata.tag()),
160                    }
161                    .into(),
162                ));
163            }
164        }
165
166        // Include partial output notes into the relevant notes
167        relevant_notes.extend(partial_notes.iter().map(|(note_details, tag)| {
168            InputNoteRecord::new(
169                note_details.clone(),
170                None,
171                ExpectedNoteState {
172                    metadata: None,
173                    after_block_num: current_block_num,
174                    tag: Some(*tag),
175                }
176                .into(),
177            )
178        }));
179
180        let tx_result = Self { transaction, relevant_notes };
181
182        Ok(tx_result)
183    }
184
185    /// Returns the [`ExecutedTransaction`].
186    pub fn executed_transaction(&self) -> &ExecutedTransaction {
187        &self.transaction
188    }
189
190    /// Returns the output notes that were generated as a result of the transaction execution.
191    pub fn created_notes(&self) -> &OutputNotes {
192        self.transaction.output_notes()
193    }
194
195    /// Returns the list of notes that are relevant to the client, based on [`NoteScreener`].
196    pub fn relevant_notes(&self) -> &[InputNoteRecord] {
197        &self.relevant_notes
198    }
199
200    /// Returns the block against which the transaction was executed.
201    pub fn block_num(&self) -> BlockNumber {
202        self.transaction.block_header().block_num()
203    }
204
205    /// Returns transaction's [`TransactionArgs`].
206    pub fn transaction_arguments(&self) -> &TransactionArgs {
207        self.transaction.tx_args()
208    }
209
210    /// Returns the [`AccountDelta`] that describes the change of state for the executing [Account].
211    pub fn account_delta(&self) -> &AccountDelta {
212        self.transaction.account_delta()
213    }
214
215    /// Returns input notes that were consumed as part of the transaction.
216    pub fn consumed_notes(&self) -> &InputNotes<InputNote> {
217        self.transaction.tx_inputs().input_notes()
218    }
219}
220
221impl From<TransactionResult> for ExecutedTransaction {
222    fn from(tx_result: TransactionResult) -> ExecutedTransaction {
223        tx_result.transaction
224    }
225}
226
227impl Serializable for TransactionResult {
228    fn write_into<W: ByteWriter>(&self, target: &mut W) {
229        self.transaction.write_into(target);
230        self.relevant_notes.write_into(target);
231    }
232}
233
234impl Deserializable for TransactionResult {
235    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
236        let transaction = ExecutedTransaction::read_from(source)?;
237        let relevant_notes = Vec::<InputNoteRecord>::read_from(source)?;
238
239        Ok(Self { transaction, relevant_notes })
240    }
241}
242
243// TRANSACTION RECORD
244// ================================================================================================
245
246/// Describes a transaction that has been executed and is being tracked on the Client.
247///
248/// Currently, the `commit_height` (and `committed` status) is set based on the height
249/// at which the transaction's output notes are committed.
250#[derive(Debug, Clone)]
251pub struct TransactionRecord {
252    pub id: TransactionId,
253    pub account_id: AccountId,
254    pub init_account_state: Digest,
255    pub final_account_state: Digest,
256    pub input_note_nullifiers: Vec<Digest>,
257    pub output_notes: OutputNotes,
258    pub transaction_script: Option<TransactionScript>,
259    pub block_num: BlockNumber,
260    pub transaction_status: TransactionStatus,
261}
262
263impl TransactionRecord {
264    #[allow(clippy::too_many_arguments)]
265    pub fn new(
266        id: TransactionId,
267        account_id: AccountId,
268        init_account_state: Digest,
269        final_account_state: Digest,
270        input_note_nullifiers: Vec<Digest>,
271        output_notes: OutputNotes,
272        transaction_script: Option<TransactionScript>,
273        block_num: BlockNumber,
274        transaction_status: TransactionStatus,
275    ) -> TransactionRecord {
276        TransactionRecord {
277            id,
278            account_id,
279            init_account_state,
280            final_account_state,
281            input_note_nullifiers,
282            output_notes,
283            transaction_script,
284            block_num,
285            transaction_status,
286        }
287    }
288}
289
290/// Represents the status of a transaction.
291#[derive(Debug, Clone, PartialEq)]
292pub enum TransactionStatus {
293    /// Transaction has been submitted but not yet committed.
294    Pending,
295    /// Transaction has been committed and included at the specified block number.
296    Committed(BlockNumber),
297    /// Transaction has been discarded and isn't included in the node.
298    Discarded,
299}
300
301impl fmt::Display for TransactionStatus {
302    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
303        match self {
304            TransactionStatus::Pending => write!(f, "Pending"),
305            TransactionStatus::Committed(block_number) => {
306                write!(f, "Committed (Block: {block_number})")
307            },
308            TransactionStatus::Discarded => write!(f, "Discarded"),
309        }
310    }
311}
312
313// TRANSACTION STORE UPDATE
314// ================================================================================================
315
316/// Represents the changes that need to be applied to the client store as a result of a
317/// transaction execution.
318pub struct TransactionStoreUpdate {
319    /// Details of the executed transaction to be inserted.
320    executed_transaction: ExecutedTransaction,
321    /// Updated account state after the [`AccountDelta`] has been applied.
322    updated_account: Account,
323    /// Information about note changes after the transaction execution.
324    note_updates: NoteUpdates,
325    /// New note tags to be tracked.
326    new_tags: Vec<NoteTagRecord>,
327}
328
329impl TransactionStoreUpdate {
330    /// Creates a new [`TransactionStoreUpdate`] instance.
331    pub fn new(
332        executed_transaction: ExecutedTransaction,
333        updated_account: Account,
334        created_input_notes: Vec<InputNoteRecord>,
335        created_output_notes: Vec<OutputNoteRecord>,
336        updated_input_notes: Vec<InputNoteRecord>,
337        new_tags: Vec<NoteTagRecord>,
338    ) -> Self {
339        Self {
340            executed_transaction,
341            updated_account,
342            note_updates: NoteUpdates::new(
343                [created_input_notes, updated_input_notes].concat(),
344                created_output_notes,
345            ),
346            new_tags,
347        }
348    }
349
350    /// Returns the executed transaction.
351    pub fn executed_transaction(&self) -> &ExecutedTransaction {
352        &self.executed_transaction
353    }
354
355    /// Returns the updated account.
356    pub fn updated_account(&self) -> &Account {
357        &self.updated_account
358    }
359
360    /// Returns the note updates that need to be applied after the transaction execution.
361    pub fn note_updates(&self) -> &NoteUpdates {
362        &self.note_updates
363    }
364
365    /// Returns the new tags that were created as part of the transaction.
366    pub fn new_tags(&self) -> &[NoteTagRecord] {
367        &self.new_tags
368    }
369}
370
371/// Contains transaction changes to apply to the store.
372#[derive(Default)]
373pub struct TransactionUpdates {
374    /// Transaction updates for any transaction that was committed between the sync request's block
375    /// number and the response's block number.
376    committed_transactions: Vec<TransactionUpdate>,
377    /// Transaction IDs for any transactions that were discarded in the sync.
378    discarded_transactions: Vec<TransactionId>,
379    /// Transactions that were pending before the sync and were not committed.
380    ///
381    /// These transactions have been pending for more than [`TX_GRACEFUL_BLOCKS`] blocks and can be
382    /// assumed to have been rejected by the network. They will be marked as discarded in the
383    /// store.
384    stale_transactions: Vec<TransactionRecord>,
385}
386
387impl TransactionUpdates {
388    /// Creates a new [`TransactionUpdate`]
389    pub fn new(
390        committed_transactions: Vec<TransactionUpdate>,
391        discarded_transactions: Vec<TransactionId>,
392        stale_transactions: Vec<TransactionRecord>,
393    ) -> Self {
394        Self {
395            committed_transactions,
396            discarded_transactions,
397            stale_transactions,
398        }
399    }
400
401    /// Extends the transaction update information with `other`.
402    pub fn extend(&mut self, other: Self) {
403        self.committed_transactions.extend(other.committed_transactions);
404        self.discarded_transactions.extend(other.discarded_transactions);
405        self.stale_transactions.extend(other.stale_transactions);
406    }
407
408    /// Returns a reference to committed transactions.
409    pub fn committed_transactions(&self) -> &[TransactionUpdate] {
410        &self.committed_transactions
411    }
412
413    /// Returns a reference to discarded transactions.
414    pub fn discarded_transactions(&self) -> &[TransactionId] {
415        &self.discarded_transactions
416    }
417
418    /// Inserts a discarded transaction into the transaction updates.
419    pub fn insert_discarded_transaction(&mut self, transaction_id: TransactionId) {
420        self.discarded_transactions.push(transaction_id);
421    }
422
423    /// Returns a reference to stale transactions.
424    pub fn stale_transactions(&self) -> &[TransactionRecord] {
425        &self.stale_transactions
426    }
427}
428
429/// Transaction management methods
430impl Client {
431    // TRANSACTION DATA RETRIEVAL
432    // --------------------------------------------------------------------------------------------
433
434    /// Retrieves tracked transactions, filtered by [`TransactionFilter`].
435    pub async fn get_transactions(
436        &self,
437        filter: TransactionFilter,
438    ) -> Result<Vec<TransactionRecord>, ClientError> {
439        self.store.get_transactions(filter).await.map_err(Into::into)
440    }
441
442    // TRANSACTION
443    // --------------------------------------------------------------------------------------------
444
445    /// Creates and executes a transaction specified by the request against the specified account,
446    /// but doesn't change the local database.
447    ///
448    /// If the transaction utilizes foreign account data, there is a chance that the client doesn't
449    /// have the required block header in the local database. In these scenarios, a sync to
450    /// the chain tip is performed, and the required block header is retrieved.
451    ///
452    /// # Errors
453    ///
454    /// - Returns [`ClientError::MissingOutputNotes`] if the [`TransactionRequest`] ouput notes are
455    ///   not a subset of executor's output notes.
456    /// - Returns a [`ClientError::TransactionExecutorError`] if the execution fails.
457    /// - Returns a [`ClientError::TransactionRequestError`] if the request is invalid.
458    pub async fn new_transaction(
459        &mut self,
460        account_id: AccountId,
461        transaction_request: TransactionRequest,
462    ) -> Result<TransactionResult, ClientError> {
463        // Validates the transaction request before executing
464        self.validate_request(account_id, &transaction_request).await?;
465
466        // Ensure authenticated notes have their inclusion proofs (a.k.a they're in a committed
467        // state). TODO: we should consider refactoring this in a way we can handle this in
468        // `get_transaction_inputs`
469        let authenticated_input_note_ids: Vec<NoteId> =
470            transaction_request.authenticated_input_note_ids().collect::<Vec<_>>();
471
472        let authenticated_note_records = self
473            .store
474            .get_input_notes(NoteFilter::List(authenticated_input_note_ids))
475            .await?;
476
477        for authenticated_note_record in authenticated_note_records {
478            if !authenticated_note_record.is_authenticated() {
479                return Err(ClientError::TransactionRequestError(
480                    TransactionRequestError::InputNoteNotAuthenticated,
481                ));
482            }
483        }
484
485        // If tx request contains unauthenticated_input_notes we should insert them
486        let unauthenticated_input_notes = transaction_request
487            .unauthenticated_input_notes()
488            .iter()
489            .cloned()
490            .map(Into::into)
491            .collect::<Vec<_>>();
492
493        self.store.upsert_input_notes(&unauthenticated_input_notes).await?;
494
495        let note_ids = transaction_request.get_input_note_ids();
496
497        let output_notes: Vec<Note> =
498            transaction_request.expected_output_notes().cloned().collect();
499
500        let future_notes: Vec<(NoteDetails, NoteTag)> =
501            transaction_request.expected_future_notes().cloned().collect();
502
503        let tx_script = transaction_request.build_transaction_script(
504            &self.get_account_interface(account_id).await?,
505            self.in_debug_mode,
506        )?;
507
508        let foreign_accounts = transaction_request.foreign_accounts().clone();
509        let mut tx_args = transaction_request.into_transaction_args(tx_script);
510
511        // Inject state and code of foreign accounts
512        let fpi_block_num =
513            self.inject_foreign_account_inputs(foreign_accounts, &mut tx_args).await?;
514
515        let block_num = if let Some(block_num) = fpi_block_num {
516            block_num
517        } else {
518            self.store.get_sync_height().await?
519        };
520
521        // Execute the transaction and get the witness
522        let executed_transaction = self
523            .tx_executor
524            .execute_transaction(account_id, block_num, &note_ids, tx_args)
525            .await?;
526
527        // Check that the expected output notes matches the transaction outcome.
528        // We compare authentication commitments where possible since that involves note IDs +
529        // metadata (as opposed to just note ID which remains the same regardless of
530        // metadata) We also do the check for partial output notes
531
532        let tx_note_auth_commitments: BTreeSet<Digest> =
533            notes_from_output(executed_transaction.output_notes())
534                .map(Note::commitment)
535                .collect();
536
537        let missing_note_ids: Vec<NoteId> = output_notes
538            .iter()
539            .filter_map(|n| (!tx_note_auth_commitments.contains(&n.commitment())).then_some(n.id()))
540            .collect();
541
542        if !missing_note_ids.is_empty() {
543            return Err(ClientError::MissingOutputNotes(missing_note_ids));
544        }
545
546        let screener = NoteScreener::new(self.store.clone());
547
548        TransactionResult::new(
549            executed_transaction,
550            screener,
551            future_notes,
552            self.get_sync_height().await?,
553            self.store.get_current_timestamp(),
554        )
555        .await
556    }
557
558    /// Proves the specified transaction using a local prover, submits it to the network, and saves
559    /// the transaction into the local database for tracking.
560    pub async fn submit_transaction(
561        &mut self,
562        tx_result: TransactionResult,
563    ) -> Result<(), ClientError> {
564        self.submit_transaction_with_prover(tx_result, self.tx_prover.clone()).await
565    }
566
567    /// Proves the specified transaction using the provided prover, submits it to the network, and
568    /// saves the transaction into the local database for tracking.
569    pub async fn submit_transaction_with_prover(
570        &mut self,
571        tx_result: TransactionResult,
572        tx_prover: Arc<dyn TransactionProver>,
573    ) -> Result<(), ClientError> {
574        let proven_transaction = self.prove_transaction(&tx_result, tx_prover).await?;
575        self.submit_proven_transaction(proven_transaction).await?;
576        self.apply_transaction(tx_result).await
577    }
578
579    /// Proves the specified transaction result using the provided prover.
580    async fn prove_transaction(
581        &mut self,
582        tx_result: &TransactionResult,
583        tx_prover: Arc<dyn TransactionProver>,
584    ) -> Result<ProvenTransaction, ClientError> {
585        info!("Proving transaction...");
586
587        let proven_transaction =
588            tx_prover.prove(tx_result.executed_transaction().clone().into()).await?;
589
590        info!("Transaction proven.");
591
592        Ok(proven_transaction)
593    }
594
595    async fn submit_proven_transaction(
596        &mut self,
597        proven_transaction: ProvenTransaction,
598    ) -> Result<(), ClientError> {
599        info!("Submitting transaction to the network...");
600        self.rpc_api.submit_proven_transaction(proven_transaction).await?;
601        info!("Transaction submitted.");
602
603        Ok(())
604    }
605
606    async fn apply_transaction(&self, tx_result: TransactionResult) -> Result<(), ClientError> {
607        let transaction_id = tx_result.executed_transaction().id();
608        let sync_height = self.get_sync_height().await?;
609
610        // Transaction was proven and submitted to the node correctly, persist note details and
611        // update account
612        info!("Applying transaction to the local store...");
613
614        let account_id = tx_result.executed_transaction().account_id();
615        let account_delta = tx_result.account_delta();
616        let account_record = self.try_get_account(account_id).await?;
617
618        if account_record.is_locked() {
619            return Err(ClientError::AccountLocked(account_id));
620        }
621
622        let mut account: Account = account_record.into();
623        account.apply_delta(account_delta)?;
624
625        if self
626            .store
627            .get_account_header_by_commitment(account.commitment())
628            .await?
629            .is_some()
630        {
631            return Err(ClientError::StoreError(StoreError::AccountCommitmentAlreadyExists(
632                account.commitment(),
633            )));
634        }
635
636        // Save only input notes that we care for (based on the note screener assessment)
637        let created_input_notes = tx_result.relevant_notes().to_vec();
638        let new_tags = created_input_notes
639            .iter()
640            .filter_map(|note| {
641                if let InputNoteState::Expected(ExpectedNoteState { tag: Some(tag), .. }) =
642                    note.state()
643                {
644                    Some(NoteTagRecord::with_note_source(*tag, note.id()))
645                } else {
646                    None
647                }
648            })
649            .collect();
650
651        // Save all output notes
652        let created_output_notes = tx_result
653            .created_notes()
654            .iter()
655            .cloned()
656            .filter_map(|output_note| {
657                OutputNoteRecord::try_from_output_note(output_note, sync_height).ok()
658            })
659            .collect::<Vec<_>>();
660
661        let consumed_note_ids = tx_result.consumed_notes().iter().map(InputNote::id).collect();
662        let consumed_notes = self.get_input_notes(NoteFilter::List(consumed_note_ids)).await?;
663
664        let mut updated_input_notes = vec![];
665        for mut input_note_record in consumed_notes {
666            if input_note_record.consumed_locally(
667                account_id,
668                transaction_id,
669                self.store.get_current_timestamp(),
670            )? {
671                updated_input_notes.push(input_note_record);
672            }
673        }
674
675        let tx_update = TransactionStoreUpdate::new(
676            tx_result.into(),
677            account,
678            created_input_notes,
679            created_output_notes,
680            updated_input_notes,
681            new_tags,
682        );
683
684        self.store.apply_transaction(tx_update).await?;
685        info!("Transaction stored.");
686        Ok(())
687    }
688
689    /// Compiles the provided transaction script source and inputs into a [`TransactionScript`].
690    pub fn compile_tx_script<T>(
691        &self,
692        inputs: T,
693        program: &str,
694    ) -> Result<TransactionScript, ClientError>
695    where
696        T: IntoIterator<Item = (Word, Vec<Felt>)>,
697    {
698        let assembler = TransactionKernel::assembler().with_debug_mode(self.in_debug_mode);
699        TransactionScript::compile(program, inputs, assembler)
700            .map_err(ClientError::TransactionScriptError)
701    }
702
703    // HELPERS
704    // --------------------------------------------------------------------------------------------
705
706    /// Helper to get the account outgoing assets.
707    ///
708    /// Any outgoing assets resulting from executing note scripts but not present in expected output
709    /// notes wouldn't be included.
710    fn get_outgoing_assets(
711        transaction_request: &TransactionRequest,
712    ) -> (BTreeMap<AccountId, u64>, BTreeSet<NonFungibleAsset>) {
713        // Get own notes assets
714        let mut own_notes_assets = match transaction_request.script_template() {
715            Some(TransactionScriptTemplate::SendNotes(notes)) => {
716                notes.iter().map(|note| (note.id(), note.assets())).collect::<BTreeMap<_, _>>()
717            },
718            _ => BTreeMap::default(),
719        };
720        // Get transaction output notes assets
721        let mut output_notes_assets = transaction_request
722            .expected_output_notes()
723            .map(|note| (note.id(), note.assets()))
724            .collect::<BTreeMap<_, _>>();
725
726        // Merge with own notes assets and delete duplicates
727        output_notes_assets.append(&mut own_notes_assets);
728
729        // Create a map of the fungible and non-fungible assets in the output notes
730        let outgoing_assets =
731            output_notes_assets.values().flat_map(|note_assets| note_assets.iter());
732
733        collect_assets(outgoing_assets)
734    }
735
736    /// Helper to get the account incoming assets.
737    async fn get_incoming_assets(
738        &self,
739        transaction_request: &TransactionRequest,
740    ) -> Result<(BTreeMap<AccountId, u64>, BTreeSet<NonFungibleAsset>), TransactionRequestError>
741    {
742        // Get incoming asset notes excluding unauthenticated ones
743        let incoming_notes_ids: Vec<_> = transaction_request
744            .input_notes()
745            .iter()
746            .filter_map(|(note_id, _)| {
747                if transaction_request
748                    .unauthenticated_input_notes()
749                    .iter()
750                    .any(|note| note.id() == *note_id)
751                {
752                    None
753                } else {
754                    Some(*note_id)
755                }
756            })
757            .collect();
758
759        let store_input_notes = self
760            .get_input_notes(NoteFilter::List(incoming_notes_ids))
761            .await
762            .map_err(|err| TransactionRequestError::NoteNotFound(err.to_string()))?;
763
764        let all_incoming_assets =
765            store_input_notes.iter().flat_map(|note| note.assets().iter()).chain(
766                transaction_request
767                    .unauthenticated_input_notes()
768                    .iter()
769                    .flat_map(|note| note.assets().iter()),
770            );
771
772        Ok(collect_assets(all_incoming_assets))
773    }
774
775    async fn validate_basic_account_request(
776        &self,
777        transaction_request: &TransactionRequest,
778        account: &Account,
779    ) -> Result<(), ClientError> {
780        // Get outgoing assets
781        let (fungible_balance_map, non_fungible_set) =
782            Client::get_outgoing_assets(transaction_request);
783
784        // Get incoming assets
785        let (incoming_fungible_balance_map, incoming_non_fungible_balance_set) =
786            self.get_incoming_assets(transaction_request).await?;
787
788        // Check if the account balance plus incoming assets is greater than or equal to the
789        // outgoing fungible assets
790        for (faucet_id, amount) in fungible_balance_map {
791            let account_asset_amount = account.vault().get_balance(faucet_id).unwrap_or(0);
792            let incoming_balance = incoming_fungible_balance_map.get(&faucet_id).unwrap_or(&0);
793            if account_asset_amount + incoming_balance < amount {
794                return Err(ClientError::AssetError(
795                    AssetError::FungibleAssetAmountNotSufficient {
796                        minuend: account_asset_amount,
797                        subtrahend: amount,
798                    },
799                ));
800            }
801        }
802
803        // Check if the account balance plus incoming assets is greater than or equal to the
804        // outgoing non fungible assets
805        for non_fungible in non_fungible_set {
806            match account.vault().has_non_fungible_asset(non_fungible) {
807                Ok(true) => (),
808                Ok(false) => {
809                    // Check if the non fungible asset is in the incoming assets
810                    if !incoming_non_fungible_balance_set.contains(&non_fungible) {
811                        return Err(ClientError::AssetError(
812                            AssetError::NonFungibleFaucetIdTypeMismatch(
813                                non_fungible.faucet_id_prefix(),
814                            ),
815                        ));
816                    }
817                },
818                _ => {
819                    return Err(ClientError::AssetError(
820                        AssetError::NonFungibleFaucetIdTypeMismatch(
821                            non_fungible.faucet_id_prefix(),
822                        ),
823                    ));
824                },
825            }
826        }
827
828        Ok(())
829    }
830
831    /// Validates that the specified transaction request can be executed by the specified account.
832    ///
833    /// This does't guarantee that the transaction will succeed, but it's useful to avoid submitting
834    /// transactions that are guaranteed to fail. Some of the validations include:
835    /// - That the account has enough balance to cover the outgoing assets.
836    /// - That the client is not too far behind the chain tip.
837    pub async fn validate_request(
838        &mut self,
839        account_id: AccountId,
840        transaction_request: &TransactionRequest,
841    ) -> Result<(), ClientError> {
842        let current_chain_tip =
843            self.rpc_api.get_block_header_by_number(None, false).await?.0.block_num();
844
845        if current_chain_tip > self.store.get_sync_height().await? + MAX_BLOCK_NUMBER_DELTA {
846            return Err(ClientError::RecencyConditionError(
847                "The client is too far behind the chain tip to execute the transaction".to_string(),
848            ));
849        }
850
851        let account: Account = self.try_get_account(account_id).await?.into();
852
853        if account.is_faucet() {
854            // TODO(SantiagoPittella): Add faucet validations.
855            Ok(())
856        } else {
857            self.validate_basic_account_request(transaction_request, &account).await
858        }
859    }
860
861    /// Retrieves the account interface for the specified account.
862    async fn get_account_interface(
863        &mut self,
864        account_id: AccountId,
865    ) -> Result<AccountInterface, ClientError> {
866        let account: Account = self.try_get_account(account_id).await?.into();
867
868        Ok(AccountInterface::from(&account))
869    }
870
871    /// Injects foreign account data inputs into `tx_args` (account proof, code commitment and
872    /// storage data). Additionally loads the account code into the transaction executor.
873    ///
874    /// For any [`ForeignAccount::Public`] in `foreing_accounts`, these pieces of data are retrieved
875    /// from the network. For any [`ForeignAccount::Private`] account, inner data is used.
876    ///
877    /// Account data is retrieved for the node's current chain tip, so we need to check whether we
878    /// currently have the corresponding block header data. Otherwise, we additionally need to
879    /// retrieve it, this implies a state sync call which may update the client in other ways.
880    ///
881    /// # Errors
882    /// - Returns a [`ClientError::RecencyConditionError`] if the foreign account proofs are too far
883    ///   in the future.
884    async fn inject_foreign_account_inputs(
885        &mut self,
886        foreign_accounts: BTreeSet<ForeignAccount>,
887        tx_args: &mut TransactionArgs,
888    ) -> Result<Option<BlockNumber>, ClientError> {
889        if foreign_accounts.is_empty() {
890            return Ok(None);
891        }
892
893        let account_ids = foreign_accounts.iter().map(ForeignAccount::account_id);
894        let known_account_codes =
895            self.store.get_foreign_account_code(account_ids.collect()).await?;
896
897        let known_account_codes: Vec<AccountCode> = known_account_codes.into_values().collect();
898
899        // Fetch account proofs
900        let (block_num, account_proofs) =
901            self.rpc_api.get_account_proofs(&foreign_accounts, known_account_codes).await?;
902
903        let mut account_proofs: BTreeMap<AccountId, AccountProof> =
904            account_proofs.into_iter().map(|proof| (proof.account_id(), proof)).collect();
905
906        for foreign_account in &foreign_accounts {
907            let (foreign_account_inputs, merkle_path) = match foreign_account {
908                ForeignAccount::Public(account_id, ..) => {
909                    let account_proof = account_proofs
910                        .remove(account_id)
911                        .expect("Proof was requested and received");
912
913                    let (foreign_account_inputs, merkle_path) = account_proof.try_into()?;
914
915                    // Update  our foreign account code cache
916                    self.store
917                        .upsert_foreign_account_code(
918                            *account_id,
919                            foreign_account_inputs.account_code().clone(),
920                        )
921                        .await?;
922
923                    (foreign_account_inputs, merkle_path)
924                },
925                ForeignAccount::Private(foreign_account_inputs) => {
926                    let account_id = foreign_account_inputs.account_header().id();
927                    let proof = account_proofs
928                        .remove(&account_id)
929                        .expect("Proof was requested and received");
930                    let merkle_path = proof.merkle_proof();
931
932                    (foreign_account_inputs.clone(), merkle_path.clone())
933                },
934            };
935
936            extend_advice_inputs_for_foreign_account(
937                tx_args,
938                &mut self.tx_executor,
939                foreign_account_inputs,
940                &merkle_path,
941            )?;
942        }
943
944        // Optionally retrieve block header if we don't have it
945        if self.store.get_block_headers(&[block_num]).await?.is_empty() {
946            info!(
947                "Getting current block header data to execute transaction with foreign account requirements"
948            );
949            let summary = self.sync_state().await?;
950
951            if summary.block_num != block_num {
952                let mut current_partial_mmr = self.build_current_partial_mmr(true).await?;
953                self.get_and_store_authenticated_block(block_num, &mut current_partial_mmr)
954                    .await?;
955            }
956        }
957
958        Ok(Some(block_num))
959    }
960
961    /// Executes the provided transaction script against the specified account, and returns the
962    /// resulting stack. Advice inputs and foreign accounts can be provided for the execution.
963    ///
964    /// The transaction will use the current sync height as the block reference.
965    pub async fn execute_program(
966        &mut self,
967        account_id: AccountId,
968        tx_script: TransactionScript,
969        advice_inputs: AdviceInputs,
970        foreign_accounts: BTreeSet<ForeignAccount>,
971    ) -> Result<[Felt; 16], ClientError> {
972        let block_ref = self.get_sync_height().await?;
973
974        let mut tx_args =
975            TransactionArgs::with_tx_script(tx_script).with_advice_inputs(advice_inputs);
976        self.inject_foreign_account_inputs(foreign_accounts, &mut tx_args).await?;
977
978        Ok(self
979            .tx_executor
980            .execute_tx_view_script(
981                account_id,
982                block_ref,
983                tx_args.tx_script().expect("Transaction script should be present").clone(),
984                tx_args.advice_inputs().clone(),
985            )
986            .await?)
987    }
988}
989
990// TESTING HELPERS
991// ================================================================================================
992
993#[cfg(feature = "testing")]
994impl Client {
995    pub async fn testing_prove_transaction(
996        &mut self,
997        tx_result: &TransactionResult,
998    ) -> Result<ProvenTransaction, ClientError> {
999        self.prove_transaction(tx_result, self.tx_prover.clone()).await
1000    }
1001
1002    pub async fn testing_submit_proven_transaction(
1003        &mut self,
1004        proven_transaction: ProvenTransaction,
1005    ) -> Result<(), ClientError> {
1006        self.submit_proven_transaction(proven_transaction).await
1007    }
1008
1009    pub async fn testing_apply_transaction(
1010        &self,
1011        tx_result: TransactionResult,
1012    ) -> Result<(), ClientError> {
1013        self.apply_transaction(tx_result).await
1014    }
1015}
1016
1017/// Extends the advice inputs with account data and Merkle proofs, and loads the necessary
1018/// [code](AccountCode) in `tx_executor`.
1019fn extend_advice_inputs_for_foreign_account(
1020    tx_args: &mut TransactionArgs,
1021    tx_executor: &mut TransactionExecutor,
1022    foreign_account_inputs: ForeignAccountInputs,
1023    merkle_path: &MerklePath,
1024) -> Result<(), ClientError> {
1025    let (account_header, storage_header, account_code, proofs) =
1026        foreign_account_inputs.into_parts();
1027
1028    let account_id = account_header.id();
1029    let foreign_id_root =
1030        Digest::from([account_id.suffix(), account_id.prefix().as_felt(), ZERO, ZERO]);
1031
1032    // Extend the advice inputs with the new data
1033    tx_args.extend_advice_map([
1034        // ACCOUNT_ID -> [ID_AND_NONCE, VAULT_ROOT, STORAGE_ROOT, CODE_ROOT]
1035        (foreign_id_root, account_header.as_elements()),
1036        // STORAGE_ROOT -> [STORAGE_SLOT_DATA]
1037        (account_header.storage_commitment(), storage_header.as_elements()),
1038        // CODE_ROOT -> [ACCOUNT_CODE_DATA]
1039        (account_header.code_commitment(), account_code.as_elements()),
1040    ]);
1041
1042    // Load merkle nodes for storage maps
1043    for proof in proofs {
1044        // Extend the merkle store and map with the storage maps
1045        tx_args.extend_merkle_store(
1046            proof.path().inner_nodes(proof.leaf().index().value(), proof.leaf().hash())?,
1047        );
1048        // Populate advice map with Sparse Merkle Tree leaf nodes
1049        tx_args
1050            .extend_advice_map(core::iter::once((proof.leaf().hash(), proof.leaf().to_elements())));
1051    }
1052
1053    // Extend the advice inputs with Merkle store data
1054    tx_args.extend_merkle_store(
1055        merkle_path.inner_nodes(account_id.prefix().as_u64(), account_header.commitment())?,
1056    );
1057
1058    tx_executor.load_account_code(&account_code);
1059
1060    Ok(())
1061}
1062
1063// HELPERS
1064// ================================================================================================
1065
1066fn collect_assets<'a>(
1067    assets: impl Iterator<Item = &'a Asset>,
1068) -> (BTreeMap<AccountId, u64>, BTreeSet<NonFungibleAsset>) {
1069    let mut fungible_balance_map = BTreeMap::new();
1070    let mut non_fungible_set = BTreeSet::new();
1071
1072    assets.for_each(|asset| match asset {
1073        Asset::Fungible(fungible) => {
1074            fungible_balance_map
1075                .entry(fungible.faucet_id())
1076                .and_modify(|balance| *balance += fungible.amount())
1077                .or_insert(fungible.amount());
1078        },
1079        Asset::NonFungible(non_fungible) => {
1080            non_fungible_set.insert(*non_fungible);
1081        },
1082    });
1083
1084    (fungible_balance_map, non_fungible_set)
1085}
1086
1087/// Extracts notes from [`OutputNotes`].
1088/// Used for:
1089/// - Checking the relevance of notes to save them as input notes.
1090/// - Validate hashes versus expected output notes after a transaction is executed.
1091pub fn notes_from_output(output_notes: &OutputNotes) -> impl Iterator<Item = &Note> {
1092    output_notes
1093        .iter()
1094        .filter(|n| matches!(n, OutputNote::Full(_)))
1095        .map(|n| match n {
1096            OutputNote::Full(n) => n,
1097            // The following todo!() applies until we have a way to support flows where we have
1098            // partial details of the note
1099            OutputNote::Header(_) | OutputNote::Partial(_) => {
1100                todo!("For now, all details should be held in OutputNote::Fulls")
1101            },
1102        })
1103}
1104
1105#[cfg(test)]
1106mod test {
1107    use miden_lib::{account::auth::RpoFalcon512, transaction::TransactionKernel};
1108    use miden_objects::{
1109        Word,
1110        account::{AccountBuilder, AccountComponent, AuthSecretKey, StorageMap, StorageSlot},
1111        asset::{Asset, FungibleAsset},
1112        crypto::dsa::rpo_falcon512::SecretKey,
1113        note::NoteType,
1114        testing::{
1115            account_component::BASIC_WALLET_CODE,
1116            account_id::{
1117                ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET, ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
1118                ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
1119            },
1120        },
1121    };
1122    use miden_tx::utils::{Deserializable, Serializable};
1123
1124    use super::PaymentTransactionData;
1125    use crate::{
1126        mock::create_test_client,
1127        transaction::{TransactionRequestBuilder, TransactionResult},
1128    };
1129
1130    #[tokio::test]
1131    async fn test_transaction_creates_two_notes() {
1132        let (mut client, _, keystore) = create_test_client().await;
1133        let asset_1: Asset =
1134            FungibleAsset::new(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET.try_into().unwrap(), 123)
1135                .unwrap()
1136                .into();
1137        let asset_2: Asset =
1138            FungibleAsset::new(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET.try_into().unwrap(), 500)
1139                .unwrap()
1140                .into();
1141
1142        let secret_key = SecretKey::new();
1143        let pub_key = secret_key.public_key();
1144        keystore.add_key(&AuthSecretKey::RpoFalcon512(secret_key)).unwrap();
1145
1146        let wallet_component = AccountComponent::compile(
1147            BASIC_WALLET_CODE,
1148            TransactionKernel::assembler(),
1149            vec![StorageSlot::Value(Word::default()), StorageSlot::Map(StorageMap::default())],
1150        )
1151        .unwrap()
1152        .with_supports_all_types();
1153
1154        let anchor_block = client.get_latest_epoch_block().await.unwrap();
1155
1156        let account = AccountBuilder::new(Default::default())
1157            .anchor((&anchor_block).try_into().unwrap())
1158            .with_component(wallet_component)
1159            .with_component(RpoFalcon512::new(pub_key))
1160            .with_assets([asset_1, asset_2])
1161            .build_existing()
1162            .unwrap();
1163
1164        client.add_account(&account, None, false).await.unwrap();
1165        client.sync_state().await.unwrap();
1166        let tx_request = TransactionRequestBuilder::pay_to_id(
1167            PaymentTransactionData::new(
1168                vec![asset_1, asset_2],
1169                account.id(),
1170                ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE.try_into().unwrap(),
1171            ),
1172            None,
1173            NoteType::Private,
1174            client.rng(),
1175        )
1176        .unwrap()
1177        .build()
1178        .unwrap();
1179
1180        let tx_result = client.new_transaction(account.id(), tx_request).await.unwrap();
1181        assert!(
1182            tx_result
1183                .created_notes()
1184                .get_note(0)
1185                .assets()
1186                .is_some_and(|assets| assets.num_assets() == 2)
1187        );
1188        // Prove and apply transaction
1189        client.testing_apply_transaction(tx_result.clone()).await.unwrap();
1190
1191        // Test serialization
1192        let bytes: std::vec::Vec<u8> = tx_result.to_bytes();
1193        let decoded = TransactionResult::read_from_bytes(&bytes).unwrap();
1194
1195        assert_eq!(tx_result, decoded);
1196    }
1197}