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