Skip to main content

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::{PaymentNoteDescription, TransactionRequestBuilder};
27//! use miden_protocol::account::AccountId;
28//! use miden_protocol::asset::FungibleAsset;
29//! use miden_protocol::note::NoteType;
30//! # use std::error::Error;
31//!
32//! /// Executes, proves and submits a P2ID transaction.
33//! ///
34//! /// This transaction is executed by `sender_id`, and creates an output note
35//! /// containing 100 tokens of `faucet_id`'s fungible asset.
36//! async fn create_and_submit_transaction<
37//!     R: rand::Rng,
38//!     AUTH: TransactionAuthenticator + Sync + 'static,
39//! >(
40//!     client: &mut Client<AUTH>,
41//!     sender_id: AccountId,
42//!     target_id: AccountId,
43//!     faucet_id: AccountId,
44//! ) -> Result<(), Box<dyn Error>> {
45//!     // Create an asset representing the amount to be transferred.
46//!     let asset = FungibleAsset::new(faucet_id, 100)?;
47//!
48//!     // Build a transaction request for a pay-to-id transaction.
49//!     let tx_request = TransactionRequestBuilder::new().build_pay_to_id(
50//!         PaymentNoteDescription::new(vec![asset.into()], sender_id, target_id),
51//!         NoteType::Private,
52//!         client.rng(),
53//!     )?;
54//!
55//!     // Execute, prove, and submit the transaction in a single call.
56//!     let _tx_id = client.submit_new_transaction(sender_id, tx_request).await?;
57//!
58//!     Ok(())
59//! }
60//! ```
61//!
62//! For more detailed information about each function and error type, refer to the specific API
63//! documentation.
64
65use alloc::boxed::Box;
66use alloc::collections::{BTreeMap, BTreeSet};
67use alloc::sync::Arc;
68use alloc::vec::Vec;
69
70use miden_protocol::account::{Account, AccountCode, AccountId};
71use miden_protocol::asset::{Asset, NonFungibleAsset};
72use miden_protocol::block::BlockNumber;
73use miden_protocol::errors::AssetError;
74use miden_protocol::note::{
75    Note,
76    NoteAttachments,
77    NoteDetails,
78    NoteId,
79    NoteRecipient,
80    NoteScript,
81    NoteTag,
82};
83use miden_protocol::transaction::AccountInputs;
84use miden_protocol::vm::MIN_STACK_DEPTH;
85use miden_protocol::{Felt, Word};
86use miden_standards::account::interface::AccountInterfaceExt;
87use miden_tx::{DataStore, NoteConsumptionChecker, TransactionExecutor};
88use tracing::info;
89
90use super::Client;
91use crate::ClientError;
92use crate::note::{NoteScreenerError, NoteUpdateTracker, StandardNote};
93use crate::rpc::domain::account::{
94    AccountStorageRequirements,
95    GetAccountRequest,
96    StorageMapFetch,
97    VaultFetch,
98};
99use crate::rpc::{AccountStateAt, NodeRpcClient};
100use crate::store::data_store::ClientDataStore;
101use crate::store::input_note_states::ExpectedNoteState;
102use crate::store::{
103    AccountRecord,
104    InputNoteRecord,
105    InputNoteState,
106    NoteFilter,
107    NoteRecordError,
108    OutputNoteRecord,
109    Store,
110    StoreError,
111    TransactionFilter,
112};
113use crate::sync::NoteTagRecord;
114use crate::transaction::batch::InMemoryBatchDataStore;
115
116pub mod batch;
117pub use batch::{BatchBuilder, BatchBuilderError};
118
119#[cfg(feature = "dap")]
120mod dap_executor;
121mod prover;
122pub use prover::TransactionProver;
123
124mod record;
125pub use record::{
126    DiscardCause,
127    TransactionDetails,
128    TransactionRecord,
129    TransactionStatus,
130    TransactionStatusVariant,
131};
132
133mod store_update;
134pub use store_update::TransactionStoreUpdate;
135
136mod request;
137pub use request::{
138    ForeignAccount,
139    NoteArgs,
140    PaymentNoteDescription,
141    PswapTransactionData,
142    SwapTransactionData,
143    TransactionRequest,
144    TransactionRequestBuilder,
145    TransactionRequestError,
146    TransactionScriptTemplate,
147};
148
149mod result;
150// RE-EXPORTS
151// ================================================================================================
152pub use miden_protocol::transaction::{
153    ExecutedTransaction,
154    InputNote,
155    InputNotes,
156    OutputNote,
157    OutputNotes,
158    ProvenTransaction,
159    PublicOutputNote,
160    RawOutputNote,
161    RawOutputNotes,
162    TransactionArgs,
163    TransactionId,
164    TransactionInputs,
165    TransactionKernel,
166    TransactionScript,
167    TransactionScriptRoot,
168    TransactionSummary,
169};
170pub use miden_protocol::vm::{AdviceInputs, AdviceMap};
171pub use miden_standards::account::interface::{AccountComponentInterface, AccountInterface};
172pub use miden_tx::auth::TransactionAuthenticator;
173pub use miden_tx::{
174    DataStoreError,
175    LocalTransactionProver,
176    ProvingOptions,
177    TransactionExecutorError,
178    TransactionProverError,
179};
180pub use result::TransactionResult;
181
182/// Transaction management methods
183impl<AUTH> Client<AUTH>
184where
185    AUTH: TransactionAuthenticator + Sync + 'static,
186{
187    // TRANSACTION DATA RETRIEVAL
188    // --------------------------------------------------------------------------------------------
189
190    /// Retrieves tracked transactions, filtered by [`TransactionFilter`].
191    pub async fn get_transactions(
192        &self,
193        filter: TransactionFilter,
194    ) -> Result<Vec<TransactionRecord>, ClientError> {
195        self.store.get_transactions(filter).await.map_err(Into::into)
196    }
197
198    // TRANSACTION BATCH
199    // --------------------------------------------------------------------------------------------
200
201    /// Open a new [`BatchBuilder`] for accumulating transactions across one or more local
202    /// accounts.
203    ///
204    /// See [`crate::transaction::batch`] for usage and constraints.
205    pub fn new_transaction_batch(&self) -> BatchBuilder<'_, AUTH> {
206        let inner_data_store = ClientDataStore::new(self.store.clone(), self.rpc_api.clone());
207        BatchBuilder {
208            client: self,
209            data_store: InMemoryBatchDataStore::new(inner_data_store),
210            pushed_txs: Vec::new(),
211            consumed_input_notes: BTreeSet::new(),
212        }
213    }
214
215    // TRANSACTION
216    // --------------------------------------------------------------------------------------------
217
218    /// Executes a transaction specified by the request against the specified account,
219    /// proves it, submits it to the network, and updates the local database.
220    ///
221    /// Uses the client's default prover (configured via
222    /// [`crate::builder::ClientBuilder::prover`]).
223    pub async fn submit_new_transaction(
224        &mut self,
225        account_id: AccountId,
226        transaction_request: TransactionRequest,
227    ) -> Result<TransactionId, ClientError> {
228        let prover = self.tx_prover.clone();
229        self.submit_new_transaction_with_prover(account_id, transaction_request, prover)
230            .await
231    }
232
233    /// Executes a transaction specified by the request against the specified account,
234    /// proves it with the provided prover, submits it to the network, and updates the local
235    /// database.
236    ///
237    /// This is useful for falling back to a different prover (e.g., local) when the default
238    /// prover (e.g., remote) fails with a [`ClientError::TransactionProvingError`].
239    pub async fn submit_new_transaction_with_prover(
240        &mut self,
241        account_id: AccountId,
242        transaction_request: TransactionRequest,
243        tx_prover: Arc<dyn TransactionProver>,
244    ) -> Result<TransactionId, ClientError> {
245        // Register any missing NTX scripts before the main transaction.
246        // The registration path contains its own full execute -> prove -> submit pipeline.
247        if !transaction_request.expected_ntx_scripts().is_empty() {
248            Box::pin(self.ensure_ntx_scripts_registered(
249                account_id,
250                transaction_request.expected_ntx_scripts(),
251                tx_prover.clone(),
252            ))
253            .await?;
254        }
255
256        let tx_result = self.execute_transaction(account_id, transaction_request).await?;
257        let tx_id = tx_result.executed_transaction().id();
258
259        let proven_transaction = self.prove_transaction_with(&tx_result, tx_prover).await?;
260        let submission_height =
261            self.submit_proven_transaction(proven_transaction, &tx_result).await?;
262
263        // The transaction has been accepted by the node; the local store update
264        // is a separate step that can fail independently. On failure, return a
265        // distinct error carrying the pending update so the caller can decide
266        // how to recover (re-apply later via `apply_transaction_update`,
267        // persist for the next session, etc.).
268        //
269        // The update is boxed so it does not inflate the enclosing future
270        // across await points (triggers clippy::large_futures).
271        let tx_update =
272            Box::new(self.get_transaction_store_update(&tx_result, submission_height).await?);
273
274        if let Err(apply_err) = self.apply_transaction_update((*tx_update).clone()).await {
275            info!(
276                "apply_transaction_update failed for submitted tx {tx_id}; returning \
277                 ApplyTransactionAfterSubmitFailed with the pending update attached: {apply_err}"
278            );
279            return Err(ClientError::ApplyTransactionAfterSubmitFailed {
280                pending_update: tx_update,
281                source: Box::new(apply_err),
282            });
283        }
284
285        Ok(tx_id)
286    }
287
288    /// Creates and executes a transaction specified by the request against the specified account,
289    /// but doesn't change the local database.
290    ///
291    /// # Errors
292    ///
293    /// - Returns [`ClientError::MissingOutputRecipients`] if the [`TransactionRequest`] output
294    ///   notes are not a subset of executor's output notes.
295    /// - Returns a [`ClientError::TransactionExecutorError`] if the execution fails.
296    /// - Returns a [`ClientError::TransactionRequestError`] if the request is invalid.
297    pub async fn execute_transaction(
298        &mut self,
299        account_id: AccountId,
300        transaction_request: TransactionRequest,
301    ) -> Result<TransactionResult, ClientError> {
302        let account: Account = self.get_native_account_record(account_id).await?.try_into()?;
303
304        let prep = self.prepare_transaction(&account, transaction_request).await?;
305
306        let data_store = ClientDataStore::new(self.store.clone(), self.rpc_api.clone());
307        data_store.register_note_scripts(prep.output_note_scripts());
308        for fpi_account in &prep.foreign_account_inputs {
309            data_store.mast_store().load_account_code(fpi_account.code());
310        }
311        data_store.register_foreign_account_inputs(prep.foreign_account_inputs);
312
313        data_store.mast_store().load_account_code(account.code());
314
315        let mut notes = prep.notes;
316        if prep.ignore_invalid_notes {
317            notes = self
318                .get_valid_input_notes(
319                    &account,
320                    notes,
321                    prep.tx_args.clone(),
322                    &prep.output_recipients,
323                )
324                .await?;
325        }
326
327        let executed_transaction = self
328            .build_executor(&data_store)?
329            .execute_transaction(account_id, prep.block_num, notes, prep.tx_args)
330            .await?;
331
332        validate_executed_transaction(&executed_transaction, &prep.output_recipients)?;
333        TransactionResult::new(executed_transaction, prep.future_notes)
334    }
335
336    /// Performs the data-store-independent setup shared by `execute_transaction` and
337    /// `execute_transaction_for_batch`: validates the request against the supplied
338    /// `account`, loads/filters input notes, builds the transaction script and args,
339    /// retrieves foreign-account inputs, and computes the reference block number.
340    ///
341    /// This method does not write to the store: any state produced by the transaction is
342    /// persisted only after the transaction executes successfully.
343    ///
344    /// `account` is the state validation runs against — for a single transaction this is
345    /// the persisted account; inside [`crate::transaction::BatchBuilder::push`] it is the
346    /// in-batch (stacked) state, so balances reflect prior pushes.
347    pub(crate) async fn prepare_transaction(
348        &self,
349        account: &Account,
350        transaction_request: TransactionRequest,
351    ) -> Result<PreparedTransaction, ClientError> {
352        let account_id = account.id();
353        self.validate_recency().await?;
354        validate_account_request(&transaction_request, account)?;
355
356        // Retrieve all input notes from the store.
357        let mut stored_note_records = self
358            .store
359            .get_input_notes(NoteFilter::List(transaction_request.input_note_ids().collect()))
360            .await?;
361
362        // Verify that none of the authenticated input notes are already consumed.
363        for note in &stored_note_records {
364            if note.is_consumed() {
365                let id = note.id().expect(
366                    "stored note records reaching this check carry metadata so id() is Some",
367                );
368                return Err(ClientError::TransactionRequestError(
369                    TransactionRequestError::InputNoteAlreadyConsumed(id),
370                ));
371            }
372        }
373
374        // Only keep authenticated input notes from the store.
375        stored_note_records.retain(InputNoteRecord::is_authenticated);
376
377        let notes = transaction_request.build_input_notes(stored_note_records)?;
378
379        let output_recipients =
380            transaction_request.expected_output_recipients().cloned().collect::<Vec<_>>();
381
382        let future_notes: Vec<(NoteDetails, NoteTag)> =
383            transaction_request.expected_future_notes().cloned().collect();
384
385        let tx_script = transaction_request
386            .build_transaction_script(&self.get_account_interface(account_id).await?)?;
387
388        let foreign_accounts = transaction_request.foreign_accounts().clone();
389
390        let (fpi_block_num, foreign_account_inputs) =
391            self.retrieve_foreign_account_inputs(foreign_accounts).await?;
392
393        let ignore_invalid_notes = transaction_request.ignore_invalid_input_notes();
394
395        let block_num = if let Some(block_num) = fpi_block_num {
396            block_num
397        } else {
398            self.store.get_sync_height().await?
399        };
400
401        let tx_args = transaction_request.into_transaction_args(tx_script);
402
403        Ok(PreparedTransaction {
404            notes,
405            output_recipients,
406            future_notes,
407            tx_args,
408            foreign_account_inputs,
409            block_num,
410            ignore_invalid_notes,
411        })
412    }
413
414    /// Proves the specified transaction using the prover configured for this client.
415    pub async fn prove_transaction(
416        &self,
417        tx_result: &TransactionResult,
418    ) -> Result<ProvenTransaction, ClientError> {
419        self.prove_transaction_with(tx_result, self.tx_prover.clone()).await
420    }
421
422    /// Proves the specified transaction using the provided prover.
423    pub async fn prove_transaction_with(
424        &self,
425        tx_result: &TransactionResult,
426        tx_prover: Arc<dyn TransactionProver>,
427    ) -> Result<ProvenTransaction, ClientError> {
428        info!("Proving transaction...");
429
430        let proven_transaction =
431            tx_prover.prove(tx_result.executed_transaction().clone().into()).await?;
432
433        info!("Transaction proven.");
434
435        Ok(proven_transaction)
436    }
437
438    /// Submits a previously proven transaction to the RPC endpoint and returns the node’s chain tip
439    /// upon mempool admission.
440    pub async fn submit_proven_transaction(
441        &mut self,
442        proven_transaction: ProvenTransaction,
443        transaction_inputs: impl Into<TransactionInputs>,
444    ) -> Result<BlockNumber, ClientError> {
445        info!("Submitting transaction to the network...");
446        let block_num = self
447            .rpc_api
448            .submit_proven_transaction(proven_transaction, transaction_inputs.into())
449            .await?;
450        info!("Transaction submitted.");
451
452        Ok(block_num)
453    }
454
455    /// Builds a [`TransactionStoreUpdate`] for the provided transaction result at the specified
456    /// submission height.
457    pub async fn get_transaction_store_update(
458        &self,
459        tx_result: &TransactionResult,
460        submission_height: BlockNumber,
461    ) -> Result<TransactionStoreUpdate, TransactionStoreUpdateError> {
462        let note_updates = self.get_note_updates(submission_height, tx_result).await?;
463
464        let mut new_tags: Vec<NoteTagRecord> = note_updates
465            .updated_input_notes()
466            .filter_map(|note| {
467                let note = note.inner();
468
469                if let InputNoteState::Expected(ExpectedNoteState { tag: Some(tag), .. }) =
470                    note.state()
471                {
472                    Some(NoteTagRecord::with_note_source(*tag, note.details_commitment()))
473                } else {
474                    None
475                }
476            })
477            .collect();
478
479        new_tags.extend(note_updates.updated_output_notes().map(|note| {
480            let note = note.inner();
481            NoteTagRecord::with_note_source(note.metadata().tag(), note.details_commitment())
482        }));
483
484        Ok(TransactionStoreUpdate::new(
485            tx_result.executed_transaction().clone(),
486            submission_height,
487            note_updates,
488            tx_result.future_notes().to_vec(),
489            new_tags,
490        ))
491    }
492
493    /// Persists the effects of a submitted transaction into the local store,
494    /// updating account data, note metadata, and future note tracking.
495    pub async fn apply_transaction(
496        &self,
497        tx_result: &TransactionResult,
498        submission_height: BlockNumber,
499    ) -> Result<(), ClientError> {
500        let tx_update = self.get_transaction_store_update(tx_result, submission_height).await?;
501
502        self.apply_transaction_update(tx_update).await
503    }
504
505    pub async fn apply_transaction_update(
506        &self,
507        tx_update: TransactionStoreUpdate,
508    ) -> Result<(), ClientError> {
509        // Transaction was proven and submitted to the node correctly, persist note details and
510        // update account
511        info!("Applying transaction to the local store...");
512
513        let executed_transaction = tx_update.executed_transaction();
514        let account_id = executed_transaction.account_id();
515
516        if self.account_reader(account_id).status().await?.is_locked() {
517            return Err(ClientError::AccountLocked(account_id));
518        }
519
520        self.store.apply_transaction(tx_update).await?;
521        info!("Transaction stored.");
522        Ok(())
523    }
524
525    /// Executes the provided transaction script against the specified account, and returns the
526    /// resulting stack. Advice inputs and foreign accounts can be provided for the execution.
527    ///
528    /// The transaction will use the current sync height as the block reference.
529    pub async fn execute_program(
530        &mut self,
531        account_id: AccountId,
532        tx_script: TransactionScript,
533        advice_inputs: AdviceInputs,
534        foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
535    ) -> Result<[Felt; MIN_STACK_DEPTH], ClientError> {
536        let (data_store, block_ref) =
537            self.prepare_program_execution(account_id, foreign_accounts).await?;
538
539        Ok(self
540            .build_executor(&data_store)?
541            .execute_tx_view_script(account_id, block_ref, tx_script, advice_inputs)
542            .await?)
543    }
544
545    /// Executes the provided transaction script with a DAP debug adapter listening for
546    /// connections, allowing interactive debugging via any DAP-compatible client.
547    #[cfg(feature = "dap")]
548    pub async fn execute_program_with_dap(
549        &mut self,
550        account_id: AccountId,
551        tx_script: TransactionScript,
552        advice_inputs: AdviceInputs,
553        foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
554    ) -> Result<[Felt; MIN_STACK_DEPTH], ClientError> {
555        let (data_store, block_ref) =
556            self.prepare_program_execution(account_id, foreign_accounts).await?;
557
558        Ok(self
559            .build_dap_executor(&data_store)?
560            .execute_tx_view_script(account_id, block_ref, tx_script, advice_inputs)
561            .await?)
562    }
563
564    // HELPERS
565    // --------------------------------------------------------------------------------------------
566
567    /// Validates that the specified transaction request can be executed by the specified account.
568    ///
569    /// This does't guarantee that the transaction will succeed, but it's useful to avoid submitting
570    /// transactions that are guaranteed to fail. Some of the validations include:
571    /// - That the account has enough balance to cover the outgoing assets.
572    /// - That the client is not too far behind the chain tip.
573    pub async fn validate_request(
574        &self,
575        account_id: AccountId,
576        transaction_request: &TransactionRequest,
577    ) -> Result<(), ClientError> {
578        self.validate_recency().await?;
579        validate_output_note_senders(transaction_request, account_id)?;
580        let account = self.try_get_account(account_id).await?;
581        validate_account_request(transaction_request, &account)
582    }
583
584    async fn validate_recency(&self) -> Result<(), ClientError> {
585        if let Some(max_block_number_delta) = self.max_block_number_delta {
586            let current_chain_tip =
587                self.rpc_api.get_block_header_by_number(None, false).await?.0.block_num();
588
589            if current_chain_tip > self.store.get_sync_height().await? + max_block_number_delta {
590                return Err(ClientError::RecencyConditionError(
591                    "The client is too far behind the chain tip to execute the transaction",
592                ));
593            }
594        }
595        Ok(())
596    }
597
598    /// Checks whether the node's `note_scripts` registry already has each of the expected NTX
599    /// scripts. For any script that is missing, creates and submits a registration transaction
600    /// that produces a public note carrying that script.
601    ///
602    /// `account_id` is the account that will execute the registration transaction.
603    ///
604    /// Standard note scripts are skipped — the NTX builder resolves those directly, so they
605    /// never need registering. A missing non-standard script is registered, not an error.
606    ///
607    /// This method is called automatically by [`Self::submit_new_transaction_with_prover`] when the
608    /// [`TransactionRequest`] contains expected NTX scripts. It can also be called directly if
609    /// you want to register scripts ahead of time.
610    pub async fn ensure_ntx_scripts_registered(
611        &mut self,
612        account_id: AccountId,
613        scripts: &[NoteScript],
614        tx_prover: Arc<dyn TransactionProver>,
615    ) -> Result<(), ClientError> {
616        let mut missing_scripts = Vec::new();
617
618        for script in scripts {
619            // Standard scripts are resolved by the NTX builder directly; no registration needed.
620            if StandardNote::from_script(script).is_some() {
621                continue;
622            }
623
624            let script_root = script.root();
625
626            // Scripts the node doesn't have are queued for registration; only RPC errors abort.
627            match self.rpc_api.get_note_script_by_root(script_root.into()).await {
628                Ok(Some(_)) => {},
629                Ok(None) => missing_scripts.push(script.clone()),
630                Err(source) => {
631                    return Err(ClientError::NtxScriptRegistrationFailed {
632                        script_root: script_root.into(),
633                        source,
634                    });
635                },
636            }
637        }
638
639        if missing_scripts.is_empty() {
640            return Ok(());
641        }
642
643        let registration_request = TransactionRequestBuilder::new().build_register_note_scripts(
644            account_id,
645            missing_scripts,
646            self.rng(),
647        )?;
648
649        let tx_result = self.execute_transaction(account_id, registration_request).await?;
650        let proven = self.prove_transaction_with(&tx_result, tx_prover).await?;
651        let submission_height = self.submit_proven_transaction(proven, &tx_result).await?;
652        self.apply_transaction(&tx_result, submission_height).await?;
653
654        Ok(())
655    }
656
657    /// Filters the provided input notes down to the subset that can be consumed by the account.
658    ///
659    /// `output_recipients` are the request's expected output recipients; their scripts are
660    /// registered on the consumption-check data store so output note creation can resolve them
661    /// without them being present in the store.
662    pub(crate) async fn get_valid_input_notes(
663        &self,
664        account: &Account,
665        mut input_notes: InputNotes<InputNote>,
666        tx_args: TransactionArgs,
667        output_recipients: &[NoteRecipient],
668    ) -> Result<InputNotes<InputNote>, ClientError> {
669        loop {
670            let data_store = ClientDataStore::new(self.store.clone(), self.rpc_api.clone());
671            data_store.register_note_scripts(output_recipients.iter().map(|r| r.script().clone()));
672
673            data_store.mast_store().load_account_code(account.code());
674            let execution = NoteConsumptionChecker::new(&self.build_executor(&data_store)?)
675                .check_notes_consumability(
676                    account.id(),
677                    self.store.get_sync_height().await?,
678                    input_notes.iter().map(|n| n.clone().into_note()).collect(),
679                    tx_args.clone(),
680                )
681                .await?;
682
683            if execution.failed().is_empty() {
684                break;
685            }
686
687            let failed_note_ids: BTreeSet<NoteId> =
688                execution.failed().iter().map(|n| n.note().id()).collect();
689            let filtered_input_notes = InputNotes::new(
690                input_notes
691                    .into_iter()
692                    .filter(|note| !failed_note_ids.contains(&note.id()))
693                    .collect(),
694            )
695            .expect("Created from a valid input notes list");
696
697            input_notes = filtered_input_notes;
698        }
699
700        Ok(input_notes)
701    }
702
703    /// Returns foreign account inputs for the required foreign accounts specified by the
704    /// transaction request.
705    ///
706    /// For any [`ForeignAccount::Public`] in `foreign_accounts`, these pieces of data are retrieved
707    /// from the network. For any [`ForeignAccount::Private`] account, inner data is used and only
708    /// a proof of the account's existence on the network is fetched.
709    async fn retrieve_foreign_account_inputs(
710        &self,
711        foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
712    ) -> Result<(Option<BlockNumber>, Vec<AccountInputs>), ClientError> {
713        if foreign_accounts.is_empty() {
714            return Ok((None, Vec::new()));
715        }
716
717        let block_num = self.store.get_sync_height().await?;
718        let mut return_foreign_account_inputs = Vec::with_capacity(foreign_accounts.len());
719
720        for foreign_account in foreign_accounts.into_values() {
721            let foreign_account_inputs = match foreign_account {
722                ForeignAccount::Public(account_id, storage_requirements) => {
723                    fetch_public_account_inputs(
724                        &self.store,
725                        &self.rpc_api,
726                        account_id,
727                        storage_requirements,
728                        AccountStateAt::Block(block_num),
729                    )
730                    .await?
731                },
732                ForeignAccount::Private(partial_account) => {
733                    let account_id = partial_account.id();
734                    let (_, account_proof) = self
735                        .rpc_api
736                        .get_account(
737                            account_id,
738                            GetAccountRequest::new().at(AccountStateAt::Block(block_num)),
739                        )
740                        .await?;
741                    let (witness, _) = account_proof.into_parts();
742                    AccountInputs::new(partial_account, witness)
743                },
744            };
745
746            return_foreign_account_inputs.push(foreign_account_inputs);
747        }
748
749        Ok((Some(block_num), return_foreign_account_inputs))
750    }
751
752    /// Prepares the data store and block reference for program execution.
753    ///
754    /// This is shared setup for both `execute_program` and `execute_program_with_dap`.
755    async fn prepare_program_execution(
756        &mut self,
757        account_id: AccountId,
758        foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
759    ) -> Result<(ClientDataStore, BlockNumber), ClientError> {
760        let (fpi_block_number, foreign_account_inputs) =
761            self.retrieve_foreign_account_inputs(foreign_accounts).await?;
762
763        let block_ref = if let Some(block_number) = fpi_block_number {
764            block_number
765        } else {
766            self.get_sync_height().await?
767        };
768
769        let account_record = self
770            .store
771            .get_account(account_id)
772            .await?
773            .ok_or(ClientError::AccountDataNotFound(account_id))?;
774
775        let account: Account = account_record.try_into()?;
776
777        let data_store = ClientDataStore::new(self.store.clone(), self.rpc_api.clone());
778
779        // Ensure code is loaded on MAST store
780        data_store.mast_store().load_account_code(account.code());
781
782        for fpi_account in &foreign_account_inputs {
783            data_store.mast_store().load_account_code(fpi_account.code());
784        }
785
786        data_store.register_foreign_account_inputs(foreign_account_inputs);
787
788        Ok((data_store, block_ref))
789    }
790
791    /// Creates a transaction executor configured with the client's runtime options,
792    /// authenticator, and source manager.
793    pub(crate) fn build_executor<'store, 'auth, STORE: DataStore + Sync>(
794        &'auth self,
795        data_store: &'store STORE,
796    ) -> Result<TransactionExecutor<'store, 'auth, STORE, AUTH>, TransactionExecutorError> {
797        let mut executor = TransactionExecutor::new(data_store)
798            .with_options(self.exec_options)?
799            .with_source_manager(self.source_manager.clone());
800        if let Some(authenticator) = self.authenticator.as_deref() {
801            executor = executor.with_authenticator(authenticator);
802        }
803        Ok(executor)
804    }
805
806    /// Loads an [`AccountRecord`] for an account that must be usable as a transaction's native
807    /// account. Errors out if the account is not tracked or if it is watched.
808    async fn get_native_account_record(
809        &self,
810        account_id: AccountId,
811    ) -> Result<AccountRecord, ClientError> {
812        let account_record = self
813            .store
814            .get_account(account_id)
815            .await?
816            .ok_or(ClientError::AccountDataNotFound(account_id))?;
817        if account_record.is_watched() {
818            return Err(ClientError::AccountIsWatched(account_id));
819        }
820        Ok(account_record)
821    }
822
823    /// Creates a transaction executor configured for DAP (Debug Adapter Protocol) debugging.
824    #[cfg(feature = "dap")]
825    pub(crate) fn build_dap_executor<'store, 'auth, STORE: DataStore + Sync>(
826        &'auth self,
827        data_store: &'store STORE,
828    ) -> Result<
829        TransactionExecutor<'store, 'auth, STORE, AUTH, dap_executor::DapProgramExecutor>,
830        TransactionExecutorError,
831    > {
832        Ok(self
833            .build_executor(data_store)?
834            .with_program_executor::<dap_executor::DapProgramExecutor>())
835    }
836
837    /// Loads the account and constructs an [`AccountInterface`] from it.
838    pub(crate) async fn get_account_interface(
839        &self,
840        account_id: AccountId,
841    ) -> Result<AccountInterface, ClientError> {
842        let account = self.try_get_account(account_id).await?;
843        Ok(AccountInterface::from_account(&account))
844    }
845
846    /// Returns [`NoteUpdateTracker`] containing the note updates generated by an executed
847    /// transaction.
848    async fn get_note_updates(
849        &self,
850        submission_height: BlockNumber,
851        tx_result: &TransactionResult,
852    ) -> Result<NoteUpdateTracker, TransactionStoreUpdateError> {
853        let executed_tx = tx_result.executed_transaction();
854        let current_timestamp = self.store.get_current_timestamp();
855        let current_block_num = self.store.get_sync_height().await?;
856
857        // New output notes
858        let new_output_notes = executed_tx
859            .output_notes()
860            .iter()
861            .cloned()
862            .filter_map(|output_note| {
863                OutputNoteRecord::try_from_output_note(output_note, submission_height).ok()
864            })
865            .collect::<Vec<_>>();
866
867        // New relevant input notes
868        let mut new_input_notes = vec![];
869        let output_notes: Vec<Note> =
870            notes_from_output(executed_tx.output_notes()).cloned().collect();
871        let note_screener = self.note_screener().clone();
872        let output_note_relevances = note_screener.can_consume_batch(&output_notes).await?;
873
874        for note in output_notes {
875            if output_note_relevances.contains_key(&note.id()) {
876                let metadata = *note.metadata();
877                let tag = metadata.tag();
878                let attachments = note.attachments().clone();
879
880                new_input_notes.push(InputNoteRecord::new(
881                    note.into(),
882                    attachments,
883                    current_timestamp,
884                    ExpectedNoteState {
885                        metadata: Some(metadata),
886                        after_block_num: submission_height,
887                        tag: Some(tag),
888                    }
889                    .into(),
890                ));
891            }
892        }
893
894        // Track future input notes described in the transaction result.
895        new_input_notes.extend(tx_result.future_notes().iter().map(|(note_details, tag)| {
896            InputNoteRecord::new(
897                note_details.clone(),
898                NoteAttachments::empty(),
899                None,
900                ExpectedNoteState {
901                    metadata: None,
902                    after_block_num: current_block_num,
903                    tag: Some(*tag),
904                }
905                .into(),
906            )
907        }));
908
909        // Locally consumed notes. Notes already tracked by the store only need their state
910        // advanced; the rest (the request's unauthenticated notes, which are not persisted
911        // before the transaction succeeds) are tracked from this point on, so records for them
912        // are built from the executed transaction's inputs.
913        let consumed_note_ids =
914            executed_tx.tx_inputs().input_notes().iter().map(InputNote::id).collect();
915
916        let consumed_notes =
917            self.store.get_input_notes(NoteFilter::List(consumed_note_ids)).await?;
918
919        let tracked_note_ids =
920            consumed_notes.iter().filter_map(InputNoteRecord::id).collect::<BTreeSet<_>>();
921
922        for input_note in executed_tx.tx_inputs().input_notes() {
923            if !tracked_note_ids.contains(&input_note.id()) {
924                let mut input_note_record = InputNoteRecord::from(input_note.clone());
925                input_note_record.consumed_locally(
926                    executed_tx.account_id(),
927                    executed_tx.id(),
928                    current_timestamp,
929                )?;
930                new_input_notes.push(input_note_record);
931            }
932        }
933
934        let mut updated_input_notes = vec![];
935
936        for mut input_note_record in consumed_notes {
937            if input_note_record.consumed_locally(
938                executed_tx.account_id(),
939                executed_tx.id(),
940                current_timestamp,
941            )? {
942                updated_input_notes.push(input_note_record);
943            }
944        }
945
946        Ok(NoteUpdateTracker::for_transaction_updates(
947            new_input_notes,
948            updated_input_notes,
949            new_output_notes,
950        ))
951    }
952}
953
954// TRANSACTION STORE UPDATE ERROR
955// ================================================================================================
956
957/// Error returned by [`Client::get_transaction_store_update`] when building the store update
958/// for a submitted transaction fails.
959#[derive(Debug, thiserror::Error)]
960pub enum TransactionStoreUpdateError {
961    #[error("store error")]
962    Store(#[from] StoreError),
963    #[error("note screener error")]
964    NoteScreener(#[from] NoteScreenerError),
965    #[error("note record error")]
966    NoteRecord(#[from] NoteRecordError),
967}
968
969// HELPERS
970// ================================================================================================
971
972/// Data-store-independent state produced during transaction preparation.
973pub(crate) struct PreparedTransaction {
974    pub(crate) notes: InputNotes<InputNote>,
975    pub(crate) output_recipients: Vec<NoteRecipient>,
976    pub(crate) future_notes: Vec<(NoteDetails, NoteTag)>,
977    pub(crate) tx_args: TransactionArgs,
978    pub(crate) foreign_account_inputs: Vec<AccountInputs>,
979    pub(crate) block_num: BlockNumber,
980    pub(crate) ignore_invalid_notes: bool,
981}
982
983impl PreparedTransaction {
984    /// Returns the scripts of the request's expected output notes. These must be registered on
985    /// the executor's data store so output note creation can resolve them during execution.
986    pub(crate) fn output_note_scripts(&self) -> impl Iterator<Item = NoteScript> + '_ {
987        self.output_recipients.iter().map(|recipient| recipient.script().clone())
988    }
989}
990
991/// Helper to get the account outgoing assets.
992///
993/// Any outgoing assets resulting from executing note scripts but not present in expected output
994/// notes wouldn't be included.
995fn get_outgoing_assets(
996    transaction_request: &TransactionRequest,
997) -> (BTreeMap<AccountId, u64>, Vec<NonFungibleAsset>) {
998    // Get own notes assets
999    let mut own_notes_assets = match transaction_request.script_template() {
1000        Some(TransactionScriptTemplate::SendNotes(notes)) => notes
1001            .iter()
1002            .map(|note| (note.id(), note.assets().clone()))
1003            .collect::<BTreeMap<_, _>>(),
1004        _ => BTreeMap::default(),
1005    };
1006    // Get transaction output notes assets
1007    let mut output_notes_assets = transaction_request
1008        .expected_output_own_notes()
1009        .into_iter()
1010        .map(|note| (note.id(), note.assets().clone()))
1011        .collect::<BTreeMap<_, _>>();
1012
1013    // Merge with own notes assets and delete duplicates
1014    output_notes_assets.append(&mut own_notes_assets);
1015
1016    // Create a map of the fungible and non-fungible assets in the output notes
1017    let outgoing_assets = output_notes_assets.values().flat_map(|note_assets| note_assets.iter());
1018
1019    request::collect_assets(outgoing_assets)
1020}
1021
1022/// Validates a transaction request against the supplied `account`. Faucets are currently
1023/// skipped; for non-faucets, defers to [`validate_basic_account_request`] for asset-balance
1024/// checks.
1025pub(super) fn validate_account_request(
1026    transaction_request: &TransactionRequest,
1027    account: &Account,
1028) -> Result<(), ClientError> {
1029    let account_interface = AccountInterface::from_account(account);
1030    if account_interface
1031        .components()
1032        .contains(&AccountComponentInterface::FungibleFaucet)
1033    {
1034        // TODO(SantiagoPittella): Add faucet validations.
1035        Ok(())
1036    } else {
1037        validate_basic_account_request(transaction_request, account)
1038    }
1039}
1040
1041/// Verifies that every output note emitted directly by the transaction declares `account_id` as
1042/// its sender.
1043///
1044/// A note's sender is bound by the kernel to the account that emits it, and note scripts (e.g.
1045/// P2IDE reclaim) authorize on that field, so an output note declaring a foreign sender can never
1046/// be executed. Catching it here yields a clear, immediate error instead of a cryptic failure deep
1047/// in transaction script building.
1048fn validate_output_note_senders(
1049    transaction_request: &TransactionRequest,
1050    account_id: AccountId,
1051) -> Result<(), ClientError> {
1052    for note in transaction_request.expected_output_own_notes() {
1053        let sender = note.metadata().sender();
1054        if sender != account_id {
1055            return Err(ClientError::TransactionRequestError(
1056                TransactionRequestError::OutputNoteSenderMismatch {
1057                    expected: account_id,
1058                    actual: sender,
1059                },
1060            ));
1061        }
1062    }
1063
1064    Ok(())
1065}
1066
1067/// Ensures a transaction request is compatible with the current account state,
1068/// primarily by checking asset balances against the requested transfers.
1069fn validate_basic_account_request(
1070    transaction_request: &TransactionRequest,
1071    account: &Account,
1072) -> Result<(), ClientError> {
1073    // Get outgoing assets
1074    let (fungible_balance_map, non_fungible_set) = get_outgoing_assets(transaction_request);
1075
1076    // Get incoming assets
1077    let (incoming_fungible_balance_map, incoming_non_fungible_balance_set) =
1078        transaction_request.incoming_assets();
1079
1080    // Aggregate the account's fungible balance per faucet in one pass. A faucet's fungible asset
1081    // may occupy more than one callback-flag vault key, so all matching entries are summed.
1082    let mut available_fungible: BTreeMap<AccountId, u64> = BTreeMap::new();
1083    for asset in account.vault().assets() {
1084        if let Asset::Fungible(fungible) = asset {
1085            let balance = available_fungible.entry(fungible.faucet_id()).or_default();
1086            *balance = balance.saturating_add(fungible.amount().as_u64());
1087        }
1088    }
1089
1090    // Check if the account balance plus incoming assets is greater than or equal to the
1091    // outgoing fungible assets
1092    for (faucet_id, amount) in fungible_balance_map {
1093        let account_asset_amount = available_fungible.get(&faucet_id).copied().unwrap_or(0);
1094        let incoming_balance = incoming_fungible_balance_map.get(&faucet_id).unwrap_or(&0);
1095        if account_asset_amount + incoming_balance < amount {
1096            return Err(ClientError::AssetError(AssetError::FungibleAssetAmountNotSufficient {
1097                minuend: account_asset_amount,
1098                subtrahend: amount,
1099            }));
1100        }
1101    }
1102
1103    // Check if the account balance plus incoming assets is greater than or equal to the
1104    // outgoing non fungible assets
1105    for non_fungible in &non_fungible_set {
1106        match account.vault().has_non_fungible_asset(*non_fungible) {
1107            Ok(true) => (),
1108            Ok(false) => {
1109                // Check if the non fungible asset is in the incoming assets
1110                if !incoming_non_fungible_balance_set.contains(non_fungible) {
1111                    return Err(ClientError::TransactionRequestError(
1112                        TransactionRequestError::MissingNonFungibleAsset(non_fungible.faucet_id()),
1113                    ));
1114                }
1115            },
1116            _ => {
1117                return Err(ClientError::TransactionRequestError(
1118                    TransactionRequestError::MissingNonFungibleAsset(non_fungible.faucet_id()),
1119                ));
1120            },
1121        }
1122    }
1123
1124    Ok(())
1125}
1126
1127/// Fetches a foreign account's proof and details from the network, converts them into
1128/// [`AccountInputs`], and caches the returned code in the store for future requests.
1129///
1130/// # Errors
1131/// Fails if the account is private: the RPC does not return account details for them, causing
1132/// [`TransactionRequestError::ForeignAccountDataMissing`].
1133pub(crate) async fn fetch_public_account_inputs(
1134    store: &Arc<dyn Store>,
1135    rpc_api: &Arc<dyn NodeRpcClient>,
1136    account_id: AccountId,
1137    storage_requirements: AccountStorageRequirements,
1138    account_state_at: AccountStateAt,
1139) -> Result<AccountInputs, ClientError> {
1140    let known_code: Option<AccountCode> =
1141        store.get_foreign_account_code(vec![account_id]).await?.into_values().next();
1142
1143    let vault = store
1144        .get_account_header(account_id)
1145        .await?
1146        .map_or(VaultFetch::Always, |(header, ..)| {
1147            VaultFetch::IfChangedFrom(header.vault_root())
1148        });
1149
1150    let (block_num, mut account_proof) = rpc_api
1151        .get_account(
1152            account_id,
1153            GetAccountRequest::new()
1154                .with_storage(StorageMapFetch::Slots(storage_requirements.clone()))
1155                .at(account_state_at)
1156                .with_known_code(known_code)
1157                .with_vault(vault),
1158        )
1159        .await?;
1160
1161    if let Some(details) = account_proof.details_mut() {
1162        rpc_api.resolve_oversize_vault(account_id, block_num, details).await?;
1163        rpc_api.resolve_oversize_storage_maps(account_id, block_num, details).await?;
1164    }
1165
1166    let account_inputs = request::account_proof_into_inputs(account_proof, &storage_requirements)?;
1167
1168    let _ = store
1169        .upsert_foreign_account_code(account_id, account_inputs.code().clone())
1170        .await
1171        .inspect_err(|err| {
1172            tracing::warn!(
1173                %account_id,
1174                %err,
1175                "Failed to persist foreign account code to store"
1176            );
1177        });
1178
1179    Ok(account_inputs)
1180}
1181
1182/// Extracts notes from [`RawOutputNotes`].
1183/// Used for:
1184/// - Checking the relevance of notes to save them as input notes.
1185/// - Validate hashes versus expected output notes after a transaction is executed.
1186pub fn notes_from_output(output_notes: &RawOutputNotes) -> impl Iterator<Item = &Note> {
1187    output_notes.iter().filter_map(|n| match n {
1188        RawOutputNote::Full(n) => Some(n),
1189        RawOutputNote::Partial(_) => None,
1190    })
1191}
1192
1193/// Validates that the executed transaction's output recipients match what was expected in the
1194/// transaction request.
1195pub(crate) fn validate_executed_transaction(
1196    executed_transaction: &ExecutedTransaction,
1197    expected_output_recipients: &[NoteRecipient],
1198) -> Result<(), ClientError> {
1199    let tx_output_recipient_digests = executed_transaction
1200        .output_notes()
1201        .iter()
1202        .filter_map(|n| n.recipient().map(NoteRecipient::digest))
1203        .collect::<Vec<_>>();
1204
1205    let missing_recipient_digest: Vec<Word> = expected_output_recipients
1206        .iter()
1207        .filter_map(|recipient| {
1208            (!tx_output_recipient_digests.contains(&recipient.digest()))
1209                .then_some(recipient.digest())
1210        })
1211        .collect();
1212
1213    if !missing_recipient_digest.is_empty() {
1214        return Err(ClientError::MissingOutputRecipients(missing_recipient_digest));
1215    }
1216
1217    Ok(())
1218}
1219
1220// TESTS
1221// ================================================================================================
1222
1223#[cfg(test)]
1224mod tests {
1225    use alloc::vec;
1226
1227    use miden_protocol::Word;
1228    use miden_protocol::account::AccountId;
1229    use miden_protocol::asset::FungibleAsset;
1230    use miden_protocol::crypto::rand::RandomCoin;
1231    use miden_protocol::note::{Note, NoteAttachments, NoteType};
1232    use miden_protocol::testing::account_id::{
1233        ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
1234        ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
1235        ACCOUNT_ID_SENDER,
1236    };
1237    use miden_standards::note::P2idNote;
1238
1239    use super::{TransactionRequestBuilder, validate_output_note_senders};
1240    use crate::ClientError;
1241    use crate::transaction::TransactionRequestError;
1242
1243    fn own_note_with_sender(sender: AccountId) -> Note {
1244        let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
1245        let target_id =
1246            AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
1247        let mut rng = RandomCoin::new(Word::default());
1248
1249        P2idNote::create(
1250            sender,
1251            target_id,
1252            vec![FungibleAsset::new(faucet_id, 100).unwrap().into()],
1253            NoteType::Public,
1254            NoteAttachments::empty(),
1255            &mut rng,
1256        )
1257        .unwrap()
1258    }
1259
1260    #[test]
1261    fn output_note_with_foreign_sender_is_rejected() {
1262        let account_id =
1263            AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
1264        let foreign_sender = AccountId::try_from(ACCOUNT_ID_SENDER).unwrap();
1265        assert_ne!(account_id, foreign_sender);
1266
1267        let request = TransactionRequestBuilder::new()
1268            .own_output_notes(vec![own_note_with_sender(foreign_sender)])
1269            .build()
1270            .unwrap();
1271
1272        let err = validate_output_note_senders(&request, account_id).unwrap_err();
1273        match err {
1274            ClientError::TransactionRequestError(
1275                TransactionRequestError::OutputNoteSenderMismatch { expected, actual },
1276            ) => {
1277                assert_eq!(expected, account_id);
1278                assert_eq!(actual, foreign_sender);
1279            },
1280            other => panic!("expected OutputNoteSenderMismatch, got {other:?}"),
1281        }
1282    }
1283
1284    #[test]
1285    fn output_note_with_matching_sender_is_accepted() {
1286        let account_id =
1287            AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
1288
1289        let request = TransactionRequestBuilder::new()
1290            .own_output_notes(vec![own_note_with_sender(account_id)])
1291            .build()
1292            .unwrap();
1293
1294        validate_output_note_senders(&request, account_id).unwrap();
1295    }
1296
1297    #[test]
1298    fn request_without_own_output_notes_is_accepted() {
1299        let account_id =
1300            AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
1301        let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
1302
1303        // A consume-only request (input note, no own output notes) must pass the sender check.
1304        let request = TransactionRequestBuilder::new()
1305            .input_notes(vec![(own_note_with_sender(faucet_id), None)])
1306            .build()
1307            .unwrap();
1308
1309        validate_output_note_senders(&request, account_id).unwrap();
1310    }
1311}