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