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