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 (data_store, block_ref) =
482            self.prepare_program_execution(account_id, foreign_accounts).await?;
483
484        Ok(self
485            .build_executor(&data_store)?
486            .execute_tx_view_script(account_id, block_ref, tx_script, advice_inputs)
487            .await?)
488    }
489
490    /// Executes the provided transaction script with a DAP debug adapter listening for
491    /// connections, allowing interactive debugging via any DAP-compatible client.
492    #[cfg(feature = "dap")]
493    pub async fn execute_program_with_dap(
494        &mut self,
495        account_id: AccountId,
496        tx_script: TransactionScript,
497        advice_inputs: AdviceInputs,
498        foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
499    ) -> Result<[Felt; 16], ClientError> {
500        let (data_store, block_ref) =
501            self.prepare_program_execution(account_id, foreign_accounts).await?;
502
503        Ok(self
504            .build_dap_executor(&data_store)?
505            .execute_tx_view_script(account_id, block_ref, tx_script, advice_inputs)
506            .await?)
507    }
508
509    // HELPERS
510    // --------------------------------------------------------------------------------------------
511
512    /// Compiles the note updates needed to be applied to the store after executing a
513    /// transaction.
514    ///
515    /// These updates include:
516    /// - New output notes.
517    /// - New input notes (only if they are relevant to the client).
518    /// - Input notes that could be created as outputs of future transactions (e.g., a SWAP payback
519    ///   note).
520    /// - Updated input notes that were consumed locally.
521    async fn get_note_updates(
522        &self,
523        submission_height: BlockNumber,
524        tx_result: &TransactionResult,
525    ) -> Result<NoteUpdateTracker, ClientError> {
526        let executed_tx = tx_result.executed_transaction();
527        let current_timestamp = self.store.get_current_timestamp();
528        let current_block_num = self.store.get_sync_height().await?;
529
530        // New output notes
531        let new_output_notes = executed_tx
532            .output_notes()
533            .iter()
534            .cloned()
535            .filter_map(|output_note| {
536                OutputNoteRecord::try_from_output_note(output_note, submission_height).ok()
537            })
538            .collect::<Vec<_>>();
539
540        // New relevant input notes
541        let mut new_input_notes = vec![];
542        let output_notes =
543            notes_from_output(executed_tx.output_notes()).cloned().collect::<Vec<_>>();
544        let note_screener = self.note_screener();
545        let output_note_relevances = note_screener.can_consume_batch(&output_notes).await?;
546
547        for note in output_notes {
548            if output_note_relevances.contains_key(&note.id()) {
549                let metadata = note.metadata().clone();
550                let tag = metadata.tag();
551
552                new_input_notes.push(InputNoteRecord::new(
553                    note.into(),
554                    current_timestamp,
555                    ExpectedNoteState {
556                        metadata: Some(metadata),
557                        after_block_num: submission_height,
558                        tag: Some(tag),
559                    }
560                    .into(),
561                ));
562            }
563        }
564
565        // Track future input notes described in the transaction result.
566        new_input_notes.extend(tx_result.future_notes().iter().map(|(note_details, tag)| {
567            InputNoteRecord::new(
568                note_details.clone(),
569                None,
570                ExpectedNoteState {
571                    metadata: None,
572                    after_block_num: current_block_num,
573                    tag: Some(*tag),
574                }
575                .into(),
576            )
577        }));
578
579        // Locally consumed notes
580        let consumed_note_ids =
581            executed_tx.tx_inputs().input_notes().iter().map(InputNote::id).collect();
582
583        let consumed_notes = self.get_input_notes(NoteFilter::List(consumed_note_ids)).await?;
584
585        let mut updated_input_notes = vec![];
586
587        for mut input_note_record in consumed_notes {
588            if input_note_record.consumed_locally(
589                executed_tx.account_id(),
590                executed_tx.id(),
591                self.store.get_current_timestamp(),
592            )? {
593                updated_input_notes.push(input_note_record);
594            }
595        }
596
597        Ok(NoteUpdateTracker::for_transaction_updates(
598            new_input_notes,
599            updated_input_notes,
600            new_output_notes,
601        ))
602    }
603
604    /// Validates that the specified transaction request can be executed by the specified account.
605    ///
606    /// This does't guarantee that the transaction will succeed, but it's useful to avoid submitting
607    /// transactions that are guaranteed to fail. Some of the validations include:
608    /// - That the account has enough balance to cover the outgoing assets.
609    /// - That the client is not too far behind the chain tip.
610    pub async fn validate_request(
611        &mut self,
612        account_id: AccountId,
613        transaction_request: &TransactionRequest,
614    ) -> Result<(), ClientError> {
615        if let Some(max_block_number_delta) = self.max_block_number_delta {
616            let current_chain_tip =
617                self.rpc_api.get_block_header_by_number(None, false).await?.0.block_num();
618
619            if current_chain_tip > self.store.get_sync_height().await? + max_block_number_delta {
620                return Err(ClientError::RecencyConditionError(
621                    "The client is too far behind the chain tip to execute the transaction",
622                ));
623            }
624        }
625
626        let account = self.try_get_account(account_id).await?;
627        if account.is_faucet() {
628            // TODO(SantiagoPittella): Add faucet validations.
629            Ok(())
630        } else {
631            validate_basic_account_request(transaction_request, &account)
632        }
633    }
634
635    /// Checks whether the node's `note_scripts` registry already has each of the expected NTX
636    /// scripts. For any script that is missing, creates and submits a registration transaction
637    /// that produces a public note carrying that script.
638    ///
639    /// `account_id` is the account that will execute the registration transaction.
640    ///
641    /// This method is called automatically by [`Self::submit_new_transaction_with_prover`] when the
642    /// [`TransactionRequest`] contains expected NTX scripts. It can also be called directly if
643    /// you want to register scripts ahead of time.
644    pub async fn ensure_ntx_scripts_registered(
645        &mut self,
646        account_id: AccountId,
647        scripts: &[NoteScript],
648        tx_prover: Arc<dyn TransactionProver>,
649    ) -> Result<(), ClientError> {
650        let mut missing_scripts = Vec::new();
651
652        for script in scripts {
653            let script_root = script.root();
654
655            // Check if the node already has this script registered.
656            match self.rpc_api.get_note_script_by_root(script_root).await {
657                Ok(_) => {},
658                Err(RpcError::RequestError { error_kind: GrpcError::NotFound, .. }) => {
659                    missing_scripts.push(script.clone());
660                },
661                Err(other) => {
662                    return Err(ClientError::NtxScriptRegistrationFailed {
663                        script_root,
664                        source: other,
665                    });
666                },
667            }
668        }
669
670        if missing_scripts.is_empty() {
671            return Ok(());
672        }
673
674        let registration_request = TransactionRequestBuilder::new().build_register_note_scripts(
675            account_id,
676            missing_scripts,
677            self.rng(),
678        )?;
679
680        let tx_result = self.execute_transaction(account_id, registration_request).await?;
681        let proven = self.prove_transaction_with(&tx_result, tx_prover).await?;
682        let submission_height = self.submit_proven_transaction(proven, &tx_result).await?;
683        self.apply_transaction(&tx_result, submission_height).await?;
684
685        Ok(())
686    }
687
688    /// Filters out invalid or non-consumable input notes by simulating
689    /// note consumption and removing any that fail validation.
690    async fn get_valid_input_notes(
691        &self,
692        account: Account,
693        mut input_notes: InputNotes<InputNote>,
694        tx_args: TransactionArgs,
695    ) -> Result<InputNotes<InputNote>, ClientError> {
696        loop {
697            let data_store = ClientDataStore::new(self.store.clone(), self.rpc_api.clone());
698
699            data_store.mast_store().load_account_code(account.code());
700            let execution = NoteConsumptionChecker::new(&self.build_executor(&data_store)?)
701                .check_notes_consumability(
702                    account.id(),
703                    self.store.get_sync_height().await?,
704                    input_notes.iter().map(|n| n.clone().into_note()).collect(),
705                    tx_args.clone(),
706                )
707                .await?;
708
709            if execution.failed.is_empty() {
710                break;
711            }
712
713            let failed_note_ids: BTreeSet<NoteId> =
714                execution.failed.iter().map(|n| n.note.id()).collect();
715            let filtered_input_notes = InputNotes::new(
716                input_notes
717                    .into_iter()
718                    .filter(|note| !failed_note_ids.contains(&note.id()))
719                    .collect(),
720            )
721            .expect("Created from a valid input notes list");
722
723            input_notes = filtered_input_notes;
724        }
725
726        Ok(input_notes)
727    }
728
729    /// Retrieves the account interface for the specified account.
730    pub(crate) async fn get_account_interface(
731        &self,
732        account_id: AccountId,
733    ) -> Result<AccountInterface, ClientError> {
734        let account = self.try_get_account(account_id).await?;
735        Ok(AccountInterface::from_account(&account))
736    }
737
738    /// Returns foreign account inputs for the required foreign accounts specified by the
739    /// transaction request.
740    ///
741    /// For any [`ForeignAccount::Public`] in `foreign_accounts`, these pieces of data are retrieved
742    /// from the network. For any [`ForeignAccount::Private`] account, inner data is used and only
743    /// a proof of the account's existence on the network is fetched.
744    ///
745    /// Account data is retrieved for the node's current chain tip, so we need to check whether we
746    /// currently have the corresponding block header data. Otherwise, we additionally need to
747    /// retrieve it, this implies a state sync call which may update the client in other ways.
748    async fn retrieve_foreign_account_inputs(
749        &mut self,
750        foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
751    ) -> Result<(Option<BlockNumber>, Vec<AccountInputs>), ClientError> {
752        if foreign_accounts.is_empty() {
753            return Ok((None, Vec::new()));
754        }
755
756        let block_num = self.get_sync_height().await?;
757        let mut return_foreign_account_inputs = Vec::with_capacity(foreign_accounts.len());
758
759        for foreign_account in foreign_accounts.into_values() {
760            let foreign_account_inputs = match foreign_account {
761                ForeignAccount::Public(account_id, storage_requirements) => {
762                    fetch_public_account_inputs(
763                        &self.store,
764                        &self.rpc_api,
765                        account_id,
766                        storage_requirements,
767                        AccountStateAt::Block(block_num),
768                    )
769                    .await?
770                },
771                ForeignAccount::Private(partial_account) => {
772                    let account_id = partial_account.id();
773                    let (_, account_proof) = self
774                        .rpc_api
775                        .get_account_proof(
776                            account_id,
777                            AccountStorageRequirements::default(),
778                            AccountStateAt::Block(block_num),
779                            None,
780                            None,
781                        )
782                        .await?;
783                    let (witness, _) = account_proof.into_parts();
784                    AccountInputs::new(partial_account, witness)
785                },
786            };
787
788            return_foreign_account_inputs.push(foreign_account_inputs);
789        }
790
791        Ok((Some(block_num), return_foreign_account_inputs))
792    }
793
794    /// Prepares the data store and block reference for program execution.
795    ///
796    /// This is shared setup for both `execute_program` and `execute_program_with_dap`.
797    async fn prepare_program_execution(
798        &mut self,
799        account_id: AccountId,
800        foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
801    ) -> Result<(ClientDataStore, BlockNumber), ClientError> {
802        let (fpi_block_number, foreign_account_inputs) =
803            self.retrieve_foreign_account_inputs(foreign_accounts).await?;
804
805        let block_ref = if let Some(block_number) = fpi_block_number {
806            block_number
807        } else {
808            self.get_sync_height().await?
809        };
810
811        let account_record = self
812            .store
813            .get_account(account_id)
814            .await?
815            .ok_or(ClientError::AccountDataNotFound(account_id))?;
816
817        let account: Account = account_record.try_into()?;
818
819        let data_store = ClientDataStore::new(self.store.clone(), self.rpc_api.clone());
820
821        data_store.register_foreign_account_inputs(foreign_account_inputs.iter().cloned());
822
823        // Ensure code is loaded on MAST store
824        data_store.mast_store().load_account_code(account.code());
825
826        for fpi_account in &foreign_account_inputs {
827            data_store.mast_store().load_account_code(fpi_account.code());
828        }
829
830        Ok((data_store, block_ref))
831    }
832
833    /// Creates a transaction executor configured with the client's runtime options,
834    /// authenticator, and source manager.
835    pub(crate) fn build_executor<'store, 'auth, STORE: DataStore + Sync>(
836        &'auth self,
837        data_store: &'store STORE,
838    ) -> Result<TransactionExecutor<'store, 'auth, STORE, AUTH>, TransactionExecutorError> {
839        let mut executor = TransactionExecutor::new(data_store)
840            .with_options(self.exec_options)?
841            .with_source_manager(self.source_manager.clone());
842        if let Some(authenticator) = self.authenticator.as_deref() {
843            executor = executor.with_authenticator(authenticator);
844        }
845
846        Ok(executor)
847    }
848
849    /// Creates a transaction executor configured for DAP (Debug Adapter Protocol) debugging.
850    #[cfg(feature = "dap")]
851    pub(crate) fn build_dap_executor<'store, 'auth, STORE: DataStore + Sync>(
852        &'auth self,
853        data_store: &'store STORE,
854    ) -> Result<
855        TransactionExecutor<'store, 'auth, STORE, AUTH, miden_debug::DapExecutor>,
856        TransactionExecutorError,
857    > {
858        Ok(self
859            .build_executor(data_store)?
860            .with_program_executor::<miden_debug::DapExecutor>())
861    }
862}
863
864// HELPERS
865// ================================================================================================
866
867/// Helper to get the account outgoing assets.
868///
869/// Any outgoing assets resulting from executing note scripts but not present in expected output
870/// notes wouldn't be included.
871fn get_outgoing_assets(
872    transaction_request: &TransactionRequest,
873) -> (BTreeMap<AccountId, u64>, Vec<NonFungibleAsset>) {
874    // Get own notes assets
875    let mut own_notes_assets = match transaction_request.script_template() {
876        Some(TransactionScriptTemplate::SendNotes(notes)) => notes
877            .iter()
878            .map(|note| (note.id(), note.assets().clone()))
879            .collect::<BTreeMap<_, _>>(),
880        _ => BTreeMap::default(),
881    };
882    // Get transaction output notes assets
883    let mut output_notes_assets = transaction_request
884        .expected_output_own_notes()
885        .into_iter()
886        .map(|note| (note.id(), note.assets().clone()))
887        .collect::<BTreeMap<_, _>>();
888
889    // Merge with own notes assets and delete duplicates
890    output_notes_assets.append(&mut own_notes_assets);
891
892    // Create a map of the fungible and non-fungible assets in the output notes
893    let outgoing_assets = output_notes_assets.values().flat_map(|note_assets| note_assets.iter());
894
895    request::collect_assets(outgoing_assets)
896}
897
898/// Ensures a transaction request is compatible with the current account state,
899/// primarily by checking asset balances against the requested transfers.
900fn validate_basic_account_request(
901    transaction_request: &TransactionRequest,
902    account: &Account,
903) -> Result<(), ClientError> {
904    // Get outgoing assets
905    let (fungible_balance_map, non_fungible_set) = get_outgoing_assets(transaction_request);
906
907    // Get incoming assets
908    let (incoming_fungible_balance_map, incoming_non_fungible_balance_set) =
909        transaction_request.incoming_assets();
910
911    // Check if the account balance plus incoming assets is greater than or equal to the
912    // outgoing fungible assets
913    for (faucet_id, amount) in fungible_balance_map {
914        let account_asset_amount = account.vault().get_balance(faucet_id).unwrap_or(0);
915        let incoming_balance = incoming_fungible_balance_map.get(&faucet_id).unwrap_or(&0);
916        if account_asset_amount + incoming_balance < amount {
917            return Err(ClientError::AssetError(AssetError::FungibleAssetAmountNotSufficient {
918                minuend: account_asset_amount,
919                subtrahend: amount,
920            }));
921        }
922    }
923
924    // Check if the account balance plus incoming assets is greater than or equal to the
925    // outgoing non fungible assets
926    for non_fungible in &non_fungible_set {
927        match account.vault().has_non_fungible_asset(*non_fungible) {
928            Ok(true) => (),
929            Ok(false) => {
930                // Check if the non fungible asset is in the incoming assets
931                if !incoming_non_fungible_balance_set.contains(non_fungible) {
932                    return Err(ClientError::AssetError(
933                        AssetError::NonFungibleFaucetIdTypeMismatch(non_fungible.faucet_id()),
934                    ));
935                }
936            },
937            _ => {
938                return Err(ClientError::AssetError(AssetError::NonFungibleFaucetIdTypeMismatch(
939                    non_fungible.faucet_id(),
940                )));
941            },
942        }
943    }
944
945    Ok(())
946}
947
948/// Fetches a foreign account's proof and details from the network, converts them into
949/// [`AccountInputs`], and caches the returned code in the store for future requests.
950///
951/// # Errors
952/// Fails if the account is private: the RPC does not return account details for them, causing
953/// [`TransactionRequestError::ForeignAccountDataMissing`].
954pub(crate) async fn fetch_public_account_inputs(
955    store: &Arc<dyn Store>,
956    rpc_api: &Arc<dyn NodeRpcClient>,
957    account_id: AccountId,
958    storage_requirements: AccountStorageRequirements,
959    account_state_at: AccountStateAt,
960) -> Result<AccountInputs, ClientError> {
961    let known_account_code: Option<AccountCode> =
962        store.get_foreign_account_code(vec![account_id]).await?.into_values().next();
963
964    let (_, account_proof) = rpc_api
965        .get_account_proof(
966            account_id,
967            storage_requirements.clone(),
968            account_state_at,
969            known_account_code,
970            Some(EMPTY_WORD),
971        )
972        .await?;
973
974    let account_inputs = request::account_proof_into_inputs(account_proof, &storage_requirements)?;
975
976    let _ = store
977        .upsert_foreign_account_code(account_id, account_inputs.code().clone())
978        .await
979        .inspect_err(|err| {
980            tracing::warn!(
981                %account_id,
982                %err,
983                "Failed to persist foreign account code to store"
984            );
985        });
986
987    Ok(account_inputs)
988}
989
990/// Extracts notes from [`RawOutputNotes`].
991/// Used for:
992/// - Checking the relevance of notes to save them as input notes.
993/// - Validate hashes versus expected output notes after a transaction is executed.
994pub fn notes_from_output(output_notes: &RawOutputNotes) -> impl Iterator<Item = &Note> {
995    output_notes.iter().filter_map(|n| match n {
996        RawOutputNote::Full(n) => Some(n),
997        RawOutputNote::Partial(_) => None,
998    })
999}
1000
1001/// Validates that the executed transaction's output recipients match what was expected in the
1002/// transaction request.
1003fn validate_executed_transaction(
1004    executed_transaction: &ExecutedTransaction,
1005    expected_output_recipients: &[NoteRecipient],
1006) -> Result<(), ClientError> {
1007    let tx_output_recipient_digests = executed_transaction
1008        .output_notes()
1009        .iter()
1010        .filter_map(|n| n.recipient().map(NoteRecipient::digest))
1011        .collect::<Vec<_>>();
1012
1013    let missing_recipient_digest: Vec<Word> = expected_output_recipients
1014        .iter()
1015        .filter_map(|recipient| {
1016            (!tx_output_recipient_digests.contains(&recipient.digest()))
1017                .then_some(recipient.digest())
1018        })
1019        .collect();
1020
1021    if !missing_recipient_digest.is_empty() {
1022        return Err(ClientError::MissingOutputRecipients(missing_recipient_digest));
1023    }
1024
1025    Ok(())
1026}