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