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