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