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