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