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::NonFungibleAsset;
72use miden_protocol::block::BlockNumber;
73use miden_protocol::errors::AssetError;
74use miden_protocol::note::{Note, NoteDetails, NoteId, NoteRecipient, NoteScript, NoteTag};
75use miden_protocol::transaction::AccountInputs;
76use miden_protocol::{EMPTY_WORD, Felt, Word};
77use miden_standards::account::interface::AccountInterfaceExt;
78use miden_tx::{DataStore, NoteConsumptionChecker, TransactionExecutor};
79use tracing::info;
80
81use super::Client;
82use crate::ClientError;
83use crate::note::NoteUpdateTracker;
84use crate::rpc::domain::account::AccountStorageRequirements;
85use crate::rpc::{AccountStateAt, GrpcError, NodeRpcClient, RpcError};
86use crate::store::data_store::ClientDataStore;
87use crate::store::input_note_states::ExpectedNoteState;
88use crate::store::{
89    InputNoteRecord,
90    InputNoteState,
91    NoteFilter,
92    OutputNoteRecord,
93    Store,
94    TransactionFilter,
95};
96use crate::sync::NoteTagRecord;
97
98mod prover;
99pub use prover::TransactionProver;
100
101mod record;
102pub use record::{
103    DiscardCause,
104    TransactionDetails,
105    TransactionRecord,
106    TransactionStatus,
107    TransactionStatusVariant,
108};
109
110mod store_update;
111pub use store_update::TransactionStoreUpdate;
112
113mod request;
114pub use request::{
115    ForeignAccount,
116    NoteArgs,
117    PaymentNoteDescription,
118    SwapTransactionData,
119    TransactionRequest,
120    TransactionRequestBuilder,
121    TransactionRequestError,
122    TransactionScriptTemplate,
123};
124
125mod result;
126// RE-EXPORTS
127// ================================================================================================
128pub use miden_protocol::transaction::{
129    ExecutedTransaction,
130    InputNote,
131    InputNotes,
132    OutputNote,
133    OutputNotes,
134    ProvenTransaction,
135    PublicOutputNote,
136    RawOutputNote,
137    RawOutputNotes,
138    TransactionArgs,
139    TransactionId,
140    TransactionInputs,
141    TransactionKernel,
142    TransactionScript,
143    TransactionSummary,
144};
145pub use miden_protocol::vm::{AdviceInputs, AdviceMap};
146pub use miden_standards::account::interface::{AccountComponentInterface, AccountInterface};
147pub use miden_tx::auth::TransactionAuthenticator;
148pub use miden_tx::{
149    DataStoreError,
150    LocalTransactionProver,
151    ProvingOptions,
152    TransactionExecutorError,
153    TransactionProverError,
154};
155pub use result::TransactionResult;
156
157/// Transaction management methods
158impl<AUTH> Client<AUTH>
159where
160    AUTH: TransactionAuthenticator + Sync + 'static,
161{
162    // TRANSACTION DATA RETRIEVAL
163    // --------------------------------------------------------------------------------------------
164
165    /// Retrieves tracked transactions, filtered by [`TransactionFilter`].
166    pub async fn get_transactions(
167        &self,
168        filter: TransactionFilter,
169    ) -> Result<Vec<TransactionRecord>, ClientError> {
170        self.store.get_transactions(filter).await.map_err(Into::into)
171    }
172
173    // TRANSACTION
174    // --------------------------------------------------------------------------------------------
175
176    /// Executes a transaction specified by the request against the specified account,
177    /// proves it, submits it to the network, and updates the local database.
178    ///
179    /// Uses the client's default prover (configured via
180    /// [`crate::builder::ClientBuilder::prover`]).
181    ///
182    /// If the transaction utilizes foreign account data, there is a chance that the client
183    /// doesn't have the required block header in the local database. In these scenarios, a sync to
184    /// the chain tip is performed, and the required block header is retrieved.
185    pub async fn submit_new_transaction(
186        &mut self,
187        account_id: AccountId,
188        transaction_request: TransactionRequest,
189    ) -> Result<TransactionId, ClientError> {
190        let prover = self.tx_prover.clone();
191        self.submit_new_transaction_with_prover(account_id, transaction_request, prover)
192            .await
193    }
194
195    /// Executes a transaction specified by the request against the specified account,
196    /// proves it with the provided prover, submits it to the network, and updates the local
197    /// database.
198    ///
199    /// This is useful for falling back to a different prover (e.g., local) when the default
200    /// prover (e.g., remote) fails with a [`ClientError::TransactionProvingError`].
201    ///
202    /// If the transaction utilizes foreign account data, there is a chance that the client
203    /// doesn't have the required block header in the local database. In these scenarios, a sync to
204    /// the chain tip is performed, and the required block header is retrieved.
205    pub async fn submit_new_transaction_with_prover(
206        &mut self,
207        account_id: AccountId,
208        transaction_request: TransactionRequest,
209        tx_prover: Arc<dyn TransactionProver>,
210    ) -> Result<TransactionId, ClientError> {
211        // Register any missing NTX scripts before the main transaction.
212        // The registration path contains its own full execute -> prove -> submit pipeline.
213        if !transaction_request.expected_ntx_scripts().is_empty() {
214            Box::pin(self.ensure_ntx_scripts_registered(
215                account_id,
216                transaction_request.expected_ntx_scripts(),
217                tx_prover.clone(),
218            ))
219            .await?;
220        }
221
222        let tx_result = self.execute_transaction(account_id, transaction_request).await?;
223        let tx_id = tx_result.executed_transaction().id();
224
225        let proven_transaction = self.prove_transaction_with(&tx_result, tx_prover).await?;
226        let submission_height =
227            self.submit_proven_transaction(proven_transaction, &tx_result).await?;
228
229        self.apply_transaction(&tx_result, submission_height).await?;
230
231        Ok(tx_id)
232    }
233
234    /// Creates and executes a transaction specified by the request against the specified account,
235    /// but doesn't change the local database.
236    ///
237    /// If the transaction utilizes foreign account data, there is a chance that the client doesn't
238    /// have the required block header in the local database. In these scenarios, a sync to
239    /// the chain tip is performed, and the required block header is retrieved.
240    ///
241    /// # Errors
242    ///
243    /// - Returns [`ClientError::MissingOutputRecipients`] if the [`TransactionRequest`] output
244    ///   notes are not a subset of executor's output notes.
245    /// - Returns a [`ClientError::TransactionExecutorError`] if the execution fails.
246    /// - Returns a [`ClientError::TransactionRequestError`] if the request is invalid.
247    pub async fn execute_transaction(
248        &mut self,
249        account_id: AccountId,
250        transaction_request: TransactionRequest,
251    ) -> Result<TransactionResult, ClientError> {
252        // Validates the transaction request before executing
253        self.validate_request(account_id, &transaction_request).await?;
254
255        // Retrieve all input notes from the store.
256        let mut stored_note_records = self
257            .store
258            .get_input_notes(NoteFilter::List(transaction_request.input_note_ids().collect()))
259            .await?;
260
261        // Verify that none of the authenticated input notes are already consumed.
262        for note in &stored_note_records {
263            if note.is_consumed() {
264                return Err(ClientError::TransactionRequestError(
265                    TransactionRequestError::InputNoteAlreadyConsumed(note.id()),
266                ));
267            }
268        }
269
270        // Only keep authenticated input notes from the store.
271        stored_note_records.retain(InputNoteRecord::is_authenticated);
272
273        let authenticated_note_ids =
274            stored_note_records.iter().map(InputNoteRecord::id).collect::<Vec<_>>();
275
276        // Upsert request notes missing from the store so they can be tracked and updated
277        // NOTE: Unauthenticated notes may be stored locally in an unverified/invalid state at this
278        // point. The upsert will replace the state to an InputNoteState::Expected (with
279        // metadata included).
280        let unauthenticated_input_notes = transaction_request
281            .input_notes()
282            .iter()
283            .filter(|n| !authenticated_note_ids.contains(&n.id()))
284            .cloned()
285            .map(Into::into)
286            .collect::<Vec<_>>();
287
288        self.store.upsert_input_notes(&unauthenticated_input_notes).await?;
289
290        let mut notes = transaction_request.build_input_notes(stored_note_records)?;
291
292        let output_recipients =
293            transaction_request.expected_output_recipients().cloned().collect::<Vec<_>>();
294
295        let future_notes: Vec<(NoteDetails, NoteTag)> =
296            transaction_request.expected_future_notes().cloned().collect();
297
298        let tx_script = transaction_request
299            .build_transaction_script(&self.get_account_interface(account_id).await?)?;
300
301        let foreign_accounts = transaction_request.foreign_accounts().clone();
302
303        // Inject state and code of foreign accounts
304        let (fpi_block_num, foreign_account_inputs) =
305            self.retrieve_foreign_account_inputs(foreign_accounts).await?;
306
307        let ignore_invalid_notes = transaction_request.ignore_invalid_input_notes();
308
309        let data_store = ClientDataStore::new(self.store.clone(), self.rpc_api.clone());
310        data_store.register_foreign_account_inputs(foreign_account_inputs.iter().cloned());
311        for fpi_account in &foreign_account_inputs {
312            data_store.mast_store().load_account_code(fpi_account.code());
313        }
314
315        // Upsert note scripts for later retrieval from the client's DataStore
316        let output_note_scripts: Vec<NoteScript> = transaction_request
317            .expected_output_recipients()
318            .map(|n| n.script().clone())
319            .collect();
320        self.store.upsert_note_scripts(&output_note_scripts).await?;
321
322        let block_num = if let Some(block_num) = fpi_block_num {
323            block_num
324        } else {
325            self.store.get_sync_height().await?
326        };
327
328        // Load account code into MAST forest store
329        // TODO: Refactor this to get account code only?
330        let account_record = self
331            .store
332            .get_account(account_id)
333            .await?
334            .ok_or(ClientError::AccountDataNotFound(account_id))?;
335        let account: Account = account_record.try_into()?;
336        data_store.mast_store().load_account_code(account.code());
337
338        // Get transaction args
339        let tx_args = transaction_request.into_transaction_args(tx_script);
340
341        if ignore_invalid_notes {
342            // Remove invalid notes
343            notes = self.get_valid_input_notes(account, notes, tx_args.clone()).await?;
344        }
345
346        // Execute the transaction and get the witness
347        let executed_transaction = self
348            .build_executor(&data_store)?
349            .execute_transaction(account_id, block_num, notes, tx_args)
350            .await?;
351
352        validate_executed_transaction(&executed_transaction, &output_recipients)?;
353        TransactionResult::new(executed_transaction, future_notes)
354    }
355
356    /// Proves the specified transaction using the prover configured for this client.
357    pub async fn prove_transaction(
358        &mut self,
359        tx_result: &TransactionResult,
360    ) -> Result<ProvenTransaction, ClientError> {
361        self.prove_transaction_with(tx_result, self.tx_prover.clone()).await
362    }
363
364    /// Proves the specified transaction using the provided prover.
365    pub async fn prove_transaction_with(
366        &mut self,
367        tx_result: &TransactionResult,
368        tx_prover: Arc<dyn TransactionProver>,
369    ) -> Result<ProvenTransaction, ClientError> {
370        info!("Proving transaction...");
371
372        let proven_transaction =
373            tx_prover.prove(tx_result.executed_transaction().clone().into()).await?;
374
375        info!("Transaction proven.");
376
377        Ok(proven_transaction)
378    }
379
380    /// Submits a previously proven transaction to the RPC endpoint and returns the node’s chain tip
381    /// upon mempool admission.
382    pub async fn submit_proven_transaction(
383        &mut self,
384        proven_transaction: ProvenTransaction,
385        transaction_inputs: impl Into<TransactionInputs>,
386    ) -> Result<BlockNumber, ClientError> {
387        info!("Submitting transaction to the network...");
388        let block_num = self
389            .rpc_api
390            .submit_proven_transaction(proven_transaction, transaction_inputs.into())
391            .await?;
392        info!("Transaction submitted.");
393
394        Ok(block_num)
395    }
396
397    /// Builds a [`TransactionStoreUpdate`] for the provided transaction result at the specified
398    /// submission height.
399    pub async fn get_transaction_store_update(
400        &self,
401        tx_result: &TransactionResult,
402        submission_height: BlockNumber,
403    ) -> Result<TransactionStoreUpdate, ClientError> {
404        let note_updates = self.get_note_updates(submission_height, tx_result).await?;
405
406        let mut new_tags: Vec<NoteTagRecord> = note_updates
407            .updated_input_notes()
408            .filter_map(|note| {
409                let note = note.inner();
410
411                if let InputNoteState::Expected(ExpectedNoteState { tag: Some(tag), .. }) =
412                    note.state()
413                {
414                    Some(NoteTagRecord::with_note_source(*tag, note.id()))
415                } else {
416                    None
417                }
418            })
419            .collect();
420
421        // Track output note tags so that `sync_notes` discovers their inclusion proofs.
422        new_tags.extend(note_updates.updated_output_notes().map(|note| {
423            let note = note.inner();
424            NoteTagRecord::with_note_source(note.metadata().tag(), note.id())
425        }));
426
427        Ok(TransactionStoreUpdate::new(
428            tx_result.executed_transaction().clone(),
429            submission_height,
430            note_updates,
431            tx_result.future_notes().to_vec(),
432            new_tags,
433        ))
434    }
435
436    /// Persists the effects of a submitted transaction into the local store,
437    /// updating account data, note metadata, and future note tracking.
438    pub async fn apply_transaction(
439        &self,
440        tx_result: &TransactionResult,
441        submission_height: BlockNumber,
442    ) -> Result<(), ClientError> {
443        let tx_update = self.get_transaction_store_update(tx_result, submission_height).await?;
444
445        self.apply_transaction_update(tx_update).await
446    }
447
448    pub async fn apply_transaction_update(
449        &self,
450        tx_update: TransactionStoreUpdate,
451    ) -> Result<(), ClientError> {
452        // Transaction was proven and submitted to the node correctly, persist note details and
453        // update account
454        info!("Applying transaction to the local store...");
455
456        let executed_transaction = tx_update.executed_transaction();
457        let account_id = executed_transaction.account_id();
458
459        if self.account_reader(account_id).status().await?.is_locked() {
460            return Err(ClientError::AccountLocked(account_id));
461        }
462
463        self.store.apply_transaction(tx_update).await?;
464        info!("Transaction stored.");
465        Ok(())
466    }
467
468    /// Executes the provided transaction script against the specified account, and returns the
469    /// resulting stack. Advice inputs and foreign accounts can be provided for the execution.
470    ///
471    /// The transaction will use the current sync height as the block reference.
472    pub async fn execute_program(
473        &mut self,
474        account_id: AccountId,
475        tx_script: TransactionScript,
476        advice_inputs: AdviceInputs,
477        foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
478    ) -> Result<[Felt; 16], ClientError> {
479        let (fpi_block_number, foreign_account_inputs) =
480            self.retrieve_foreign_account_inputs(foreign_accounts).await?;
481
482        let block_ref = if let Some(block_number) = fpi_block_number {
483            block_number
484        } else {
485            self.get_sync_height().await?
486        };
487
488        let account_record = self
489            .store
490            .get_account(account_id)
491            .await?
492            .ok_or(ClientError::AccountDataNotFound(account_id))?;
493
494        let account: Account = account_record.try_into()?;
495
496        let data_store = ClientDataStore::new(self.store.clone(), self.rpc_api.clone());
497
498        data_store.register_foreign_account_inputs(foreign_account_inputs.iter().cloned());
499
500        // Ensure code is loaded on MAST store
501        data_store.mast_store().load_account_code(account.code());
502
503        for fpi_account in &foreign_account_inputs {
504            data_store.mast_store().load_account_code(fpi_account.code());
505        }
506
507        Ok(self
508            .build_executor(&data_store)?
509            .execute_tx_view_script(account_id, block_ref, tx_script, advice_inputs)
510            .await?)
511    }
512
513    // HELPERS
514    // --------------------------------------------------------------------------------------------
515
516    /// Compiles the note updates needed to be applied to the store after executing a
517    /// transaction.
518    ///
519    /// These updates include:
520    /// - New output notes.
521    /// - New input notes (only if they are relevant to the client).
522    /// - Input notes that could be created as outputs of future transactions (e.g., a SWAP payback
523    ///   note).
524    /// - Updated input notes that were consumed locally.
525    async fn get_note_updates(
526        &self,
527        submission_height: BlockNumber,
528        tx_result: &TransactionResult,
529    ) -> Result<NoteUpdateTracker, ClientError> {
530        let executed_tx = tx_result.executed_transaction();
531        let current_timestamp = self.store.get_current_timestamp();
532        let current_block_num = self.store.get_sync_height().await?;
533
534        // New output notes
535        let new_output_notes = executed_tx
536            .output_notes()
537            .iter()
538            .cloned()
539            .filter_map(|output_note| {
540                OutputNoteRecord::try_from_output_note(output_note, submission_height).ok()
541            })
542            .collect::<Vec<_>>();
543
544        // New relevant input notes
545        let mut new_input_notes = vec![];
546        let output_notes =
547            notes_from_output(executed_tx.output_notes()).cloned().collect::<Vec<_>>();
548        let note_screener = self.note_screener();
549        let output_note_relevances = note_screener.can_consume_batch(&output_notes).await?;
550
551        for note in output_notes {
552            if output_note_relevances.contains_key(&note.id()) {
553                let metadata = note.metadata().clone();
554                let tag = metadata.tag();
555
556                new_input_notes.push(InputNoteRecord::new(
557                    note.into(),
558                    current_timestamp,
559                    ExpectedNoteState {
560                        metadata: Some(metadata),
561                        after_block_num: submission_height,
562                        tag: Some(tag),
563                    }
564                    .into(),
565                ));
566            }
567        }
568
569        // Track future input notes described in the transaction result.
570        new_input_notes.extend(tx_result.future_notes().iter().map(|(note_details, tag)| {
571            InputNoteRecord::new(
572                note_details.clone(),
573                None,
574                ExpectedNoteState {
575                    metadata: None,
576                    after_block_num: current_block_num,
577                    tag: Some(*tag),
578                }
579                .into(),
580            )
581        }));
582
583        // Locally consumed notes
584        let consumed_note_ids =
585            executed_tx.tx_inputs().input_notes().iter().map(InputNote::id).collect();
586
587        let consumed_notes = self.get_input_notes(NoteFilter::List(consumed_note_ids)).await?;
588
589        let mut updated_input_notes = vec![];
590
591        for mut input_note_record in consumed_notes {
592            if input_note_record.consumed_locally(
593                executed_tx.account_id(),
594                executed_tx.id(),
595                self.store.get_current_timestamp(),
596            )? {
597                updated_input_notes.push(input_note_record);
598            }
599        }
600
601        Ok(NoteUpdateTracker::for_transaction_updates(
602            new_input_notes,
603            updated_input_notes,
604            new_output_notes,
605        ))
606    }
607
608    /// Validates that the specified transaction request can be executed by the specified account.
609    ///
610    /// This does't guarantee that the transaction will succeed, but it's useful to avoid submitting
611    /// transactions that are guaranteed to fail. Some of the validations include:
612    /// - That the account has enough balance to cover the outgoing assets.
613    /// - That the client is not too far behind the chain tip.
614    pub async fn validate_request(
615        &mut self,
616        account_id: AccountId,
617        transaction_request: &TransactionRequest,
618    ) -> Result<(), ClientError> {
619        if let Some(max_block_number_delta) = self.max_block_number_delta {
620            let current_chain_tip =
621                self.rpc_api.get_block_header_by_number(None, false).await?.0.block_num();
622
623            if current_chain_tip > self.store.get_sync_height().await? + max_block_number_delta {
624                return Err(ClientError::RecencyConditionError(
625                    "The client is too far behind the chain tip to execute the transaction",
626                ));
627            }
628        }
629
630        let account = self.try_get_account(account_id).await?;
631        if account.is_faucet() {
632            // TODO(SantiagoPittella): Add faucet validations.
633            Ok(())
634        } else {
635            validate_basic_account_request(transaction_request, &account)
636        }
637    }
638
639    /// Checks whether the node's `note_scripts` registry already has each of the expected NTX
640    /// scripts. For any script that is missing, creates and submits a registration transaction
641    /// that produces a public note carrying that script.
642    ///
643    /// `account_id` is the account that will execute the registration transaction.
644    ///
645    /// This method is called automatically by [`Self::submit_new_transaction_with_prover`] when the
646    /// [`TransactionRequest`] contains expected NTX scripts. It can also be called directly if
647    /// you want to register scripts ahead of time.
648    pub async fn ensure_ntx_scripts_registered(
649        &mut self,
650        account_id: AccountId,
651        scripts: &[NoteScript],
652        tx_prover: Arc<dyn TransactionProver>,
653    ) -> Result<(), ClientError> {
654        let mut missing_scripts = Vec::new();
655
656        for script in scripts {
657            let script_root = script.root();
658
659            // Check if the node already has this script registered.
660            match self.rpc_api.get_note_script_by_root(script_root).await {
661                Ok(_) => {},
662                Err(RpcError::RequestError { error_kind: GrpcError::NotFound, .. }) => {
663                    missing_scripts.push(script.clone());
664                },
665                Err(other) => {
666                    return Err(ClientError::NtxScriptRegistrationFailed {
667                        script_root,
668                        source: other,
669                    });
670                },
671            }
672        }
673
674        if missing_scripts.is_empty() {
675            return Ok(());
676        }
677
678        let registration_request = TransactionRequestBuilder::new().build_register_note_scripts(
679            account_id,
680            missing_scripts,
681            self.rng(),
682        )?;
683
684        let tx_result = self.execute_transaction(account_id, registration_request).await?;
685        let proven = self.prove_transaction_with(&tx_result, tx_prover).await?;
686        let submission_height = self.submit_proven_transaction(proven, &tx_result).await?;
687        self.apply_transaction(&tx_result, submission_height).await?;
688
689        Ok(())
690    }
691
692    /// Filters out invalid or non-consumable input notes by simulating
693    /// note consumption and removing any that fail validation.
694    async fn get_valid_input_notes(
695        &self,
696        account: Account,
697        mut input_notes: InputNotes<InputNote>,
698        tx_args: TransactionArgs,
699    ) -> Result<InputNotes<InputNote>, ClientError> {
700        loop {
701            let data_store = ClientDataStore::new(self.store.clone(), self.rpc_api.clone());
702
703            data_store.mast_store().load_account_code(account.code());
704            let execution = NoteConsumptionChecker::new(&self.build_executor(&data_store)?)
705                .check_notes_consumability(
706                    account.id(),
707                    self.store.get_sync_height().await?,
708                    input_notes.iter().map(|n| n.clone().into_note()).collect(),
709                    tx_args.clone(),
710                )
711                .await?;
712
713            if execution.failed.is_empty() {
714                break;
715            }
716
717            let failed_note_ids: BTreeSet<NoteId> =
718                execution.failed.iter().map(|n| n.note.id()).collect();
719            let filtered_input_notes = InputNotes::new(
720                input_notes
721                    .into_iter()
722                    .filter(|note| !failed_note_ids.contains(&note.id()))
723                    .collect(),
724            )
725            .expect("Created from a valid input notes list");
726
727            input_notes = filtered_input_notes;
728        }
729
730        Ok(input_notes)
731    }
732
733    /// Retrieves the account interface for the specified account.
734    pub(crate) async fn get_account_interface(
735        &self,
736        account_id: AccountId,
737    ) -> Result<AccountInterface, ClientError> {
738        let account = self.try_get_account(account_id).await?;
739        Ok(AccountInterface::from_account(&account))
740    }
741
742    /// Returns foreign account inputs for the required foreign accounts specified by the
743    /// transaction request.
744    ///
745    /// For any [`ForeignAccount::Public`] in `foreign_accounts`, these pieces of data are retrieved
746    /// from the network. For any [`ForeignAccount::Private`] account, inner data is used and only
747    /// a proof of the account's existence on the network is fetched.
748    ///
749    /// Account data is retrieved for the node's current chain tip, so we need to check whether we
750    /// currently have the corresponding block header data. Otherwise, we additionally need to
751    /// retrieve it, this implies a state sync call which may update the client in other ways.
752    async fn retrieve_foreign_account_inputs(
753        &mut self,
754        foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
755    ) -> Result<(Option<BlockNumber>, Vec<AccountInputs>), ClientError> {
756        if foreign_accounts.is_empty() {
757            return Ok((None, Vec::new()));
758        }
759
760        let block_num = self.get_sync_height().await?;
761        let mut return_foreign_account_inputs = Vec::with_capacity(foreign_accounts.len());
762
763        for foreign_account in foreign_accounts.into_values() {
764            let foreign_account_inputs = match foreign_account {
765                ForeignAccount::Public(account_id, storage_requirements) => {
766                    fetch_public_account_inputs(
767                        &self.store,
768                        &self.rpc_api,
769                        account_id,
770                        storage_requirements,
771                        AccountStateAt::Block(block_num),
772                    )
773                    .await?
774                },
775                ForeignAccount::Private(partial_account) => {
776                    let account_id = partial_account.id();
777                    let (_, account_proof) = self
778                        .rpc_api
779                        .get_account_proof(
780                            account_id,
781                            AccountStorageRequirements::default(),
782                            AccountStateAt::Block(block_num),
783                            None,
784                            None,
785                        )
786                        .await?;
787                    let (witness, _) = account_proof.into_parts();
788                    AccountInputs::new(partial_account, witness)
789                },
790            };
791
792            return_foreign_account_inputs.push(foreign_account_inputs);
793        }
794
795        Ok((Some(block_num), return_foreign_account_inputs))
796    }
797
798    /// Creates a transaction executor configured with the client's runtime options,
799    /// authenticator, and source manager.
800    pub(crate) fn build_executor<'store, 'auth, STORE: DataStore + Sync>(
801        &'auth self,
802        data_store: &'store STORE,
803    ) -> Result<TransactionExecutor<'store, 'auth, STORE, AUTH>, TransactionExecutorError> {
804        let mut executor = TransactionExecutor::new(data_store).with_options(self.exec_options)?;
805        if let Some(authenticator) = self.authenticator.as_deref() {
806            executor = executor.with_authenticator(authenticator);
807        }
808        executor = executor.with_source_manager(self.source_manager.clone());
809
810        Ok(executor)
811    }
812}
813
814// HELPERS
815// ================================================================================================
816
817/// Helper to get the account outgoing assets.
818///
819/// Any outgoing assets resulting from executing note scripts but not present in expected output
820/// notes wouldn't be included.
821fn get_outgoing_assets(
822    transaction_request: &TransactionRequest,
823) -> (BTreeMap<AccountId, u64>, Vec<NonFungibleAsset>) {
824    // Get own notes assets
825    let mut own_notes_assets = match transaction_request.script_template() {
826        Some(TransactionScriptTemplate::SendNotes(notes)) => notes
827            .iter()
828            .map(|note| (note.id(), note.assets().clone()))
829            .collect::<BTreeMap<_, _>>(),
830        _ => BTreeMap::default(),
831    };
832    // Get transaction output notes assets
833    let mut output_notes_assets = transaction_request
834        .expected_output_own_notes()
835        .into_iter()
836        .map(|note| (note.id(), note.assets().clone()))
837        .collect::<BTreeMap<_, _>>();
838
839    // Merge with own notes assets and delete duplicates
840    output_notes_assets.append(&mut own_notes_assets);
841
842    // Create a map of the fungible and non-fungible assets in the output notes
843    let outgoing_assets = output_notes_assets.values().flat_map(|note_assets| note_assets.iter());
844
845    request::collect_assets(outgoing_assets)
846}
847
848/// Ensures a transaction request is compatible with the current account state,
849/// primarily by checking asset balances against the requested transfers.
850fn validate_basic_account_request(
851    transaction_request: &TransactionRequest,
852    account: &Account,
853) -> Result<(), ClientError> {
854    // Get outgoing assets
855    let (fungible_balance_map, non_fungible_set) = get_outgoing_assets(transaction_request);
856
857    // Get incoming assets
858    let (incoming_fungible_balance_map, incoming_non_fungible_balance_set) =
859        transaction_request.incoming_assets();
860
861    // Check if the account balance plus incoming assets is greater than or equal to the
862    // outgoing fungible assets
863    for (faucet_id, amount) in fungible_balance_map {
864        let account_asset_amount = account.vault().get_balance(faucet_id).unwrap_or(0);
865        let incoming_balance = incoming_fungible_balance_map.get(&faucet_id).unwrap_or(&0);
866        if account_asset_amount + incoming_balance < amount {
867            return Err(ClientError::AssetError(AssetError::FungibleAssetAmountNotSufficient {
868                minuend: account_asset_amount,
869                subtrahend: amount,
870            }));
871        }
872    }
873
874    // Check if the account balance plus incoming assets is greater than or equal to the
875    // outgoing non fungible assets
876    for non_fungible in &non_fungible_set {
877        match account.vault().has_non_fungible_asset(*non_fungible) {
878            Ok(true) => (),
879            Ok(false) => {
880                // Check if the non fungible asset is in the incoming assets
881                if !incoming_non_fungible_balance_set.contains(non_fungible) {
882                    return Err(ClientError::AssetError(
883                        AssetError::NonFungibleFaucetIdTypeMismatch(non_fungible.faucet_id()),
884                    ));
885                }
886            },
887            _ => {
888                return Err(ClientError::AssetError(AssetError::NonFungibleFaucetIdTypeMismatch(
889                    non_fungible.faucet_id(),
890                )));
891            },
892        }
893    }
894
895    Ok(())
896}
897
898/// Fetches a foreign account's proof and details from the network, converts them into
899/// [`AccountInputs`], and caches the returned code in the store for future requests.
900///
901/// # Errors
902/// Fails if the account is private: the RPC does not return account details for them, causing
903/// [`TransactionRequestError::ForeignAccountDataMissing`].
904pub(crate) async fn fetch_public_account_inputs(
905    store: &Arc<dyn Store>,
906    rpc_api: &Arc<dyn NodeRpcClient>,
907    account_id: AccountId,
908    storage_requirements: AccountStorageRequirements,
909    account_state_at: AccountStateAt,
910) -> Result<AccountInputs, ClientError> {
911    let known_account_code: Option<AccountCode> =
912        store.get_foreign_account_code(vec![account_id]).await?.into_values().next();
913
914    let (_, account_proof) = rpc_api
915        .get_account_proof(
916            account_id,
917            storage_requirements.clone(),
918            account_state_at,
919            known_account_code,
920            Some(EMPTY_WORD),
921        )
922        .await?;
923
924    let account_inputs = request::account_proof_into_inputs(account_proof, &storage_requirements)?;
925
926    let _ = store
927        .upsert_foreign_account_code(account_id, account_inputs.code().clone())
928        .await
929        .inspect_err(|err| {
930            tracing::warn!(
931                %account_id,
932                %err,
933                "Failed to persist foreign account code to store"
934            );
935        });
936
937    Ok(account_inputs)
938}
939
940/// Extracts notes from [`RawOutputNotes`].
941/// Used for:
942/// - Checking the relevance of notes to save them as input notes.
943/// - Validate hashes versus expected output notes after a transaction is executed.
944pub fn notes_from_output(output_notes: &RawOutputNotes) -> impl Iterator<Item = &Note> {
945    output_notes.iter().filter_map(|n| match n {
946        RawOutputNote::Full(n) => Some(n),
947        RawOutputNote::Partial(_) => None,
948    })
949}
950
951/// Validates that the executed transaction's output recipients match what was expected in the
952/// transaction request.
953fn validate_executed_transaction(
954    executed_transaction: &ExecutedTransaction,
955    expected_output_recipients: &[NoteRecipient],
956) -> Result<(), ClientError> {
957    let tx_output_recipient_digests = executed_transaction
958        .output_notes()
959        .iter()
960        .filter_map(|n| n.recipient().map(NoteRecipient::digest))
961        .collect::<Vec<_>>();
962
963    let missing_recipient_digest: Vec<Word> = expected_output_recipients
964        .iter()
965        .filter_map(|recipient| {
966            (!tx_output_recipient_digests.contains(&recipient.digest()))
967                .then_some(recipient.digest())
968        })
969        .collect();
970
971    if !missing_recipient_digest.is_empty() {
972        return Err(ClientError::MissingOutputRecipients(missing_recipient_digest));
973    }
974
975    Ok(())
976}