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