Skip to main content

miden_client/transaction/
mod.rs

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