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