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 as
10//!     `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::{
24//!     crypto::FeltRng,
25//!     transaction::{PaymentTransactionData, TransactionRequestBuilder, TransactionResult},
26//!     Client,
27//! };
28//! use miden_objects::{account::AccountId, asset::FungibleAsset, note::NoteType};
29//! # use std::error::Error;
30//!
31//! /// Executes, proves and submits a P2ID transaction.
32//! ///
33//! /// This transaction is executed by `sender_id`, and creates an output note
34//! /// containing 100 tokens of `faucet_id`'s fungible asset.
35//! async fn create_and_submit_transaction<R: rand::Rng>(
36//!     client: &mut Client<impl FeltRng>,
37//!     sender_id: AccountId,
38//!     target_id: AccountId,
39//!     faucet_id: AccountId,
40//! ) -> Result<(), Box<dyn Error>> {
41//!     // Create an asset representing the amount to be transferred.
42//!     let asset = FungibleAsset::new(faucet_id, 100)?;
43//!
44//!     // Build a transaction request for a pay-to-id transaction.
45//!     let tx_request = TransactionRequestBuilder::pay_to_id(
46//!         PaymentTransactionData::new(vec![asset.into()], sender_id, target_id),
47//!         None, // No recall height
48//!         NoteType::Private,
49//!         client.rng(),
50//!     )?
51//!     .build();
52//!
53//!     // Execute the transaction. This returns a TransactionResult.
54//!     let tx_result: TransactionResult = client.new_transaction(sender_id, tx_request).await?;
55//!
56//!     // Prove and submit the transaction, persisting its details to the local store.
57//!     client.submit_transaction(tx_result).await?;
58//!
59//!     Ok(())
60//! }
61//! ```
62//!
63//! For more detailed information about each function and error type, refer to the specific API
64//! documentation.
65
66use alloc::{
67    collections::{BTreeMap, BTreeSet},
68    string::{String, ToString},
69    sync::Arc,
70    vec::Vec,
71};
72use core::fmt::{self};
73
74pub use miden_lib::transaction::TransactionKernel;
75use miden_objects::{
76    account::{Account, AccountCode, AccountDelta, AccountId, AccountType},
77    asset::{Asset, NonFungibleAsset},
78    block::BlockNumber,
79    crypto::merkle::MerklePath,
80    note::{Note, NoteDetails, NoteId, NoteTag},
81    transaction::{InputNotes, TransactionArgs},
82    AssetError, Digest, Felt, Word, ZERO,
83};
84use miden_tx::{
85    utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable},
86    TransactionExecutor,
87};
88pub use miden_tx::{
89    LocalTransactionProver, ProvingOptions, TransactionProver, TransactionProverError,
90};
91use script_builder::{AccountCapabilities, AccountInterface};
92use tracing::info;
93
94use super::{Client, FeltRng};
95use crate::{
96    note::{NoteScreener, NoteUpdates},
97    rpc::domain::account::AccountProof,
98    store::{
99        input_note_states::ExpectedNoteState, InputNoteRecord, InputNoteState, NoteFilter,
100        OutputNoteRecord, StoreError, TransactionFilter,
101    },
102    sync::NoteTagRecord,
103    ClientError,
104};
105
106mod request;
107pub use request::{
108    ForeignAccount, ForeignAccountInputs, NoteArgs, PaymentTransactionData, SwapTransactionData,
109    TransactionRequest, TransactionRequestBuilder, TransactionRequestError,
110    TransactionScriptTemplate,
111};
112
113mod script_builder;
114pub use miden_objects::transaction::{
115    ExecutedTransaction, InputNote, OutputNote, OutputNotes, ProvenTransaction, TransactionId,
116    TransactionScript,
117};
118pub use miden_tx::{DataStoreError, TransactionExecutorError};
119pub use script_builder::TransactionScriptBuilderError;
120
121// TRANSACTION RESULT
122// --------------------------------------------------------------------------------------------
123
124/// Represents the result of executing a transaction by the client.
125///
126/// It contains an [ExecutedTransaction], and a list of `relevant_notes` that contains the
127/// `output_notes` that the client has to store as input notes, based on the NoteScreener
128/// output from filtering the transaction's output notes or some partial note we expect to receive
129/// in the future (you can check at swap notes for an example of this).
130#[derive(Clone, Debug, PartialEq)]
131pub struct TransactionResult {
132    transaction: ExecutedTransaction,
133    relevant_notes: Vec<InputNoteRecord>,
134}
135
136impl TransactionResult {
137    /// Screens the output notes to store and track the relevant ones, and instantiates a
138    /// [TransactionResult].
139    pub async fn new(
140        transaction: ExecutedTransaction,
141        note_screener: NoteScreener,
142        partial_notes: Vec<(NoteDetails, NoteTag)>,
143        current_block_num: BlockNumber,
144        current_timestamp: Option<u64>,
145    ) -> Result<Self, ClientError> {
146        let mut relevant_notes = vec![];
147
148        for note in notes_from_output(transaction.output_notes()) {
149            let account_relevance = note_screener.check_relevance(note).await?;
150
151            if !account_relevance.is_empty() {
152                let metadata = *note.metadata();
153                relevant_notes.push(InputNoteRecord::new(
154                    note.into(),
155                    current_timestamp,
156                    ExpectedNoteState {
157                        metadata: Some(metadata),
158                        after_block_num: current_block_num,
159                        tag: Some(metadata.tag()),
160                    }
161                    .into(),
162                ));
163            }
164        }
165
166        // Include partial output notes into the relevant notes
167        relevant_notes.extend(partial_notes.iter().map(|(note_details, tag)| {
168            InputNoteRecord::new(
169                note_details.clone(),
170                None,
171                ExpectedNoteState {
172                    metadata: None,
173                    after_block_num: current_block_num,
174                    tag: Some(*tag),
175                }
176                .into(),
177            )
178        }));
179
180        let tx_result = Self { transaction, relevant_notes };
181
182        Ok(tx_result)
183    }
184
185    /// Returns the [ExecutedTransaction].
186    pub fn executed_transaction(&self) -> &ExecutedTransaction {
187        &self.transaction
188    }
189
190    /// Returns the output notes that were generated as a result of the transaction execution.
191    pub fn created_notes(&self) -> &OutputNotes {
192        self.transaction.output_notes()
193    }
194
195    /// Returns the list of notes that are relevant to the client, based on [NoteScreener].
196    pub fn relevant_notes(&self) -> &[InputNoteRecord] {
197        &self.relevant_notes
198    }
199
200    /// Returns the block against which the transaction was executed.
201    pub fn block_num(&self) -> BlockNumber {
202        self.transaction.block_header().block_num()
203    }
204
205    /// Returns transaction's [TransactionArgs].
206    pub fn transaction_arguments(&self) -> &TransactionArgs {
207        self.transaction.tx_args()
208    }
209
210    /// Returns the [AccountDelta] that describes the change of state for the executing [Account].
211    pub fn account_delta(&self) -> &AccountDelta {
212        self.transaction.account_delta()
213    }
214
215    /// Returns input notes that were consumed as part of the transaction.
216    pub fn consumed_notes(&self) -> &InputNotes<InputNote> {
217        self.transaction.tx_inputs().input_notes()
218    }
219}
220
221impl From<TransactionResult> for ExecutedTransaction {
222    fn from(tx_result: TransactionResult) -> ExecutedTransaction {
223        tx_result.transaction
224    }
225}
226
227impl Serializable for TransactionResult {
228    fn write_into<W: ByteWriter>(&self, target: &mut W) {
229        self.transaction.write_into(target);
230        self.relevant_notes.write_into(target);
231    }
232}
233
234impl Deserializable for TransactionResult {
235    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
236        let transaction = ExecutedTransaction::read_from(source)?;
237        let relevant_notes = Vec::<InputNoteRecord>::read_from(source)?;
238
239        Ok(Self { transaction, relevant_notes })
240    }
241}
242
243// TRANSACTION RECORD
244// --------------------------------------------------------------------------------------------
245
246/// Describes a transaction that has been executed and is being tracked on the Client.
247///
248/// Currently, the `commit_height` (and `committed` status) is set based on the height
249/// at which the transaction's output notes are committed.
250#[derive(Debug, Clone)]
251pub struct TransactionRecord {
252    pub id: TransactionId,
253    pub account_id: AccountId,
254    pub init_account_state: Digest,
255    pub final_account_state: Digest,
256    pub input_note_nullifiers: Vec<Digest>,
257    pub output_notes: OutputNotes,
258    pub transaction_script: Option<TransactionScript>,
259    pub block_num: BlockNumber,
260    pub transaction_status: TransactionStatus,
261}
262
263impl TransactionRecord {
264    #[allow(clippy::too_many_arguments)]
265    pub fn new(
266        id: TransactionId,
267        account_id: AccountId,
268        init_account_state: Digest,
269        final_account_state: Digest,
270        input_note_nullifiers: Vec<Digest>,
271        output_notes: OutputNotes,
272        transaction_script: Option<TransactionScript>,
273        block_num: BlockNumber,
274        transaction_status: TransactionStatus,
275    ) -> TransactionRecord {
276        TransactionRecord {
277            id,
278            account_id,
279            init_account_state,
280            final_account_state,
281            input_note_nullifiers,
282            output_notes,
283            transaction_script,
284            block_num,
285            transaction_status,
286        }
287    }
288}
289
290/// Represents the status of a transaction.
291#[derive(Debug, Clone, PartialEq)]
292pub enum TransactionStatus {
293    /// Transaction has been submitted but not yet committed.
294    Pending,
295    /// Transaction has been committed and included at the specified block number.
296    Committed(BlockNumber),
297    /// Transaction has been discarded and isn't included in the node.
298    Discarded,
299}
300
301impl fmt::Display for TransactionStatus {
302    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
303        match self {
304            TransactionStatus::Pending => write!(f, "Pending"),
305            TransactionStatus::Committed(block_number) => {
306                write!(f, "Committed (Block: {})", block_number)
307            },
308            TransactionStatus::Discarded => write!(f, "Discarded"),
309        }
310    }
311}
312
313// TRANSACTION STORE UPDATE
314// --------------------------------------------------------------------------------------------
315
316/// Represents the changes that need to be applied to the client store as a result of a
317/// transaction execution.
318pub struct TransactionStoreUpdate {
319    /// Details of the executed transaction to be inserted.
320    executed_transaction: ExecutedTransaction,
321    /// Updated account state after the [AccountDelta] has been applied.
322    updated_account: Account,
323    /// Information about note changes after the transaction execution.
324    note_updates: NoteUpdates,
325    /// New note tags to be tracked.
326    new_tags: Vec<NoteTagRecord>,
327}
328
329impl TransactionStoreUpdate {
330    /// Creates a new [TransactionStoreUpdate] instance.
331    pub fn new(
332        executed_transaction: ExecutedTransaction,
333        updated_account: Account,
334        created_input_notes: Vec<InputNoteRecord>,
335        created_output_notes: Vec<OutputNoteRecord>,
336        updated_input_notes: Vec<InputNoteRecord>,
337        new_tags: Vec<NoteTagRecord>,
338    ) -> Self {
339        Self {
340            executed_transaction,
341            updated_account,
342            note_updates: NoteUpdates::new(
343                created_input_notes,
344                created_output_notes,
345                updated_input_notes,
346                vec![],
347            ),
348            new_tags,
349        }
350    }
351
352    /// Returns the executed transaction.
353    pub fn executed_transaction(&self) -> &ExecutedTransaction {
354        &self.executed_transaction
355    }
356
357    /// Returns the updated account.
358    pub fn updated_account(&self) -> &Account {
359        &self.updated_account
360    }
361
362    /// Returns the note updates that need to be applied after the transaction execution.
363    pub fn note_updates(&self) -> &NoteUpdates {
364        &self.note_updates
365    }
366
367    /// Returns the new tags that were created as part of the transaction.
368    pub fn new_tags(&self) -> &[NoteTagRecord] {
369        &self.new_tags
370    }
371}
372
373/// Transaction management methods
374impl<R: FeltRng> Client<R> {
375    // TRANSACTION DATA RETRIEVAL
376    // --------------------------------------------------------------------------------------------
377
378    /// Retrieves tracked transactions, filtered by [TransactionFilter].
379    pub async fn get_transactions(
380        &self,
381        filter: TransactionFilter,
382    ) -> Result<Vec<TransactionRecord>, ClientError> {
383        self.store.get_transactions(filter).await.map_err(|err| err.into())
384    }
385
386    // TRANSACTION
387    // --------------------------------------------------------------------------------------------
388
389    /// Creates and executes a transaction specified by the request against the specified account,
390    /// but doesn't change the local database.
391    ///
392    /// If the transaction utilizes foreign account data, there is a chance that the client doesn't
393    /// have the required block header in the local database. In these scenarios, a sync to
394    /// the chain tip is performed, and the required block header is retrieved.
395    ///
396    /// # Errors
397    ///
398    /// - Returns [ClientError::MissingOutputNotes] if the [TransactionRequest] ouput notes are not
399    ///   a subset of executor's output notes.
400    /// - Returns a [ClientError::TransactionExecutorError] if the execution fails.
401    /// - Returns a [ClientError::TransactionRequestError] if the request is invalid.
402    pub async fn new_transaction(
403        &mut self,
404        account_id: AccountId,
405        transaction_request: TransactionRequest,
406    ) -> Result<TransactionResult, ClientError> {
407        // Validates the transaction request before executing
408        self.validate_request(account_id, &transaction_request).await?;
409
410        // Ensure authenticated notes have their inclusion proofs (a.k.a they're in a committed
411        // state). TODO: we should consider refactoring this in a way we can handle this in
412        // `get_transaction_inputs`
413        let authenticated_input_note_ids: Vec<NoteId> =
414            transaction_request.authenticated_input_note_ids().collect::<Vec<_>>();
415
416        let authenticated_note_records = self
417            .store
418            .get_input_notes(NoteFilter::List(authenticated_input_note_ids))
419            .await?;
420
421        for authenticated_note_record in authenticated_note_records {
422            if !authenticated_note_record.is_authenticated() {
423                return Err(ClientError::TransactionRequestError(
424                    TransactionRequestError::InputNoteNotAuthenticated,
425                ));
426            }
427        }
428
429        // If tx request contains unauthenticated_input_notes we should insert them
430        let unauthenticated_input_notes = transaction_request
431            .unauthenticated_input_notes()
432            .iter()
433            .cloned()
434            .map(|note| note.into())
435            .collect::<Vec<_>>();
436
437        self.store.upsert_input_notes(&unauthenticated_input_notes).await?;
438
439        let note_ids = transaction_request.get_input_note_ids();
440
441        let output_notes: Vec<Note> =
442            transaction_request.expected_output_notes().cloned().collect();
443
444        let future_notes: Vec<(NoteDetails, NoteTag)> =
445            transaction_request.expected_future_notes().cloned().collect();
446
447        let tx_script = transaction_request.build_transaction_script(
448            self.get_account_capabilities(account_id).await?,
449            self.in_debug_mode,
450        )?;
451
452        let foreign_accounts = transaction_request.foreign_accounts().clone();
453        let mut tx_args = transaction_request.into_transaction_args(tx_script);
454
455        // Inject state and code of foreign accounts
456        let fpi_block_num =
457            self.inject_foreign_account_inputs(foreign_accounts, &mut tx_args).await?;
458
459        let block_num = if let Some(block_num) = fpi_block_num {
460            block_num
461        } else {
462            self.store.get_sync_height().await?
463        };
464
465        // Execute the transaction and get the witness
466        let executed_transaction = self
467            .tx_executor
468            .execute_transaction(account_id, block_num, &note_ids, tx_args)
469            .await?;
470
471        // Check that the expected output notes matches the transaction outcome.
472        // We compare authentication hashes where possible since that involves note IDs + metadata
473        // (as opposed to just note ID which remains the same regardless of metadata)
474        // We also do the check for partial output notes
475
476        let tx_note_auth_hashes: BTreeSet<Digest> =
477            notes_from_output(executed_transaction.output_notes())
478                .map(|note| note.hash())
479                .collect();
480
481        let missing_note_ids: Vec<NoteId> = output_notes
482            .iter()
483            .filter_map(|n| (!tx_note_auth_hashes.contains(&n.hash())).then_some(n.id()))
484            .collect();
485
486        if !missing_note_ids.is_empty() {
487            return Err(ClientError::MissingOutputNotes(missing_note_ids));
488        }
489
490        let screener = NoteScreener::new(self.store.clone());
491
492        TransactionResult::new(
493            executed_transaction,
494            screener,
495            future_notes,
496            self.get_sync_height().await?,
497            self.store.get_current_timestamp(),
498        )
499        .await
500    }
501
502    /// Proves the specified transaction using a local prover, submits it to the network, and saves
503    /// the transaction into the local database for tracking.
504    pub async fn submit_transaction(
505        &mut self,
506        tx_result: TransactionResult,
507    ) -> Result<(), ClientError> {
508        self.submit_transaction_with_prover(tx_result, self.tx_prover.clone()).await
509    }
510
511    /// Proves the specified transaction using the provided prover, submits it to the network, and
512    /// saves the transaction into the local database for tracking.
513    pub async fn submit_transaction_with_prover(
514        &mut self,
515        tx_result: TransactionResult,
516        tx_prover: Arc<dyn TransactionProver>,
517    ) -> Result<(), ClientError> {
518        let proven_transaction = self.prove_transaction(&tx_result, tx_prover).await?;
519        self.submit_proven_transaction(proven_transaction).await?;
520        self.apply_transaction(tx_result).await
521    }
522
523    /// Proves the specified transaction result using the provided prover.
524    async fn prove_transaction(
525        &mut self,
526        tx_result: &TransactionResult,
527        tx_prover: Arc<dyn TransactionProver>,
528    ) -> Result<ProvenTransaction, ClientError> {
529        info!("Proving transaction...");
530
531        let proven_transaction =
532            tx_prover.prove(tx_result.executed_transaction().clone().into()).await?;
533
534        info!("Transaction proven.");
535
536        Ok(proven_transaction)
537    }
538
539    async fn submit_proven_transaction(
540        &mut self,
541        proven_transaction: ProvenTransaction,
542    ) -> Result<(), ClientError> {
543        info!("Submitting transaction to the network...");
544        self.rpc_api.submit_proven_transaction(proven_transaction).await?;
545        info!("Transaction submitted.");
546
547        Ok(())
548    }
549
550    async fn apply_transaction(&self, tx_result: TransactionResult) -> Result<(), ClientError> {
551        let transaction_id = tx_result.executed_transaction().id();
552        let sync_height = self.get_sync_height().await?;
553
554        // Transaction was proven and submitted to the node correctly, persist note details and
555        // update account
556        info!("Applying transaction to the local store...");
557
558        let account_id = tx_result.executed_transaction().account_id();
559        let account_delta = tx_result.account_delta();
560        let account_record = self.try_get_account(account_id).await?;
561
562        if account_record.is_locked() {
563            return Err(ClientError::AccountLocked(account_id));
564        }
565
566        let mut account: Account = account_record.into();
567        account.apply_delta(account_delta)?;
568
569        if self.store.get_account_header_by_hash(account.hash()).await?.is_some() {
570            return Err(ClientError::StoreError(StoreError::AccountHashAlreadyExists(
571                account.hash(),
572            )));
573        }
574
575        // Save only input notes that we care for (based on the note screener assessment)
576        let created_input_notes = tx_result.relevant_notes().to_vec();
577        let new_tags = created_input_notes
578            .iter()
579            .filter_map(|note| {
580                if let InputNoteState::Expected(ExpectedNoteState { tag: Some(tag), .. }) =
581                    note.state()
582                {
583                    Some(NoteTagRecord::with_note_source(*tag, note.id()))
584                } else {
585                    None
586                }
587            })
588            .collect();
589
590        // Save all output notes
591        let created_output_notes = tx_result
592            .created_notes()
593            .iter()
594            .cloned()
595            .filter_map(|output_note| {
596                OutputNoteRecord::try_from_output_note(output_note, sync_height).ok()
597            })
598            .collect::<Vec<_>>();
599
600        let consumed_note_ids = tx_result.consumed_notes().iter().map(|note| note.id()).collect();
601        let consumed_notes = self.get_input_notes(NoteFilter::List(consumed_note_ids)).await?;
602
603        let mut updated_input_notes = vec![];
604        for mut input_note_record in consumed_notes {
605            if input_note_record.consumed_locally(
606                account_id,
607                transaction_id,
608                self.store.get_current_timestamp(),
609            )? {
610                updated_input_notes.push(input_note_record);
611            }
612        }
613
614        let tx_update = TransactionStoreUpdate::new(
615            tx_result.into(),
616            account,
617            created_input_notes,
618            created_output_notes,
619            updated_input_notes,
620            new_tags,
621        );
622
623        self.store.apply_transaction(tx_update).await?;
624        info!("Transaction stored.");
625        Ok(())
626    }
627
628    /// Compiles the provided transaction script source and inputs into a [TransactionScript].
629    pub fn compile_tx_script<T>(
630        &self,
631        inputs: T,
632        program: &str,
633    ) -> Result<TransactionScript, ClientError>
634    where
635        T: IntoIterator<Item = (Word, Vec<Felt>)>,
636    {
637        let assembler = TransactionKernel::assembler().with_debug_mode(self.in_debug_mode);
638        TransactionScript::compile(program, inputs, assembler)
639            .map_err(ClientError::TransactionScriptError)
640    }
641
642    // HELPERS
643    // --------------------------------------------------------------------------------------------
644
645    /// Helper to get the account outgoing assets.
646    ///
647    /// Any outgoing assets resulting from executing note scripts but not present in expected output
648    /// notes wouldn't be included.
649    fn get_outgoing_assets(
650        &self,
651        transaction_request: &TransactionRequest,
652    ) -> (BTreeMap<AccountId, u64>, BTreeSet<NonFungibleAsset>) {
653        // Get own notes assets
654        let mut own_notes_assets = match transaction_request.script_template() {
655            Some(TransactionScriptTemplate::SendNotes(notes)) => {
656                notes.iter().map(|note| (note.id(), note.assets())).collect::<BTreeMap<_, _>>()
657            },
658            _ => Default::default(),
659        };
660        // Get transaction output notes assets
661        let mut output_notes_assets = transaction_request
662            .expected_output_notes()
663            .map(|note| (note.id(), note.assets()))
664            .collect::<BTreeMap<_, _>>();
665
666        // Merge with own notes assets and delete duplicates
667        output_notes_assets.append(&mut own_notes_assets);
668
669        // Create a map of the fungible and non-fungible assets in the output notes
670        let outgoing_assets =
671            output_notes_assets.values().flat_map(|note_assets| note_assets.iter());
672
673        collect_assets(outgoing_assets)
674    }
675
676    /// Helper to get the account incoming assets.
677    async fn get_incoming_assets(
678        &self,
679        transaction_request: &TransactionRequest,
680    ) -> Result<(BTreeMap<AccountId, u64>, BTreeSet<NonFungibleAsset>), TransactionRequestError>
681    {
682        // Get incoming asset notes excluding unauthenticated ones
683        let incoming_notes_ids: Vec<_> = transaction_request
684            .input_notes()
685            .iter()
686            .filter_map(|(note_id, _)| {
687                if transaction_request
688                    .unauthenticated_input_notes()
689                    .iter()
690                    .any(|note| note.id() == *note_id)
691                {
692                    None
693                } else {
694                    Some(*note_id)
695                }
696            })
697            .collect();
698
699        let store_input_notes = self
700            .get_input_notes(NoteFilter::List(incoming_notes_ids))
701            .await
702            .map_err(|err| TransactionRequestError::NoteNotFound(err.to_string()))?;
703
704        let all_incoming_assets =
705            store_input_notes.iter().flat_map(|note| note.assets().iter()).chain(
706                transaction_request
707                    .unauthenticated_input_notes()
708                    .iter()
709                    .flat_map(|note| note.assets().iter()),
710            );
711
712        Ok(collect_assets(all_incoming_assets))
713    }
714
715    async fn validate_basic_account_request(
716        &self,
717        transaction_request: &TransactionRequest,
718        account: &Account,
719    ) -> Result<(), ClientError> {
720        // Get outgoing assets
721        let (fungible_balance_map, non_fungible_set) =
722            self.get_outgoing_assets(transaction_request);
723
724        // Get incoming assets
725        let (incoming_fungible_balance_map, incoming_non_fungible_balance_set) =
726            self.get_incoming_assets(transaction_request).await?;
727
728        // Check if the account balance plus incoming assets is greater than or equal to the
729        // outgoing fungible assets
730        for (faucet_id, amount) in fungible_balance_map {
731            let account_asset_amount = account.vault().get_balance(faucet_id).unwrap_or(0);
732            let incoming_balance = incoming_fungible_balance_map.get(&faucet_id).unwrap_or(&0);
733            if account_asset_amount + incoming_balance < amount {
734                return Err(ClientError::AssetError(
735                    AssetError::FungibleAssetAmountNotSufficient {
736                        minuend: account_asset_amount,
737                        subtrahend: amount,
738                    },
739                ));
740            }
741        }
742
743        // Check if the account balance plus incoming assets is greater than or equal to the
744        // outgoing non fungible assets
745        for non_fungible in non_fungible_set {
746            match account.vault().has_non_fungible_asset(non_fungible) {
747                Ok(true) => (),
748                Ok(false) => {
749                    // Check if the non fungible asset is in the incoming assets
750                    if !incoming_non_fungible_balance_set.contains(&non_fungible) {
751                        return Err(ClientError::AssetError(
752                            AssetError::NonFungibleFaucetIdTypeMismatch(
753                                non_fungible.faucet_id_prefix(),
754                            ),
755                        ));
756                    }
757                },
758                _ => {
759                    return Err(ClientError::AssetError(
760                        AssetError::NonFungibleFaucetIdTypeMismatch(
761                            non_fungible.faucet_id_prefix(),
762                        ),
763                    ));
764                },
765            }
766        }
767
768        Ok(())
769    }
770
771    /// Validates that the specified transaction request can be executed by the specified account.
772    ///
773    /// This function checks that the account has enough balance to cover the outgoing assets. This
774    /// does't guarantee that the transaction will succeed, but it's useful to avoid submitting
775    /// transactions that are guaranteed to fail.
776    pub async fn validate_request(
777        &self,
778        account_id: AccountId,
779        transaction_request: &TransactionRequest,
780    ) -> Result<(), ClientError> {
781        let account: Account = self.try_get_account(account_id).await?.into();
782
783        if account.is_faucet() {
784            // TODO(SantiagoPittella): Add faucet validations.
785            Ok(())
786        } else {
787            self.validate_basic_account_request(transaction_request, &account).await
788        }
789    }
790
791    /// Retrieves the account capabilities for the specified account.
792    async fn get_account_capabilities(
793        &self,
794        account_id: AccountId,
795    ) -> Result<AccountCapabilities, ClientError> {
796        let account: Account = self.try_get_account(account_id).await?.into();
797        let account_auth = self.try_get_account_auth(account_id).await?;
798
799        // TODO: we should check if the account actually exposes the interfaces we're trying to use
800        let account_capabilities = match account.account_type() {
801            AccountType::FungibleFaucet => AccountInterface::BasicFungibleFaucet,
802            AccountType::NonFungibleFaucet => todo!("Non fungible faucet not supported yet"),
803            AccountType::RegularAccountImmutableCode | AccountType::RegularAccountUpdatableCode => {
804                AccountInterface::BasicWallet
805            },
806        };
807
808        Ok(AccountCapabilities {
809            account_id,
810            auth: account_auth,
811            interfaces: account_capabilities,
812        })
813    }
814
815    /// Injects foreign account data inputs into `tx_args` (account proof, code commitment and
816    /// storage data). Additionally loads the account code into the transaction executor.
817    ///
818    /// For any [ForeignAccount::Public] in `foreing_accounts`, these pieces of data are retrieved
819    /// from the network. For any [ForeignAccount::Private] account, inner data is used.
820    ///
821    /// Account data is retrieved for the node's current chain tip, so we need to check whether we
822    /// currently have the corresponding block header data. Otherwise, we additionally need to
823    /// retrieve it.
824    async fn inject_foreign_account_inputs(
825        &mut self,
826        foreign_accounts: BTreeSet<ForeignAccount>,
827        tx_args: &mut TransactionArgs,
828    ) -> Result<Option<BlockNumber>, ClientError> {
829        if foreign_accounts.is_empty() {
830            return Ok(None);
831        }
832
833        let account_ids = foreign_accounts.iter().map(|acc| acc.account_id());
834        let known_account_codes =
835            self.store.get_foreign_account_code(account_ids.collect()).await?;
836
837        let known_account_codes: Vec<AccountCode> = known_account_codes.into_values().collect();
838
839        // Fetch account proofs
840        let (block_num, account_proofs) =
841            self.rpc_api.get_account_proofs(&foreign_accounts, known_account_codes).await?;
842
843        let mut account_proofs: BTreeMap<AccountId, AccountProof> =
844            account_proofs.into_iter().map(|proof| (proof.account_id(), proof)).collect();
845
846        for foreign_account in foreign_accounts.iter() {
847            let (foreign_account_inputs, merkle_path) = match foreign_account {
848                ForeignAccount::Public(account_id, ..) => {
849                    let account_proof = account_proofs
850                        .remove(account_id)
851                        .expect("Proof was requested and received");
852
853                    let (foreign_account_inputs, merkle_path) = account_proof.try_into()?;
854
855                    // Update  our foreign account code cache
856                    self.store
857                        .upsert_foreign_account_code(
858                            *account_id,
859                            foreign_account_inputs.account_code().clone(),
860                        )
861                        .await?;
862
863                    (foreign_account_inputs, merkle_path)
864                },
865                ForeignAccount::Private(foreign_account_inputs) => {
866                    let account_id = foreign_account_inputs.account_header().id();
867                    let proof = account_proofs
868                        .remove(&account_id)
869                        .expect("Proof was requested and received");
870                    let merkle_path = proof.merkle_proof();
871
872                    (foreign_account_inputs.clone(), merkle_path.clone())
873                },
874            };
875
876            extend_advice_inputs_for_foreign_account(
877                tx_args,
878                &mut self.tx_executor,
879                foreign_account_inputs,
880                &merkle_path,
881            )?;
882        }
883
884        // Optionally retrieve block header if we don't have it
885        if self.store.get_block_headers(&[block_num]).await?.is_empty() {
886            info!("Getting current block header data to execute transaction with foreign account requirements");
887            let summary = self.sync_state().await?;
888
889            if summary.block_num != block_num {
890                let mut current_partial_mmr = self.build_current_partial_mmr(true).await?;
891                self.get_and_store_authenticated_block(block_num, &mut current_partial_mmr)
892                    .await?;
893            }
894        }
895
896        Ok(Some(block_num))
897    }
898}
899
900// TESTING HELPERS
901// ================================================================================================
902
903#[cfg(feature = "testing")]
904impl<R: FeltRng> Client<R> {
905    pub async fn testing_prove_transaction(
906        &mut self,
907        tx_result: &TransactionResult,
908    ) -> Result<ProvenTransaction, ClientError> {
909        self.prove_transaction(tx_result, self.tx_prover.clone()).await
910    }
911
912    pub async fn testing_submit_proven_transaction(
913        &mut self,
914        proven_transaction: ProvenTransaction,
915    ) -> Result<(), ClientError> {
916        self.submit_proven_transaction(proven_transaction).await
917    }
918
919    pub async fn testing_apply_transaction(
920        &self,
921        tx_result: TransactionResult,
922    ) -> Result<(), ClientError> {
923        self.apply_transaction(tx_result).await
924    }
925}
926
927/// Extends the advice inputs with account data and Merkle proofs, and loads the necessary
928/// [code](AccountCode) in `tx_executor`.
929fn extend_advice_inputs_for_foreign_account(
930    tx_args: &mut TransactionArgs,
931    tx_executor: &mut TransactionExecutor,
932    foreign_account_inputs: ForeignAccountInputs,
933    merkle_path: &MerklePath,
934) -> Result<(), ClientError> {
935    let (account_header, storage_header, account_code, proofs) =
936        foreign_account_inputs.into_parts();
937
938    let account_id = account_header.id();
939    let foreign_id_root =
940        Digest::from([account_id.suffix(), account_id.prefix().as_felt(), ZERO, ZERO]);
941
942    // Extend the advice inputs with the new data
943    tx_args.extend_advice_map([
944        // ACCOUNT_ID -> [ID_AND_NONCE, VAULT_ROOT, STORAGE_ROOT, CODE_ROOT]
945        (foreign_id_root, account_header.as_elements()),
946        // STORAGE_ROOT -> [STORAGE_SLOT_DATA]
947        (account_header.storage_commitment(), storage_header.as_elements()),
948        // CODE_ROOT -> [ACCOUNT_CODE_DATA]
949        (account_header.code_commitment(), account_code.as_elements()),
950    ]);
951
952    // Load merkle nodes for storage maps
953    for proof in proofs {
954        // Extend the merkle store and map with the storage maps
955        tx_args.extend_merkle_store(
956            proof.path().inner_nodes(proof.leaf().index().value(), proof.leaf().hash())?,
957        );
958        // Populate advice map with Sparse Merkle Tree leaf nodes
959        tx_args
960            .extend_advice_map(core::iter::once((proof.leaf().hash(), proof.leaf().to_elements())));
961    }
962
963    // Extend the advice inputs with Merkle store data
964    tx_args.extend_merkle_store(
965        merkle_path.inner_nodes(account_id.prefix().as_u64(), account_header.hash())?,
966    );
967
968    tx_executor.load_account_code(&account_code);
969
970    Ok(())
971}
972
973// HELPERS
974// ================================================================================================
975
976fn collect_assets<'a>(
977    assets: impl Iterator<Item = &'a Asset>,
978) -> (BTreeMap<AccountId, u64>, BTreeSet<NonFungibleAsset>) {
979    let mut fungible_balance_map = BTreeMap::new();
980    let mut non_fungible_set = BTreeSet::new();
981
982    assets.for_each(|asset| match asset {
983        Asset::Fungible(fungible) => {
984            fungible_balance_map
985                .entry(fungible.faucet_id())
986                .and_modify(|balance| *balance += fungible.amount())
987                .or_insert(fungible.amount());
988        },
989        Asset::NonFungible(non_fungible) => {
990            non_fungible_set.insert(*non_fungible);
991        },
992    });
993
994    (fungible_balance_map, non_fungible_set)
995}
996
997pub(crate) fn prepare_word(word: &Word) -> String {
998    word.iter().map(|x| x.as_int().to_string()).collect::<Vec<_>>().join(".")
999}
1000
1001/// Extracts notes from [OutputNotes].
1002/// Used for:
1003/// - Checking the relevance of notes to save them as input notes.
1004/// - Validate hashes versus expected output notes after a transaction is executed.
1005pub fn notes_from_output(output_notes: &OutputNotes) -> impl Iterator<Item = &Note> {
1006    output_notes
1007        .iter()
1008        .filter(|n| matches!(n, OutputNote::Full(_)))
1009        .map(|n| match n {
1010            OutputNote::Full(n) => n,
1011            // The following todo!() applies until we have a way to support flows where we have
1012            // partial details of the note
1013            OutputNote::Header(_) | OutputNote::Partial(_) => {
1014                todo!("For now, all details should be held in OutputNote::Fulls")
1015            },
1016        })
1017}
1018
1019#[cfg(test)]
1020mod test {
1021    use miden_lib::{account::auth::RpoFalcon512, transaction::TransactionKernel};
1022    use miden_objects::{
1023        account::{AccountBuilder, AccountComponent, StorageMap, StorageSlot},
1024        asset::{Asset, FungibleAsset},
1025        crypto::dsa::rpo_falcon512::SecretKey,
1026        note::NoteType,
1027        testing::{
1028            account_component::BASIC_WALLET_CODE,
1029            account_id::{
1030                ACCOUNT_ID_FUNGIBLE_FAUCET_OFF_CHAIN, ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN,
1031                ACCOUNT_ID_REGULAR_ACCOUNT_IMMUTABLE_CODE_ON_CHAIN,
1032            },
1033        },
1034        Word,
1035    };
1036    use miden_tx::utils::{Deserializable, Serializable};
1037
1038    use super::PaymentTransactionData;
1039    use crate::{
1040        mock::create_test_client,
1041        transaction::{TransactionRequestBuilder, TransactionResult},
1042    };
1043
1044    #[tokio::test]
1045    async fn test_transaction_creates_two_notes() {
1046        let (mut client, _) = create_test_client().await;
1047        let asset_1: Asset =
1048            FungibleAsset::new(ACCOUNT_ID_FUNGIBLE_FAUCET_OFF_CHAIN.try_into().unwrap(), 123)
1049                .unwrap()
1050                .into();
1051        let asset_2: Asset =
1052            FungibleAsset::new(ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN.try_into().unwrap(), 500)
1053                .unwrap()
1054                .into();
1055
1056        let secret_key = SecretKey::new();
1057
1058        let wallet_component = AccountComponent::compile(
1059            BASIC_WALLET_CODE,
1060            TransactionKernel::assembler(),
1061            vec![StorageSlot::Value(Word::default()), StorageSlot::Map(StorageMap::default())],
1062        )
1063        .unwrap()
1064        .with_supports_all_types();
1065
1066        let anchor_block = client.get_latest_epoch_block().await.unwrap();
1067
1068        let account = AccountBuilder::new(Default::default())
1069            .anchor((&anchor_block).try_into().unwrap())
1070            .with_component(wallet_component)
1071            .with_component(RpoFalcon512::new(secret_key.public_key()))
1072            .with_assets([asset_1, asset_2])
1073            .build_existing()
1074            .unwrap();
1075
1076        client
1077            .add_account(
1078                &account,
1079                None,
1080                &miden_objects::account::AuthSecretKey::RpoFalcon512(secret_key.clone()),
1081                false,
1082            )
1083            .await
1084            .unwrap();
1085        client.sync_state().await.unwrap();
1086        let tx_request = TransactionRequestBuilder::pay_to_id(
1087            PaymentTransactionData::new(
1088                vec![asset_1, asset_2],
1089                account.id(),
1090                ACCOUNT_ID_REGULAR_ACCOUNT_IMMUTABLE_CODE_ON_CHAIN.try_into().unwrap(),
1091            ),
1092            None,
1093            NoteType::Private,
1094            client.rng(),
1095        )
1096        .unwrap()
1097        .build();
1098
1099        let tx_result = client.new_transaction(account.id(), tx_request).await.unwrap();
1100        assert!(tx_result
1101            .created_notes()
1102            .get_note(0)
1103            .assets()
1104            .is_some_and(|assets| assets.num_assets() == 2));
1105        // Prove and apply transaction
1106        client.testing_apply_transaction(tx_result.clone()).await.unwrap();
1107
1108        // Test serialization
1109        let bytes: std::vec::Vec<u8> = tx_result.to_bytes();
1110        let decoded = TransactionResult::read_from_bytes(&bytes).unwrap();
1111
1112        assert_eq!(tx_result, decoded);
1113    }
1114}